Compare commits

..

79 Commits

Author SHA1 Message Date
Simon Larsen
48f86579be Merge pull request #2164 from OneUptime/snyk-upgrade-2d5ead31cbe58e2bd5c3f9e186213162
[Snyk] Upgrade react-router-dom from 6.30.1 to 6.30.2
2025-12-07 20:27:33 +00:00
Simon Larsen
34e1ceec33 Merge pull request #2163 from OneUptime/snyk-upgrade-42f7d10f38ab259b26178999a6727268
[Snyk] Upgrade react-router-dom from 6.30.1 to 6.30.2
2025-12-07 20:27:26 +00:00
Simon Larsen
edba39d475 Merge pull request #2162 from OneUptime/chore/npm-audit-fix
chore: npm audit fix
2025-12-07 20:27:21 +00:00
snyk-bot
5385eb2076 fix: upgrade react-router-dom from 6.30.1 to 6.30.2
Snyk has created this PR to upgrade react-router-dom from 6.30.1 to 6.30.2.

See this package in npm:
react-router-dom

See this project in Snyk:
https://app.snyk.io/org/oneuptime-RsC2nshvQ2Vnr35jHvMnMP/project/40b17bc5-1bd4-48b1-88f1-5b4dc1400e80?utm_source=github&utm_medium=referral&page=upgrade-pr
2025-12-07 10:15:16 +00:00
snyk-bot
40597b7647 fix: upgrade react-router-dom from 6.30.1 to 6.30.2
Snyk has created this PR to upgrade react-router-dom from 6.30.1 to 6.30.2.

See this package in npm:
react-router-dom

See this project in Snyk:
https://app.snyk.io/org/oneuptime-RsC2nshvQ2Vnr35jHvMnMP/project/8ca4ee75-8bc5-43a1-a3bc-244ceebf1437?utm_source=github&utm_medium=referral&page=upgrade-pr
2025-12-07 10:15:12 +00:00
simlarsen
2e5cc47522 chore: npm audit fix 2025-12-06 01:45:01 +00:00
Nawaz Dhandala
e6c7eceb57 fix: streamline SMS notification messages for various job handlers 2025-12-05 21:27:07 +00:00
Nawaz Dhandala
ef2bb2f7b6 fix: update Loader component test IDs and change test environment to jsdom 2025-12-05 21:05:11 +00:00
Nawaz Dhandala
049dc02a5f fix: bump version to 9.2.9 2025-12-05 20:39:33 +00:00
Nawaz Dhandala
100f46ab3c fix: update badge styles for previous and current state indicators in StateTransition template 2025-12-05 19:02:52 +00:00
Nawaz Dhandala
e21d080e6f fix: add previous status and color to notification data in SendStatusChangeNotification 2025-12-05 18:55:49 +00:00
Nawaz Dhandala
3740382e76 fix: enhance status badge display with color indicators in StatusTransition template 2025-12-05 18:53:41 +00:00
Nawaz Dhandala
d3864e268b fix: improve table styling and layout in StatusPageSubscriberReport template 2025-12-05 17:42:29 +00:00
Nawaz Dhandala
d3db3fd174 fix: remove outdated reliability copilot workflow file 2025-12-05 13:52:27 +00:00
Nawaz Dhandala
9f9e337350 chore: update version number to 9.2.8 2025-12-05 13:45:39 +00:00
Nawaz Dhandala
1e84ece07e fix: simplify import statements in Queue.ts for better clarity 2025-12-05 13:32:17 +00:00
Nawaz Dhandala
ee4981bd19 fix: enhance job handling in Queue class to manage repeatable jobs 2025-12-05 13:31:25 +00:00
Nawaz Dhandala
f8802eea24 fix: format code for better readability in Landing.spec.ts 2025-12-05 11:10:15 +00:00
Nawaz Dhandala
5b45cab822 fix: update link selector for OneUptime in Landing.spec.ts 2025-12-05 11:03:33 +00:00
Simon Larsen
908f16d769 Merge pull request #2159 from OneUptime/snyk-upgrade-2036816bc1d34768c430f289cb384bca
[Snyk] Upgrade react-router-dom from 6.30.1 to 6.30.2
2025-12-05 08:41:21 +00:00
Simon Larsen
e4852e5799 Merge pull request #2160 from OneUptime/chore/npm-audit-fix
chore: npm audit fix
2025-12-05 08:41:14 +00:00
simlarsen
06e672abdd chore: npm audit fix 2025-12-05 01:51:29 +00:00
snyk-bot
efed184276 fix: upgrade react-router-dom from 6.30.1 to 6.30.2
Snyk has created this PR to upgrade react-router-dom from 6.30.1 to 6.30.2.

See this package in npm:
react-router-dom

See this project in Snyk:
https://app.snyk.io/org/oneuptime-RsC2nshvQ2Vnr35jHvMnMP/project/5dd2ef9c-1270-4729-aff4-e407805f7a9c?utm_source=github&utm_medium=referral&page=upgrade-pr
2025-12-05 00:44:09 +00:00
Simon Larsen
9bd14ec3f3 Merge pull request #2158 from OneUptime/monitor-more-info
refactor: implement detailed error handling for request failures acro…
2025-12-04 22:00:18 +00:00
Nawaz Dhandala
9950f1502e refactor: enhance promise handling and error rejection in DNS and traceroute operations 2025-12-04 21:57:42 +00:00
Nawaz Dhandala
43432261e1 refactor: improve formatting and readability in error handling and network monitoring code 2025-12-04 21:54:13 +00:00
Nawaz Dhandala
bd2b8ba1fb refactor: add detailed logging for request failure scenarios in monitor response 2025-12-04 21:48:27 +00:00
Nawaz Dhandala
03742ab6f4 refactor: implement detailed error handling for request failures across monitors 2025-12-04 20:25:55 +00:00
Nawaz Dhandala
7324bff68b refactor: add NetworkPathTrace and NetworkPathMonitor for comprehensive network diagnostics 2025-12-04 19:46:34 +00:00
Nawaz Dhandala
8dbfa524e5 refactor: enhance error message validation in API error handling 2025-12-04 18:15:50 +00:00
Nawaz Dhandala
b2ef34f45f refactor: improve error handling in API class for more meaningful messages 2025-12-04 18:14:54 +00:00
Nawaz Dhandala
1ec9c885f3 refactor: enhance type annotations for SEO-related functions and improve sitemap entry configuration 2025-12-04 15:20:23 +00:00
Nawaz Dhandala
007973aa86 refactor: improve code formatting for better readability in Routes and Sitemap 2025-12-04 15:10:49 +00:00
Nawaz Dhandala
500101350f refactor: update version check logic to prevent re-releasing existing versions 2025-12-04 15:01:36 +00:00
Nawaz Dhandala
524fcae430 chore: update version number to 9.2.7 2025-12-04 15:00:26 +00:00
Nawaz Dhandala
7ec8fc5b1c chore: revert version number to 9.2.5 2025-12-04 15:00:09 +00:00
Nawaz Dhandala
6af3daa98e chore: update version number to 9.2.7 2025-12-04 14:41:16 +00:00
Nawaz Dhandala
1d35614cd3 refactor: correct grammar in the blog call-to-action header for clarity 2025-12-04 14:40:34 +00:00
Nawaz Dhandala
91219c9a96 refactor: correct grammar and improve consistency in meta descriptions and content across multiple views 2025-12-04 14:29:32 +00:00
Nawaz Dhandala
65ca7623d5 refactor: correct pluralization in product descriptions for consistency 2025-12-04 14:29:29 +00:00
Nawaz Dhandala
c569977b45 refactor: enhance sitemap configuration with detailed priority and change frequency settings 2025-12-04 14:28:47 +00:00
Nawaz Dhandala
2263916a9f refactor: implement SEO enhancements with structured metadata and dynamic canonical URLs 2025-12-04 14:26:47 +00:00
Nawaz Dhandala
2cca728dfc refactor: improve accessibility by enhancing skip link and alt text for logo 2025-12-04 14:20:58 +00:00
Nawaz Dhandala
ed687a1639 refactor: enhance alt text for images and wrap main content in <main> tags for improved accessibility 2025-12-04 14:17:52 +00:00
Nawaz Dhandala
270199806c refactor: remove LLM-related Docker workflows and associated files 2025-12-04 13:47:12 +00:00
Nawaz Dhandala
30a3c5e1b2 refactor: improve type definition for getMetricChartType in DashboardChartComponent 2025-12-04 13:38:55 +00:00
Nawaz Dhandala
0c5bd31023 refactor: remove unused chart type determination logic in MonitorMetrics 2025-12-04 13:32:44 +00:00
Nawaz Dhandala
84a75b7af6 chore: bump version to 9.2.6 2025-12-04 13:30:27 +00:00
Nawaz Dhandala
e25be96040 feat: add rounded corners to BarChart rendering for improved aesthetics 2025-12-04 13:29:48 +00:00
Nawaz Dhandala
7777f7d9aa feat: add syncid prop to BarChart component for synchronized chart rendering 2025-12-04 13:15:09 +00:00
Nawaz Dhandala
8e37df3fc0 feat: add dropdown support for chart type selection in Dashboard components 2025-12-04 12:43:55 +00:00
Nawaz Dhandala
88c9e0beb5 feat: add BarChart component and integrate chart type handling in MetricCharts and MonitorMetrics 2025-12-04 12:36:41 +00:00
Nawaz Dhandala
d751537473 feat: add migration for postmortemPostedAt column in Incident table 2025-12-04 12:22:50 +00:00
Nawaz Dhandala
60be6c00e9 chore: bump version to 9.2.5 2025-12-04 11:45:50 +00:00
Nawaz Dhandala
91bf55dc20 refactor: simplify logging for previous status duration and improve code formatting 2025-12-04 09:17:31 +00:00
Nawaz Dhandala
d20a125742 refactor: update previous status duration handling in notification template and logging 2025-12-04 09:14:41 +00:00
Nawaz Dhandala
d10bcd2edd refactor: enhance code readability by adding comments for previous status duration calculation 2025-12-04 08:59:04 +00:00
Nawaz Dhandala
0b32408bf2 refactor: update notification template and improve duration calculation logic 2025-12-04 08:58:42 +00:00
Nawaz Dhandala
269fbd3f24 feat: add dep-check script and remove tsconfig-paths from devDependencies 2025-12-04 08:51:40 +00:00
Nawaz Dhandala
2640ea8c10 refactor: streamline previous status duration calculation in notification 2025-12-03 23:21:07 +00:00
Nawaz Dhandala
f3180d3a83 feat: add previous status duration calculation to status change notification 2025-12-03 23:18:35 +00:00
Nawaz Dhandala
7727fe835f refactor: improve parameter destructuring for createIncidentStateChangedNotification method 2025-12-03 23:17:23 +00:00
Nawaz Dhandala
00fbfbc08e chore: bump version to 9.2.4 2025-12-03 23:08:21 +00:00
Nawaz Dhandala
44d1183066 feat: add monitor destination and request type details to notification templates and services 2025-12-03 23:08:04 +00:00
Nawaz Dhandala
0ccef797ab chore: bump version to 9.2.3 2025-12-03 22:44:46 +00:00
Nawaz Dhandala
9914fb905f fix: update background color for previous and current state/status badges to transparent 2025-12-03 22:43:03 +00:00
Nawaz Dhandala
35ecc19ceb fix: update background color for current state/status badge in notification templates 2025-12-03 22:37:01 +00:00
Nawaz Dhandala
fa0362f739 feat: update notification templates to include state transition details for alerts, incidents, monitors, and scheduled maintenance 2025-12-03 22:36:09 +00:00
Nawaz Dhandala
8ea9084d9e feat: enhance notification templates and logic to include previous state information for alerts, incidents, monitors, and scheduled maintenance 2025-12-03 22:25:45 +00:00
Simon Larsen
eeb31a2250 Merge pull request #2154 from OneUptime/email-improve
Email improve
2025-12-03 21:40:46 +00:00
Nawaz Dhandala
b58c91dbab fix: update version number to 9.2.2 2025-12-03 21:40:11 +00:00
Nawaz Dhandala
868bf4d3e1 fix: remove unnecessary empty row from DetailBoxEnd template for improved clarity 2025-12-03 21:37:20 +00:00
Nawaz Dhandala
a3fc20b393 fix: update incident and event title fields for improved clarity in email templates 2025-12-03 21:14:32 +00:00
Nawaz Dhandala
c8dad04b5c fix: add border-radius to logo images for improved aesthetics 2025-12-03 21:10:15 +00:00
Nawaz Dhandala
ee7db393f8 fix: update email templates to remove empty fields for improved clarity 2025-12-03 21:06:09 +00:00
Nawaz Dhandala
e52da9fef2 fix: remove emojis from email titles for consistency 2025-12-03 21:00:19 +00:00
Nawaz Dhandala
9332df5648 Refactor email templates for improved styling and structure
- Updated EmailTitle template to enhance title styling and added a title block comment.
- Adjusted spacing in End and Footer templates for better layout.
- Enhanced Footer template with new styling and added a powered by link.
- Modified InfoBlock template for improved text styling and added an info block comment.
- Refined Start template with new background color and added a top spacer.
- Updated Style template with new link and badge styles for better visual consistency.
- Enhanced SupportBlock template with a more engaging support message.
- Improved Thanks template with a more personalized closing message.
- Added TitleBlock comments for better organization.
- Updated UnsubscribeBlock for clearer subscription management options.
- Replaced VerticalSpace with a table-based spacer for consistent spacing.
- Enhanced SignupWelcomeEmail with improved messaging and button text.
- Updated SubscriberAnnouncementCreated template for better clarity and button integration.
- Refined SubscriberIncidentCreated template for improved incident details presentation.
- Enhanced SubscriberIncidentPostmortemCreated template for better postmortem details.
- Updated SubscriberIncidentStateChanged template for clearer incident state updates.
- Refined SubscriberScheduledMaintenanceEventCreated template for better event details.
- Introduced StatusBadge template for consistent incident status representation.
2025-12-03 20:40:46 +00:00
Nawaz Dhandala
120fc2ad71 fix: change field type for subscriber notification from Toggle to Checkbox 2025-12-03 19:35:49 +00:00
Nawaz Dhandala
1a7672748f fix: update title for status page visibility and add conditional display for subscriber notification 2025-12-03 19:35:07 +00:00
153 changed files with 3417 additions and 1367 deletions

View File

@@ -22,19 +22,18 @@ jobs:
read-version:
runs-on: ubuntu-latest
permissions:
contents: write
contents: read
outputs:
major_minor: ${{ steps.determine.outputs.semver_base }}
semver_base: ${{ steps.determine.outputs.semver_base }}
major: ${{ steps.determine.outputs.major }}
minor: ${{ steps.determine.outputs.minor }}
patch: ${{ steps.determine.outputs.patch }}
updated: ${{ steps.determine.outputs.updated }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.ref }}
- name: Determine semver base and persist patch
- name: Check version and verify not already released
id: determine
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -48,6 +47,8 @@ jobs:
exit 1
fi
echo "Version from VERSION file: $VERSION_RAW"
IFS='.' read -r major minor patch <<< "$VERSION_RAW"
if [[ -z "$minor" ]]; then
echo "VERSION must contain major and minor components" >&2
@@ -63,50 +64,27 @@ jobs:
fi
done
target_patch="$patch"
updated="false"
pr_url=""
latest_tag="$(gh release view --repo "$REPOSITORY" --json tagName --jq '.tagName' 2>/dev/null || echo "")"
if [[ -n "$latest_tag" ]]; then
latest_tag="${latest_tag#v}"
latest_tag_core="${latest_tag%%+*}"
latest_tag_core="${latest_tag_core%%-*}"
IFS='.' read -r rel_major rel_minor rel_patch _ <<< "$latest_tag_core"
rel_patch="${rel_patch:-0}"
if [[ "$rel_major" =~ ^[0-9]+$ && "$rel_minor" =~ ^[0-9]+$ && "$rel_patch" =~ ^[0-9]+$ ]]; then
if [[ "$rel_major" == "$major" && "$rel_minor" == "$minor" ]]; then
target_patch=$((rel_patch + 1))
fi
fi
# Check if this version is already released on GitHub
echo "Checking if version $VERSION_RAW is already released on GitHub..."
if gh release view "$VERSION_RAW" --repo "$REPOSITORY" &>/dev/null; then
echo "::error::Version $VERSION_RAW is already released on GitHub. Please update the VERSION file to a new version."
exit 1
fi
# Also check with 'v' prefix just in case
if gh release view "v$VERSION_RAW" --repo "$REPOSITORY" &>/dev/null; then
echo "::error::Version v$VERSION_RAW is already released on GitHub. Please update the VERSION file to a new version."
exit 1
fi
new_version="${major}.${minor}.${target_patch}"
if [[ "$new_version" != "$VERSION_RAW" ]]; then
echo "$new_version" > VERSION
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git add VERSION
if ! git diff --cached --quiet; then
branch_name="chore/bump-version-${new_version}-$(date +%s)"
git checkout -b "$branch_name"
git commit -m "chore: bump version to ${new_version} [skip ci]"
git push origin "$branch_name"
pr_title="chore: bump version to ${new_version}"
pr_body=$'Automated change to VERSION to align release workflow with master.\n\nCreated by GitHub Actions release workflow.'
gh pr create --repo "$REPOSITORY" --base master --head "$branch_name" --title "$pr_title" --body "$pr_body"
pr_url="$(gh pr view "$branch_name" --repo "$REPOSITORY" --json url --jq '.url' 2>/dev/null || true)"
updated="true"
fi
fi
echo "✅ Version $VERSION_RAW is not yet released. Proceeding with release."
echo "semver_base=${new_version}" >> "$GITHUB_OUTPUT"
echo "semver_base=${VERSION_RAW}" >> "$GITHUB_OUTPUT"
echo "major=${major}" >> "$GITHUB_OUTPUT"
echo "minor=${minor}" >> "$GITHUB_OUTPUT"
echo "patch=${target_patch}" >> "$GITHUB_OUTPUT"
echo "updated=${updated}" >> "$GITHUB_OUTPUT"
echo "pr_url=${pr_url}" >> "$GITHUB_OUTPUT"
echo "Using version base: ${new_version}"
echo "patch=${patch}" >> "$GITHUB_OUTPUT"
echo "Using version: ${VERSION_RAW}"
@@ -1647,101 +1625,6 @@ jobs:
run: bash ./Scripts/NPM/PublishAllPackages.sh
llm-docker-image-deploy:
needs: [generate-build-number, read-version]
runs-on: ubuntu-latest
steps:
# Docker compose needs a lot of space to build images, so we need to free up some space first in the GitHub Actions runner
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: false
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: true
swap-storage: true
- name: Docker Meta
id: meta
uses: docker/metadata-action@v4
with:
images: |
oneuptime/llm
ghcr.io/oneuptime/llm
tags: |
type=raw,value=release,enable=true
type=semver,value=${{needs.read-version.outputs.major_minor}},pattern={{version}},enable=true
- uses: actions/checkout@v4
with:
ref: ${{ github.ref }}
- uses: actions/setup-node@v4
with:
node-version: latest
# - name: Setup Git LFS
# run: git lfs install
# # Cannot do this, no space on the gitHub standard runner. We need to use the large runner which is selfhosted
# - name: Download the Model from Hugging Face
# run: mkdir -p ./LLM/Models && cd ./LLM/Models && git clone https://${{ secrets.HUGGING_FACE_USERNAME }}:${{ secrets.HUGGING_FACE_PASSWORD }}@huggingface.co/meta-llama/Meta-Llama-3-8B-Instruct
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
image: tonistiigi/binfmt:qemu-v10.0.4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Generate Dockerfile from Dockerfile.tpl
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: npm run prerun
# Build and deploy nginx.
- name: Login to Docker Hub
uses: nick-fields/retry@v3
with:
timeout_minutes: 5
max_attempts: 3
command: |
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
- name: Login to GitHub Container Registry
uses: nick-fields/retry@v3
with:
timeout_minutes: 5
max_attempts: 3
command: |
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
- name: Build and push
uses: nick-fields/retry@v3
with:
timeout_minutes: 45
max_attempts: 3
command: |
bash ./Scripts/GHA/build_docker_images.sh \
--image llm \
--version "${{needs.read-version.outputs.major_minor}}" \
--dockerfile ./LLM/Dockerfile \
--context ./LLM \
--platforms linux/amd64 \
--git-sha "${{ github.sha }}"
docs-docker-image-deploy:
needs: [generate-build-number, read-version]
runs-on: ubuntu-latest
@@ -2131,7 +2014,6 @@ jobs:
- app-docker-image-deploy
- copilot-docker-image-deploy
- accounts-docker-image-deploy
- llm-docker-image-deploy
- docs-docker-image-deploy
- worker-docker-image-deploy
- workflow-docker-image-deploy
@@ -2162,7 +2044,6 @@ jobs:
"app",
"copilot",
"accounts",
"llm",
"docs",
"worker",
"workflow",
@@ -2221,7 +2102,7 @@ jobs:
test-e2e-release-saas:
runs-on: ubuntu-latest
needs: [telemetry-docker-image-deploy, publish-mcp-server, copilot-docker-image-deploy, docs-docker-image-deploy, api-reference-docker-image-deploy, workflow-docker-image-deploy, llm-docker-image-deploy, accounts-docker-image-deploy, admin-dashboard-docker-image-deploy, app-docker-image-deploy, dashboard-docker-image-deploy, probe-ingest-docker-image-deploy, server-monitor-ingest-docker-image-deploy, isolated-vm-docker-image-deploy, home-docker-image-deploy, worker-docker-image-deploy, otel-collector-docker-image-deploy, probe-docker-image-deploy, status-page-docker-image-deploy, test-docker-image-deploy, test-server-docker-image-deploy, publish-npm-packages, e2e-docker-image-deploy, helm-chart-deploy, generate-build-number, read-version, nginx-docker-image-deploy, incoming-request-ingest-docker-image-deploy]
needs: [telemetry-docker-image-deploy, publish-mcp-server, copilot-docker-image-deploy, docs-docker-image-deploy, api-reference-docker-image-deploy, workflow-docker-image-deploy, accounts-docker-image-deploy, admin-dashboard-docker-image-deploy, app-docker-image-deploy, dashboard-docker-image-deploy, probe-ingest-docker-image-deploy, server-monitor-ingest-docker-image-deploy, isolated-vm-docker-image-deploy, home-docker-image-deploy, worker-docker-image-deploy, otel-collector-docker-image-deploy, probe-docker-image-deploy, status-page-docker-image-deploy, test-docker-image-deploy, test-server-docker-image-deploy, publish-npm-packages, e2e-docker-image-deploy, helm-chart-deploy, generate-build-number, read-version, nginx-docker-image-deploy, incoming-request-ingest-docker-image-deploy]
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
@@ -2309,7 +2190,7 @@ jobs:
test-e2e-release-self-hosted:
runs-on: ubuntu-latest
# After all the jobs runs
needs: [telemetry-docker-image-deploy, publish-mcp-server, copilot-docker-image-deploy, incoming-request-ingest-docker-image-deploy, docs-docker-image-deploy, api-reference-docker-image-deploy, workflow-docker-image-deploy, llm-docker-image-deploy, accounts-docker-image-deploy, admin-dashboard-docker-image-deploy, app-docker-image-deploy, dashboard-docker-image-deploy, probe-ingest-docker-image-deploy, server-monitor-ingest-docker-image-deploy, isolated-vm-docker-image-deploy, home-docker-image-deploy, worker-docker-image-deploy, otel-collector-docker-image-deploy, probe-docker-image-deploy, status-page-docker-image-deploy, test-docker-image-deploy, test-server-docker-image-deploy, publish-npm-packages, e2e-docker-image-deploy, helm-chart-deploy, generate-build-number, read-version, nginx-docker-image-deploy]
needs: [telemetry-docker-image-deploy, publish-mcp-server, copilot-docker-image-deploy, incoming-request-ingest-docker-image-deploy, docs-docker-image-deploy, api-reference-docker-image-deploy, workflow-docker-image-deploy, accounts-docker-image-deploy, admin-dashboard-docker-image-deploy, app-docker-image-deploy, dashboard-docker-image-deploy, probe-ingest-docker-image-deploy, server-monitor-ingest-docker-image-deploy, isolated-vm-docker-image-deploy, home-docker-image-deploy, worker-docker-image-deploy, otel-collector-docker-image-deploy, probe-docker-image-deploy, status-page-docker-image-deploy, test-docker-image-deploy, test-server-docker-image-deploy, publish-npm-packages, e2e-docker-image-deploy, helm-chart-deploy, generate-build-number, read-version, nginx-docker-image-deploy]
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:

View File

@@ -1,32 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
name: "OneUptime Reliability Copilot"
on:
push:
branches: [ master ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
schedule:
# Run every day at midnight UTC
- cron: '0 0 * * *'
jobs:
analyze:
name: Analyze Code
runs-on: ubuntu-latest
steps:
# Run Reliability Copilot in Docker Container
- name: Run Copilot
run: |
docker run --rm \
-e ONEUPTIME_URL="https://test.oneuptime.com" \
-e ONEUPTIME_REPOSITORY_SECRET_KEY="${{ secrets.COPILOT_ONEUPTIME_REPOSITORY_SECRET_KEY }}" \
-e CODE_REPOSITORY_PASSWORD="${{ github.token }}" \
-e CODE_REPOSITORY_USERNAME="simlarsen" \
-e OPENAI_API_KEY="${{ secrets.OPENAI_API_KEY }}" \
--net=host oneuptime/copilot:test

View File

@@ -207,103 +207,6 @@ jobs:
llm-docker-image-deploy:
needs: [read-version, generate-build-number]
runs-on: ubuntu-latest
steps:
# Docker compose needs a lot of space to build images, so we need to free up some space first in the GitHub Actions runner
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: false
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: true
swap-storage: true
- name: Docker Meta
id: meta
uses: docker/metadata-action@v4
with:
images: |
oneuptime/llm
ghcr.io/oneuptime/llm
tags: |
type=raw,value=test,enable=true
type=raw,value=${{needs.read-version.outputs.major_minor}}-test,enable=true
- uses: actions/checkout@v4
with:
ref: ${{ github.ref }}
- uses: actions/setup-node@v4
with:
node-version: latest
# - name: Setup Git LFS
# run: git lfs install
# # Cannot do this, no space on the gitHub standard runner. We need to use the large runner which is selfhosted
# - name: Download the Model from Hugging Face
# run: mkdir -p ./LLM/Models && cd ./LLM/Models && git clone https://${{ secrets.HUGGING_FACE_USERNAME }}:${{ secrets.HUGGING_FACE_PASSWORD }}@huggingface.co/meta-llama/Meta-Llama-3-8B-Instruct
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
image: tonistiigi/binfmt:qemu-v10.0.4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Generate Dockerfile from Dockerfile.tpl
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: npm run prerun
# Build and deploy nginx.
- name: Login to Docker Hub
uses: nick-fields/retry@v3
with:
timeout_minutes: 5
max_attempts: 3
command: |
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
- name: Login to GitHub Container Registry
uses: nick-fields/retry@v3
with:
timeout_minutes: 5
max_attempts: 3
command: |
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io --username "${{ github.repository_owner }}" --password-stdin
- name: Build and push
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: |
bash ./Scripts/GHA/build_docker_images.sh \
--image llm \
--version "${{needs.read-version.outputs.major_minor}}-test" \
--dockerfile ./LLM/Dockerfile \
--context ./LLM \
--platforms linux/amd64 \
--git-sha "${{ github.sha }}" \
--extra-tags test \
--extra-enterprise-tags enterprise-test
nginx-docker-image-deploy:
needs: [read-version, generate-build-number]
runs-on: ubuntu-latest
@@ -1965,7 +1868,7 @@ jobs:
test-helm-chart:
runs-on: ubuntu-latest
needs: [infrastructure-agent-deploy, publish-mcp-server, llm-docker-image-deploy, publish-terraform-provider, telemetry-docker-image-deploy, copilot-docker-image-deploy, docs-docker-image-deploy, worker-docker-image-deploy, workflow-docker-image-deploy, isolated-vm-docker-image-deploy, home-docker-image-deploy, api-reference-docker-image-deploy, test-server-docker-image-deploy, test-docker-image-deploy, probe-ingest-docker-image-deploy, server-monitor-ingest-docker-image-deploy, probe-docker-image-deploy, dashboard-docker-image-deploy, admin-dashboard-docker-image-deploy, app-docker-image-deploy, accounts-docker-image-deploy, otel-collector-docker-image-deploy, status-page-docker-image-deploy, nginx-docker-image-deploy, e2e-docker-image-deploy, incoming-request-ingest-docker-image-deploy]
needs: [infrastructure-agent-deploy, publish-mcp-server, publish-terraform-provider, telemetry-docker-image-deploy, copilot-docker-image-deploy, docs-docker-image-deploy, worker-docker-image-deploy, workflow-docker-image-deploy, isolated-vm-docker-image-deploy, home-docker-image-deploy, api-reference-docker-image-deploy, test-server-docker-image-deploy, test-docker-image-deploy, probe-ingest-docker-image-deploy, server-monitor-ingest-docker-image-deploy, probe-docker-image-deploy, dashboard-docker-image-deploy, admin-dashboard-docker-image-deploy, app-docker-image-deploy, accounts-docker-image-deploy, otel-collector-docker-image-deploy, status-page-docker-image-deploy, nginx-docker-image-deploy, e2e-docker-image-deploy, incoming-request-ingest-docker-image-deploy]
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:

View File

@@ -33,7 +33,7 @@
"ejs": "^3.1.10",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.30.1",
"react-router-dom": "^6.30.2",
"use-async-effect": "^2.2.7"
},
"devDependencies": {

View File

@@ -12,7 +12,7 @@
"ejs": "^3.1.10",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.30.1"
"react-router-dom": "^6.30.2"
},
"devDependencies": {
"@types/node": "^16.11.35",
@@ -347,9 +347,9 @@
"dev": true
},
"node_modules/@remix-run/router": {
"version": "1.23.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz",
"integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==",
"version": "1.23.1",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.1.tgz",
"integrity": "sha512-vDbaOzF7yT2Qs4vO6XV1MHcJv+3dgR1sT+l3B8xxOVhUC336prMvqrvsLL/9Dnw2xr6Qhz4J0dmS0llNAbnUmQ==",
"license": "MIT",
"engines": {
"node": ">=14.0.0"
@@ -946,12 +946,12 @@
}
},
"node_modules/react-router": {
"version": "6.30.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz",
"integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==",
"version": "6.30.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.2.tgz",
"integrity": "sha512-H2Bm38Zu1bm8KUE5NVWRMzuIyAV8p/JrOaBJAwVmp37AXG72+CZJlEBw6pdn9i5TBgLMhNDgijS4ZlblpHyWTA==",
"license": "MIT",
"dependencies": {
"@remix-run/router": "1.23.0"
"@remix-run/router": "1.23.1"
},
"engines": {
"node": ">=14.0.0"
@@ -961,13 +961,13 @@
}
},
"node_modules/react-router-dom": {
"version": "6.30.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz",
"integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==",
"version": "6.30.2",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.2.tgz",
"integrity": "sha512-l2OwHn3UUnEVUqc6/1VMmR1cvZryZ3j3NzapC2eUXO1dB0sYp5mvwdjiXhpUbRb21eFow3qSxpP8Yv6oAU824Q==",
"license": "MIT",
"dependencies": {
"@remix-run/router": "1.23.0",
"react-router": "6.30.1"
"@remix-run/router": "1.23.1",
"react-router": "6.30.2"
},
"engines": {
"node": ">=14.0.0"

View File

@@ -8,7 +8,7 @@
"ejs": "^3.1.10",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.30.1"
"react-router-dom": "^6.30.2"
},
"scripts": {
"dev-build": "NODE_ENV=development node esbuild.config.js",

View File

@@ -13,10 +13,8 @@
{{> DetailBoxField title="Current State: " text=currentState }}
{{> DetailBoxField title="Resources Affected: " text=resourcesAffected }}
{{> DetailBoxField title="Severity: " text=alertSeverity }}
{{> DetailBoxField title="Root Cause: " text="" }}
{{> DetailBoxField title="" text=rootCause }}
{{> DetailBoxField title="Description: " text="" }}
{{> DetailBoxField title="" text=alertDescription }}
{{> DetailBoxField title="Root Cause: " text=rootCause }}
{{> DetailBoxField title="Description: " text=alertDescription }}
{{> DetailBoxEnd this }}

View File

@@ -13,10 +13,8 @@
{{> DetailBoxField title="Current State: " text=currentState }}
{{> DetailBoxField title="Resources Affected: " text=resourcesAffected }}
{{> DetailBoxField title="Severity: " text=incidentSeverity }}
{{> DetailBoxField title="Root Cause: " text="" }}
{{> DetailBoxField title="" text=rootCause }}
{{> DetailBoxField title="Description: " text="" }}
{{> DetailBoxField title="" text=incidentDescription }}
{{> DetailBoxField title="Root Cause: " text=rootCause }}
{{> DetailBoxField title="Description: " text=incidentDescription }}
{{> DetailBoxEnd this }}

View File

@@ -13,8 +13,7 @@
{{> DetailBoxField title="Current State: " text=currentState }}
{{> DetailBoxField title="Resources Affected: " text=resourcesAffected }}
{{> DetailBoxField title="Severity: " text=alertSeverity }}
{{> DetailBoxField title="Description: " text="" }}
{{> DetailBoxField title="" text=alertDescription }}
{{> DetailBoxField title="Description: " text=alertDescription }}
{{> DetailBoxEnd this }}

View File

@@ -14,11 +14,10 @@
{{> DetailBoxField title="Resources Affected: " text=resourcesAffected }}
{{> DetailBoxField title="Severity: " text=alertSeverity }}
{{#if isPrivateNote}}
{{> DetailBoxField title="Private Note: " text="" }}
{{> DetailBoxField title="Private Note: " text=note }}
{{else}}
{{> DetailBoxField title="Public Note: " text="" }}
{{> DetailBoxField title="Public Note: " text=note }}
{{/if}}
{{> DetailBoxField title="" text=note }}
{{> DetailBoxEnd this }}

View File

@@ -13,16 +13,12 @@
{{> DetailBoxField title="Current State: " text=currentState }}
{{> DetailBoxField title="Resources Affected: " text=resourcesAffected }}
{{> DetailBoxField title="Alert Declared By: " text=declaredBy }}
{{> DetailBoxField title="Alert Declared At: " text="" }}
{{> DetailBoxField title="" text=declaredAt }}
{{> DetailBoxField title="Alert Declared At: " text=declaredAt }}
{{> DetailBoxField title="Severity: " text=alertSeverity }}
{{> DetailBoxField title="Root Cause: " text="" }}
{{> DetailBoxField title="" text=rootCause }}
{{> DetailBoxField title="Description: " text="" }}
{{> DetailBoxField title="" text=alertDescription }}
{{> DetailBoxField title="Root Cause: " text=rootCause }}
{{> DetailBoxField title="Description: " text=alertDescription }}
{{#ifNotCond remediationNotes ""}}
{{> DetailBoxField title="Remediation Notes: " text="" }}
{{> DetailBoxField title="" text=remediationNotes }}
{{> DetailBoxField title="Remediation Notes: " text=remediationNotes }}
{{/ifNotCond}}
{{> DetailBoxEnd this }}

View File

@@ -4,19 +4,17 @@
{{> Logo this}}
{{> EmailTitle title=(concat "Alert: " alertTitle) }}
{{> InfoBlock info=(concat "Alert state changed to - " currentState)}}
{{> InfoBlock info="Alert state has changed"}}
{{> InfoBlock info="Here are the details: "}}
{{> DetailBoxStart this }}
{{> StateTransition this}}
{{> DetailBoxField title="Alert Title:" text=alertTitle }}
{{> DetailBoxField title="New State: " text=currentState }}
{{> DetailBoxField title="State changed at: " text="" }}
{{> DetailBoxField title="" text=stateChangedAt }}
{{> DetailBoxField title="Resources Affected: " text=resourcesAffected }}
{{> DetailBoxField title="Severity: " text=alertSeverity }}
{{> DetailBoxField title="Description: " text="" }}
{{> DetailBoxField title="" text=alertDescription }}
{{> DetailBoxField title="State changed at:" text=stateChangedAt }}
{{> DetailBoxField title="Resources Affected:" text=resourcesAffected }}
{{> DetailBoxField title="Severity:" text=alertSeverity }}
{{> DetailBoxField title="Description:" text=alertDescription }}
{{> DetailBoxEnd this }}

View File

@@ -2,15 +2,20 @@
{{> Logo this}}
{{> EmailTitle title="Forgot Password? We're here to help." }}
{{> EmailTitle title="Reset Your Password" }}
{{> InfoBlock info="Please click on the 'Reset your password' button below which will help you reset your password and once you're done, You're good to go!"}}
{{> InfoBlock info="We received a request to reset your password. Click the button below to create a new password for your account."}}
{{> ButtonBlock buttonUrl=tokenVerifyUrl buttonText="Reset your password"}}
{{> ButtonBlock buttonUrl=tokenVerifyUrl buttonText="Reset Password"}}
{{> InfoBlock info="You can also copy and paste this link:"}}
{{> VerticalSpace this}}
{{> InfoBlock info="Or copy and paste this link into your browser:"}}
{{> InfoBlock info=tokenVerifyUrl}}
{{> InfoBlock info="This password reset link expires in 24 hours."}}
{{> VerticalSpace this}}
{{> InfoBlock info="<strong>Note:</strong> This password reset link will expire in 24 hours. If you didn't request this reset, you can safely ignore this email."}}
{{> SupportBlock this }}

View File

@@ -13,8 +13,7 @@
{{> DetailBoxField title="Current State: " text=currentState }}
{{> DetailBoxField title="Resources Affected: " text=resourcesAffected }}
{{> DetailBoxField title="Severity: " text=incidentSeverity }}
{{> DetailBoxField title="Description: " text="" }}
{{> DetailBoxField title="" text=incidentDescription }}
{{> DetailBoxField title="Description: " text=incidentDescription }}
{{> DetailBoxEnd this }}

View File

@@ -14,11 +14,10 @@
{{> DetailBoxField title="Resources Affected: " text=resourcesAffected }}
{{> DetailBoxField title="Severity: " text=incidentSeverity }}
{{#if isPrivateNote}}
{{> DetailBoxField title="Private Note: " text="" }}
{{> DetailBoxField title="Private Note: " text=note }}
{{else}}
{{> DetailBoxField title="Public Note: " text="" }}
{{> DetailBoxField title="Public Note: " text=note }}
{{/if}}
{{> DetailBoxField title="" text=note }}
{{> DetailBoxEnd this }}

View File

@@ -13,16 +13,12 @@
{{> DetailBoxField title="Current State: " text=currentState }}
{{> DetailBoxField title="Resources Affected: " text=resourcesAffected }}
{{> DetailBoxField title="Incident Declared By: " text=declaredBy }}
{{> DetailBoxField title="Incident Declared At: " text="" }}
{{> DetailBoxField title="" text=declaredAt }}
{{> DetailBoxField title="Incident Declared At: " text=declaredAt }}
{{> DetailBoxField title="Severity: " text=incidentSeverity }}
{{> DetailBoxField title="Root Cause: " text="" }}
{{> DetailBoxField title="" text=rootCause }}
{{> DetailBoxField title="Description: " text="" }}
{{> DetailBoxField title="" text=incidentDescription }}
{{> DetailBoxField title="Root Cause: " text=rootCause }}
{{> DetailBoxField title="Description: " text=incidentDescription }}
{{#ifNotCond remediationNotes ""}}
{{> DetailBoxField title="Remediation Notes: " text="" }}
{{> DetailBoxField title="" text=remediationNotes }}
{{> DetailBoxField title="Remediation Notes: " text=remediationNotes }}
{{/ifNotCond}}
{{> DetailBoxEnd this }}

View File

@@ -4,19 +4,17 @@
{{> Logo this}}
{{> EmailTitle title=(concat "Incident: " incidentTitle) }}
{{> InfoBlock info=(concat "Incident state changed to - " currentState)}}
{{> InfoBlock info="Incident state has changed"}}
{{> InfoBlock info="Here are the details: "}}
{{> DetailBoxStart this }}
{{> StateTransition this}}
{{> DetailBoxField title="Incident Title:" text=incidentTitle }}
{{> DetailBoxField title="New State: " text=currentState }}
{{> DetailBoxField title="State changed at: " text="" }}
{{> DetailBoxField title="" text=stateChangedAt }}
{{> DetailBoxField title="Resources Affected: " text=resourcesAffected }}
{{> DetailBoxField title="Severity: " text=incidentSeverity }}
{{> DetailBoxField title="Description: " text="" }}
{{> DetailBoxField title="" text=incidentDescription }}
{{> DetailBoxField title="State changed at:" text=stateChangedAt }}
{{> DetailBoxField title="Resources Affected:" text=resourcesAffected }}
{{> DetailBoxField title="Severity:" text=incidentSeverity }}
{{> DetailBoxField title="Description:" text=incidentDescription }}
{{> DetailBoxEnd this }}

View File

@@ -2,28 +2,33 @@
{{> Logo this}}
{{> EmailTitle title=(concat "You have been invited to " projectName) }}
{{> EmailTitle title=(concat "👋 You're Invited to " projectName) }}
{{#ifCond isNewUser "true"}}
{{> InfoBlock info="Please sign up to a new account to accept this invitation"}}
{{> InfoBlock info="You've been invited to join a project on OneUptime. Create your account to get started."}}
{{> ButtonBlock buttonUrl=registerLink buttonText="Sign up to a new account"}}
{{> ButtonBlock buttonUrl=registerLink buttonText="Create Your Account"}}
{{> InfoBlock info="You can also copy and paste this link:"}}
{{> VerticalSpace this}}
{{> InfoBlock info="Or copy and paste this link into your browser:"}}
{{> InfoBlock info=registerLink}}
{{/ifCond}}
{{#ifCond isNewUser "false"}}
{{> InfoBlock info="Please sign in to your account to see all your invitations and manage them."}}
{{> InfoBlock info="You've been invited to join a project on OneUptime. Sign in to view and manage your invitations."}}
{{> ButtonBlock buttonUrl=signInLink buttonText="Sign in to OneUptime"}}
{{> ButtonBlock buttonUrl=signInLink buttonText="Sign In to OneUptime"}}
{{> InfoBlock info="You can also copy and paste this link:"}}
{{> VerticalSpace this}}
{{> InfoBlock info="Or copy and paste this link into your browser:"}}
{{> InfoBlock info=signInLink}}
{{/ifCond}}
{{> VerticalSpace this}}
{{> InfoBlock info="If you have not signed up to OneUptime so far. You'll be redirected to the account sign up page to sign up first."}}
{{> InfoBlock info="<em>If you don't have an account yet, you'll be redirected to create one first.</em>"}}
{{> SupportBlock this }}

View File

@@ -10,9 +10,15 @@
{{> DetailBoxStart this }}
{{> DetailBoxField title="Monitor Name:" text=monitorName }}
{{#ifNotCond monitorDestination ""}}
{{#ifCond monitorType "API"}}
{{> DetailBoxField title="Monitor URL:" text=(concat requestType " " monitorDestination) }}
{{else}}
{{> DetailBoxField title="Monitor Destination:" text=monitorDestination }}
{{/ifCond}}
{{/ifNotCond}}
{{> DetailBoxField title="Current Status: " text=currentStatus }}
{{> DetailBoxField title="Description: " text="" }}
{{> DetailBoxField title="" text=monitorDescription }}
{{> DetailBoxField title="Description: " text=monitorDescription }}
{{> DetailBoxEnd this }}

View File

@@ -10,9 +10,15 @@
{{> DetailBoxStart this }}
{{> DetailBoxField title="Monitor Name:" text=monitorName }}
{{#ifNotCond monitorDestination ""}}
{{#ifCond monitorType "API"}}
{{> DetailBoxField title="Monitor URL:" text=(concat requestType " " monitorDestination) }}
{{else}}
{{> DetailBoxField title="Monitor Destination:" text=monitorDestination }}
{{/ifCond}}
{{/ifNotCond}}
{{> DetailBoxField title="Current Status: " text=currentStatus }}
{{> DetailBoxField title="Description: " text="" }}
{{> DetailBoxField title="" text=monitorDescription }}
{{> DetailBoxField title="Description: " text=monitorDescription }}
{{> DetailBoxEnd this }}

View File

@@ -4,21 +4,28 @@
{{> Logo this}}
{{> EmailTitle title=(concat "Monitor: " monitorName) }}
{{> InfoBlock info=(concat "Monitor status changed to - " currentStatus)}}
{{> InfoBlock info="Monitor status has changed"}}
{{> InfoBlock info="Here are the details: "}}
{{> DetailBoxStart this }}
{{> DetailBoxField title="Monitor Name:" text=monitorName }}
{{> DetailBoxField title="New Status: " text=currentStatus }}
{{#ifNotCond rootCause ""}}
{{> DetailBoxField title="Root Cause: " text="" }}
{{> DetailBoxField title="" text=rootCause }}
{{> StatusTransition this}}
{{#ifNotCond previousStatusDurationText ""}}
{{> DetailBoxField title="Duration in Previous Status:" text=previousStatusDurationText }}
{{/ifNotCond}}
{{> DetailBoxField title="Status changed at: " text="" }}
{{> DetailBoxField title="" text=statusChangedAt }}
{{> DetailBoxField title="Description: " text="" }}
{{> DetailBoxField title="" text=monitorDescription }}
{{> DetailBoxField title="Monitor Name:" text=monitorName }}
{{#ifNotCond monitorDestination ""}}
{{#ifCond monitorType "API"}}
{{> DetailBoxField title="Monitor URL:" text=(concat requestType " " monitorDestination) }}
{{else}}
{{> DetailBoxField title="Monitor Destination:" text=monitorDestination }}
{{/ifCond}}
{{/ifNotCond}}
{{#ifNotCond rootCause ""}}
{{> DetailBoxField title="Root Cause:" text=rootCause }}
{{/ifNotCond}}
{{> DetailBoxField title="Status changed at:" text=statusChangedAt }}
{{> DetailBoxField title="Description:" text=monitorDescription }}
{{> DetailBoxEnd this }}

View File

@@ -8,8 +8,14 @@
{{> DetailBoxStart this }}
{{> DetailBoxField title="Monitor Name:" text=monitorName }}
{{> DetailBoxField title="Monitor Description: " text="" }}
{{> DetailBoxField title="" text=monitorDescription }}
{{#ifNotCond monitorDestination ""}}
{{#ifCond monitorType "API"}}
{{> DetailBoxField title="Monitor URL:" text=(concat requestType " " monitorDestination) }}
{{else}}
{{> DetailBoxField title="Monitor Destination:" text=monitorDestination }}
{{/ifCond}}
{{/ifNotCond}}
{{> DetailBoxField title="Monitor Description: " text=monitorDescription }}
{{> DetailBoxField title="Probe Status: " text=currentStatus }}
{{> DetailBoxEnd this }}

View File

@@ -1,3 +1,4 @@
<!-- Primary Action Button -->
<table class="st-Copy st-Width st-Width--mobile" border="0" cellpadding="0" cellspacing="0"
width="600" style="min-width: 600px;">
<tbody>
@@ -8,26 +9,26 @@
<div class="st-Spacer st-Spacer--filler"></div>
</td>
<td class="st-Font st-Font--body"
style="border: 0; margin: 0; padding: 0; color: #000000; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif; font-size: 16px; line-height: 24px;">
style="border: 0; margin: 0; padding: 0; color: #1a1a2e; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif; font-size: 16px; line-height: 24px;">
<!-- Button & Modifier: fullWidth -->
<!-- Button & Modifier: fullWidth with gradient -->
<table class="st-Button st-Button--fullWidth" border="0" cellpadding="0" cellspacing="0"
width="100%">
<tbody>
<tr>
<td align="center" class="st-Button-area" height="38" valign="middle"
style="border: 0; margin: 0; padding: 0; background-color: #000000; border-radius: 5px; text-align: center;">
<td align="center" class="st-Button-area" height="48" valign="middle"
style="border: 0; margin: 0; padding: 0; background: linear-gradient(135deg, #1a1a2e 0%, #000000 100%); border-radius: 8px; text-align: center; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);">
<a class="st-Button-link"
style="border: 0; margin: 0; padding: 0; color: #ffffff; display: block; height: 38px; text-align: center; text-decoration: none;"
style="border: 0; margin: 0; padding: 0; color: #ffffff; display: block; height: 48px; text-align: center; text-decoration: none;"
href={{buttonUrl}}>
<span class="st-Button-internal"
style="border: 0; margin: 0; padding: 0; color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif; font-size: 16px; font-weight: bold; height: 38px; line-height: 38px; mso-line-height-rule: exactly; text-decoration: none; vertical-align: middle; white-space: nowrap; width: 100%;">{{buttonText}}</span>
style="border: 0; margin: 0; padding: 0; color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif; font-size: 15px; font-weight: 600; height: 48px; line-height: 48px; mso-line-height-rule: exactly; text-decoration: none; vertical-align: middle; white-space: nowrap; width: 100%; letter-spacing: 0.3px;">{{buttonText}}</span>
</a>
</td>
</tr>
</tbody>
</table>
<!-- /Button & Modifier: fullWidth -->
<!-- /Button & Modifier: fullWidth with gradient -->
</td>
<td class="st-Spacer st-Spacer--gutter"
@@ -37,10 +38,11 @@
</td>
</tr>
<tr>
<td class="st-Spacer st-Spacer--stacked" colspan="3" height="12"
<td class="st-Spacer st-Spacer--stacked" colspan="3" height="16"
style="border: 0; margin: 0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
</tr>
</tbody>
</table>
</table>
<!-- /Primary Action Button -->

View File

@@ -19,7 +19,7 @@
<a style="border: 0; margin: 0; padding: 0; text-decoration: none;" href={{homeURL}}>
<img alt="OneUptime" height="70" border="0"
style="height:70px; border: 0; margin: 0; padding: 0; color: #000000; display: block; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif; font-size: 12px; font-weight: normal;"
style="height:70px; border: 0; margin: 0; padding: 0; color: #000000; display: block; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif; font-size: 12px; font-weight: normal; border-radius: 10%;"
src="{{logoUrl}}">
</a>
</div>

View File

@@ -1,4 +1,4 @@
<!-- End Detail Card Field Container -->
</td>
</tr>
</tbody>
@@ -10,4 +10,5 @@
</td>
</tr>
</tbody>
</table>
</table>
<!-- /Detail Card Container -->

View File

@@ -1,7 +1,9 @@
<p style="Margin:0;font-size:16px;font-family:'inter','helvetica neue',helvetica,arial,sans-serif;line-height:30px;color:#424761">
<!-- Detail Card Field -->
<div class="st-DetailCard-field" style="padding: 10px 0; border-bottom: 1px solid #e2e8f0;">
{{#if title}}
<strong>{{{title}}} </strong>{{{text}}} </span>
{{else}}
{{{text}}}
<p class="st-DetailCard-label" style="Margin:0 0 4px 0;font-size:11px;font-family:'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;line-height:16px;color:#64748b;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;">{{{title}}}</p>
{{/if}}
</p>
{{#if text}}
<p class="st-DetailCard-value" style="Margin:0;font-size:15px;font-family:'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;line-height:24px;color:#1e293b;font-weight:500;">{{{text}}}</p>
{{/if}}
</div>

View File

@@ -1,19 +1,20 @@
<table class="st-Copy st-Width st-Width--mobile" border="0" cellpadding="0" cellspacing="0"
width="500" style="min-width: 500px;margin: 10px 50px;">
<!-- Detail Card Container -->
<table class="st-Copy st-Width st-Width--mobile st-DetailCard" border="0" cellpadding="0" cellspacing="0"
width="472" style="min-width: 472px; margin: 16px 64px; border-radius: 12px; overflow: hidden;">
<tbody>
<tr style="border-collapse:collapse">
<td width="720" align="center" valign="top" style="padding:0;Margin:0"></td>
<td width="100%" align="center" valign="top" style="padding:0;Margin:0"></td>
</tr>
<tr style="border-collapse:collapse">
<td align="left"
style="padding:0;Margin:0;padding-top:0px;padding-left:40px;padding-right:40px;padding-bottom:30px;background-color:#f7f8fa;border-radius: 5px">
style="padding:0;Margin:0;padding-top:0px;padding-left:0;padding-right:0;padding-bottom:0;background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);border-radius: 12px; border: 1px solid #e2e8f0; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);">
<table cellpadding="0" cellspacing="0" width="100%"
style="border-collapse:collapse;border-spacing:0px">
<tbody>
<tr style="border-collapse:collapse">
<td width="720" align="center" valign="top" style="padding:0;Margin:0">
<td width="100%" align="center" valign="top" style="padding:0;Margin:0">
<table cellpadding="0" cellspacing="0" width="100%" role="presentation"
style="border-collapse:collapse;border-spacing:0px">
<tbody>
<tr style="border-collapse:collapse">
<td align="left" style="padding:0;Margin:0;padding-top:30px">
<td align="left" style="padding:24px 28px 0 28px;Margin:0;">

View File

@@ -1,3 +1,4 @@
<!-- Email Title Block -->
<table class="st-Copy st-Width st-Width--mobile" border="0" cellpadding="0" cellspacing="0"
width="600" style="min-width: 600px;">
<tbody>
@@ -7,10 +8,10 @@
width="64">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
<td class="st-Font st-Font--body"
style="color: #000000 !important; border:0;margin:0;padding:0; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Ubuntu,sans-serif;font-size:16px;line-height:24px">
<td class="st-Font st-Font--title"
style="color: #1a1a2e !important; border:0;margin:0;padding:0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif;font-size:24px;line-height:32px;font-weight:700;">
<h3>{{title}}</h3>
<h2 style="margin: 0; padding: 0; font-size: 24px; line-height: 32px; font-weight: 700; color: #1a1a2e;">{{title}}</h2>
</td>
<td class="st-Spacer st-Spacer--gutter"
@@ -20,10 +21,11 @@
</td>
</tr>
<tr>
<td class="st-Spacer st-Spacer--stacked" colspan="3" height="12"
<td class="st-Spacer st-Spacer--stacked" colspan="3" height="16"
style="border: 0; margin: 0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
</tr>
</tbody>
</table>
</table>
<!-- /Email Title Block -->

View File

@@ -7,7 +7,7 @@
</td>
</tr>
<tr>
<td class="st-Spacer st-Spacer--emailEnd" height="64"
<td class="st-Spacer st-Spacer--emailEnd" height="48"
style="border: 0; margin: 0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;">
<div class="st-Spacer st-Spacer--filler">&nbsp;</div>
</td>

View File

@@ -1,8 +1,9 @@
<!-- Footer Block -->
<table class="st-Footer st-Width st-Width--mobile" border="0" cellpadding="0" cellspacing="0"
width="600" style="min-width: 600px;">
<tbody>
<tr>
<td class="st-Spacer st-Spacer--divider" colspan="3" height="20"
<td class="st-Spacer st-Spacer--divider" colspan="3" height="24"
style="border: 0; margin: 0; padding: 0; font-size: 1px; line-height: 1px; max-height: 1px; mso-line-height-rule: exactly;">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
@@ -13,7 +14,7 @@
width="64">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
<td bgcolor="#fdfdfd" colspan="2" height="1"
<td bgcolor="#e5e7eb" colspan="2" height="1"
style="border: 0; margin: 0; padding: 0; font-size: 1px; line-height: 1px; max-height: 1px; mso-line-height-rule: exactly;">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
@@ -24,7 +25,7 @@
</td>
</tr>
<tr>
<td class="st-Spacer st-Spacer--divider" colspan="3" height="31"
<td class="st-Spacer st-Spacer--divider" colspan="3" height="24"
style="border: 0; margin: 0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
@@ -36,10 +37,10 @@
<div class="st-Spacer st-Spacer--filler"></div>
</td>
<td class="st-Font st-Font--caption"
style="border: 0; margin: 0;padding: 0; color: #8898aa; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif; font-size: 12px; line-height: 16px;">
style="border: 0; margin: 0;padding: 0; color: #6b7280; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif; font-size: 13px; line-height: 18px; text-align: center;">
<span class="st-Delink st-Delink--footer"
style="border: 0; margin: 0; padding: 0; color: #8898aa; text-decoration: none;">
© {{year}} OneUptime
style="border: 0; margin: 0; padding: 0; color: #6b7280; text-decoration: none;">
© {{year}} OneUptime · Powered by <a href="https://oneuptime.com" style="color: #1a1a2e; text-decoration: none; font-weight: 500;">OneUptime</a>
</span>
</td>
<td class="st-Spacer st-Spacer--gutter"
@@ -49,10 +50,11 @@
</td>
</tr>
<tr>
<td class="st-Spacer st-Spacer--emailEnd" colspan="3" height="64"
<td class="st-Spacer st-Spacer--emailEnd" colspan="3" height="48"
style="border: 0; margin: 0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
</tr>
</tbody>
</table>
</table>
<!-- /Footer Block -->

View File

@@ -1,4 +1,4 @@
<!-- Info Block -->
<table class="st-Copy st-Width st-Width--mobile" border="0" cellpadding="0" cellspacing="0"
width="600" style="min-width: 600px;">
<tbody>
@@ -9,7 +9,7 @@
<div class="st-Spacer st-Spacer--filler"></div>
</td>
<td class="st-Font st-Font--body"
style="border: 0; margin: 0; padding: 0; color: #000000 !important; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif; font-size: 16px; line-height: 24px;">
style="border: 0; margin: 0; padding: 0; color: #374151 !important; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif; font-size: 15px; line-height: 26px;">
{{{info}}}
</td>
<td class="st-Spacer st-Spacer--gutter"
@@ -19,12 +19,11 @@
</td>
</tr>
<tr>
<td class="st-Spacer st-Spacer--stacked" colspan="3" height="12"
<td class="st-Spacer st-Spacer--stacked" colspan="3" height="14"
style="border: 0; margin: 0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
</tr>
</tbody>
</table>
<!-- /Info Block -->

View File

@@ -18,7 +18,7 @@
<a style="border: 0; margin: 0; padding: 0; text-decoration: none;" href={{homeURL}}>
<img height="70" width="70" alt="OneUptime" border="0"
style="height:70px; width:70px; border: 0; margin: 0; padding: 0; color: #000000; display: block; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif; font-size: 12px; font-weight: normal;"
style="height:70px; width:70px; border: 0; margin: 0; padding: 0; color: #000000; display: block; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif; font-size: 12px; font-weight: normal; border-radius: 10%;"
src="https://res.cloudinary.com/deityhub/image/upload/v1637736803/1png.png">
</a>
</div>

View File

@@ -5,21 +5,33 @@
{{> Header}}
<body class="st-Email" bgcolor="f7f7f7"
<body class="st-Email" bgcolor="f3f4f6"
style="border: 0; margin: 0; padding: 0; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; min-width: 100%; width: 100%;"
override="fix">
<!-- Background -->
<table class="st-Background" bgcolor="f7f7f7" border="0" cellpadding="0" cellspacing="0" width="100%"
<table class="st-Background" bgcolor="f3f4f6" border="0" cellpadding="0" cellspacing="0" width="100%"
style="border: 0; margin: 0; padding: 0;">
<tbody>
<tr>
<td style="border: 0; margin: 0; padding: 0;">
<!-- Top Spacer -->
<table align="center" border="0" cellpadding="0" cellspacing="0" width="600" style="min-width: 600px;">
<tbody>
<tr>
<td height="32" style="border: 0; margin: 0; padding: 0; font-size: 1px; line-height: 1px;">
<div>&nbsp;</div>
</td>
</tr>
</tbody>
</table>
<!-- /Top Spacer -->
<!-- Wrapper -->
<table class="st-Wrapper" align="center" bgcolor="ffffff" border="0" cellpadding="0" cellspacing="0"
width="600"
style="border-bottom-left-radius: 5px; border-bottom-right-radius: 5px; margin: 0 auto; min-width: 600px;">
style="border-radius: 12px; margin: 0 auto; min-width: 600px; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03); border-top: 4px solid #1a1a2e;">
<tbody>
<tr>
<td style="border: 0; margin: 0; padding: 0;">

View File

@@ -0,0 +1,31 @@
<!-- State Transition Block - Inline version for DetailBox -->
<div class="st-DetailCard-field" style="padding: 16px 0; border-bottom: 1px solid #e2e8f0;">
<p class="st-DetailCard-label" style="Margin:0 0 12px 0;font-size:11px;font-family:'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;line-height:16px;color:#64748b;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;">STATE CHANGE</p>
<table cellpadding="0" cellspacing="0" border="0" style="border-collapse: collapse;">
<tbody>
<tr>
{{#if previousState}}
<!-- Previous State Badge -->
<td align="center" valign="middle" style="padding: 0;">
<span style="display: inline-block; white-space: nowrap; padding: 6px 14px; border-radius: 6px; font-size: 13px; font-weight: 600; background-color: transparent; color: {{previousStateColor}}; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif; line-height: 16px; vertical-align: middle;">
<span style="width: 10px; height: 10px; border-radius: 4px; background-color: {{previousStateColor}}; display: inline-block; margin-right: 8px; vertical-align: middle;"></span>
<span style="display: inline-block; line-height: 16px; vertical-align: middle;">{{previousState}}</span>
</span>
</td>
<!-- Arrow -->
<td align="center" valign="middle" style="padding: 0 12px;">
<span style="font-size: 20px; color: #94a3b8; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif;">→</span>
</td>
{{/if}}
<!-- Current State Badge -->
<td align="center" valign="middle" style="padding: 0;">
<span style="display: inline-block; white-space: nowrap; padding: 6px 14px; border-radius: 6px; font-size: 13px; font-weight: 600; background-color: transparent; color: {{currentStateColor}}; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif; line-height: 16px; vertical-align: middle;">
<span style="width: 10px; height: 10px; border-radius: 4px; background-color: {{currentStateColor}}; display: inline-block; margin-right: 8px; vertical-align: middle;"></span>
<span style="display: inline-block; line-height: 16px; vertical-align: middle;">{{currentState}}</span>
</span>
</td>
</tr>
</tbody>
</table>
</div>
<!-- /State Transition Block -->

View File

@@ -0,0 +1,58 @@
<!-- Status Badge - Used for incident states, severity levels, etc. -->
<table class="st-Copy st-Width st-Width--mobile" border="0" cellpadding="0" cellspacing="0"
width="600" style="min-width: 600px;">
<tbody>
<tr>
<td class="st-Spacer st-Spacer--gutter"
style="border: 0; margin:0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;"
width="64">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
<td style="border: 0; margin: 0; padding: 0;">
{{#ifCond badgeType "critical"}}
<span class="st-Badge st-Badge--critical" style="display: inline-block; padding: 6px 14px; border-radius: 9999px; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; background-color: #fef2f2; color: #dc2626; border: 1px solid #fecaca; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif;">
{{badgeText}}
</span>
{{/ifCond}}
{{#ifCond badgeType "warning"}}
<span class="st-Badge st-Badge--warning" style="display: inline-block; padding: 6px 14px; border-radius: 9999px; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; background-color: #fffbeb; color: #d97706; border: 1px solid #fde68a; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif;">
{{badgeText}}
</span>
{{/ifCond}}
{{#ifCond badgeType "success"}}
<span class="st-Badge st-Badge--success" style="display: inline-block; padding: 6px 14px; border-radius: 9999px; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; background-color: #f0fdf4; color: #16a34a; border: 1px solid #bbf7d0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif;">
{{badgeText}}
</span>
{{/ifCond}}
{{#ifCond badgeType "info"}}
<span class="st-Badge st-Badge--info" style="display: inline-block; padding: 6px 14px; border-radius: 9999px; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; background-color: #eff6ff; color: #2563eb; border: 1px solid #bfdbfe; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif;">
{{badgeText}}
</span>
{{/ifCond}}
{{#ifCond badgeType "neutral"}}
<span class="st-Badge st-Badge--neutral" style="display: inline-block; padding: 6px 14px; border-radius: 9999px; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; background-color: #f3f4f6; color: #4b5563; border: 1px solid #d1d5db; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif;">
{{badgeText}}
</span>
{{/ifCond}}
</td>
<td class="st-Spacer st-Spacer--gutter"
style="border: 0; margin:0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;"
width="64">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
</tr>
<tr>
<td class="st-Spacer st-Spacer--stacked" colspan="3" height="12"
style="border: 0; margin: 0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
</tr>
</tbody>
</table>
<!-- /Status Badge -->

View File

@@ -0,0 +1,31 @@
<!-- Status Transition Block - Inline version for DetailBox -->
<div class="st-DetailCard-field" style="padding: 16px 0; border-bottom: 1px solid #e2e8f0;">
<p class="st-DetailCard-label" style="Margin:0 0 12px 0;font-size:11px;font-family:'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;line-height:16px;color:#64748b;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;">STATUS CHANGE</p>
<table cellpadding="0" cellspacing="0" border="0" style="border-collapse: collapse;">
<tbody>
<tr>
{{#if previousStatus}}
<!-- Previous Status Badge -->
<td align="center" valign="middle" style="padding: 0;">
<span style="display: inline-block; white-space: nowrap; padding: 6px 14px; border-radius: 6px; font-size: 13px; font-weight: 600; background-color: transparent; color: {{previousStatusColor}}; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif; line-height: 16px; vertical-align: middle;">
<span style="width: 10px; height: 10px; border-radius: 9999px; background-color: {{previousStatusColor}}; display: inline-block; margin-right: 8px; vertical-align: middle;"></span>
<span style="display: inline-block; line-height: 16px; vertical-align: middle;">{{previousStatus}}</span>
</span>
</td>
<!-- Arrow -->
<td align="center" valign="middle" style="padding: 0 12px;">
<span style="font-size: 20px; color: #94a3b8; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif;">→</span>
</td>
{{/if}}
<!-- Current Status Badge -->
<td align="center" valign="middle" style="padding: 0;">
<span style="display: inline-block; white-space: nowrap; padding: 6px 14px; border-radius: 6px; font-size: 13px; font-weight: 600; background-color: transparent; color: {{currentStatusColor}}; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif; line-height: 16px; vertical-align: middle;">
<span style="width: 10px; height: 10px; border-radius: 9999px; background-color: {{currentStatusColor}}; display: inline-block; margin-right: 8px; vertical-align: middle;"></span>
<span style="display: inline-block; line-height: 16px; vertical-align: middle;">{{currentStatus}}</span>
</span>
</td>
</tr>
</tbody>
</table>
</div>
<!-- /Status Transition Block -->

View File

@@ -52,7 +52,7 @@
**/
span.st-Delink a {
color: #000000 !important;
color: #1a1a2e !important;
text-decoration: none !important;
}
@@ -66,7 +66,7 @@
/** Modifier: title */
span.st-Delink.st-Delink--title a {
color: #000000 !important;
color: #1a1a2e !important;
text-decoration: none !important;
}
@@ -74,14 +74,111 @@
/** Modifier: footer */
span.st-Delink.st-Delink--footer a {
color: #8898aa !important;
color: #6b7280 !important;
text-decoration: none !important;
}
/** */
.ii a[href] {
color: #000000;
color: #1a1a2e;
}
/**
* # Links
* - Styled links with modern colors
**/
a.st-Link {
color: #1a1a2e !important;
text-decoration: none !important;
transition: color 0.2s ease !important;
}
a.st-Link:hover {
color: #000000 !important;
text-decoration: underline !important;
}
/**
* # Status Badges
* - Colored badges for incident/alert states
**/
.st-Badge {
display: inline-block !important;
padding: 4px 12px !important;
border-radius: 9999px !important;
font-size: 12px !important;
font-weight: 600 !important;
text-transform: uppercase !important;
letter-spacing: 0.5px !important;
}
.st-Badge--critical {
background-color: #fef2f2 !important;
color: #dc2626 !important;
border: 1px solid #fecaca !important;
}
.st-Badge--warning {
background-color: #fffbeb !important;
color: #d97706 !important;
border: 1px solid #fde68a !important;
}
.st-Badge--success {
background-color: #f0fdf4 !important;
color: #16a34a !important;
border: 1px solid #bbf7d0 !important;
}
.st-Badge--info {
background-color: #f3f4f6 !important;
color: #1a1a2e !important;
border: 1px solid #d1d5db !important;
}
.st-Badge--neutral {
background-color: #f3f4f6 !important;
color: #4b5563 !important;
border: 1px solid #d1d5db !important;
}
/**
* # Detail Cards
* - Modern card styling for detail boxes
**/
.st-DetailCard {
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%) !important;
border: 1px solid #e2e8f0 !important;
border-radius: 12px !important;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05) !important;
}
.st-DetailCard-field {
padding: 12px 0 !important;
border-bottom: 1px solid #e2e8f0 !important;
}
.st-DetailCard-field:last-child {
border-bottom: none !important;
}
.st-DetailCard-label {
color: #64748b !important;
font-size: 12px !important;
font-weight: 600 !important;
text-transform: uppercase !important;
letter-spacing: 0.5px !important;
margin-bottom: 4px !important;
}
.st-DetailCard-value {
color: #1e293b !important;
font-size: 15px !important;
font-weight: 500 !important;
}
/**
@@ -107,7 +204,7 @@
/** Modifier: gutter */
body[override] td.st-Spacer.st-Spacer--gutter {
width: 32px !important;
width: 24px !important;
}
/** */
@@ -134,8 +231,8 @@
body[override] td.st-Font.st-Font--title,
body[override] td.st-Font.st-Font--title span,
body[override] td.st-Font.st-Font--title a {
font-size: 28px !important;
line-height: 36px !important;
font-size: 26px !important;
line-height: 34px !important;
}
/** */
@@ -144,8 +241,8 @@
body[override] td.st-Font.st-Font--header,
body[override] td.st-Font.st-Font--header span,
body[override] td.st-Font.st-Font--header a {
font-size: 24px !important;
line-height: 32px !important;
font-size: 22px !important;
line-height: 30px !important;
}
/** */
@@ -154,8 +251,8 @@
body[override] td.st-Font.st-Font--body,
body[override] td.st-Font.st-Font--body span,
body[override] td.st-Font.st-Font--body a {
font-size: 18px !important;
line-height: 28px !important;
font-size: 16px !important;
line-height: 26px !important;
}
/** */
@@ -164,8 +261,8 @@
body[override] td.st-Font.st-Font--caption,
body[override] td.st-Font.st-Font--caption span,
body[override] td.st-Font.st-Font--caption a {
font-size: 14px !important;
line-height: 20px !important;
font-size: 13px !important;
line-height: 19px !important;
}
/** */
@@ -185,7 +282,7 @@
body[override] table.st-Divider td.st-Spacer.st-Spacer--gutter,
body[override] tr.st-Divider td.st-Spacer.st-Spacer--gutter {
background-color: #000000;
background-color: #1a1a2e;
}
/**
@@ -212,10 +309,23 @@
body[override] table.st-Button td.st-Button-area,
body[override] table.st-Button td.st-Button-area a.st-Button-link,
body[override] table.st-Button td.st-Button-area span.st-Button-internal {
height: 44px !important;
line-height: 44px !important;
font-size: 18px !important;
height: 48px !important;
line-height: 48px !important;
font-size: 16px !important;
vertical-align: middle !important;
}
/**
* # Detail Cards - Mobile
**/
body[override] .st-DetailCard {
border-radius: 8px !important;
}
body[override] .st-Badge {
font-size: 11px !important;
padding: 3px 10px !important;
}
}
</style>

View File

@@ -1,3 +1,4 @@
{{> InfoBlock info="If you need help. Please do not hesitate to contact OneUptime support team at support@oneuptime.com."}}
<!-- Support Block -->
{{> InfoBlock info="Need help? Contact our support team at <a href='mailto:support@oneuptime.com' style='color: #1a1a2e; text-decoration: none; font-weight: 500;'>support@oneuptime.com</a>"}}
{{> Thanks }}
<!-- /Support Block -->

View File

@@ -1,2 +1,3 @@
{{> InfoBlock info="Thanks, have a great day."}}
{{> InfoBlock info="OneUptime Team."}}
<!-- Thanks Block -->
{{> InfoBlock info="<span style='color: #374151;'>Best regards,</span><br><strong style='color: #1a1a2e;'>The OneUptime Team</strong>"}}
<!-- /Thanks Block -->

View File

@@ -1,4 +1,4 @@
<!-- Title Block -->
<table class="st-Copy st-Width st-Width--mobile" border="0" cellpadding="0" cellspacing="0"
width="600" style="min-width: 600px;">
<tbody>
@@ -9,8 +9,8 @@ width="600" style="min-width: 600px;">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
<td class="st-Font st-Font--body"
style="border: 0; margin: 0; padding: 0; color: #000000 !important; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif; font-size: 16px; line-height: 24px;">
<strong>{{{title}}}</strong>
style="border: 0; margin: 0; padding: 0; color: #1a1a2e !important; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif; font-size: 15px; line-height: 26px;">
<strong style="color: #1a1a2e; font-weight: 600;">{{{title}}}</strong>
</td>
<td class="st-Spacer st-Spacer--gutter"
style="border: 0; margin:0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;"
@@ -19,11 +19,11 @@ width="600" style="min-width: 600px;">
</td>
</tr>
<tr>
<td class="st-Spacer st-Spacer--stacked" colspan="3" height="12"
<td class="st-Spacer st-Spacer--stacked" colspan="3" height="14"
style="border: 0; margin: 0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
</tr>
</tbody>
</table>
<!-- /Title Block -->

View File

@@ -1,3 +1,4 @@
<!-- Unsubscribe Block -->
<table class="st-Copy st-Width st-Width--mobile" border="0" cellpadding="0" cellspacing="0"
width="600" style="min-width: 600px;">
<tbody>
@@ -7,10 +8,10 @@
width="64">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
<td class="st-Font st-Font--body"
style="border: 0; margin: 0; padding: 0; color: #000000 !important; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif; font-size: 12px; line-height: 20px;">
<td class="st-Font st-Font--caption"
style="border: 0; margin: 0; padding: 0; color: #6b7280 !important; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, sans-serif; font-size: 13px; line-height: 20px;">
If you wish to change preferences or unsubscribe, please click <a href="{{unsubscribeUrl}}">here.</a>
<span style="color: #6b7280;">Want to change your notification preferences?</span> <a href="{{unsubscribeUrl}}" style="color: #1a1a2e; text-decoration: none; font-weight: 500;">Manage subscription</a>
</td>
<td class="st-Spacer st-Spacer--gutter"
@@ -20,10 +21,11 @@
</td>
</tr>
<tr>
<td class="st-Spacer st-Spacer--stacked" colspan="3" height="12"
<td class="st-Spacer st-Spacer--stacked" colspan="3" height="14"
style="border: 0; margin: 0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;">
<div class="st-Spacer st-Spacer--filler"></div>
</td>
</tr>
</tbody>
</table>
</table>
<!-- /Unsubscribe Block -->

View File

@@ -1,3 +1,12 @@
<div style="margin-top: 10px; margin-bottom: 10px">
</div>
<!-- Vertical Spacer -->
<table class="st-Copy st-Width st-Width--mobile" border="0" cellpadding="0" cellspacing="0"
width="600" style="min-width: 600px;">
<tbody>
<tr>
<td height="20" style="border: 0; margin: 0; padding: 0; font-size: 1px; line-height: 1px; mso-line-height-rule: exactly;">
<div class="st-Spacer st-Spacer--filler">&nbsp;</div>
</td>
</tr>
</tbody>
</table>
<!-- /Vertical Spacer -->

View File

@@ -10,8 +10,7 @@
{{> DetailBoxField title="Probe Name:" text=probeName }}
{{> DetailBoxField title="Probe Description:" text=probeDescription }}
{{> DetailBoxField title="Probe Status:" text=probeStatus }}
{{> DetailBoxField title="Status Since:" text="" }}
{{> DetailBoxField title="" text=lastAlive }}
{{> DetailBoxField title="Status Since:" text=lastAlive }}
{{> DetailBoxField title="Project Name: " text=projectName }}
{{> DetailBoxEnd this }}

View File

@@ -11,8 +11,7 @@
{{> DetailBoxStart this }}
{{> DetailBoxField title="Scheduled Maintenance Title:" text=scheduledMaintenanceTitle }}
{{> DetailBoxField title="Current State: " text=currentState }}
{{> DetailBoxField title="Description: " text="" }}
{{> DetailBoxField title="" text=scheduledMaintenanceDescription }}
{{> DetailBoxField title="Description: " text=scheduledMaintenanceDescription }}
{{> DetailBoxEnd this }}

View File

@@ -12,11 +12,10 @@
{{> DetailBoxField title="Scheduled Maintenance Title:" text=scheduledMaintenanceTitle }}
{{> DetailBoxField title="Current State: " text=currentState }}
{{#if isPrivateNote}}
{{> DetailBoxField title="Private Note: " text="" }}
{{> DetailBoxField title="Private Note: " text=note }}
{{else}}
{{> DetailBoxField title="Public Note: " text="" }}
{{> DetailBoxField title="Public Note: " text=note }}
{{/if}}
{{> DetailBoxField title="" text=note }}
{{> DetailBoxEnd this }}

View File

@@ -11,8 +11,7 @@
{{> DetailBoxStart this }}
{{> DetailBoxField title="Scheduled Maintenance Title:" text=scheduledMaintenanceTitle }}
{{> DetailBoxField title="Current State: " text=currentState }}
{{> DetailBoxField title="Description: " text="" }}
{{> DetailBoxField title="" text=scheduledMaintenanceDescription }}
{{> DetailBoxField title="Description: " text=scheduledMaintenanceDescription }}
{{> DetailBoxEnd this }}

View File

@@ -4,17 +4,15 @@
{{> Logo this}}
{{> EmailTitle title=(concat "Scheduled Maintenance: " scheduledMaintenanceTitle) }}
{{> InfoBlock info=(concat "Scheduled Maintenance status changed to - " currentState)}}
{{> InfoBlock info="Scheduled Maintenance state has changed"}}
{{> InfoBlock info="Here are the details: "}}
{{> DetailBoxStart this }}
{{> StateTransition this}}
{{> DetailBoxField title="Scheduled Maintenance Title:" text=scheduledMaintenanceTitle }}
{{> DetailBoxField title="New State: " text=currentState }}
{{> DetailBoxField title="State changed at: " text="" }}
{{> DetailBoxField title="" text=stateChangedAt }}
{{> DetailBoxField title="Description: " text="" }}
{{> DetailBoxField title="" text=scheduledMaintenanceDescription }}
{{> DetailBoxField title="State changed at:" text=stateChangedAt }}
{{> DetailBoxField title="Description:" text=scheduledMaintenanceDescription }}
{{> DetailBoxEnd this }}

View File

@@ -1,14 +1,20 @@
{{> Start this}}
{{> Logo this}}
{{> EmailTitle title="Welcome to OneUptime. Please verify your email." }}
{{> InfoBlock info="Thank you for signing up. Next step would be to verify your email account. Can you please click on 'Verify Email' button which will help us to get your email verified. "}}
{{> ButtonBlock buttonUrl=tokenVerifyUrl buttonText="Verify Email"}}
{{> EmailTitle title="🎉 Welcome to OneUptime!" }}
{{> InfoBlock info="You can also copy and paste this link:"}}
{{> InfoBlock info="Thank you for signing up! To get started, please verify your email address by clicking the button below."}}
{{> ButtonBlock buttonUrl=tokenVerifyUrl buttonText="Verify Email Address"}}
{{> VerticalSpace this}}
{{> InfoBlock info="Or copy and paste this link into your browser:"}}
{{> InfoBlock info=tokenVerifyUrl}}
{{> VerticalSpace this}}
{{> SupportBlock this }}
{{> Footer this}}

View File

@@ -10,8 +10,7 @@
{{> DetailBoxStart this }}
{{> DetailBoxField title="Status Page Name:" text=statusPageName }}
{{> DetailBoxField title="Description: " text="" }}
{{> DetailBoxField title="" text=statusPageDescription }}
{{> DetailBoxField title="Description: " text=statusPageDescription }}
{{> DetailBoxEnd this }}

View File

@@ -6,8 +6,7 @@
{{> InfoBlock info=(concat "A new announcement has been created for : " statusPageName) }}
{{> DetailBoxStart this }}
{{> DetailBoxField title=announcementTitle text="" }}
{{> DetailBoxField title="" text=announcementDescription }}
{{> DetailBoxField title=announcementTitle text=announcementDescription }}
{{> DetailBoxEnd this }}

View File

@@ -10,8 +10,7 @@
{{> DetailBoxStart this }}
{{> DetailBoxField title="Status Page Name:" text=statusPageName }}
{{> DetailBoxField title="Description: " text="" }}
{{> DetailBoxField title="" text=statusPageDescription }}
{{> DetailBoxField title="Description: " text=statusPageDescription }}
{{> DetailBoxEnd this }}

View File

@@ -17,19 +17,19 @@
{{#ifCond hasResources "true"}}
{{> InfoBlock info="Here is the more detailed report:"}}
<table style="min-width: 500px;margin: 10px 50px; fotn-size: 15px; margin-top: 10px; margin-bottom: 20px">
<tr style="background-color: #424761; color: white; padding: 1px">
<th>Resource Name</th>
<th>Uptime %</th>
<th>Downtime</th>
<th>Incidents</th>
<table style="width: calc(100% - 64px); margin: 18px 32px; border-collapse: collapse; font-size: 14px; font-family: 'Helvetica Neue', Arial, sans-serif; color: #1f2733;">
<tr>
<th style="background: #353b4d; color: #ffffff; padding: 13px 12px; text-align: left; font-weight: 700; letter-spacing: 0.3px; border-bottom: 1px solid #2b3040; border-top-left-radius: 10px;">Resource Name</th>
<th style="background: #353b4d; color: #ffffff; padding: 13px 12px; text-align: right; font-weight: 700; letter-spacing: 0.3px; border-bottom: 1px solid #2b3040;">Uptime %</th>
<th style="background: #353b4d; color: #ffffff; padding: 13px 12px; text-align: right; font-weight: 700; letter-spacing: 0.3px; border-bottom: 1px solid #2b3040;">Downtime</th>
<th style="background: #353b4d; color: #ffffff; padding: 13px 12px; text-align: right; font-weight: 700; letter-spacing: 0.3px; border-bottom: 1px solid #2b3040; border-top-right-radius: 10px;">Incidents</th>
</tr>
{{#each report.resources}}
<tr style="padding: 1px;">
<td>{{this.resourceName}}</td>
<td style="text-align: right">{{this.uptimePercentAsString}}</td>
<td style="text-align: right">{{this.downtimeInHoursAndMinutes}}</td>
<td style="text-align: right">{{this.totalIncidentCount}}</td>
<tr style="background-color: {{#if @odd}}#f0f3f9{{else}}#ffffff{{/if}};">
<td style="padding: 12px 12px; border-bottom: 1px solid #e5e8f0;">{{this.resourceName}}</td>
<td style="padding: 12px 12px; text-align: right; border-bottom: 1px solid #e5e8f0; font-weight: 600;">{{this.uptimePercentAsString}}</td>
<td style="padding: 12px 12px; text-align: right; border-bottom: 1px solid #e5e8f0;">{{this.downtimeInHoursAndMinutes}}</td>
<td style="padding: 12px 12px; text-align: right; border-bottom: 1px solid #e5e8f0; font-weight: 600;">{{this.totalIncidentCount}}</td>
</tr>
{{/each}}
</table>

View File

@@ -1,26 +1,32 @@
{{> Start this}}
{{> CustomLogo this}}
{{> EmailTitle title=(concat "Announcement: " announcementTitle) }}
{{> InfoBlock info=(concat "A new announcement has been created for : " statusPageName) }}
{{> EmailTitle title=(concat "📢 Announcement: " announcementTitle) }}
{{> InfoBlock info=(concat "A new announcement has been posted for " statusPageName ".") }}
{{> DetailBoxStart this }}
{{> DetailBoxField title=announcementTitle text="" }}
{{> DetailBoxField title="" text=announcementDescription }}
{{> DetailBoxField title="Announcement" text=announcementTitle }}
{{#if announcementDescription}}
{{> DetailBoxField title="Details" text=announcementDescription }}
{{/if}}
{{> DetailBoxEnd this }}
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}
{{#if detailsUrl}}
{{> InfoBlock info=(concat "Find further information here: " detailsUrl)}}
{{> ButtonBlock buttonUrl=detailsUrl buttonText="View Announcement"}}
{{else}}
{{> InfoBlock info=(concat "Find further information here: " statusPageUrl)}}
{{> ButtonBlock buttonUrl=statusPageUrl buttonText="View Status Page"}}
{{/if}}
{{> VerticalSpace this}}
{{#if subscriberEmailNotificationFooterText}}
{{> InfoBlock info=subscriberEmailNotificationFooterText }}
{{/if}}
{{> UnsubscribeBlock this}}
{{> VerticalSpace this}}
{{> Footer this}}
{{> End this}}

View File

@@ -1,35 +1,34 @@
{{> Start this}}
{{> CustomLogo this}}
{{> EmailTitle title=(concat "Incident: " incidentTitle) }}
{{> InfoBlock info="A new incident has been created. Here are the details: "}}
{{> EmailTitle title=(concat "New Incident: " incidentTitle) }}
{{> InfoBlock info="A new incident has been reported that may affect the services you're subscribed to."}}
{{> DetailBoxStart this }}
{{> DetailBoxField title=incidentTitle text="" }}
{{> DetailBoxField title="Resources Affected: " text=resourcesAffected }}
{{> DetailBoxField title="Severity: " text=incidentSeverity }}
{{> DetailBoxField title="Description: " text="" }}
{{> DetailBoxField title="" text=incidentDescription }}
{{> DetailBoxField title="Incident" text=incidentTitle }}
{{> DetailBoxField title="Severity" text=incidentSeverity }}
{{> DetailBoxField title="Affected Resources" text=resourcesAffected }}
{{#if incidentDescription}}
{{> DetailBoxField title="Description" text=incidentDescription }}
{{/if}}
{{> DetailBoxEnd this }}
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}
{{#if detailsUrl}}
{{> InfoBlock info=(concat "Find further information here: " detailsUrl)}}
{{> ButtonBlock buttonUrl=detailsUrl buttonText="View Incident Details"}}
{{else}}
{{> InfoBlock info=(concat "Find further information here: " statusPageUrl)}}
{{> ButtonBlock buttonUrl=statusPageUrl buttonText="View Status Page"}}
{{/if}}
{{> UnsubscribeBlock this}}
{{> VerticalSpace this}}
{{#if subscriberEmailNotificationFooterText}}
{{> InfoBlock info=subscriberEmailNotificationFooterText }}
{{/if}}
{{> UnsubscribeBlock this}}
{{> Footer this}}
{{> End this}}

View File

@@ -6,11 +6,10 @@
{{> InfoBlock info="A new note has been added to the incident. Here are the details: "}}
{{> DetailBoxStart this }}
{{> DetailBoxField title=incidentTitle text="" }}
{{> DetailBoxField title="Incident Title: " text=incidentTitle }}
{{> DetailBoxField title="Resources Affected: " text=resourcesAffected }}
{{> DetailBoxField title="Severity: " text=incidentSeverity }}
{{> DetailBoxField title="Note: " text="" }}
{{> DetailBoxField title="" text=note }}
{{> DetailBoxField title="Note: " text=note }}
{{> DetailBoxEnd this }}

View File

@@ -1,35 +1,34 @@
{{> Start this}}
{{> CustomLogo this}}
{{> EmailTitle title=(concat "Postmortem Published: " incidentTitle) }}
{{> InfoBlock info="A postmortem has been published for an incident. Here are the details: "}}
{{> InfoBlock info="A postmortem report has been published for an incident that affected services you're subscribed to."}}
{{> DetailBoxStart this }}
{{> DetailBoxField title=incidentTitle text="" }}
{{> DetailBoxField title="Resources Affected: " text=resourcesAffected }}
{{> DetailBoxField title="Severity: " text=incidentSeverity }}
{{> DetailBoxField title="Postmortem: " text="" }}
{{> DetailBoxField title="" text=postmortemNote }}
{{> DetailBoxField title="Incident" text=incidentTitle }}
{{> DetailBoxField title="Severity" text=incidentSeverity }}
{{> DetailBoxField title="Affected Resources" text=resourcesAffected }}
{{#if postmortemNote}}
{{> DetailBoxField title="Postmortem Summary" text=postmortemNote }}
{{/if}}
{{> DetailBoxEnd this }}
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}
{{#if detailsUrl}}
{{> InfoBlock info=(concat "Find further information here: " detailsUrl)}}
{{> ButtonBlock buttonUrl=detailsUrl buttonText="Read Full Postmortem"}}
{{else}}
{{> InfoBlock info=(concat "Find further information here: " statusPageUrl)}}
{{> ButtonBlock buttonUrl=statusPageUrl buttonText="View Status Page"}}
{{/if}}
{{> UnsubscribeBlock this}}
{{> VerticalSpace this}}
{{#if subscriberEmailNotificationFooterText}}
{{> InfoBlock info=subscriberEmailNotificationFooterText }}
{{/if}}
{{> UnsubscribeBlock this}}
{{> Footer this}}
{{> End this}}

View File

@@ -1,26 +1,32 @@
{{> Start this}}
{{> CustomLogo this}}
{{> EmailTitle title=emailTitle }}
{{> InfoBlock info="Incident state has changed. Here are the details: "}}
{{> InfoBlock info="The status of an incident affecting services you're subscribed to has been updated."}}
{{> DetailBoxStart this }}
{{> DetailBoxField title=incidentTitle text="" }}
{{> DetailBoxField title="Resources Affected: " text=resourcesAffected }}
{{> DetailBoxField title="Severity: " text=incidentSeverity }}
{{> DetailBoxField title="State: " text=incidentState }}
{{> DetailBoxField title="Incident" text=incidentTitle }}
{{> DetailBoxField title="Current State" text=incidentState }}
{{> DetailBoxField title="Severity" text=incidentSeverity }}
{{> DetailBoxField title="Affected Resources" text=resourcesAffected }}
{{> DetailBoxEnd this }}
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}
{{#if detailsUrl}}
{{> InfoBlock info=(concat "Find further information here: " detailsUrl)}}
{{> ButtonBlock buttonUrl=detailsUrl buttonText="View Incident Details"}}
{{else}}
{{> InfoBlock info=(concat "Find further information here: " statusPageUrl)}}
{{> ButtonBlock buttonUrl=statusPageUrl buttonText="View Status Page"}}
{{/if}}
{{> VerticalSpace this}}
{{#if subscriberEmailNotificationFooterText}}
{{> InfoBlock info=subscriberEmailNotificationFooterText }}
{{/if}}
{{> UnsubscribeBlock this}}
{{> VerticalSpace this}}
{{> Footer this}}
{{> End this}}

View File

@@ -1,36 +1,37 @@
{{> Start this}}
{{> CustomLogo this}}
{{> EmailTitle title=(concat "Scheduled Maintenance Event: " eventTitle) }}
{{> InfoBlock info="Here are more details for this scheduled event: "}}
{{> EmailTitle title=(concat "Scheduled Maintenance: " eventTitle) }}
{{> InfoBlock info="A scheduled maintenance event has been announced for services you're subscribed to."}}
{{> DetailBoxStart this }}
{{> DetailBoxField title=eventTitle text="" }}
{{> DetailBoxField title="Event Status: " text="Scheduled" }}
{{> DetailBoxField title="Maintenance Event" text=eventTitle }}
{{> DetailBoxField title="Status" text="Scheduled" }}
{{> DetailBoxField title="Scheduled Time" text=scheduledAt }}
{{#if resourcesAffected}}
{{> DetailBoxField title="Resources Affected: " text=resourcesAffected }}
{{> DetailBoxField title="Affected Resources" text=resourcesAffected }}
{{/if}}
{{#if eventDescription}}
{{> DetailBoxField title="Description" text=eventDescription }}
{{/if}}
{{> DetailBoxField title="Event Scheduled At: " text="" }}
{{> DetailBoxField title="" text=scheduledAt }}
{{> DetailBoxField title="Event Description: " text="" }}
{{> DetailBoxField title="" text=eventDescription }}
{{> DetailBoxEnd this }}
{{> InfoBlock info=(concat subscriberEmailNotificationFooterText "") }}
{{#if detailsUrl}}
{{> InfoBlock info=(concat "Find further information here: " detailsUrl)}}
{{> ButtonBlock buttonUrl=detailsUrl buttonText="View Maintenance Details"}}
{{else}}
{{> InfoBlock info=(concat "Find further information here: " statusPageUrl)}}
{{> ButtonBlock buttonUrl=statusPageUrl buttonText="View Status Page"}}
{{/if}}
{{> VerticalSpace this}}
{{#if subscriberEmailNotificationFooterText}}
{{> InfoBlock info=subscriberEmailNotificationFooterText }}
{{/if}}
{{> UnsubscribeBlock this}}
{{> VerticalSpace this}}
{{> Footer this}}
{{> End this}}

View File

@@ -9,13 +9,12 @@
{{> DetailBoxStart this }}
{{> DetailBoxField title=eventTitle text="" }}
{{> DetailBoxField title="Event Title: " text=eventTitle }}
{{> DetailBoxField title="Event Description: " text=eventDescription }}
{{#if resourcesAffected}}
{{> DetailBoxField title="Resources Affected: " text=resourcesAffected }}
{{/if}}
{{> DetailBoxField title="Note: " text="" }}
{{> DetailBoxField title="" text=note }}
{{> DetailBoxField title="Note: " text=note }}
{{> DetailBoxEnd this }}

View File

@@ -10,7 +10,7 @@
{{> DetailBoxStart this }}
{{> DetailBoxField title=eventTitle text="" }}
{{> DetailBoxField title="Event Title: " text=eventTitle }}
{{> DetailBoxField title="Event State: " text=eventState }}
{{#if resourcesAffected}}
{{> DetailBoxField title="Resources Affected: " text=resourcesAffected }}

25
App/package-lock.json generated
View File

@@ -1788,7 +1788,8 @@
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
"license": "BSD-3-Clause"
},
"node_modules/buffer-from": {
"version": "1.1.2",
@@ -2126,6 +2127,7 @@
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
}
@@ -3596,21 +3598,23 @@
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"node_modules/jwa": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
"integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz",
"integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==",
"license": "MIT",
"dependencies": {
"buffer-equal-constant-time": "1.0.1",
"buffer-equal-constant-time": "^1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jws": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz",
"integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==",
"license": "MIT",
"dependencies": {
"jwa": "^1.4.1",
"jwa": "^1.4.2",
"safe-buffer": "^5.0.1"
}
},
@@ -4329,7 +4333,8 @@
"type": "consulting",
"url": "https://feross.org/support"
}
]
],
"license": "MIT"
},
"node_modules/sax": {
"version": "1.3.0",

View File

@@ -0,0 +1,17 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class MigrationName1764850876394 implements MigrationInterface {
public name = "MigrationName1764850876394";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "Incident" ADD "postmortemPostedAt" TIMESTAMP WITH TIME ZONE`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "Incident" DROP COLUMN "postmortemPostedAt"`,
);
}
}

View File

@@ -190,6 +190,7 @@ import { MigrationName1764324618043 } from "./1764324618043-MigrationName";
import { MigrationName1764762146063 } from "./1764762146063-MigrationName";
import { MigrationName1764767371788 } from "./1764767371788-MigrationName";
import { MigrationName1764789433216 } from "./1764789433216-MigrationName";
import { MigrationName1764850876394 } from "./1764850876394-MigrationName";
export default [
InitialMigration,
@@ -384,4 +385,5 @@ export default [
MigrationName1764762146063,
MigrationName1764767371788,
MigrationName1764789433216,
MigrationName1764850876394,
];

View File

@@ -1,7 +1,7 @@
import { ClusterKey } from "../EnvironmentConfig";
import Dictionary from "../../Types/Dictionary";
import { JSONObject } from "../../Types/JSON";
import { Queue as BullQueue, Job, JobsOptions } from "bullmq";
import { Queue as BullQueue, Job, JobsOptions, RepeatableJob } from "bullmq";
import { ExpressAdapter } from "@bull-board/express";
import { createBullBoard } from "@bull-board/api";
import { BullMQAdapter } from "@bull-board/api/bullMQAdapter";
@@ -196,14 +196,29 @@ export default class Queue {
jobId: sanitizedJobId,
};
const queue: BullQueue = this.getQueue(queueName);
if (options && options.scheduleAt) {
optionsObject.repeat = {
pattern: options.scheduleAt,
// keep repeatable job keyed by jobId so multiple workers do not register duplicates
jobId: sanitizedJobId,
};
const repeatableJobs: RepeatableJob[] = await queue.getRepeatableJobs();
for (const repeatableJob of repeatableJobs) {
const isSameJob: boolean =
repeatableJob.name === jobName &&
repeatableJob.pattern === options.scheduleAt;
if (isSameJob) {
await queue.removeRepeatableByKey(repeatableJob.key);
}
}
}
const job: Job | undefined =
await this.getQueue(queueName).getJob(sanitizedJobId);
const job: Job | undefined = await queue.getJob(sanitizedJobId);
if (job) {
await job.remove();
@@ -211,9 +226,7 @@ export default class Queue {
if (options?.repeatableKey) {
// remove existing repeatable job
await this.getQueue(queueName).removeRepeatableByKey(
options?.repeatableKey,
);
await queue.removeRepeatableByKey(options?.repeatableKey);
}
// Store repeatable jobs for re-adding on reconnect
@@ -228,11 +241,7 @@ export default class Queue {
};
}
const jobAdded: Job = await this.getQueue(queueName).add(
jobName,
data,
optionsObject,
);
const jobAdded: Job = await queue.add(jobName, data, optionsObject);
return jobAdded;
}

View File

@@ -26,6 +26,8 @@ import { JSONObject } from "../../Types/JSON";
import MonitorType, {
MonitorTypeHelper,
} from "../../Types/Monitor/MonitorType";
import MonitorSteps from "../../Types/Monitor/MonitorSteps";
import MonitorStep from "../../Types/Monitor/MonitorStep";
import ObjectID from "../../Types/ObjectID";
import PositiveNumber from "../../Types/PositiveNumber";
import Typeof from "../../Types/Typeof";
@@ -71,11 +73,61 @@ import { createWhatsAppMessageFromTemplate } from "../Utils/WhatsAppTemplateUtil
import { WhatsAppMessagePayload } from "../../Types/WhatsApp/WhatsAppMessage";
import MetricService from "./MetricService";
export interface MonitorDestinationInfo {
monitorDestination: string;
requestType: string;
monitorType: string;
}
export class Service extends DatabaseService<Model> {
public constructor() {
super(Model);
}
public getMonitorDestinationInfo(monitor: Model): MonitorDestinationInfo {
let monitorDestination: string = "";
let requestType: string = "";
const monitorType: MonitorType | undefined = monitor.monitorType;
if (monitor.monitorSteps) {
const monitorSteps: MonitorSteps = monitor.monitorSteps;
const stepsArray: Array<MonitorStep> =
monitorSteps.data?.monitorStepsInstanceArray || [];
if (stepsArray.length > 0) {
const firstStep: MonitorStep | undefined = stepsArray[0];
// Get monitor destination
if (firstStep?.data?.monitorDestination) {
monitorDestination =
firstStep.data.monitorDestination.toString() || "";
}
// Get request type for API monitors
if (monitorType === MonitorType.API && firstStep?.data?.requestType) {
requestType = firstStep.data.requestType;
}
// For port monitors, append port to destination
if (
monitorType === MonitorType.Port &&
firstStep?.data?.monitorDestinationPort
) {
const port: string = firstStep.data.monitorDestinationPort.toString();
if (monitorDestination && port) {
monitorDestination = `${monitorDestination}:${port}`;
}
}
}
}
return {
monitorDestination,
requestType,
monitorType: monitorType || "",
};
}
public async refreshMonitorCurrentStatus(monitorId: ObjectID): Promise<void> {
const monitor: Model | null = await this.findOneById({
id: monitorId,
@@ -1135,6 +1187,8 @@ ${createdItem.description?.trim() || "No description provided."}
name: true,
},
description: true,
monitorType: true,
monitorSteps: true,
},
props: {
isRoot: true,
@@ -1172,6 +1226,10 @@ ${createdItem.description?.trim() || "No description provided."}
? "Disabled"
: "Enabled";
// Get monitor destination info using the helper function
const destinationInfo: MonitorDestinationInfo =
this.getMonitorDestinationInfo(monitor);
const vars: Dictionary<string> = {
title: title,
monitorName: monitor.name!,
@@ -1184,6 +1242,9 @@ ${createdItem.description?.trim() || "No description provided."}
monitorViewLink: (
await this.getMonitorLinkInDashboard(monitor.projectId!, monitor.id!)
).toString(),
monitorDestination: destinationInfo.monitorDestination,
requestType: destinationInfo.requestType,
monitorType: destinationInfo.monitorType,
};
if (doesResourceHasOwners === true) {
@@ -1259,6 +1320,8 @@ ${createdItem.description?.trim() || "No description provided."}
name: true,
},
description: true,
monitorType: true,
monitorSteps: true,
},
props: {
isRoot: true,
@@ -1292,6 +1355,10 @@ ${createdItem.description?.trim() || "No description provided."}
? "Disconnected"
: "Connected";
// Get monitor destination info using the helper function
const destinationInfo: MonitorDestinationInfo =
this.getMonitorDestinationInfo(monitor);
const vars: Dictionary<string> = {
title: `Probes for monitor ${monitor.name} is ${status}.`,
monitorName: monitor.name!,
@@ -1304,6 +1371,9 @@ ${createdItem.description?.trim() || "No description provided."}
monitorViewLink: (
await this.getMonitorLinkInDashboard(monitor.projectId!, monitor.id!)
).toString(),
monitorDestination: destinationInfo.monitorDestination,
requestType: destinationInfo.requestType,
monitorType: destinationInfo.monitorType,
};
if (doesResourceHasOwners === true) {

View File

@@ -26,6 +26,7 @@ import MonitorEvaluationSummary, {
} from "../../../Types/Monitor/MonitorEvaluationSummary";
import ProbeApiIngestResponse from "../../../Types/Probe/ProbeApiIngestResponse";
import ProbeMonitorResponse from "../../../Types/Probe/ProbeMonitorResponse";
import RequestFailedDetails from "../../../Types/Probe/RequestFailedDetails";
import IncomingMonitorRequest from "../../../Types/Monitor/IncomingMonitor/IncomingMonitorRequest";
import MonitorType from "../../../Types/Monitor/MonitorType";
import { CheckOn, CriteriaFilter } from "../../../Types/Monitor/CriteriaFilter";
@@ -472,6 +473,7 @@ ${contextBlock}
}): string | null {
const requestDetails: Array<string> = [];
const responseDetails: Array<string> = [];
const failureDetails: Array<string> = [];
const probeResponse: ProbeMonitorResponse | null =
MonitorCriteriaDataExtractor.getProbeMonitorResponse(input.dataToProcess);
@@ -526,6 +528,34 @@ ${contextBlock}
);
}
// Add Request Failed Details if available
if (probeResponse?.requestFailedDetails) {
const requestFailedDetails: RequestFailedDetails =
probeResponse.requestFailedDetails;
if (requestFailedDetails.failedPhase) {
failureDetails.push(
`- Failed Phase: ${requestFailedDetails.failedPhase}`,
);
}
if (requestFailedDetails.errorCode) {
failureDetails.push(`- Error Code: ${requestFailedDetails.errorCode}`);
}
if (requestFailedDetails.errorDescription) {
failureDetails.push(
`- Error Description: ${requestFailedDetails.errorDescription}`,
);
}
if (requestFailedDetails.rawErrorMessage) {
failureDetails.push(
`- Raw Error Message: ${requestFailedDetails.rawErrorMessage}`,
);
}
}
const sections: Array<string> = [];
if (requestDetails.length > 0) {
@@ -536,6 +566,12 @@ ${contextBlock}
sections.push(`\n\n**Response Snapshot**\n${responseDetails.join("\n")}`);
}
if (failureDetails.length > 0) {
sections.push(
`\n\n**Request Failed Details**\n${failureDetails.join("\n")}`,
);
}
if (!sections.length) {
return null;
}

View File

@@ -41,12 +41,22 @@ export default class PushNotificationUtil {
incidentTitle: string;
projectName: string;
newState: string;
previousState?: string;
incidentViewLink: string;
}): PushNotificationMessage {
const { incidentTitle, projectName, newState, incidentViewLink } = params;
const {
incidentTitle,
projectName,
newState,
previousState,
incidentViewLink,
} = params;
const stateChangeText: string = previousState
? `Incident state changed from ${previousState} to ${newState}`
: `Incident state changed to ${newState}`;
return PushNotificationUtil.applyDefaults({
title: `Incident Updated: ${incidentTitle}`,
body: `Incident state changed to ${newState} in ${projectName}. Click to view details.`,
body: `${stateChangeText} in ${projectName}. Click to view details.`,
clickAction: incidentViewLink,
url: incidentViewLink,
tag: "incident-state-changed",
@@ -56,6 +66,7 @@ export default class PushNotificationUtil {
incidentTitle: incidentTitle,
projectName: projectName,
newState: newState,
previousState: previousState,
url: incidentViewLink,
},
});
@@ -113,12 +124,22 @@ export default class PushNotificationUtil {
monitorName: string;
projectName: string;
newStatus: string;
previousStatus?: string;
monitorViewLink: string;
}): PushNotificationMessage {
const { monitorName, projectName, newStatus, monitorViewLink } = params;
const {
monitorName,
projectName,
newStatus,
previousStatus,
monitorViewLink,
} = params;
const statusChangeText: string = previousStatus
? `Monitor status changed from ${previousStatus} to ${newStatus}`
: `Monitor status changed to ${newStatus}`;
return PushNotificationUtil.applyDefaults({
title: `Monitor ${newStatus}: ${monitorName}`,
body: `Monitor status changed to ${newStatus} in ${projectName}. Click to view details.`,
body: `${statusChangeText} in ${projectName}. Click to view details.`,
clickAction: monitorViewLink,
url: monitorViewLink,
tag: "monitor-status-changed",
@@ -128,6 +149,7 @@ export default class PushNotificationUtil {
monitorName: monitorName,
projectName: projectName,
newStatus: newStatus,
previousStatus: previousStatus,
url: monitorViewLink,
},
});

View File

@@ -14,7 +14,7 @@ describe("Loader tests", () => {
loaderType={LoaderType.Bar}
/>,
);
const barLoader: HTMLElement = screen.getByRole("bar-loader");
const barLoader: HTMLElement = screen.getByTestId("bar-loader");
expect(barLoader).toBeInTheDocument();
});
test("it should render if beats loader show up", () => {
@@ -25,7 +25,7 @@ describe("Loader tests", () => {
loaderType={LoaderType.Beats}
/>,
);
const beatLoader: HTMLElement = screen.getByRole("beat-loader");
const beatLoader: HTMLElement = screen.getByTestId("beat-loader");
expect(beatLoader).toBeInTheDocument();
});
});

View File

@@ -1,4 +1,5 @@
import DashboardBaseComponent from "./DashboardBaseComponent";
import { DropdownOption } from "../../../UI/Components/Dropdown/Dropdown";
export enum ComponentInputType {
Text = "Text",
@@ -9,6 +10,7 @@ export enum ComponentInputType {
Decimal = "Decimal",
MetricsQueryConfig = "MetricsQueryConfig",
LongText = "Long Text",
Dropdown = "Dropdown",
}
export interface ComponentArgument<T extends DashboardBaseComponent> {
@@ -19,4 +21,5 @@ export interface ComponentArgument<T extends DashboardBaseComponent> {
id: keyof T["arguments"];
isAdvanced?: boolean | undefined;
placeholder?: string | undefined;
dropdownOptions?: Array<DropdownOption> | undefined;
}

View File

@@ -1,6 +1,7 @@
import MetricQueryConfigData from "../../Metrics/MetricQueryConfigData";
import ObjectID from "../../ObjectID";
import DashboardComponentType from "../DashboardComponentType";
import DashboardChartType from "../Chart/ChartType";
import BaseComponent from "./DashboardBaseComponent";
export default interface DashboardChartComponent extends BaseComponent {
@@ -12,5 +13,6 @@ export default interface DashboardChartComponent extends BaseComponent {
chartDescription?: string | undefined;
legendText?: string | undefined;
legendUnit?: string | undefined;
chartType?: DashboardChartType | undefined;
};
}

View File

@@ -2,6 +2,11 @@ import AggregatedModel from "../BaseDatabase/AggregatedModel";
import MetricAliasData from "./MetricAliasData";
import MetricQueryData from "./MetricQueryData";
export enum MetricChartType {
LINE = "line",
BAR = "bar",
}
export interface ChartSeries {
title: string;
}
@@ -10,4 +15,5 @@ export default interface MetricQueryConfigData {
metricAliasData?: MetricAliasData | undefined;
metricQueryData: MetricQueryData;
getSeries?: ((data: AggregatedModel) => ChartSeries) | undefined;
chartType?: MetricChartType | undefined;
}

View File

@@ -0,0 +1,43 @@
/**
* Represents a single hop in a network traceroute
*/
export interface TraceRouteHop {
hopNumber: number;
address: string | undefined; // IP address of the hop, undefined if the hop timed out
hostName: string | undefined; // Hostname of the hop if DNS lookup succeeded
roundTripTimeInMS: number | undefined; // RTT in milliseconds, undefined if timed out
isTimeout: boolean;
}
/**
* Represents the result of a traceroute operation
*/
export interface TraceRoute {
hops: Array<TraceRouteHop>;
destinationAddress: string;
destinationHostName: string | undefined;
isComplete: boolean; // Whether the traceroute reached the destination
totalHops: number;
failedHop: number | undefined; // The hop number where the route failed, if any
failureMessage: string | undefined; // Failure message at the failed hop
}
/**
* Represents DNS resolution information
*/
export interface DNSLookupResult {
hostName: string;
resolvedAddresses: Array<string>; // List of resolved IP addresses
resolvedInMS: number; // Time taken for DNS resolution
isSuccess: boolean;
errorMessage: string | undefined;
}
/**
* Complete network path diagnosis result combining DNS and traceroute
*/
export default interface NetworkPathTrace {
dnsLookup?: DNSLookupResult | undefined;
traceRoute?: TraceRoute | undefined;
timestamp: Date;
}

View File

@@ -9,6 +9,7 @@ import SyntheticMonitorResponse from "../Monitor/SyntheticMonitors/SyntheticMoni
import MonitorEvaluationSummary from "../Monitor/MonitorEvaluationSummary";
import ObjectID from "../ObjectID";
import Port from "../Port";
import RequestFailedDetails from "./RequestFailedDetails";
export default interface ProbeMonitorResponse {
projectId: ObjectID;
@@ -23,6 +24,7 @@ export default interface ProbeMonitorResponse {
monitorId: ObjectID;
probeId: ObjectID;
failureCause: string;
requestFailedDetails?: RequestFailedDetails | undefined;
sslResponse?: SslMonitorResponse | undefined;
syntheticMonitorResponse?: Array<SyntheticMonitorResponse> | undefined;
customCodeMonitorResponse?: CustomCodeMonitorResponse | undefined;

View File

@@ -0,0 +1,36 @@
/*
* This type holds detailed error context for when a request fails
* This helps users understand exactly where and why a request failed
*/
export enum RequestFailedPhase {
// DNS resolution failed - could not resolve hostname to IP
DNSResolution = "DNS Resolution",
// TCP connection failed - could not establish connection to server
TCPConnection = "TCP Connection",
// TLS/SSL handshake failed
TLSHandshake = "TLS Handshake",
// Request was sent but timed out waiting for response
RequestTimeout = "Request Timeout",
// Server responded with an error status code
ServerResponse = "Server Response",
// Request was aborted
RequestAborted = "Request Aborted",
// Network error - general network failure
NetworkError = "Network Error",
// Certificate error
CertificateError = "Certificate Error",
// Unknown error
Unknown = "Unknown",
}
export default interface RequestFailedDetails {
// The phase at which the request failed
failedPhase: RequestFailedPhase;
// The error code from axios/node (e.g., ECONNREFUSED, ETIMEDOUT, ENOTFOUND, etc.)
errorCode?: string | undefined;
// A detailed, user-friendly explanation of what went wrong
errorDescription: string;
// The raw error message (for debugging purposes)
rawErrorMessage?: string | undefined;
}

View File

@@ -0,0 +1,76 @@
import { BarChart } from "../ChartLibrary/BarChart/BarChart";
import React, { FunctionComponent, ReactElement, useEffect } from "react";
import SeriesPoint from "../Types/SeriesPoints";
import { XAxis } from "../Types/XAxis/XAxis";
import YAxis from "../Types/YAxis/YAxis";
import ChartDataPoint from "../ChartLibrary/Types/ChartDataPoint";
import DataPointUtil from "../Utils/DataPoint";
export interface ComponentProps {
data: Array<SeriesPoint>;
xAxis: XAxis;
yAxis: YAxis;
sync: boolean;
heightInPx?: number | undefined;
}
export interface BarInternalProps extends ComponentProps {
syncid: string;
}
const BarChartElement: FunctionComponent<BarInternalProps> = (
props: BarInternalProps,
): ReactElement => {
const [records, setRecords] = React.useState<Array<ChartDataPoint>>([]);
const categories: Array<string> = props.data.map((item: SeriesPoint) => {
return item.seriesName;
});
useEffect(() => {
if (!props.data || props.data.length === 0) {
setRecords([]);
}
const records: Array<ChartDataPoint> = DataPointUtil.getChartDataPoints({
seriesPoints: props.data,
xAxis: props.xAxis,
yAxis: props.yAxis,
});
setRecords(records);
}, [props.data]);
const className: string = props.heightInPx ? `` : "h-80";
const style: React.CSSProperties = props.heightInPx
? { height: `${props.heightInPx}px` }
: {};
return (
<BarChart
className={className}
style={style}
data={records}
tickGap={1}
index={"Time"}
categories={categories}
colors={[
"indigo",
"rose",
"emerald",
"amber",
"cyan",
"gray",
"pink",
"lime",
"fuchsia",
]}
valueFormatter={props.yAxis.options.formatter || undefined}
showTooltip={true}
yAxisWidth={60}
syncid={props.sync ? props.syncid : undefined}
/>
);
};
export default BarChartElement;

View File

@@ -1,5 +1,8 @@
import Text from "../../../../Types/Text";
import LineChart, { ComponentProps as LineChartProps } from "../Line/LineChart";
import BarChartElement, {
ComponentProps as BarChartProps,
} from "../Bar/BarChart";
import React, { FunctionComponent, ReactElement } from "react";
export enum ChartType {
@@ -13,7 +16,7 @@ export interface Chart {
title: string;
description?: string | undefined;
type: ChartType;
props: LineChartProps;
props: LineChartProps | BarChartProps;
}
export interface ComponentProps {
@@ -55,7 +58,36 @@ const ChartGroup: FunctionComponent<ComponentProps> = (
)}
<LineChart
key={index}
{...chart.props}
{...(chart.props as LineChartProps)}
syncid={syncId}
heightInPx={props.heightInPx}
/>
</div>
);
case ChartType.BAR:
return (
<div
key={index}
className={`p-6 ${props.hideCard ? "" : "rounded-md bg-white shadow"} ${props.chartCssClass || ""}`}
>
<h2
data-testid="card-details-heading"
id="card-details-heading"
className="text-lg font-medium leading-6 text-gray-900"
>
{chart.title}
</h2>
{chart.description && (
<p
data-testid="card-description"
className="mt-1 text-sm text-gray-500 w-full hidden md:block"
>
{chart.description}
</p>
)}
<BarChartElement
key={index}
{...(chart.props as BarChartProps)}
syncid={syncId}
heightInPx={props.heightInPx}
/>

View File

@@ -82,12 +82,42 @@ const renderShape: (
width = Math.abs(width); // width must be a positive number
}
// Radius for rounded corners at top
const radius: number = Math.min(4, width / 2, height / 2);
/*
* Create path with rounded corners at the top only (for horizontal layout)
* For vertical layout, round the right side
*/
let path: string;
if (layout === "horizontal") {
// Rounded top corners for horizontal bars
path = `
M ${x},${y + height}
L ${x},${y + radius}
Q ${x},${y} ${x + radius},${y}
L ${x + width - radius},${y}
Q ${x + width},${y} ${x + width},${y + radius}
L ${x + width},${y + height}
Z
`;
} else {
// Rounded right corners for vertical bars
path = `
M ${x},${y}
L ${x + width - radius},${y}
Q ${x + width},${y} ${x + width},${y + radius}
L ${x + width},${y + height - radius}
Q ${x + width},${y + height} ${x + width - radius},${y + height}
L ${x},${y + height}
Z
`;
}
return (
<rect
x={x}
y={y}
width={width}
height={height}
<path
d={path}
opacity={
activeBar || (activeLegend && activeLegend !== name)
? deepEqual(activeBar, { ...payload, value })
@@ -615,6 +645,7 @@ interface BarChartProps extends React.HTMLAttributes<HTMLDivElement> {
legendPosition?: "left" | "center" | "right";
tooltipCallback?: (tooltipCallbackContent: TooltipProps) => void;
customTooltip?: React.ComponentType<TooltipProps>;
syncid?: string | undefined;
}
const BarChart: React.ForwardRefExoticComponent<
@@ -753,6 +784,7 @@ const BarChart: React.ForwardRefExoticComponent<
<ResponsiveContainer>
<RechartsBarChart
data={data}
syncId={props.syncid?.toString() || ""}
{...(hasOnValueChange && (activeLegend || activeBar)
? {
onClick: handleChartClick,

View File

@@ -25,9 +25,9 @@ const Loader: FunctionComponent<ComponentProps> = ({
if (loaderType === LoaderType.Bar) {
return (
<div
role={`bar-loader mt-1 ${className}`}
className="flex justify-center"
data-testid="loader"
role="presentation"
className={`flex justify-center mt-1 ${className}`.trim()}
data-testid="bar-loader"
>
<BarLoader height={4} width={size} color={color.toString()} />
</div>
@@ -37,9 +37,9 @@ const Loader: FunctionComponent<ComponentProps> = ({
if (loaderType === LoaderType.Beats) {
return (
<div
role="beat-loader mt-1"
className="justify-center"
data-testid="loader"
role="presentation"
className={`justify-center mt-1 ${className}`.trim()}
data-testid="beat-loader"
>
<BeatLoader size={size} color={color.toString()} />
</div>

View File

@@ -11,6 +11,9 @@ import URL from "../Types/API/URL";
import Dictionary from "../Types/Dictionary";
import APIException from "../Types/Exception/ApiException";
import { JSONArray, JSONObject } from "../Types/JSON";
import RequestFailedDetails, {
RequestFailedPhase,
} from "../Types/Probe/RequestFailedDetails";
import axios, {
AxiosError,
AxiosProgressEvent,
@@ -525,7 +528,49 @@ export default class API {
// get url from error
const url: string = error?.config?.url || "";
const errorMessage: string = error.message || error.toString();
// Get a meaningful error message, avoiding generic "Error" strings
let errorMessage: string = error.message || "";
// If error message is empty or just "Error", try to get more details from the error
if (
!errorMessage ||
errorMessage.toLowerCase() === "error" ||
errorMessage.trim() === ""
) {
// Check for common axios error codes
if (error.code) {
switch (error.code) {
case "ECONNREFUSED":
errorMessage = "Connection refused";
break;
case "ECONNRESET":
errorMessage = "Connection reset";
break;
case "ETIMEDOUT":
errorMessage = "Connection timed out";
break;
case "ENOTFOUND":
errorMessage = "Host not found";
break;
case "ECONNABORTED":
errorMessage = "Connection aborted";
break;
case "ERR_NETWORK":
errorMessage = "Network error";
break;
case "ERR_BAD_REQUEST":
errorMessage = "Bad request";
break;
case "ERR_BAD_RESPONSE":
errorMessage = "Bad response from server";
break;
default:
errorMessage = error.code || "Unknown error";
}
} else {
errorMessage = "Request failed";
}
}
throw new APIException(`Request failed to ${url}. ${errorMessage}`, error);
}
@@ -534,7 +579,37 @@ export default class API {
let errorString: string = error.message || error.toString();
if (error instanceof APIException) {
errorString = `${error.message?.toString()} ${error.error?.message || error.error?.toString() || ""}`;
// Get the nested error message, but avoid duplicating or adding empty/generic messages
let nestedErrorMessage: string = "";
if (error.error) {
// Get the error message, avoiding generic "Error" or empty strings
const errMsg: string = error.error.message || "";
const errStr: string = error.error.toString() || "";
// Check if the error message is meaningful (not just "Error" or empty)
if (
errMsg &&
errMsg.trim().toLowerCase() !== "error" &&
errMsg.trim() !== ""
) {
nestedErrorMessage = errMsg;
} else if (
errStr &&
errStr.trim().toLowerCase() !== "error" &&
errStr.trim().toLowerCase() !== "error:" &&
errStr.trim() !== "" &&
!errStr.toLowerCase().startsWith("error:")
) {
nestedErrorMessage = errStr;
}
}
// Only append nested error if it's meaningful and not already in the main message
if (nestedErrorMessage && !error.message?.includes(nestedErrorMessage)) {
errorString = `${error.message?.toString()} ${nestedErrorMessage}`;
} else {
errorString = error.message?.toString() || "";
}
}
// Handle AggregateError by extracting the underlying error messages
@@ -626,4 +701,232 @@ export default class API {
return errorString;
}
/**
* Extracts detailed error information from an axios error or generic error.
* This provides more context about where and why a request failed.
*/
public static getRequestFailedDetails(
error: AxiosError | Error | unknown,
): RequestFailedDetails {
const axiosError: AxiosError | null = axios.isAxiosError(error)
? (error as AxiosError)
: null;
const errorCode: string | undefined = axiosError?.code;
const rawErrorMessage: string =
(error as Error)?.message || String(error) || "Unknown error";
// Helper to determine the phase and description based on error code/message
const lowerMessage: string = rawErrorMessage.toLowerCase();
// DNS resolution failures
if (errorCode === "ENOTFOUND" || lowerMessage.includes("enotfound")) {
return {
failedPhase: RequestFailedPhase.DNSResolution,
errorCode: errorCode || "ENOTFOUND",
errorDescription:
"DNS resolution failed. The hostname could not be resolved to an IP address. Please verify the hostname is correct and that DNS is working properly.",
rawErrorMessage,
};
}
// Connection refused
if (errorCode === "ECONNREFUSED" || lowerMessage.includes("econnrefused")) {
return {
failedPhase: RequestFailedPhase.TCPConnection,
errorCode: errorCode || "ECONNREFUSED",
errorDescription:
"Connection refused. The server actively refused the connection. This usually means no service is listening on the specified port, or a firewall is blocking the connection.",
rawErrorMessage,
};
}
// Connection reset
if (
errorCode === "ECONNRESET" ||
lowerMessage.includes("econnreset") ||
lowerMessage.includes("connection reset")
) {
return {
failedPhase: RequestFailedPhase.TCPConnection,
errorCode: errorCode || "ECONNRESET",
errorDescription:
"Connection reset. The connection was forcibly closed by the server or a network device. This can happen due to server restarts, load balancer timeouts, or network issues.",
rawErrorMessage,
};
}
// Connection aborted
if (
errorCode === "ECONNABORTED" ||
lowerMessage.includes("econnaborted") ||
lowerMessage.includes("connection aborted")
) {
return {
failedPhase: RequestFailedPhase.RequestAborted,
errorCode: errorCode || "ECONNABORTED",
errorDescription:
"Connection aborted. The request was aborted, possibly due to a timeout or the connection being closed unexpectedly.",
rawErrorMessage,
};
}
// Timeout errors
if (
errorCode === "ETIMEDOUT" ||
errorCode === "ESOCKETTIMEDOUT" ||
lowerMessage.includes("timeout") ||
lowerMessage.includes("exceeded")
) {
return {
failedPhase: RequestFailedPhase.RequestTimeout,
errorCode: errorCode || "TIMEOUT",
errorDescription:
"Request timed out. The server did not respond within the allowed time. This could be due to network latency, server overload, or the server being unresponsive.",
rawErrorMessage,
};
}
// SSL/TLS Certificate errors
if (
lowerMessage.includes("certificate has expired") ||
lowerMessage.includes("cert_has_expired")
) {
return {
failedPhase: RequestFailedPhase.CertificateError,
errorCode: "CERT_HAS_EXPIRED",
errorDescription:
"SSL certificate has expired. The server's SSL certificate is no longer valid. The certificate needs to be renewed.",
rawErrorMessage,
};
}
if (
lowerMessage.includes("self-signed certificate") ||
lowerMessage.includes("self signed certificate") ||
lowerMessage.includes("depth_zero_self_signed_cert")
) {
return {
failedPhase: RequestFailedPhase.CertificateError,
errorCode: "SELF_SIGNED_CERT",
errorDescription:
"Self-signed certificate detected. The server is using a self-signed SSL certificate that is not trusted by default. Consider using a certificate from a trusted Certificate Authority.",
rawErrorMessage,
};
}
if (
lowerMessage.includes("certificate signed by unknown authority") ||
lowerMessage.includes("unable_to_verify_leaf_signature") ||
lowerMessage.includes("unable to verify")
) {
return {
failedPhase: RequestFailedPhase.CertificateError,
errorCode: "CERT_UNKNOWN_AUTHORITY",
errorDescription:
"SSL certificate signed by unknown authority. The certificate chain could not be verified against known Certificate Authorities.",
rawErrorMessage,
};
}
if (
lowerMessage.includes("ssl") ||
lowerMessage.includes("tls") ||
lowerMessage.includes("certificate") ||
lowerMessage.includes("handshake")
) {
return {
failedPhase: RequestFailedPhase.TLSHandshake,
errorCode: errorCode || "TLS_ERROR",
errorDescription:
"TLS/SSL handshake failed. There was an error establishing a secure connection to the server. This could be due to certificate issues, protocol mismatches, or the server not supporting HTTPS.",
rawErrorMessage,
};
}
// Network errors
if (lowerMessage.includes("network error") || errorCode === "ERR_NETWORK") {
return {
failedPhase: RequestFailedPhase.NetworkError,
errorCode: errorCode || "NETWORK_ERROR",
errorDescription:
"Network error occurred. Unable to reach the server due to network connectivity issues. Please check your network connection and ensure the server is accessible.",
rawErrorMessage,
};
}
// Host unreachable
if (
errorCode === "EHOSTUNREACH" ||
lowerMessage.includes("host unreachable")
) {
return {
failedPhase: RequestFailedPhase.TCPConnection,
errorCode: errorCode || "EHOSTUNREACH",
errorDescription:
"Host unreachable. The network path to the server could not be found. This may be due to routing issues or the host being offline.",
rawErrorMessage,
};
}
// Network unreachable
if (
errorCode === "ENETUNREACH" ||
lowerMessage.includes("network unreachable")
) {
return {
failedPhase: RequestFailedPhase.TCPConnection,
errorCode: errorCode || "ENETUNREACH",
errorDescription:
"Network unreachable. There is no route to the network where the server resides. This is typically a routing or connectivity issue.",
rawErrorMessage,
};
}
// Server responded with error status
if (axiosError?.response) {
const status: number = axiosError.response.status;
let description: string = `Server responded with HTTP status ${status}.`;
if (status >= 500) {
description += " This indicates a server-side error.";
} else if (status === 404) {
description += " The requested resource was not found.";
} else if (status === 403) {
description += " Access to the resource is forbidden.";
} else if (status === 401) {
description += " Authentication is required or has failed.";
} else if (status === 400) {
description += " The request was malformed or invalid.";
} else if (status >= 400) {
description += " This indicates a client-side error.";
}
return {
failedPhase: RequestFailedPhase.ServerResponse,
errorCode: `HTTP_${status}`,
errorDescription: description,
rawErrorMessage,
};
}
// Request was made but no response received
if (axiosError?.request && !axiosError?.response) {
return {
failedPhase: RequestFailedPhase.NetworkError,
errorCode: errorCode || "NO_RESPONSE",
errorDescription:
"No response received from the server. The request was sent but no response was returned. This could indicate the server is down, unreachable, or the request timed out.",
rawErrorMessage,
};
}
// Default/Unknown error
return {
failedPhase: RequestFailedPhase.Unknown,
errorCode: errorCode,
errorDescription: `Request failed: ${API.getFriendlyErrorMessage(error as Error)}`,
rawErrorMessage,
};
}
}

View File

@@ -7,6 +7,7 @@ import {
ComponentInputType,
} from "../../../Types/Dashboard/DashboardComponents/ComponentArgument";
import DashboardComponentType from "../../../Types/Dashboard/DashboardComponentType";
import DashboardChartType from "../../../Types/Dashboard/Chart/ChartType";
export default class DashboardChartComponentUtil extends DashboardBaseComponentUtil {
public static override getDefaultComponent(): DashboardChartComponent {
@@ -27,6 +28,7 @@ export default class DashboardChartComponentUtil extends DashboardBaseComponentU
groupBy: undefined,
},
},
chartType: DashboardChartType.Line,
},
};
}
@@ -38,6 +40,24 @@ export default class DashboardChartComponentUtil extends DashboardBaseComponentU
ComponentArgument<DashboardChartComponent>
> = [];
componentArguments.push({
name: "Chart Type",
description: "Select the type of chart to display",
required: true,
type: ComponentInputType.Dropdown,
id: "chartType",
dropdownOptions: [
{
label: "Line Chart",
value: DashboardChartType.Line,
},
{
label: "Bar Chart",
value: DashboardChartType.Bar,
},
],
});
componentArguments.push({
name: "Chart Configuration",
description: "Please select the metrics to display on the chart",

View File

@@ -26,7 +26,7 @@
"transform": {
".(ts|tsx)": "ts-jest"
},
"testEnvironment": "node",
"testEnvironment": "jsdom",
"collectCoverage": false,
"coverageReporters": [
"text",

View File

@@ -11458,23 +11458,23 @@
}
},
"node_modules/jwa": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
"integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz",
"integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==",
"license": "MIT",
"dependencies": {
"buffer-equal-constant-time": "1.0.1",
"buffer-equal-constant-time": "^1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jws": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz",
"integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==",
"license": "MIT",
"dependencies": {
"jwa": "^1.4.1",
"jwa": "^1.4.2",
"safe-buffer": "^5.0.1"
}
},
@@ -17357,12 +17357,12 @@
}
},
"node_modules/web-push/node_modules/jws": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
"license": "MIT",
"dependencies": {
"jwa": "^2.0.0",
"jwa": "^2.0.1",
"safe-buffer": "^5.0.1"
}
},

View File

@@ -19,7 +19,6 @@
"devDependencies": {
"@types/node": "^17.0.45",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.6.3"
},
"engines": {
@@ -301,19 +300,6 @@
"node": ">=0.3.1"
}
},
"node_modules/json5": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true,
"license": "MIT",
"bin": {
"json5": "lib/cli.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/make-error": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
@@ -321,26 +307,6 @@
"dev": true,
"license": "ISC"
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/strip-bom": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
"integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/ts-node": {
"version": "10.9.2",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
@@ -385,21 +351,6 @@
}
}
},
"node_modules/tsconfig-paths": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz",
"integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==",
"dev": true,
"license": "MIT",
"dependencies": {
"json5": "^2.2.2",
"minimist": "^1.2.6",
"strip-bom": "^3.0.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",

View File

@@ -9,6 +9,7 @@
"scripts": {
"build": "tsc",
"compile": "tsc",
"dep-check": "npm install -g depcheck && depcheck ./ --skip-missing=true",
"dev": "ts-node --transpile-only -r tsconfig-paths/register src/Index.ts",
"start": "node --enable-source-maps ./build/dist/Index.js",
"clear-modules": "rm -rf node_modules && rm -f package-lock.json && npm install"
@@ -21,7 +22,6 @@
},
"devDependencies": {
"@types/node": "^17.0.45",
"tsconfig-paths": "^4.2.0",
"ts-node": "^10.9.2",
"typescript": "^5.6.3"
},

View File

@@ -12532,12 +12532,12 @@
}
},
"../Common/node_modules/jws": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz",
"integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==",
"license": "MIT",
"dependencies": {
"jwa": "^1.4.1",
"jwa": "^1.4.2",
"safe-buffer": "^5.0.1"
}
},
@@ -18586,12 +18586,12 @@
}
},
"../Common/node_modules/web-push/node_modules/jws": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
"license": "MIT",
"dependencies": {
"jwa": "^2.0.0",
"jwa": "^2.0.1",
"safe-buffer": "^5.0.1"
}
},

View File

@@ -37,7 +37,7 @@
"ejs": "^3.1.10",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.30.1",
"react-router-dom": "^6.30.2",
"reactflow": "^11.11.2",
"stripe": "^11.0.0",
"use-async-effect": "^2.2.6"

View File

@@ -160,6 +160,7 @@ const ArgumentsForm: FunctionComponent<ComponentProps> = (
placeholder: arg.placeholder,
...ComponentInputTypeToFormFieldType.getFormFieldTypeByComponentInputType(
arg.type,
arg.dropdownOptions,
),
getCustomElement: getCustomElememnt(arg),
};

View File

@@ -5,6 +5,7 @@ import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchem
export default class ComponentInputTypeToFormFieldType {
public static getFormFieldTypeByComponentInputType(
componentInputType: ComponentInputType,
dropdownOptions?: Array<DropdownOption> | undefined,
): {
fieldType: FormFieldSchemaType;
dropdownOptions?: Array<DropdownOption> | undefined;
@@ -52,6 +53,13 @@ export default class ComponentInputTypeToFormFieldType {
};
}
if (componentInputType === ComponentInputType.Dropdown) {
return {
fieldType: FormFieldSchemaType.Dropdown,
dropdownOptions: dropdownOptions || [],
};
}
return {
fieldType: FormFieldSchemaType.Text,
dropdownOptions: [],

View File

@@ -10,10 +10,13 @@ import MetricUtil from "../../Metrics/Utils/Metrics";
import API from "Common/UI/Utils/API/API";
import ComponentLoader from "Common/UI/Components/ComponentLoader/ComponentLoader";
import JSONFunctions from "Common/Types/JSONFunctions";
import MetricQueryConfigData from "Common/Types/Metrics/MetricQueryConfigData";
import MetricQueryConfigData, {
MetricChartType,
} from "Common/Types/Metrics/MetricQueryConfigData";
import Icon from "Common/UI/Components/Icon/Icon";
import IconProp from "Common/Types/Icon/IconProp";
import { RangeStartAndEndDateTimeUtil } from "Common/Types/Time/RangeStartAndEndDateTime";
import DashboardChartType from "Common/Types/Dashboard/Chart/ChartType";
export interface ComponentProps extends DashboardBaseComponentProps {
component: DashboardChartComponent;
@@ -141,6 +144,16 @@ const DashboardChartComponentElement: FunctionComponent<ComponentProps> = (
// add title and description.
type GetMetricChartType = () => MetricChartType;
// Convert dashboard chart type to metric chart type
const getMetricChartType: GetMetricChartType = (): MetricChartType => {
if (props.component.arguments.chartType === DashboardChartType.Bar) {
return MetricChartType.BAR;
}
return MetricChartType.LINE;
};
const chartMetricViewData: MetricViewData = {
queryConfigs: props.component.arguments.metricQueryConfig
? [
@@ -154,6 +167,7 @@ const DashboardChartComponentElement: FunctionComponent<ComponentProps> = (
legend: props.component.arguments.legendText || undefined,
legendUnit: props.component.arguments.legendUnit || undefined,
},
chartType: getMetricChartType(),
},
]
: [],

View File

@@ -10,7 +10,10 @@ import { XAxisAggregateType } from "Common/UI/Components/Charts/Types/XAxis/XAxi
import MetricsAggregationType from "Common/Types/Metrics/MetricsAggregationType";
import SeriesPoint from "Common/UI/Components/Charts/Types/SeriesPoints";
import MetricViewData from "Common/Types/Metrics/MetricViewData";
import { ChartSeries } from "Common/Types/Metrics/MetricQueryConfigData";
import {
ChartSeries,
MetricChartType,
} from "Common/Types/Metrics/MetricQueryConfigData";
import AggregatedModel from "Common/Types/BaseDatabase/AggregatedModel";
import YAxisType from "Common/UI/Components/Charts/Types/YAxis/YAxisType";
import { YAxisPrecision } from "Common/UI/Components/Charts/Types/YAxis/YAxis";
@@ -156,9 +159,15 @@ const MetricCharts: FunctionComponent<ComponentProps> = (
});
}
// Determine chart type - use BAR for bar chart type, LINE for everything else
const chartType: ChartType =
queryConfig.chartType === MetricChartType.BAR
? ChartType.BAR
: ChartType.LINE;
const chart: Chart = {
id: index.toString(),
type: ChartType.LINE,
type: chartType,
title:
queryConfig.metricAliasData?.title ||
queryConfig.metricQueryData.filterData.metricName?.toString() ||

View File

@@ -17,6 +17,11 @@ const PingMonitorView: FunctionComponent<ComponentProps> = (
responseTimeInMs = Math.round(responseTimeInMs);
}
// Check if there are error details to show
const hasErrorDetails: boolean = Boolean(
props.probeMonitorResponse.requestFailedDetails,
);
return (
<div className="space-y-5">
<div className="flex space-x-3">
@@ -55,6 +60,50 @@ const PingMonitorView: FunctionComponent<ComponentProps> = (
}
/>
</div>
{props.probeMonitorResponse.failureCause && (
<div className="flex space-x-3">
<InfoCard
className="w-full shadow-none border-2 border-gray-100 "
title="Error"
value={props.probeMonitorResponse.failureCause?.toString() || "-"}
/>
</div>
)}
{/* Error Details Section */}
{hasErrorDetails && (
<div className="space-y-3">
<div className="flex space-x-3">
<InfoCard
className="w-1/2 shadow-none border-2 border-gray-100 "
title="Failed At"
value={
props.probeMonitorResponse.requestFailedDetails?.failedPhase ||
"-"
}
/>
<InfoCard
className="w-1/2 shadow-none border-2 border-gray-100 "
title="Error Code"
value={
props.probeMonitorResponse.requestFailedDetails?.errorCode ||
"-"
}
/>
</div>
<div className="flex space-x-3">
<InfoCard
className="w-full shadow-none border-2 border-gray-100 "
title="Error Details"
value={
props.probeMonitorResponse.requestFailedDetails
?.errorDescription || "-"
}
/>
</div>
</div>
)}
</div>
);
};

View File

@@ -43,6 +43,11 @@ const WebsiteMonitorSummaryView: FunctionComponent<ComponentProps> = (
});
}
// Check if there are error details to show
const hasErrorDetails: boolean = Boolean(
props.probeMonitorResponse.requestFailedDetails,
);
return (
<div className="space-y-5">
<div className="flex space-x-3">
@@ -88,6 +93,40 @@ const WebsiteMonitorSummaryView: FunctionComponent<ComponentProps> = (
</div>
)}
{/* Error Details Section */}
{hasErrorDetails && (
<div className="space-y-3">
<div className="flex space-x-3">
<InfoCard
className="w-1/2 shadow-none border-2 border-gray-100 "
title="Failed At"
value={
props.probeMonitorResponse.requestFailedDetails?.failedPhase ||
"-"
}
/>
<InfoCard
className="w-1/2 shadow-none border-2 border-gray-100 "
title="Error Code"
value={
props.probeMonitorResponse.requestFailedDetails?.errorCode ||
"-"
}
/>
</div>
<div className="flex space-x-3">
<InfoCard
className="w-full shadow-none border-2 border-gray-100 "
title="Error Details"
value={
props.probeMonitorResponse.requestFailedDetails
?.errorDescription || "-"
}
/>
</div>
</div>
)}
{showMoreDetails && fields.length > 0 && (
<div>
<Detail<ProbeMonitorResponse>

View File

@@ -63,8 +63,7 @@ const POSTMORTEM_FORM_FIELDS: Fields<Incident> = [
title: "Postmortem Published At",
fieldType: FormFieldSchemaType.DateTime,
required: false,
description:
"Set the posted-on timestamp subscribers will see. ",
description: "Set the posted-on timestamp subscribers will see. ",
placeholder: "Select date and time",
getDefaultValue: () => {
return OneUptimeDate.getCurrentDate();
@@ -86,7 +85,7 @@ const POSTMORTEM_FORM_FIELDS: Fields<Incident> = [
notifySubscribersOnPostmortemPublished: true,
},
title: "Notify Subscribers",
fieldType: FormFieldSchemaType.Toggle,
fieldType: FormFieldSchemaType.Checkbox,
required: false,
description: "Notify subscribers when this postmortem is published.",
defaultValue: true,
@@ -259,7 +258,7 @@ const IncidentPostmortem: FunctionComponent<
field: {
showPostmortemOnStatusPage: true,
},
title: "Visible on Status Page?",
title: "Postmortem visible on Status Page?",
fieldType: FieldType.Boolean,
},
{
@@ -268,6 +267,9 @@ const IncidentPostmortem: FunctionComponent<
},
title: "Notify Subscribers",
fieldType: FieldType.Boolean,
showIf: (item: Incident): boolean => {
return Boolean(item.showPostmortemOnStatusPage);
},
},
{
field: {

159
E2E/README.md Normal file
View File

@@ -0,0 +1,159 @@
# E2E Tests
End-to-end tests for OneUptime using [Playwright](https://playwright.dev/).
## Prerequisites
- Node.js (v18 or higher recommended)
- npm
## Installation
```bash
cd E2E
npm install
```
This will automatically install Playwright browsers and dependencies via the `preinstall` script.
## Configuration
The tests use environment variables for configuration. Set the following variables before running tests in config.env:
| Variable | Description | Default |
|----------|-------------|---------|
| `HOST` | The hostname to test against | `localhost` |
| `HTTP_PROTOCOL` | Protocol to use (`http` or `https`) | `http` |
| `BILLING_ENABLED` | Enable billing-related tests | `false` |
| `E2E_TEST_IS_USER_REGISTERED` | Whether a test user is already registered | `false` |
| `E2E_TEST_REGISTERED_USER_EMAIL` | Email of the registered test user | - |
| `E2E_TEST_REGISTERED_USER_PASSWORD` | Password of the registered test user | - |
| `E2E_TEST_STATUS_PAGE_URL` | URL of a status page to test | - |
| `E2E_TESTS_FAILED_WEBHOOK_URL` | Webhook URL to call on test failure | - |
### Example
```bash
export HOST=staging.oneuptime.com
export HTTP_PROTOCOL=https
export BILLING_ENABLED=true
```
## Running Tests
### Run all tests
```bash
npm test
```
### Run tests in debug mode
```bash
npm run debug-tests
```
### Run specific test file
```bash
npx playwright test Tests/Home/Landing.spec.ts
```
### Run tests for a specific browser
```bash
# Chromium only
npx playwright test --project=chromium
# Firefox only
npx playwright test --project=firefox
```
### Run tests with UI mode
```bash
npx playwright test --ui
```
### Run a specific test by name
```bash
npx playwright test -g "oneUptime link navigate to homepage"
```
## Test Structure
```
E2E/
├── Tests/
│ ├── Accounts/ # Account-related tests (login, registration)
│ ├── App/ # Main application tests
│ ├── Home/ # Homepage tests
│ ├── IncomingRequestIngest/
│ ├── ProbeIngest/
│ ├── StatusPage/ # Status page tests
│ └── TelemetryIngest/
├── Config.ts # Environment configuration
├── playwright.config.ts # Playwright configuration
└── package.json
```
## Viewing Test Reports
After running tests, an HTML report is generated. Open it with:
```bash
npx playwright show-report
```
## Test Configuration
The Playwright configuration (`playwright.config.ts`) includes:
- **Timeout**: 240 seconds per test
- **Retries**: 3 retries on failure
- **Browsers**: Chromium and Firefox
- **Tracing**: Enabled for debugging failed tests
## Debugging
### View traces
When tests fail, traces are collected. View them with:
```bash
npx playwright show-trace test-results/<test-name>/trace.zip
```
### Run in headed mode
```bash
npx playwright test --headed
```
### Slow down execution
```bash
npx playwright test --headed --slow-mo=1000
```
## CI/CD
For CI environments, the `CI` environment variable is automatically detected:
- `test.only` usage will fail the build
- Parallel test execution is disabled (workers: 1)
## Troubleshooting
### Playwright browsers not installed
```bash
npx playwright install
npx playwright install-deps
```
### Clear and reinstall dependencies
```bash
npm run clear-modules
```

View File

@@ -28,7 +28,10 @@ test.describe("check if pages loades with its title", () => {
return;
}
await page.getByRole("link", { name: "OneUptime", exact: true }).click();
await page
.getByRole("link", { name: /OneUptime/ })
.first()
.click();
await expect(page).toHaveURL(
URL.fromString(BASE_URL.toString()).toString(),

View File

@@ -4,6 +4,7 @@ import { StaticPath, ViewsPath } from "./Utils/Config";
import NotFoundUtil from "./Utils/NotFound";
import ProductCompare, { Product } from "./Utils/ProductCompare";
import generateSitemapXml from "./Utils/Sitemap";
import { getPageSEO, PageSEOData } from "./Utils/PageSEO";
import DatabaseConfig from "Common/Server/DatabaseConfig";
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
import HTTPResponse from "Common/Types/API/HTTPResponse";
@@ -28,16 +29,32 @@ import "./Jobs/UpdateBlog";
import { Host, IsBillingEnabled } from "Common/Server/EnvironmentConfig";
import LocalCache from "Common/Server/Infrastructure/LocalCache";
// Helper to get SEO data and merge with homeUrl for templates
const getSEOForPath: (
path: string,
homeUrl: string,
) => PageSEOData & { fullCanonicalUrl: string } = (
path: string,
homeUrl: string,
): PageSEOData & { fullCanonicalUrl: string } => {
const seo: PageSEOData = getPageSEO(path);
const baseUrl: string = homeUrl.replace(/\/$/, "");
return {
...seo,
fullCanonicalUrl: `${baseUrl}${seo.canonicalPath}`,
};
};
const HomeFeatureSet: FeatureSet = {
init: async (): Promise<void> => {
const app: ExpressApplication = Express.getExpressApp();
/*
* Routes
* Middleware to inject baseUrl for templates (used for canonical links)
* Middleware to inject baseUrl and SEO data for templates
*/
app.use(
async (_req: ExpressRequest, res: ExpressResponse, next: () => void) => {
async (req: ExpressRequest, res: ExpressResponse, next: () => void) => {
if (!res.locals["homeUrl"]) {
try {
// Try to get cached home URL first.
@@ -59,12 +76,21 @@ const HomeFeatureSet: FeatureSet = {
res.locals["homeUrl"] = "https://oneuptime.com";
}
}
// Inject SEO data for current path
res.locals["seo"] = getSEOForPath(
req.path,
res.locals["homeUrl"] as string,
);
next();
},
);
app.get("/", (_req: ExpressRequest, res: ExpressResponse) => {
const { reviewsList1, reviewsList2, reviewsList3 } = Reviews;
const seo: PageSEOData & { fullCanonicalUrl: string } = getSEOForPath(
"/",
res.locals["homeUrl"] as string,
);
res.render(`${ViewsPath}/index`, {
support: false,
@@ -76,6 +102,7 @@ const HomeFeatureSet: FeatureSet = {
reviewsList1,
reviewsList2,
reviewsList3,
seo,
});
});
@@ -90,14 +117,23 @@ const HomeFeatureSet: FeatureSet = {
);
app.get("/support", async (_req: ExpressRequest, res: ExpressResponse) => {
const seo: PageSEOData & { fullCanonicalUrl: string } = getSEOForPath(
"/support",
res.locals["homeUrl"] as string,
);
res.render(`${ViewsPath}/support`, {
enableGoogleTagManager: IsBillingEnabled,
seo,
});
});
app.get(
"/oss-friends",
async (_req: ExpressRequest, res: ExpressResponse) => {
const seo: PageSEOData & { fullCanonicalUrl: string } = getSEOForPath(
"/oss-friends",
res.locals["homeUrl"] as string,
);
res.render(`${ViewsPath}/oss-friends`, {
ossFriends: OSSFriends.map((friend: OSSFriend) => {
return {
@@ -106,6 +142,7 @@ const HomeFeatureSet: FeatureSet = {
};
}),
enableGoogleTagManager: IsBillingEnabled,
seo,
});
},
);
@@ -877,15 +914,24 @@ const HomeFeatureSet: FeatureSet = {
},
];
const seo: PageSEOData & { fullCanonicalUrl: string } = getSEOForPath(
"/pricing",
res.locals["homeUrl"] as string,
);
res.render(`${ViewsPath}/pricing`, {
pricing,
enableGoogleTagManager: IsBillingEnabled,
seo,
});
});
app.get(
"/enterprise/demo",
(_req: ExpressRequest, res: ExpressResponse) => {
const seo: PageSEOData & { fullCanonicalUrl: string } = getSEOForPath(
"/enterprise/demo",
res.locals["homeUrl"] as string,
);
res.render(`${ViewsPath}/demo`, {
support: false,
enableGoogleTagManager: IsBillingEnabled,
@@ -893,6 +939,7 @@ const HomeFeatureSet: FeatureSet = {
cta: false,
blackLogo: true,
requestDemoCta: false,
seo,
});
},
);
@@ -900,8 +947,13 @@ const HomeFeatureSet: FeatureSet = {
app.get(
"/product/status-page",
(_req: ExpressRequest, res: ExpressResponse) => {
const seo: PageSEOData & { fullCanonicalUrl: string } = getSEOForPath(
"/product/status-page",
res.locals["homeUrl"] as string,
);
res.render(`${ViewsPath}/status-page`, {
enableGoogleTagManager: IsBillingEnabled,
seo,
});
},
);
@@ -909,15 +961,25 @@ const HomeFeatureSet: FeatureSet = {
app.get(
"/product/logs-management",
(_req: ExpressRequest, res: ExpressResponse) => {
const seo: PageSEOData & { fullCanonicalUrl: string } = getSEOForPath(
"/product/logs-management",
res.locals["homeUrl"] as string,
);
res.render(`${ViewsPath}/logs-management`, {
enableGoogleTagManager: IsBillingEnabled,
seo,
});
},
);
app.get("/product/apm", (_req: ExpressRequest, res: ExpressResponse) => {
const seo: PageSEOData & { fullCanonicalUrl: string } = getSEOForPath(
"/product/apm",
res.locals["homeUrl"] as string,
);
res.render(`${ViewsPath}/apm`, {
enableGoogleTagManager: IsBillingEnabled,
seo,
});
});
@@ -999,11 +1061,16 @@ const HomeFeatureSet: FeatureSet = {
gitHubCommits = commits;
}
const seo: PageSEOData & { fullCanonicalUrl: string } = getSEOForPath(
"/about",
res.locals["homeUrl"] as string,
);
res.render(`${ViewsPath}/about`, {
contributors: gitHubContributors,
basicInfo: gitHubBasicInfo,
commits: gitHubCommits,
enableGoogleTagManager: IsBillingEnabled,
seo,
});
});
@@ -1038,8 +1105,13 @@ const HomeFeatureSet: FeatureSet = {
app.get(
"/product/monitoring",
(_req: ExpressRequest, res: ExpressResponse) => {
const seo: PageSEOData & { fullCanonicalUrl: string } = getSEOForPath(
"/product/monitoring",
res.locals["homeUrl"] as string,
);
res.render(`${ViewsPath}/monitoring`, {
enableGoogleTagManager: IsBillingEnabled,
seo,
});
},
);
@@ -1047,8 +1119,13 @@ const HomeFeatureSet: FeatureSet = {
app.get(
"/product/on-call",
(_req: ExpressRequest, res: ExpressResponse) => {
const seo: PageSEOData & { fullCanonicalUrl: string } = getSEOForPath(
"/product/on-call",
res.locals["homeUrl"] as string,
);
res.render(`${ViewsPath}/on-call`, {
enableGoogleTagManager: IsBillingEnabled,
seo,
});
},
);
@@ -1056,8 +1133,13 @@ const HomeFeatureSet: FeatureSet = {
app.get(
"/product/workflows",
(_req: ExpressRequest, res: ExpressResponse) => {
const seo: PageSEOData & { fullCanonicalUrl: string } = getSEOForPath(
"/product/workflows",
res.locals["homeUrl"] as string,
);
res.render(`${ViewsPath}/workflows`, {
enableGoogleTagManager: IsBillingEnabled,
seo,
});
},
);
@@ -1065,8 +1147,13 @@ const HomeFeatureSet: FeatureSet = {
app.get(
"/product/incident-management",
(_req: ExpressRequest, res: ExpressResponse) => {
const seo: PageSEOData & { fullCanonicalUrl: string } = getSEOForPath(
"/product/incident-management",
res.locals["homeUrl"] as string,
);
res.render(`${ViewsPath}/incident-management`, {
enableGoogleTagManager: IsBillingEnabled,
seo,
});
},
);
@@ -1081,6 +1168,10 @@ const HomeFeatureSet: FeatureSet = {
app.get(
"/enterprise/overview",
(_req: ExpressRequest, res: ExpressResponse) => {
const seo: PageSEOData & { fullCanonicalUrl: string } = getSEOForPath(
"/enterprise/overview",
res.locals["homeUrl"] as string,
);
res.render(`${ViewsPath}/enterprise-overview.ejs`, {
support: false,
enableGoogleTagManager: IsBillingEnabled,
@@ -1088,6 +1179,7 @@ const HomeFeatureSet: FeatureSet = {
cta: true,
blackLogo: false,
requestDemoCta: true,
seo,
});
},
);
@@ -1366,6 +1458,10 @@ const HomeFeatureSet: FeatureSet = {
if (!productConfig) {
return NotFoundUtil.renderNotFound(res);
}
const seo: PageSEOData & { fullCanonicalUrl: string } = getSEOForPath(
`/compare/${req.params["product"]}`,
res.locals["homeUrl"] as string,
);
res.render(`${ViewsPath}/product-compare.ejs`, {
support: false,
enableGoogleTagManager: IsBillingEnabled,
@@ -1375,6 +1471,7 @@ const HomeFeatureSet: FeatureSet = {
requestDemoCta: false,
productConfig,
onlyShowCompareTable: false,
seo,
});
},
);

533
Home/Utils/PageSEO.ts Normal file
View File

@@ -0,0 +1,533 @@
/*
* Page-specific SEO metadata configuration for OneUptime landing pages
* This provides structured data for search engines and AI agents
*/
export interface BreadcrumbItem {
name: string;
url: string;
}
export interface PageSEOData {
title: string;
description: string;
canonicalPath: string; // e.g., "/product/monitoring" - will be combined with homeUrl
ogImage?: string; // defaults to /img/og-image.png if not specified
ogType?: string; // defaults to "website"
twitterCard?: "summary" | "summary_large_image"; // defaults to "summary_large_image" for product pages
breadcrumbs: BreadcrumbItem[];
// For SoftwareApplication schema on product pages
softwareApplication?: {
name: string;
applicationCategory: string;
operatingSystem: string;
description: string;
features: string[];
};
// Page type for conditional schema rendering
pageType:
| "home"
| "product"
| "pricing"
| "legal"
| "blog"
| "about"
| "support"
| "enterprise"
| "compare"
| "other";
}
// Default SEO data factory
export const createDefaultSEO: (
title: string,
description: string,
canonicalPath: string,
pageType?: PageSEOData["pageType"],
) => PageSEOData = (
title: string,
description: string,
canonicalPath: string,
pageType: PageSEOData["pageType"] = "other",
): PageSEOData => {
return {
title,
description,
canonicalPath,
pageType,
breadcrumbs: [{ name: "Home", url: "/" }],
};
};
// Page-specific SEO configurations
export const PageSEOConfig: Record<string, PageSEOData> = {
// Homepage
"/": {
title: "OneUptime | Complete Monitoring & Observability Platform",
description:
"OneUptime is an open-source complete observability platform. Monitor websites, APIs, and servers. Get alerts, manage incidents, and keep customers informed with status pages. Free tier available.",
canonicalPath: "/",
ogType: "website",
twitterCard: "summary_large_image",
pageType: "home",
breadcrumbs: [{ name: "Home", url: "/" }],
},
// Product Pages
"/product/status-page": {
title: "Status Page | Free Public & Private Status Pages | OneUptime",
description:
"Create unlimited public and private status pages. Keep customers informed about incidents and scheduled maintenance. Custom branding, unlimited subscribers, SSL included. Open source.",
canonicalPath: "/product/status-page",
ogImage: "/img/status-pages.png",
twitterCard: "summary_large_image",
pageType: "product",
breadcrumbs: [
{ name: "Home", url: "/" },
{ name: "Products", url: "/#products" },
{ name: "Status Page", url: "/product/status-page" },
],
softwareApplication: {
name: "OneUptime Status Page",
applicationCategory: "DeveloperApplication",
operatingSystem: "Web, Cloud",
description:
"Create public and private status pages to communicate service health to customers and stakeholders.",
features: [
"Unlimited public and private status pages",
"Custom branding and domain",
"Unlimited subscribers",
"Email, SMS, and webhook notifications",
"Scheduled maintenance announcements",
"Incident timeline and postmortems",
"SSL certificates included",
"API access",
],
},
},
"/product/monitoring": {
title: "Uptime Monitoring | Website, API, Server Monitoring | OneUptime",
description:
"Monitor websites, APIs, servers, and any resource in real-time. Get instant alerts when things go wrong. Supports HTTP, TCP, UDP, DNS, SSL, ping monitoring. Open source.",
canonicalPath: "/product/monitoring",
ogImage: "/img/monitor.png",
twitterCard: "summary_large_image",
pageType: "product",
breadcrumbs: [
{ name: "Home", url: "/" },
{ name: "Products", url: "/#products" },
{ name: "Monitoring", url: "/product/monitoring" },
],
softwareApplication: {
name: "OneUptime Monitoring",
applicationCategory: "DeveloperApplication",
operatingSystem: "Web, Cloud",
description:
"Real-time uptime monitoring for websites, APIs, servers, and infrastructure.",
features: [
"Website and API monitoring",
"Server and infrastructure monitoring",
"Synthetic monitoring with Playwright",
"Custom monitoring criteria",
"Multi-location checks",
"1-second monitoring intervals",
"SSL certificate monitoring",
"Response time tracking",
],
},
},
"/product/incident-management": {
title:
"Incident Management Software | Resolve Incidents Faster | OneUptime",
description:
"Streamline incident response with OneUptime. Track incidents, collaborate in real-time, conduct postmortems, and improve MTTR. Integrates with Slack, PagerDuty, and more. Open source.",
canonicalPath: "/product/incident-management",
ogImage: "/img/incident-report.png",
twitterCard: "summary_large_image",
pageType: "product",
breadcrumbs: [
{ name: "Home", url: "/" },
{ name: "Products", url: "/#products" },
{ name: "Incident Management", url: "/product/incident-management" },
],
softwareApplication: {
name: "OneUptime Incident Management",
applicationCategory: "DeveloperApplication",
operatingSystem: "Web, Cloud",
description:
"Complete incident management platform to detect, respond, and resolve incidents faster.",
features: [
"Incident tracking and timeline",
"Real-time collaboration",
"Postmortem reports",
"Custom incident states and severity",
"Slack and Teams integration",
"Automated incident workflows",
"MTTR and incident metrics",
"Root cause analysis",
],
},
},
"/product/on-call": {
title:
"On-Call Management & Alerting | Schedules & Escalations | OneUptime",
description:
"On-call scheduling, alerting, and escalation policies. Alert the right people at the right time via SMS, phone, email, Slack. Rotation schedules and override support. Open source.",
canonicalPath: "/product/on-call",
ogImage: "/img/on-call.png",
twitterCard: "summary_large_image",
pageType: "product",
breadcrumbs: [
{ name: "Home", url: "/" },
{ name: "Products", url: "/#products" },
{ name: "On-Call & Alerts", url: "/product/on-call" },
],
softwareApplication: {
name: "OneUptime On-Call Management",
applicationCategory: "DeveloperApplication",
operatingSystem: "Web, Cloud, iOS, Android",
description:
"On-call scheduling, alerting, and escalation management for DevOps and SRE teams.",
features: [
"On-call schedules and rotations",
"Escalation policies",
"SMS, phone, and email alerts",
"Slack and Teams notifications",
"Override and swap shifts",
"Mobile app notifications",
"Alert deduplication",
"On-call reports",
],
},
},
"/product/logs-management": {
title: "Log Management | Fast Log Search & Analysis | OneUptime",
description:
"Centralized log management with blazing fast search. Ingest logs from any source via OpenTelemetry, Fluentd, or API. Set up alerts on log patterns. Open source.",
canonicalPath: "/product/logs-management",
ogImage: "/img/logs.png",
twitterCard: "summary_large_image",
pageType: "product",
breadcrumbs: [
{ name: "Home", url: "/" },
{ name: "Products", url: "/#products" },
{ name: "Logs Management", url: "/product/logs-management" },
],
softwareApplication: {
name: "OneUptime Logs Management",
applicationCategory: "DeveloperApplication",
operatingSystem: "Web, Cloud",
description:
"Centralized log management and analysis with fast search and alerting.",
features: [
"OpenTelemetry log ingestion",
"Fluentd and Fluent Bit support",
"Full-text log search",
"Log pattern detection",
"Log-based alerting",
"Application and container logs",
"Custom retention policies",
"1000+ source integrations",
],
},
},
"/product/apm": {
title: "APM | Application Performance Monitoring | OneUptime",
description:
"Monitor application performance with distributed tracing, metrics, and error tracking. OpenTelemetry native. Track latency, throughput, and errors across services. Open source.",
canonicalPath: "/product/apm",
ogImage: "/img/apm.png",
twitterCard: "summary_large_image",
pageType: "product",
breadcrumbs: [
{ name: "Home", url: "/" },
{ name: "Products", url: "/#products" },
{ name: "APM", url: "/product/apm" },
],
softwareApplication: {
name: "OneUptime APM",
applicationCategory: "DeveloperApplication",
operatingSystem: "Web, Cloud",
description:
"Application performance monitoring with distributed tracing, metrics, and error tracking.",
features: [
"Distributed tracing",
"Performance metrics",
"Error tracking and exceptions",
"Service dependency maps",
"OpenTelemetry native",
"Latency analysis",
"Throughput monitoring",
"Custom dashboards",
],
},
},
"/product/workflows": {
title: "Workflow Automation | No-Code Integrations | OneUptime",
description:
"Build automated workflows without code. Connect 5000+ services, automate incident response, and create custom integrations. Trigger on any event. Open source.",
canonicalPath: "/product/workflows",
ogImage: "/img/workflows.png",
twitterCard: "summary_large_image",
pageType: "product",
breadcrumbs: [
{ name: "Home", url: "/" },
{ name: "Products", url: "/#products" },
{ name: "Workflows", url: "/product/workflows" },
],
softwareApplication: {
name: "OneUptime Workflows",
applicationCategory: "DeveloperApplication",
operatingSystem: "Web, Cloud",
description:
"No-code workflow automation for incident response and integrations.",
features: [
"Visual workflow builder",
"5000+ integrations",
"Event-driven triggers",
"Conditional logic",
"Custom code blocks",
"Webhook triggers",
"Scheduled workflows",
"Error handling and retries",
],
},
},
// Pricing
"/pricing": {
title: "Pricing | Free Tier & Paid Plans | OneUptime",
description:
"OneUptime pricing starts free. Get status pages, monitoring, incident management, and more. Transparent pricing with no hidden fees. Enterprise plans available.",
canonicalPath: "/pricing",
twitterCard: "summary_large_image",
pageType: "pricing",
breadcrumbs: [
{ name: "Home", url: "/" },
{ name: "Pricing", url: "/pricing" },
],
},
// Enterprise
"/enterprise/overview": {
title: "Enterprise | Self-Hosted & Cloud | OneUptime",
description:
"OneUptime for enterprise. Self-hosted deployment, SSO/SAML, advanced security, SLA guarantees, dedicated support. SOC 2, HIPAA, GDPR compliant.",
canonicalPath: "/enterprise/overview",
twitterCard: "summary_large_image",
pageType: "enterprise",
breadcrumbs: [
{ name: "Home", url: "/" },
{ name: "Enterprise", url: "/enterprise/overview" },
],
},
"/enterprise/demo": {
title: "Request Demo | See OneUptime in Action | OneUptime",
description:
"Schedule a personalized demo of OneUptime. See how our observability platform can help your team monitor, respond, and resolve issues faster.",
canonicalPath: "/enterprise/demo",
twitterCard: "summary",
pageType: "enterprise",
breadcrumbs: [
{ name: "Home", url: "/" },
{ name: "Enterprise", url: "/enterprise/overview" },
{ name: "Request Demo", url: "/enterprise/demo" },
],
},
// About & Support
"/about": {
title: "About Us | Open Source Observability | OneUptime",
description:
"Learn about OneUptime, the open-source observability platform. Built by engineers, for engineers. Meet our contributors and learn our mission.",
canonicalPath: "/about",
twitterCard: "summary",
pageType: "about",
breadcrumbs: [
{ name: "Home", url: "/" },
{ name: "About", url: "/about" },
],
},
"/support": {
title: "Support | Help & Documentation | OneUptime",
description:
"Get help with OneUptime. Access documentation, community support, and contact our team. Enterprise customers get priority support.",
canonicalPath: "/support",
twitterCard: "summary",
pageType: "support",
breadcrumbs: [
{ name: "Home", url: "/" },
{ name: "Support", url: "/support" },
],
},
"/oss-friends": {
title: "OSS Friends | Open Source Partners | OneUptime",
description:
"Meet our open source friends and partners. OneUptime is proud to be part of the open source community.",
canonicalPath: "/oss-friends",
twitterCard: "summary",
pageType: "other",
breadcrumbs: [
{ name: "Home", url: "/" },
{ name: "OSS Friends", url: "/oss-friends" },
],
},
// Legal pages
"/legal": {
title: "Legal Center | Terms, Privacy, Compliance | OneUptime",
description:
"OneUptime legal documents including terms of service, privacy policy, GDPR, SOC 2, HIPAA compliance information.",
canonicalPath: "/legal",
twitterCard: "summary",
pageType: "legal",
breadcrumbs: [
{ name: "Home", url: "/" },
{ name: "Legal", url: "/legal" },
],
},
"/legal/terms": {
title: "Terms of Service | OneUptime",
description: "OneUptime terms of service and conditions of use.",
canonicalPath: "/legal/terms",
twitterCard: "summary",
pageType: "legal",
breadcrumbs: [
{ name: "Home", url: "/" },
{ name: "Legal", url: "/legal" },
{ name: "Terms of Service", url: "/legal/terms" },
],
},
"/legal/privacy": {
title: "Privacy Policy | OneUptime",
description:
"OneUptime privacy policy. Learn how we collect, use, and protect your data.",
canonicalPath: "/legal/privacy",
twitterCard: "summary",
pageType: "legal",
breadcrumbs: [
{ name: "Home", url: "/" },
{ name: "Legal", url: "/legal" },
{ name: "Privacy Policy", url: "/legal/privacy" },
],
},
"/legal/gdpr": {
title: "GDPR Compliance | OneUptime",
description:
"OneUptime GDPR compliance information. We are committed to protecting EU citizen data rights.",
canonicalPath: "/legal/gdpr",
twitterCard: "summary",
pageType: "legal",
breadcrumbs: [
{ name: "Home", url: "/" },
{ name: "Legal", url: "/legal" },
{ name: "GDPR", url: "/legal/gdpr" },
],
},
"/legal/soc-2": {
title: "SOC 2 Compliance | OneUptime",
description:
"OneUptime SOC 2 Type II compliance. Our security controls are audited annually.",
canonicalPath: "/legal/soc-2",
twitterCard: "summary",
pageType: "legal",
breadcrumbs: [
{ name: "Home", url: "/" },
{ name: "Legal", url: "/legal" },
{ name: "SOC 2", url: "/legal/soc-2" },
],
},
"/legal/hipaa": {
title: "HIPAA Compliance | OneUptime",
description:
"OneUptime HIPAA compliance for healthcare organizations. We sign BAAs for enterprise customers.",
canonicalPath: "/legal/hipaa",
twitterCard: "summary",
pageType: "legal",
breadcrumbs: [
{ name: "Home", url: "/" },
{ name: "Legal", url: "/legal" },
{ name: "HIPAA", url: "/legal/hipaa" },
],
},
};
// Helper to get SEO data for a path, with fallback
export const getPageSEO: (path: string) => PageSEOData = (
path: string,
): PageSEOData => {
// Exact match first
if (PageSEOConfig[path]) {
return PageSEOConfig[path];
}
// For compare pages, create dynamic SEO
if (path.startsWith("/compare/")) {
const product: string = path.replace("/compare/", "");
const productName: string = product
.split("-")
.map((word: string) => {
return word.charAt(0).toUpperCase() + word.slice(1);
})
.join(" ");
return {
title: `OneUptime vs ${productName} | Comparison | OneUptime`,
description: `Compare OneUptime with ${productName}. See features, pricing, and why teams choose OneUptime as their observability platform.`,
canonicalPath: path,
twitterCard: "summary_large_image",
pageType: "compare",
breadcrumbs: [
{ name: "Home", url: "/" },
{ name: "Compare", url: "/#compare" },
{ name: `vs ${productName}`, url: path },
],
};
}
// For legal subpages not explicitly defined
if (path.startsWith("/legal/")) {
const section: string = path.replace("/legal/", "");
const sectionName: string = section
.split("-")
.map((word: string) => {
return word.charAt(0).toUpperCase() + word.slice(1);
})
.join(" ");
return {
title: `${sectionName} | Legal | OneUptime`,
description: `OneUptime ${sectionName.toLowerCase()} legal information and compliance documentation.`,
canonicalPath: path,
twitterCard: "summary",
pageType: "legal",
breadcrumbs: [
{ name: "Home", url: "/" },
{ name: "Legal", url: "/legal" },
{ name: sectionName, url: path },
],
};
}
// Default fallback
return createDefaultSEO(
"OneUptime | Complete Observability Platform",
"OneUptime monitors websites, APIs, and servers and alerts your team if something goes wrong. It also keeps your customers updated about any downtime.",
path,
"other",
);
};
export default PageSEOConfig;

View File

@@ -115,7 +115,7 @@ const products: Dictionary<Product> = {
data: [
{
title: "Monitor anything",
description: "Server, Containers, API's, Websites, IoT and more.",
description: "Server, Containers, APIs, Websites, IoT and more.",
productColumn: "",
oneuptimeColumn: "tick",
},
@@ -268,7 +268,7 @@ const products: Dictionary<Product> = {
data: [
{
title: "Monitor anything",
description: "Server, Containers, API's, Websites, IoT and more.",
description: "Server, Containers, APIs, Websites, IoT and more.",
productColumn: "",
oneuptimeColumn: "tick",
},
@@ -422,7 +422,7 @@ const products: Dictionary<Product> = {
data: [
{
title: "Monitor anything",
description: "Server, Containers, API's, Websites, IoT and more.",
description: "Server, Containers, APIs, Websites, IoT and more.",
productColumn: "Monitors only API and Websites.",
oneuptimeColumn: "tick",
},
@@ -572,7 +572,7 @@ const products: Dictionary<Product> = {
data: [
{
title: "Monitor anything",
description: "Server, Containers, API's, Websites, IoT and more.",
description: "Server, Containers, APIs, Websites, IoT and more.",
productColumn: "",
oneuptimeColumn: "tick",
},

View File

@@ -11,6 +11,90 @@ interface CachedSitemap {
generatedAt: number; // epoch ms
}
// Priority and changefreq configuration for different page types
type ChangeFrequency =
| "always"
| "hourly"
| "daily"
| "weekly"
| "monthly"
| "yearly"
| "never";
interface SitemapPageConfig {
priority: number;
changefreq: ChangeFrequency;
}
const PAGE_CONFIG: Record<string, SitemapPageConfig> = {
// Homepage - highest priority
"/": { priority: 1.0, changefreq: "daily" },
// Core product pages - high priority
"/product/status-page": { priority: 0.9, changefreq: "weekly" },
"/product/monitoring": { priority: 0.9, changefreq: "weekly" },
"/product/incident-management": { priority: 0.9, changefreq: "weekly" },
"/product/on-call": { priority: 0.9, changefreq: "weekly" },
"/product/logs-management": { priority: 0.9, changefreq: "weekly" },
"/product/apm": { priority: 0.9, changefreq: "weekly" },
"/product/workflows": { priority: 0.9, changefreq: "weekly" },
// Important pages
"/pricing": { priority: 0.9, changefreq: "weekly" },
"/enterprise/demo": { priority: 0.9, changefreq: "weekly" },
"/enterprise/overview": { priority: 0.8, changefreq: "weekly" },
"/about": { priority: 0.7, changefreq: "weekly" },
"/support": { priority: 0.7, changefreq: "weekly" },
// Documentation and reference
"/docs": { priority: 0.7, changefreq: "weekly" },
"/reference": { priority: 0.7, changefreq: "weekly" },
// Blog section
"/blog": { priority: 0.7, changefreq: "daily" },
// Community and legal
"/oss-friends": { priority: 0.3, changefreq: "monthly" },
};
// Default config for pages not explicitly listed
const DEFAULT_CONFIG: SitemapPageConfig = {
priority: 0.5,
changefreq: "monthly",
};
// Blog post config
const BLOG_POST_CONFIG: SitemapPageConfig = {
priority: 0.6,
changefreq: "monthly",
};
// Blog tag config
const BLOG_TAG_CONFIG: SitemapPageConfig = {
priority: 0.4,
changefreq: "weekly",
};
// Compare page config
const COMPARE_PAGE_CONFIG: SitemapPageConfig = {
priority: 0.7,
changefreq: "monthly",
};
function getPageConfig(path: string): SitemapPageConfig {
// Check for exact match first
if (PAGE_CONFIG[path]) {
return PAGE_CONFIG[path];
}
// Check for prefix matches
if (path.startsWith("/legal")) {
return { priority: 0.3, changefreq: "monthly" };
}
return DEFAULT_CONFIG;
}
// 10 minutes TTL
const TTL_MS: number = 10 * 60 * 1000;
let cache: CachedSitemap | null = null;
@@ -102,7 +186,7 @@ export const generateSitemapXml: () => Promise<string> =
// Blog posts
const blogPosts: Array<BlogPostHeader> =
await BlogPostUtil.getBlogPostList();
const blogPostEntries: any[] = blogPosts.map((post: BlogPostHeader) => {
const blogPostEntries: Entry[] = blogPosts.map((post: BlogPostHeader) => {
// post.blogUrl already contains /blog/post/<slug>/view relative or absolute? In BlogPostUtil it's relative (starts with /blog...), so ensure absolute.
const loc: string = post.blogUrl.startsWith("http")
? post.blogUrl
@@ -110,16 +194,20 @@ export const generateSitemapXml: () => Promise<string> =
return {
loc,
lastmod: new Date(post.postDate).toISOString(),
priority: BLOG_POST_CONFIG.priority,
changefreq: BLOG_POST_CONFIG.changefreq,
};
});
// Blog tags
const tags: string[] = await BlogPostUtil.getTags();
const tagEntries: any[] = tags.map((tag: string) => {
const tagEntries: Entry[] = tags.map((tag: string) => {
const tagSlug: string = tag.toLowerCase().replace(/\s+/g, "-").trim();
return {
loc: `${baseUrl.toString()}blog/tag/${tagSlug}`,
lastmod: OneUptimeDate.getCurrentDate().toISOString(),
priority: BLOG_TAG_CONFIG.priority,
changefreq: BLOG_TAG_CONFIG.changefreq,
};
});
@@ -128,18 +216,25 @@ export const generateSitemapXml: () => Promise<string> =
interface Entry {
loc: string;
lastmod: string;
priority: number;
changefreq: ChangeFrequency;
}
const entries: Entry[] = [
...staticPaths.map((p: string) => {
const config: SitemapPageConfig = getPageConfig(p);
return {
loc: `${baseUrl.toString()}${p.replace(/^\//, "")}`,
lastmod: timestamp,
priority: config.priority,
changefreq: config.changefreq,
};
}),
...productComparePaths.map((p: string) => {
return {
loc: `${baseUrl.toString()}${p.replace(/^\//, "")}`,
lastmod: timestamp,
priority: COMPARE_PAGE_CONFIG.priority,
changefreq: COMPARE_PAGE_CONFIG.changefreq,
};
}),
...blogPostEntries,
@@ -174,6 +269,8 @@ export const generateSitemapXml: () => Promise<string> =
const urlEle: XMLBuilder = urlset.ele("url");
urlEle.ele("loc").txt(entry.loc);
urlEle.ele("lastmod").txt(entry.lastmod);
urlEle.ele("changefreq").txt(entry.changefreq);
urlEle.ele("priority").txt(entry.priority.toFixed(1));
}
const xml: string = urlset.end({ prettyPrint: true });

View File

@@ -4,7 +4,7 @@
<div class="py-16">
<div class="mx-auto max-w-8xl px-6 lg:px-8">
<div class="mx-auto max-w-4xl text-center">
<h1 class="text-3xl font-bold tracking-tight text-gray-900">OneUptime is open source observability platform.</h1>
<h1 class="text-3xl font-bold tracking-tight text-gray-900">OneUptime is an open source observability platform.</h1>
<p class="mt-6 text-lg leading-8 text-gray-600">Monitor, Observe, Debug, Resolve. Everything you need to build reliable software in one open source platform. Get started today.</p>
<div class="mt-10 flex items-center justify-center gap-x-6">
<a href="/accounts/register" class="rounded-md bg-indigo-600 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Get started</a>

Some files were not shown because too many files have changed in this diff Show More