Compare commits

...

118 Commits

Author SHA1 Message Date
Simon Larsen
d708fbbb52 feat: Add proxy configuration examples to Custom Probe documentation and component 2025-09-01 14:49:40 +01:00
Simon Larsen
03bceb959e feat: Enhance proxy support in SSL and Synthetic Monitors to prefer HTTPS, fallback to HTTP 2025-09-01 14:47:01 +01:00
Simon Larsen
efa411206e feat: Update SSL and Synthetic Monitors to use HTTPS proxy configuration 2025-09-01 14:41:57 +01:00
Simon Larsen
27fd99f2e8 feat: Update proxy configuration to support separate HTTP and HTTPS proxy URLs 2025-09-01 14:40:54 +01:00
Simon Larsen
07361bfeb7 feat: Enhance SyntheticMonitor with proxy support in browser launch options 2025-09-01 14:07:15 +01:00
Simon Larsen
bc8a5be0fa feat: Add proxy support for CustomCodeMonitor and SyntheticMonitor with logging 2025-09-01 14:00:55 +01:00
Simon Larsen
518768078a feat: Implement proxy configuration for HTTP requests and add ProxyConfig utility 2025-09-01 13:56:29 +01:00
Simon Larsen
86e95f99ff feat: Add PROXY_URL configuration option for probe and update example env file 2025-09-01 12:52:50 +01:00
Simon Larsen
ea48f56097 feat: Add custom styles for code blocks in blog posts 2025-09-01 12:43:03 +01:00
Nawaz Dhandala
b8b9dd859a Refactor migration files for consistency and readability; update BillingService and ProjectService for improved code clarity; enhance Countries interface formatting; standardize string quotes in various components; fix minor formatting issues in Settings and SendAnnouncementCreatedNotification. 2025-08-27 14:50:48 +01:00
Simon Larsen
d28c14ef24 feat: Update projectId reference in status page notification logic 2025-08-27 14:49:33 +01:00
Simon Larsen
670bec2a12 feat: Validate projectId and statusPageId in getStatusPageLinkInDashboard method 2025-08-27 14:48:40 +01:00
Simon Larsen
aff24845a8 feat: Add financeAccountingEmail handling in updateCustomerBusinessDetails method 2025-08-27 14:26:59 +01:00
Simon Larsen
f280e97c1b feat: Add migration for financeAccountingEmail field in Project model 2025-08-27 14:14:02 +01:00
Simon Larsen
62facf62dd feat: Add financeAccountingEmail field to Project model and update billing settings 2025-08-27 14:12:16 +01:00
Simon Larsen
db0387d81a feat: Update placeholder condition to include empty string check 2025-08-27 14:06:33 +01:00
Simon Larsen
5c4b19ab3d feat: Update nodemon configurations to improve performance and debugging options 2025-08-27 13:41:05 +01:00
Simon Larsen
463755fa4d This will be synced to Stripe and appear on future invoices. 2025-08-27 13:40:26 +01:00
Simon Larsen
85888572de feat: Update subscriber notification statuses to 'Success' for existing records in Incident and related tables 2025-08-27 13:20:07 +01:00
Simon Larsen
475bb25b2d feat: Add businessDetailsCountry field to Project migration and update index 2025-08-27 13:14:40 +01:00
Simon Larsen
badd200aed feat: Add country selection dropdown for billing details and implement country options 2025-08-27 13:14:01 +01:00
Simon Larsen
b40d87cbc9 feat: Add business details country field to Project model and update billing services to handle country code 2025-08-27 13:05:54 +01:00
Simon Larsen
36d0066b3a refactor: Simplify migration by removing unnecessary constraints and columns from Project and GlobalConfig tables 2025-08-27 13:03:01 +01:00
Simon Larsen
a49a0b2cba fix: Ensure blog post cards maintain full height for consistent layout 2025-08-27 12:48:41 +01:00
Simon Larsen
bada97d474 feat: Enhance customer address handling in Stripe by mapping business details to structured address fields 2025-08-27 12:42:17 +01:00
Simon Larsen
a1699f2d55 feat: Add business details field to Project model and update Stripe customer details 2025-08-27 12:37:09 +01:00
Simon Larsen
a11e054291 feat: Add custom link rendering with Tailwind styles and external link handling 2025-08-27 11:19:20 +01:00
Simon Larsen
47cf7ba763 fix: Increase timeout for SSL provisioning and delete old data jobs to accommodate longer processing times 2025-08-27 10:16:35 +01:00
Nawaz Dhandala
4e0dfb3664 fix: Simplify BadDataException handling for disabled and missing monitors in ingestion processes 2025-08-27 10:12:13 +01:00
Simon Larsen
250cb9e547 fix: Gracefully handle expected BadDataException cases for disabled and missing monitors in probe ingestion 2025-08-27 10:10:59 +01:00
Simon Larsen
541257e3c6 fix: Handle expected BadDataException cases for disabled and missing monitors in server monitor ingestion 2025-08-27 10:10:23 +01:00
Simon Larsen
ed43686736 fix: Centralize "Monitor disabled" message and improve error handling for disabled monitors 2025-08-27 10:09:26 +01:00
Simon Larsen
9ca45f23e3 fix: Replace hardcoded "Monitor not found" messages with centralized exception messages 2025-08-27 09:58:34 +01:00
Simon Larsen
e3573a9b77 fix: Refactor monitor not found error handling to use centralized exception messages 2025-08-27 09:55:19 +01:00
Simon Larsen
c9e78044e6 fix: Improve error handling in incoming request ingestion worker to handle disabled monitors gracefully 2025-08-27 09:48:54 +01:00
Nawaz Dhandala
813581dec5 fix: Add return type to logoutUser method and specify type for route in navigateToLoginPage method 2025-08-26 21:39:35 +01:00
Nawaz Dhandala
e528decf73 fix: Refactor QueueWorker options handling; improve logoutUser method formatting and navigation logic in StatusPageUtil 2025-08-26 21:36:56 +01:00
Simon Larsen
42ef41ede8 fix: Enhance QueueWorker options with lock duration and max stalled count; improve telemetry processing with yielding to avoid stall detection 2025-08-26 21:32:38 +01:00
Simon Larsen
af26472db4 fix: Simplify email validation logic and improve user lookup in SCIM user operations 2025-08-26 21:24:39 +01:00
Simon Larsen
44b5c8b668 fix: Enhance email validation and logging in SCIM user operations 2025-08-26 20:47:50 +01:00
Simon Larsen
d821b88ed7 fix: Update Docker image tags and labels for multiple services in release workflow 2025-08-26 18:52:15 +01:00
Simon Larsen
1df43e21ff fix: Refactor logoutUser method and enhance navigation logging in StatusPageUtil 2025-08-26 18:37:49 +01:00
Simon Larsen
76ca6ee7e1 fix: Add missing continuation for APP_VERSION build argument in multiple Docker image deploy jobs 2025-08-26 18:25:32 +01:00
Simon Larsen
dac731a57b refactor: Remove unused mock for ProjectUserService in TeamMemberService tests 2025-08-26 16:56:52 +01:00
Simon Larsen
0f4b248598 fix: Add missing context for Docker image build in nginx deployment 2025-08-26 16:53:25 +01:00
Simon Larsen
b2c14e0380 fix: Add missing build context for multiple Docker image deploy jobs 2025-08-26 16:51:51 +01:00
Simon Larsen
3ab9705bbe fix: Allow deletion of teams in Users component by setting isDeleteable to true 2025-08-26 16:34:19 +01:00
Nawaz Dhandala
40812c8749 refactor: Clean up whitespace in TeamMemberService and SCIM files; update description formatting in Users component 2025-08-26 15:59:22 +01:00
Simon Larsen
45ae1501f2 refactor: Replace ProjectUser with TeamMember in SCIM query for team members 2025-08-26 15:55:23 +01:00
Simon Larsen
13d9f19606 refactor: Remove ProjectUserService calls to streamline TeamMemberService operations 2025-08-26 15:52:28 +01:00
Simon Larsen
ad3221310a refactor: Remove ProjectUser model and associated service to streamline user management 2025-08-26 15:51:06 +01:00
Simon Larsen
659042fcfb fix: Update isCreateable property to false for Teams component 2025-08-26 14:43:54 +01:00
Simon Larsen
d65b9c7b29 feat: Refactor Teams component to use TeamMember model and update filtering logic 2025-08-26 14:42:14 +01:00
Simon Larsen
dc77206e6f feat: Add debug logging for SCIM team operations to track user additions 2025-08-26 14:25:31 +01:00
Nawaz Dhandala
9c1910d3f1 refactor: Remove unnecessary context argument from Docker build commands in workflows 2025-08-26 14:09:31 +01:00
Nawaz Dhandala
afe8f8e6f4 refactor: Implement retry mechanism for account and isolated VM compilation steps in workflows 2025-08-26 13:15:54 +01:00
Nawaz Dhandala
015bd0f870 refactor: Implement retry mechanism for Docker image builds in multiple workflows 2025-08-26 13:12:18 +01:00
Nawaz Dhandala
383c145186 refactor: Integrate retry mechanism for Docker image builds in workflows 2025-08-26 13:06:55 +01:00
Simon Larsen
f155795e6b fix: Remove single quotes from changelog delimiter for correct output formatting 2025-08-26 11:56:22 +01:00
Nawaz Dhandala
757f5b5721 refactor: Improve type annotations for better clarity in error handling 2025-08-26 11:42:15 +01:00
Nawaz Dhandala
694215df06 refactor: Improve code formatting for better readability in multiple files 2025-08-26 11:40:01 +01:00
Nawaz Dhandala
0eb6022f1d Merge branch 'master' of https://github.com/OneUptime/oneuptime 2025-08-26 11:38:10 +01:00
Nawaz Dhandala
3109006828 refactor: Replace regex literals with RegExp constructor for improved clarity in MarkdownViewer and StringUtils 2025-08-26 11:38:08 +01:00
Simon Larsen
272695bd11 feat: Enhance error handling and logging in processMetricsAsync and processTracesAsync methods for improved robustness 2025-08-26 11:36:47 +01:00
Simon Larsen
330e3bc106 fix: Correct syntax error in JavaScript expression example for incoming request monitors 2025-08-26 11:26:01 +01:00
Nawaz Dhandala
c7876bf3a3 refactor: Improve regex pattern for fact extraction and enhance code formatting in OtelIngestService 2025-08-26 11:22:36 +01:00
Simon Larsen
345ada5404 feat: Enhance error handling and logging in processLogsAsync method for improved telemetry data ingestion 2025-08-26 11:16:32 +01:00
Simon Larsen
4f97b1b460 feat: Enhance image loading and layout stability across blog views 2025-08-25 10:46:19 +01:00
Nawaz Dhandala
e35ef1809f refactor: Improve code formatting and consistency across Microsoft Teams integration files 2025-08-21 14:45:34 +01:00
Simon Larsen
c2926f3542 feat: Implement structured MessageCard creation from markdown for Microsoft Teams
- Added a method to build a structured MessageCard from markdown input, enhancing message formatting for Teams.
- Extracted title, facts, and actions from markdown to improve rendering in Teams notifications.
2025-08-21 14:43:13 +01:00
Simon Larsen
9495b4bd47 feat: Add Microsoft Teams incoming webhook option to subscriber settings
- Introduced a new property for Microsoft Teams incoming webhook URL in the StatusPageSubscriberService.
- Enhanced subscriber configuration to support Microsoft Teams notifications.
2025-08-21 14:25:49 +01:00
Simon Larsen
ad3f36fdf5 refactor: Simplify Slack and Microsoft Teams notification handling in StatusPageSubscriberService
- Removed try-catch blocks for sending notifications and replaced them with promise chaining for better readability.
- Added logging for successful notification sends and error handling directly in the promise catch.
2025-08-21 14:11:06 +01:00
Simon Larsen
f2221b0a40 feat: Implement Microsoft Teams webhook validation and notification in StatusPage subscriber service
- Added validation for Microsoft Teams incoming webhook URL during subscriber setup.
- Implemented notification sending to Microsoft Teams channel upon successful subscription.
- Updated SideMenu components to reflect the new naming convention for Microsoft Teams subscribers.
2025-08-21 14:04:20 +01:00
Simon Larsen
62fbc1f4be feat: Enhance Microsoft Teams subscriber validation and handling in StatusPage API
- Added validation to ensure Microsoft Teams subscribers are only processed if enabled.
- Updated error messages to include Microsoft Teams workspace name requirements.
- Implemented handling for Microsoft Teams incoming webhook URL and workspace name in subscriber setup.
2025-08-21 13:52:35 +01:00
Simon Larsen
054a2bc8f5 feat: Enable Microsoft Teams subscribers in StatusPage API 2025-08-21 13:49:28 +01:00
Simon Larsen
896787109c feat: Add Microsoft Teams subscriber option to Email, Slack, and SMS subscription pages 2025-08-21 13:47:13 +01:00
Simon Larsen
3a55fcc872 feat: Update microsoftTeamsIncomingWebhookUrl column type to text and add migration 2025-08-21 13:27:44 +01:00
Simon Larsen
2945a48d05 feat: Update microsoftTeamsWorkspaceName column type to VeryLongText and add migration 2025-08-21 13:15:44 +01:00
Simon Larsen
da3a7ddb2e feat: Add migration for Microsoft Teams subscriber functionality in StatusPage 2025-08-21 12:57:16 +01:00
Simon Larsen
04a0bfedaa fix: Make Microsoft Teams subscriber prop required in SideMenu component 2025-08-21 12:17:00 +01:00
Simon Larsen
fa5c7b1e73 feat: Add Microsoft Teams subscriber functionality
- Implemented Microsoft Teams subscribers in the dashboard side menu.
- Created Microsoft Teams subscriber settings page with toggle options.
- Added routes for Microsoft Teams subscribers in the routing configuration.
- Updated page map and route map to include Microsoft Teams subscriber paths.
- Enhanced the app state management to handle Microsoft Teams subscription settings.
- Integrated Microsoft Teams notification sending in the announcement and incident jobs.
- Developed Microsoft Teams subscription management and creation forms.
- Added UI components for managing Microsoft Teams subscribers in the status page.
2025-08-21 12:09:53 +01:00
Simon Larsen
a1c2918cd7 feat: Update homepage heading for improved clarity and emphasis on monitoring capabilities 2025-08-21 10:24:49 +01:00
Simon Larsen
91b11b12c1 fix: Update canonical links for blog posts and remove redundant canonical tags in head partials 2025-08-21 10:15:52 +01:00
Simon Larsen
778a34d631 feat: Implement fallback to commit messages in changelog if empty 2025-08-20 09:17:53 +01:00
Simon Larsen
6dbd838ca4 refactor: Remove mobile redirect script from homepage for cleaner code 2025-08-19 22:23:20 +01:00
Simon Larsen
e09634dc6f feat: Enhance blog listing with featured post display and improved layout 2025-08-19 22:21:12 +01:00
Simon Larsen
af60715de2 Merge branch 'master' of github.com:OneUptime/oneuptime 2025-08-19 22:11:14 +01:00
Simon Larsen
3b4c54876e feat: Update default page size for blog pagination from 50 to 24 for improved performance 2025-08-19 22:11:12 +01:00
Nawaz Dhandala
e357100e46 refactor: Remove redundant dynamic section checks from sitemap tests 2025-08-19 20:43:08 +01:00
Simon Larsen
7f3a50076d feat: Add reference section check to sitemap tests for improved coverage 2025-08-19 14:25:37 +01:00
Simon Larsen
9d182b6d55 feat: Add additional static paths to sitemap generation for improved coverage 2025-08-19 14:25:16 +01:00
Simon Larsen
33fce0b53c feat: Implement caching for home URL retrieval to improve performance 2025-08-19 14:15:47 +01:00
Nawaz Dhandala
3db8419349 refactor: Simplify error handling in BlogPostUtil by removing unused error variables 2025-08-19 13:50:35 +01:00
Nawaz Dhandala
dfc324b099 refactor: Improve code formatting and readability in Markdown and BlogPost utilities 2025-08-19 12:58:17 +01:00
Simon Larsen
36521ef37c feat: Update blog post listing to improve layout and add Open Source Commitment section 2025-08-19 12:55:09 +01:00
Simon Larsen
a6f336340e feat: Implement pagination and tag filtering for blog posts 2025-08-19 12:40:51 +01:00
Simon Larsen
c36f782192 feat: Refactor blog post listing and tags display for improved layout and user experience 2025-08-19 12:34:23 +01:00
Simon Larsen
5219f1cfc0 feat: Remove unused imports and the getNameOfGitHubUser function from BlogPostUtil class 2025-08-19 12:29:27 +01:00
Simon Larsen
7f84d50baa feat: Add optional bio field for authors and update blog post template to display it 2025-08-19 12:28:12 +01:00
Simon Larsen
cd2ce3f1a8 feat: Add a newline for improved readability in BlogPostUtil class 2025-08-19 12:15:23 +01:00
Simon Larsen
01b0e01ca8 feat: Implement caching for blog metadata and improve author resolution without GitHub API calls 2025-08-19 12:07:33 +01:00
Simon Larsen
73dc6bb5db feat: Remove social media image display from blog post template 2025-08-19 11:55:38 +01:00
Simon Larsen
ab7fc1c244 feat: Enhance Markdown renderer with improved code block styling and add support for horizontal rules, emphasis, and strikethrough 2025-08-19 11:50:48 +01:00
Simon Larsen
3927bea29c feat: Enhance Markdown rendering with improved list, table, and inline code support; update tag display styling for better visual consistency 2025-08-19 11:47:21 +01:00
Simon Larsen
6060d66c2b feat: Revamp blog post layout with enhanced styling, author details, and reading time estimation 2025-08-19 11:37:10 +01:00
Nawaz Dhandala
9f4869b05f test: Enhance sitemap tests to validate presence of dynamic sections 2025-08-19 10:07:11 +01:00
Nawaz Dhandala
17bdfee012 refactor: Simplify regex usage in sitemap tests and improve middleware formatting 2025-08-19 10:05:59 +01:00
Simon Larsen
4988b9fc7a feat: Refactor mobile menu implementation for improved accessibility and usability 2025-08-19 10:04:20 +01:00
Simon Larsen
9edc6b9f18 feat: Enhance mobile navigation and improve header styling for better accessibility 2025-08-19 09:58:03 +01:00
Simon Larsen
525e19faa6 feat: Improve tab accessibility and keyboard navigation with semantic identifiers 2025-08-19 09:49:23 +01:00
Simon Larsen
588e8976d2 feat: Add middleware to inject home URL for canonical links and update canonical tag in head-basic.ejs 2025-08-19 09:47:59 +01:00
Simon Larsen
d5e28e98fb Merge branch 'master' of github.com:OneUptime/oneuptime 2025-08-19 09:39:45 +01:00
Simon Larsen
0e84bc9c40 feat: Enhance accessibility and keyboard navigation for product tabs 2025-08-19 09:39:42 +01:00
Nawaz Dhandala
39e8b1da6b refactor: Enhance type annotations and improve code readability in sitemap tests 2025-08-18 13:47:35 +01:00
Nawaz Dhandala
66c4badd94 refactor: Improve formatting of product pages check in sitemap tests 2025-08-18 13:41:01 +01:00
Simon Larsen
a245fabc34 feat: Add end-to-end tests for sitemap loading and validation 2025-08-18 13:39:19 +01:00
Nawaz Dhandala
fa9fce2774 refactor: Improve type annotations and error handling in various modules 2025-08-18 12:59:17 +01:00
Nawaz Dhandala
a256f4be54 Refactor logging statements for improved readability and consistency across services
- Updated logging statements in Sitemap.ts to enhance code clarity.
- Reformatted logger.info calls in IncomingRequestIngest, OpenTelemetryIngest, Probe, ProbeIngest, and ServerMonitorIngest to use multi-line formatting for better readability.
- Adjusted import statements in Probe to follow a consistent multi-line format.
2025-08-18 12:48:23 +01:00
144 changed files with 6644 additions and 3222 deletions

View File

@@ -23,7 +23,11 @@ jobs:
# build image for accounts service
- name: build docker image
run: sudo docker build -f ./Accounts/Dockerfile .
uses: nick-fields/retry@v3
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build -f ./Accounts/Dockerfile .
docker-build-isolated-vm:
runs-on: ubuntu-latest
@@ -38,7 +42,11 @@ jobs:
# build image for accounts service
- name: build docker image
run: sudo docker build -f ./IsolatedVM/Dockerfile .
uses: nick-fields/retry@v3
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build -f ./IsolatedVM/Dockerfile .
docker-build-home:
runs-on: ubuntu-latest
@@ -53,7 +61,11 @@ jobs:
# build image for accounts service
- name: build docker image
run: sudo docker build -f ./Home/Dockerfile .
uses: nick-fields/retry@v3
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build -f ./Home/Dockerfile .
docker-build-worker:
runs-on: ubuntu-latest
@@ -68,7 +80,11 @@ jobs:
# build image for accounts service
- name: build docker image
run: sudo docker build -f ./Worker/Dockerfile .
uses: nick-fields/retry@v3
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build -f ./Worker/Dockerfile .
docker-build-workflow:
runs-on: ubuntu-latest
@@ -83,7 +99,11 @@ jobs:
# build image for accounts service
- name: build docker image
run: sudo docker build -f ./Workflow/Dockerfile .
uses: nick-fields/retry@v3
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build -f ./Workflow/Dockerfile .
docker-build-api-reference:
runs-on: ubuntu-latest
@@ -98,7 +118,11 @@ jobs:
# build image for accounts service
- name: build docker image
run: sudo docker build -f ./APIReference/Dockerfile .
uses: nick-fields/retry@v3
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build -f ./APIReference/Dockerfile .
docker-build-docs:
runs-on: ubuntu-latest
@@ -113,7 +137,11 @@ jobs:
# build image for accounts service
- name: build docker image
run: sudo docker build -f ./Docs/Dockerfile .
uses: nick-fields/retry@v3
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build -f ./Docs/Dockerfile .
docker-build-otel-collector:
@@ -129,7 +157,11 @@ jobs:
# build image for accounts service
- name: build docker image
run: sudo docker build -f ./OTelCollector/Dockerfile .
uses: nick-fields/retry@v3
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build -f ./OTelCollector/Dockerfile .
docker-build-app:
runs-on: ubuntu-latest
@@ -145,7 +177,11 @@ jobs:
# build image for accounts service
- name: build docker image
run: sudo docker build -f ./App/Dockerfile .
uses: nick-fields/retry@v3
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build -f ./App/Dockerfile .
docker-build-copilot:
@@ -161,7 +197,11 @@ jobs:
# build image for accounts service
- name: build docker image
run: sudo docker build -f ./Copilot/Dockerfile .
uses: nick-fields/retry@v3
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build -f ./Copilot/Dockerfile .
docker-build-e2e:
runs-on: ubuntu-latest
@@ -177,7 +217,11 @@ jobs:
# build image for accounts service
- name: build docker image
run: sudo docker build -f ./E2E/Dockerfile .
uses: nick-fields/retry@v3
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build -f ./E2E/Dockerfile .
docker-build-admin-dashboard:
runs-on: ubuntu-latest
@@ -192,7 +236,11 @@ jobs:
# build image for home
- name: build docker image
run: sudo docker build -f ./AdminDashboard/Dockerfile .
uses: nick-fields/retry@v3
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build -f ./AdminDashboard/Dockerfile .
docker-build-dashboard:
runs-on: ubuntu-latest
@@ -207,7 +255,11 @@ jobs:
# build image for home
- name: build docker image
run: sudo docker build -f ./Dashboard/Dockerfile .
uses: nick-fields/retry@v3
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build -f ./Dashboard/Dockerfile .
docker-build-probe:
runs-on: ubuntu-latest
@@ -222,7 +274,11 @@ jobs:
# build image probe api
- name: build docker image
run: sudo docker build -f ./Probe/Dockerfile .
uses: nick-fields/retry@v3
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build -f ./Probe/Dockerfile .
docker-build-probe-ingest:
runs-on: ubuntu-latest
@@ -237,7 +293,11 @@ jobs:
# build image probe api
- name: build docker image
run: sudo docker build -f ./ProbeIngest/Dockerfile .
uses: nick-fields/retry@v3
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build -f ./ProbeIngest/Dockerfile .
docker-build-server-monitor-ingest:
runs-on: ubuntu-latest
@@ -252,7 +312,11 @@ jobs:
# build image probe api
- name: build docker image
run: sudo docker build -f ./ServerMonitorIngest/Dockerfile .
uses: nick-fields/retry@v3
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build -f ./ServerMonitorIngest/Dockerfile .
docker-build-open-telemetry-ingest:
runs-on: ubuntu-latest
@@ -267,7 +331,11 @@ jobs:
# build image probe api
- name: build docker image
run: sudo docker build -f ./OpenTelemetryIngest/Dockerfile .
uses: nick-fields/retry@v3
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build -f ./OpenTelemetryIngest/Dockerfile .
docker-build-incoming-request-ingest:
runs-on: ubuntu-latest
@@ -282,7 +350,11 @@ jobs:
# build image probe api
- name: build docker image
run: sudo docker build -f ./IncomingRequestIngest/Dockerfile .
uses: nick-fields/retry@v3
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build -f ./IncomingRequestIngest/Dockerfile .
docker-build-fluent-ingest:
runs-on: ubuntu-latest
@@ -297,7 +369,11 @@ jobs:
# build image probe api
- name: build docker image
run: sudo docker build -f ./FluentIngest/Dockerfile .
uses: nick-fields/retry@v3
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build -f ./FluentIngest/Dockerfile .
docker-build-status-page:
runs-on: ubuntu-latest
@@ -312,7 +388,11 @@ jobs:
# build image for home
- name: build docker image
run: sudo docker build -f ./StatusPage/Dockerfile .
uses: nick-fields/retry@v3
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build -f ./StatusPage/Dockerfile .
docker-build-test-server:
runs-on: ubuntu-latest
@@ -327,4 +407,8 @@ jobs:
# build image for mail service
- name: build docker image
run: sudo docker build -f ./TestServer/Dockerfile .
uses: nick-fields/retry@v3
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build -f ./TestServer/Dockerfile .

View File

@@ -20,7 +20,12 @@ jobs:
with:
node-version: latest
- run: cd Common && npm install
- run: cd Accounts && npm install && npm run compile && npm run dep-check
- name: Compile Accounts
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd Accounts && npm install && npm run compile && npm run dep-check
compile-isolated-vm:
runs-on: ubuntu-latest
@@ -32,7 +37,12 @@ jobs:
with:
node-version: latest
- run: cd Common && npm install
- run: cd IsolatedVM && npm install && npm run compile && npm run dep-check
- name: Compile IsolatedVM
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd IsolatedVM && npm install && npm run compile && npm run dep-check
compile-common:
runs-on: ubuntu-latest
@@ -43,7 +53,12 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: latest
- run: cd Common && npm install && npm run compile && npm run dep-check
- name: Compile Common
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd Common && npm install && npm run compile && npm run dep-check
compile-app:
runs-on: ubuntu-latest
@@ -55,7 +70,12 @@ jobs:
with:
node-version: latest
- run: cd Common && npm install
- run: cd App && npm install && npm run compile && npm run dep-check
- name: Compile App
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd App && npm install && npm run compile && npm run dep-check
compile-home:
runs-on: ubuntu-latest
@@ -67,7 +87,12 @@ jobs:
with:
node-version: latest
- run: cd Common && npm install
- run: cd Home && npm install && npm run compile && npm run dep-check
- name: Compile Home
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd Home && npm install && npm run compile && npm run dep-check
compile-worker:
runs-on: ubuntu-latest
@@ -79,7 +104,12 @@ jobs:
with:
node-version: latest
- run: cd Common && npm install
- run: cd Worker && npm install && npm run compile && npm run dep-check
- name: Compile Worker
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd Worker && npm install && npm run compile && npm run dep-check
compile-workflow:
runs-on: ubuntu-latest
@@ -91,7 +121,12 @@ jobs:
with:
node-version: latest
- run: cd Common && npm install
- run: cd Workflow && npm install && npm run compile && npm run dep-check
- name: Compile Workflow
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd Workflow && npm install && npm run compile && npm run dep-check
compile-api-reference:
runs-on: ubuntu-latest
@@ -103,7 +138,12 @@ jobs:
with:
node-version: latest
- run: cd Common && npm install
- run: cd APIReference && npm install && npm run compile && npm run dep-check
- name: Compile API Reference
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd APIReference && npm install && npm run compile && npm run dep-check
compile-docs-reference:
runs-on: ubuntu-latest
@@ -115,7 +155,12 @@ jobs:
with:
node-version: latest
- run: cd Common && npm install
- run: cd Docs && npm install && npm run compile && npm run dep-check
- name: Compile Docs Reference
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd Docs && npm install && npm run compile && npm run dep-check
compile-copilot:
runs-on: ubuntu-latest
@@ -127,7 +172,12 @@ jobs:
with:
node-version: latest
- run: cd Common && npm install
- run: cd Copilot && npm install && npm run compile && npm run dep-check
- name: Compile Copilot
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd Copilot && npm install && npm run compile && npm run dep-check
compile-nginx:
runs-on: ubuntu-latest
@@ -140,7 +190,12 @@ jobs:
node-version: latest
- run: cd Common && npm install
- run: cd Nginx && npm install && npm run compile && npm run dep-check
- name: Compile Nginx
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd Nginx && npm install && npm run compile && npm run dep-check
compile-infrastructure-agent:
runs-on: ubuntu-latest
@@ -150,7 +205,12 @@ jobs:
- uses: actions/checkout@v4
# Setup Go
- uses: actions/setup-go@v5
- run: cd InfrastructureAgent && go build .
- name: Compile Infrastructure Agent
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd InfrastructureAgent && go build .
compile-admin-dashboard:
@@ -164,7 +224,12 @@ jobs:
node-version: latest
- run: cd Common && npm install
- run: cd AdminDashboard && npm install && npm run compile && npm run dep-check
- name: Compile Admin Dashboard
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd AdminDashboard && npm install && npm run compile && npm run dep-check
compile-dashboard:
runs-on: ubuntu-latest
@@ -177,7 +242,12 @@ jobs:
node-version: latest
- run: cd Common && npm install
- run: cd Dashboard && npm install && npm run compile && npm run dep-check
- name: Compile Dashboard
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd Dashboard && npm install && npm run compile && npm run dep-check
compile-e2e:
@@ -191,7 +261,12 @@ jobs:
node-version: latest
- run: sudo apt-get update
- run: cd Common && npm install
- run: cd E2E && npm install && npm run compile && npm run dep-check
- name: Compile E2E
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd E2E && npm install && npm run compile && npm run dep-check
compile-probe:
runs-on: ubuntu-latest
@@ -203,7 +278,12 @@ jobs:
with:
node-version: latest
- run: cd Common && npm install
- run: cd Probe && npm install && npm run compile && npm run dep-check
- name: Compile Probe
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd Probe && npm install && npm run compile && npm run dep-check
compile-probe-ingest:
runs-on: ubuntu-latest
@@ -215,7 +295,12 @@ jobs:
with:
node-version: latest
- run: cd Common && npm install
- run: cd ProbeIngest && npm install && npm run compile && npm run dep-check
- name: Compile Probe Ingest
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd ProbeIngest && npm install && npm run compile && npm run dep-check
compile-server-monitor-ingest:
runs-on: ubuntu-latest
@@ -227,7 +312,12 @@ jobs:
with:
node-version: latest
- run: cd Common && npm install
- run: cd ServerMonitorIngest && npm install && npm run compile && npm run dep-check
- name: Compile Server Monitor Ingest
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd ServerMonitorIngest && npm install && npm run compile && npm run dep-check
compile-open-telemetry-ingest:
runs-on: ubuntu-latest
@@ -239,7 +329,12 @@ jobs:
with:
node-version: latest
- run: cd Common && npm install
- run: cd OpenTelemetryIngest && npm install && npm run compile && npm run dep-check
- name: Compile Open Telemetry Ingest
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd OpenTelemetryIngest && npm install && npm run compile && npm run dep-check
compile-incoming-request-ingest:
@@ -252,7 +347,12 @@ jobs:
with:
node-version: latest
- run: cd Common && npm install
- run: cd IncomingRequestIngest && npm install && npm run compile && npm run dep-check
- name: Compile Incoming Request Ingest
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd IncomingRequestIngest && npm install && npm run compile && npm run dep-check
compile-fluent-ingest:
runs-on: ubuntu-latest
@@ -264,7 +364,12 @@ jobs:
with:
node-version: latest
- run: cd Common && npm install
- run: cd FluentIngest && npm install && npm run compile && npm run dep-check
- name: Compile Fluent Ingest
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd FluentIngest && npm install && npm run compile && npm run dep-check
compile-status-page:
@@ -278,7 +383,12 @@ jobs:
node-version: latest
- run: cd Common && npm install
- run: cd StatusPage && npm install && npm run compile && npm run dep-check
- name: Compile Status Page
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd StatusPage && npm install && npm run compile && npm run dep-check
compile-test-server:
runs-on: ubuntu-latest
@@ -290,7 +400,12 @@ jobs:
with:
node-version: latest
- run: cd Common && npm install
- run: cd TestServer && npm install && npm run compile && npm run dep-check
- name: Compile Test Server
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd TestServer && npm install && npm run compile && npm run dep-check
compile-mcp:
runs-on: ubuntu-latest
@@ -302,4 +417,9 @@ jobs:
with:
node-version: latest
- run: cd Common && npm install
- run: cd MCP && npm install && npm run compile && npm run dep-check
- name: Compile MCP
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd MCP && npm install && npm run compile && npm run dep-check

View File

@@ -184,18 +184,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker images
run: |
docker buildx build \
--platform linux/amd64,linux/arm64 \
--file ./MCP/Dockerfile.tpl \
--tag oneuptime/mcp-server:${{ steps.version.outputs.version }} \
--tag oneuptime/mcp-server:release \
--tag ghcr.io/oneuptime/mcp-server:${{ steps.version.outputs.version }} \
--tag ghcr.io/oneuptime/mcp-server:release \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=${{ steps.version.outputs.version }} \
--push .
echo "✅ Pushed Docker images to Docker Hub and GitHub Container Registry"
uses: nick-fields/retry@v3
with:
timeout_minutes: 45
max_attempts: 3
command: |
docker buildx build \
--platform linux/amd64,linux/arm64 \
--file ./MCP/Dockerfile.tpl \
--tag oneuptime/mcp-server:${{ steps.version.outputs.version }} \
--tag oneuptime/mcp-server:release \
--tag ghcr.io/oneuptime/mcp-server:${{ steps.version.outputs.version }} \
--tag ghcr.io/oneuptime/mcp-server:release \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=${{ steps.version.outputs.version }} \
--push .
echo "✅ Pushed Docker images to Docker Hub and GitHub Container Registry"
- name: Upload MCP server artifact
uses: actions/upload-artifact@v4
@@ -252,17 +256,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./Nginx/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
timeout_minutes: 45
max_attempts: 3
command: |
docker buildx build \
--file ./Nginx/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--tag oneuptime/nginx:release \
--tag oneuptime/nginx:7.0.${{needs.generate-build-number.outputs.build_number}} \
--tag ghcr.io/oneuptime/nginx:release \
--tag ghcr.io/oneuptime/nginx:7.0.${{needs.generate-build-number.outputs.build_number}} \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
.
e2e-docker-image-deploy:
needs: [generate-build-number]
@@ -312,17 +321,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./E2E/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
timeout_minutes: 45
max_attempts: 3
command: |
docker buildx build \
--file ./E2E/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--tag oneuptime/e2e:release \
--tag oneuptime/e2e:7.0.${{needs.generate-build-number.outputs.build_number}} \
--tag ghcr.io/oneuptime/e2e:release \
--tag ghcr.io/oneuptime/e2e:7.0.${{needs.generate-build-number.outputs.build_number}} \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
.
isolated-vm-docker-image-deploy:
needs: [generate-build-number]
@@ -372,17 +386,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./IsolatedVM/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
timeout_minutes: 45
max_attempts: 3
command: |
docker buildx build \
--file ./IsolatedVM/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--tag oneuptime/isolated-vm:release \
--tag oneuptime/isolated-vm:7.0.${{needs.generate-build-number.outputs.build_number}} \
--tag ghcr.io/oneuptime/isolated-vm:release \
--tag ghcr.io/oneuptime/isolated-vm:7.0.${{needs.generate-build-number.outputs.build_number}} \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
.
home-docker-image-deploy:
needs: [generate-build-number]
@@ -432,17 +451,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./Home/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
timeout_minutes: 45
max_attempts: 3
command: |
docker buildx build \
--file ./Home/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--tag oneuptime/home:release \
--tag oneuptime/home:7.0.${{needs.generate-build-number.outputs.build_number}} \
--tag ghcr.io/oneuptime/home:release \
--tag ghcr.io/oneuptime/home:7.0.${{needs.generate-build-number.outputs.build_number}} \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
.
@@ -495,17 +519,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./TestServer/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
timeout_minutes: 45
max_attempts: 3
command: |
docker buildx build \
--file ./TestServer/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--tag oneuptime/test-server:release \
--tag oneuptime/test-server:7.0.${{needs.generate-build-number.outputs.build_number}} \
--tag ghcr.io/oneuptime/test-server:release \
--tag ghcr.io/oneuptime/test-server:7.0.${{needs.generate-build-number.outputs.build_number}} \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
.
otel-collector-docker-image-deploy:
needs: [generate-build-number]
@@ -555,17 +584,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./OTelCollector/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
timeout_minutes: 45
max_attempts: 3
command: |
docker buildx build \
--file ./OTelCollector/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--tag oneuptime/otel-collector:release \
--tag oneuptime/otel-collector:7.0.${{needs.generate-build-number.outputs.build_number}} \
--tag ghcr.io/oneuptime/otel-collector:release \
--tag ghcr.io/oneuptime/otel-collector:7.0.${{needs.generate-build-number.outputs.build_number}} \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
.
@@ -617,17 +651,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./StatusPage/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
timeout_minutes: 45
max_attempts: 3
command: |
docker buildx build \
--file ./StatusPage/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--tag oneuptime/status-page:release \
--tag oneuptime/status-page:7.0.${{needs.generate-build-number.outputs.build_number}} \
--tag ghcr.io/oneuptime/status-page:release \
--tag ghcr.io/oneuptime/status-page:7.0.${{needs.generate-build-number.outputs.build_number}} \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
.
test-docker-image-deploy:
needs: [generate-build-number]
@@ -677,17 +716,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./Tests/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
timeout_minutes: 45
max_attempts: 3
command: |
docker buildx build \
--file ./Tests/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--tag oneuptime/test:release \
--tag oneuptime/test:7.0.${{needs.generate-build-number.outputs.build_number}} \
--tag ghcr.io/oneuptime/test:release \
--tag ghcr.io/oneuptime/test:7.0.${{needs.generate-build-number.outputs.build_number}} \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
.
probe-ingest-docker-image-deploy:
needs: [generate-build-number]
@@ -737,17 +781,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./ProbeIngest/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
timeout_minutes: 45
max_attempts: 3
command: |
docker buildx build \
--file ./ProbeIngest/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--tag oneuptime/probe-ingest:release \
--tag oneuptime/probe-ingest:7.0.${{needs.generate-build-number.outputs.build_number}} \
--tag ghcr.io/oneuptime/probe-ingest:release \
--tag ghcr.io/oneuptime/probe-ingest:7.0.${{needs.generate-build-number.outputs.build_number}} \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
.
server-monitor-ingest-docker-image-deploy:
@@ -798,17 +847,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./ServerMonitorIngest/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
timeout_minutes: 45
max_attempts: 3
command: |
docker buildx build \
--file ./ServerMonitorIngest/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--tag oneuptime/server-monitor-ingest:release \
--tag oneuptime/server-monitor-ingest:7.0.${{needs.generate-build-number.outputs.build_number}} \
--tag ghcr.io/oneuptime/server-monitor-ingest:release \
--tag ghcr.io/oneuptime/server-monitor-ingest:7.0.${{needs.generate-build-number.outputs.build_number}} \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
.
@@ -860,17 +914,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./OpenTelemetryIngest/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
timeout_minutes: 45
max_attempts: 3
command: |
docker buildx build \
--file ./OpenTelemetryIngest/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--tag oneuptime/open-telemetry-ingest:release \
--tag oneuptime/open-telemetry-ingest:7.0.${{needs.generate-build-number.outputs.build_number}} \
--tag ghcr.io/oneuptime/open-telemetry-ingest:release \
--tag ghcr.io/oneuptime/open-telemetry-ingest:7.0.${{needs.generate-build-number.outputs.build_number}} \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
.
incoming-request-ingest-docker-image-deploy:
@@ -921,17 +980,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./IncomingRequestIngest/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
timeout_minutes: 45
max_attempts: 3
command: |
docker buildx build \
--file ./IncomingRequestIngest/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--tag oneuptime/incoming-request-ingest:release \
--tag oneuptime/incoming-request-ingest:7.0.${{needs.generate-build-number.outputs.build_number}} \
--tag ghcr.io/oneuptime/incoming-request-ingest:release \
--tag ghcr.io/oneuptime/incoming-request-ingest:7.0.${{needs.generate-build-number.outputs.build_number}} \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
.
fluent-ingest-docker-image-deploy:
needs: [generate-build-number]
@@ -981,17 +1045,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./FluentIngest/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
timeout_minutes: 45
max_attempts: 3
command: |
docker buildx build \
--file ./FluentIngest/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--tag oneuptime/fluent-ingest:release \
--tag oneuptime/fluent-ingest:7.0.${{needs.generate-build-number.outputs.build_number}} \
--tag ghcr.io/oneuptime/fluent-ingest:release \
--tag ghcr.io/oneuptime/fluent-ingest:7.0.${{needs.generate-build-number.outputs.build_number}} \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
.
probe-docker-image-deploy:
needs: [generate-build-number]
@@ -1041,17 +1110,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./Probe/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
timeout_minutes: 45
max_attempts: 3
command: |
docker buildx build \
--file ./Probe/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--tag oneuptime/probe:release \
--tag oneuptime/probe:7.0.${{needs.generate-build-number.outputs.build_number}} \
--tag ghcr.io/oneuptime/probe:release \
--tag ghcr.io/oneuptime/probe:7.0.${{needs.generate-build-number.outputs.build_number}} \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
.
admin-dashboard-docker-image-deploy:
needs: [generate-build-number]
@@ -1101,17 +1175,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./AdminDashboard/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
timeout_minutes: 45
max_attempts: 3
command: |
docker buildx build \
--file ./AdminDashboard/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--tag oneuptime/admin-dashboard:release \
--tag oneuptime/admin-dashboard:7.0.${{needs.generate-build-number.outputs.build_number}} \
--tag ghcr.io/oneuptime/admin-dashboard:release \
--tag ghcr.io/oneuptime/admin-dashboard:7.0.${{needs.generate-build-number.outputs.build_number}} \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
.
dashboard-docker-image-deploy:
@@ -1162,17 +1241,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./Dashboard/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
timeout_minutes: 45
max_attempts: 3
command: |
docker buildx build \
--file ./Dashboard/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--tag oneuptime/dashboard:release \
--tag oneuptime/dashboard:7.0.${{needs.generate-build-number.outputs.build_number}} \
--tag ghcr.io/oneuptime/dashboard:release \
--tag ghcr.io/oneuptime/dashboard:7.0.${{needs.generate-build-number.outputs.build_number}} \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
.
app-docker-image-deploy:
needs: [generate-build-number]
@@ -1222,17 +1306,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./App/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
timeout_minutes: 45
max_attempts: 3
command: |
docker buildx build \
--file ./App/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--tag oneuptime/app:release \
--tag oneuptime/app:7.0.${{needs.generate-build-number.outputs.build_number}} \
--tag ghcr.io/oneuptime/app:release \
--tag ghcr.io/oneuptime/app:7.0.${{needs.generate-build-number.outputs.build_number}} \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
.
copilot-docker-image-deploy:
@@ -1283,17 +1372,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./Copilot/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
timeout_minutes: 45
max_attempts: 3
command: |
docker buildx build \
--file ./Copilot/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--tag oneuptime/copilot:release \
--tag oneuptime/copilot:7.0.${{needs.generate-build-number.outputs.build_number}} \
--tag ghcr.io/oneuptime/copilot:release \
--tag ghcr.io/oneuptime/copilot:7.0.${{needs.generate-build-number.outputs.build_number}} \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
.
accounts-docker-image-deploy:
needs: [generate-build-number]
@@ -1343,17 +1437,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./Accounts/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
timeout_minutes: 45
max_attempts: 3
command: |
docker buildx build \
--file ./Accounts/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--tag oneuptime/accounts:release \
--tag oneuptime/accounts:7.0.${{needs.generate-build-number.outputs.build_number}} \
--tag ghcr.io/oneuptime/accounts:release \
--tag ghcr.io/oneuptime/accounts:7.0.${{needs.generate-build-number.outputs.build_number}} \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
.
publish-npm-packages:
@@ -1443,17 +1542,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./LLM/Dockerfile
context: ./LLM
platforms: linux/amd64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
timeout_minutes: 45
max_attempts: 3
command: |
docker buildx build \
--file ./LLM/Dockerfile \
--platform linux/amd64 \
--push \
--tag oneuptime/llm:release \
--tag oneuptime/llm:7.0.${{needs.generate-build-number.outputs.build_number}} \
--tag ghcr.io/oneuptime/llm:release \
--tag ghcr.io/oneuptime/llm:7.0.${{needs.generate-build-number.outputs.build_number}} \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
./LLM
docs-docker-image-deploy:
needs: generate-build-number
@@ -1505,17 +1609,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./Docs/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
timeout_minutes: 45
max_attempts: 3
command: |
docker buildx build \
--file ./Docs/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--tag oneuptime/docs:release \
--tag oneuptime/docs:7.0.${{needs.generate-build-number.outputs.build_number}} \
--tag ghcr.io/oneuptime/docs:release \
--tag ghcr.io/oneuptime/docs:7.0.${{needs.generate-build-number.outputs.build_number}} \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
.
@@ -1570,17 +1679,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./Worker/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
timeout_minutes: 45
max_attempts: 3
command: |
docker buildx build \
--file ./Worker/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--tag oneuptime/worker:release \
--tag oneuptime/worker:7.0.${{needs.generate-build-number.outputs.build_number}} \
--tag ghcr.io/oneuptime/worker:release \
--tag ghcr.io/oneuptime/worker:7.0.${{needs.generate-build-number.outputs.build_number}} \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
.
@@ -1635,17 +1749,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./Workflow/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
timeout_minutes: 45
max_attempts: 3
command: |
docker buildx build \
--file ./Workflow/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--tag oneuptime/workflow:release \
--tag oneuptime/workflow:7.0.${{needs.generate-build-number.outputs.build_number}} \
--tag ghcr.io/oneuptime/workflow:release \
--tag ghcr.io/oneuptime/workflow:7.0.${{needs.generate-build-number.outputs.build_number}} \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
.
@@ -1761,17 +1880,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./APIReference/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
timeout_minutes: 45
max_attempts: 3
command: |
docker buildx build \
--file ./APIReference/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--tag oneuptime/api-reference:release \
--tag oneuptime/api-reference:7.0.${{needs.generate-build-number.outputs.build_number}} \
--tag ghcr.io/oneuptime/api-reference:release \
--tag ghcr.io/oneuptime/api-reference:7.0.${{needs.generate-build-number.outputs.build_number}} \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
.
@@ -1898,12 +2022,49 @@ jobs:
configuration: "./Scripts/Release/ChangelogConfig.json"
- run: echo "Changelog:"
- run: echo "${{steps.build_changelog.outputs.changelog}}"
- name: Fallback to commit messages if changelog empty
id: fallback_changelog
shell: bash
run: |
set -euo pipefail
CHANGELOG_CONTENT="${{steps.build_changelog.outputs.changelog}}"
OLD_PLACEHOLDER="No significant changes were made. We have just fixed minor bugs for this release. You can find the detailed information in the commit history."
NEW_PLACEHOLDER="(auto) No categorized pull requests. Fallback will list raw commit messages."
if echo "$CHANGELOG_CONTENT" | grep -Fq "$OLD_PLACEHOLDER" || echo "$CHANGELOG_CONTENT" | grep -Fq "$NEW_PLACEHOLDER"; then
echo "Detected empty placeholder changelog. Building commit list fallback."
# Find previous tag (skip the most recent tag which might be for an older release). If none, include all commits.
if prev_tag=$(git describe --tags --abbrev=0 $(git rev-list --tags --skip=1 --max-count=1) 2>/dev/null); then
echo "Previous tag: $prev_tag"
commits=$(git log --pretty=format:'- %s (%h)' "$prev_tag"..HEAD)
else
echo "No previous tag found; using full commit history on this branch."
commits=$(git log --pretty=format:'- %s (%h)')
fi
# If still empty (e.g., no commits), keep placeholder to avoid empty body.
if [ -z "$commits" ]; then
commits="(no commits found)"
fi
{
echo "changelog<<EOF"
echo "## Commit Messages"
echo ""
echo "$commits"
echo "EOF"
} >> "$GITHUB_OUTPUT"
else
# Pass through original changelog
{
echo "changelog<<EOF"
echo "$CHANGELOG_CONTENT"
echo "EOF"
} >> "$GITHUB_OUTPUT"
fi
- uses: ncipollo/release-action@v1
with:
tag: "7.0.${{needs.generate-build-number.outputs.build_number}}"
artifactErrorsFailBuild: true
body: |
${{steps.build_changelog.outputs.changelog}}
${{steps.fallback_changelog.outputs.changelog}}
infrastructure-agent-deploy:

View File

@@ -77,17 +77,21 @@ jobs:
ls -la "$PROVIDER_DIR" || true
- name: Test Go build
run: |
PROVIDER_DIR="./Terraform"
if [ -d "$PROVIDER_DIR" ] && [ -f "$PROVIDER_DIR/go.mod" ]; then
cd "$PROVIDER_DIR"
echo "🔨 Testing Go build..."
go mod tidy
go build -v ./...
echo "✅ Go build successful"
else
echo "⚠️ Cannot test build - missing go.mod or provider directory"
fi
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: |
PROVIDER_DIR="./Terraform"
if [ -d "$PROVIDER_DIR" ] && [ -f "$PROVIDER_DIR/go.mod" ]; then
cd "$PROVIDER_DIR"
echo "🔨 Testing Go build..."
go mod tidy
go build -v ./...
echo "✅ Go build successful"
else
echo "⚠️ Cannot test build - missing go.mod or provider directory"
fi
- name: Upload Terraform provider as artifact
uses: actions/upload-artifact@v4

View File

@@ -176,18 +176,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker images (test)
run: |
docker buildx build \
--platform linux/amd64,linux/arm64 \
--file ./MCP/Dockerfile.tpl \
--tag oneuptime/mcp-server:${{ steps.version.outputs.version }} \
--tag oneuptime/mcp-server:test \
--tag ghcr.io/oneuptime/mcp-server:${{ steps.version.outputs.version }} \
--tag ghcr.io/oneuptime/mcp-server:test \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=${{ steps.version.outputs.version }} \
--push .
echo "✅ Pushed test Docker images to Docker Hub and GitHub Container Registry"
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: |
docker buildx build \
--platform linux/amd64,linux/arm64 \
--file ./MCP/Dockerfile.tpl \
--tag oneuptime/mcp-server:${{ steps.version.outputs.version }} \
--tag oneuptime/mcp-server:test \
--tag ghcr.io/oneuptime/mcp-server:${{ steps.version.outputs.version }} \
--tag ghcr.io/oneuptime/mcp-server:test \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=${{ steps.version.outputs.version }} \
--push .
echo "✅ Pushed test Docker images to Docker Hub and GitHub Container Registry"
- name: Upload MCP server artifact
uses: actions/upload-artifact@v4
@@ -269,18 +273,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./LLM/Dockerfile
context: ./LLM
# arm64 is not supported by the base image of the LLM
platforms: linux/amd64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
timeout_minutes: 30
max_attempts: 3
command: |
docker buildx build \
--file ./LLM/Dockerfile \
--platform linux/amd64 \
--push \
--tag oneuptime/llm:test \
--tag oneuptime/llm:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--tag ghcr.io/oneuptime/llm:test \
--tag ghcr.io/oneuptime/llm:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
./LLM
nginx-docker-image-deploy:
@@ -332,17 +340,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./Nginx/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
timeout_minutes: 30
max_attempts: 3
command: |
docker buildx build \
--file ./Nginx/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--tag oneuptime/nginx:test \
--tag oneuptime/nginx:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--tag ghcr.io/oneuptime/nginx:test \
--tag ghcr.io/oneuptime/nginx:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
.
e2e-docker-image-deploy:
@@ -394,17 +407,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./E2E/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
timeout_minutes: 30
max_attempts: 3
command: |
docker buildx build \
--file ./E2E/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--tag oneuptime/e2e:test \
--tag oneuptime/e2e:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--tag ghcr.io/oneuptime/e2e:test \
--tag ghcr.io/oneuptime/e2e:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
.
test-server-docker-image-deploy:
needs: generate-build-number
@@ -455,17 +473,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./TestServer/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
timeout_minutes: 30
max_attempts: 3
command: |
docker buildx build \
--file ./TestServer/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--tag oneuptime/test-server:test \
--tag oneuptime/test-server:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--tag ghcr.io/oneuptime/test-server:test \
--tag ghcr.io/oneuptime/test-server:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
.
otel-collector-docker-image-deploy:
needs: generate-build-number
@@ -516,17 +539,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./OTelCollector/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
timeout_minutes: 30
max_attempts: 3
command: |
docker buildx build \
--file ./OTelCollector/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--tag oneuptime/otel-collector:test \
--tag oneuptime/otel-collector:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--tag ghcr.io/oneuptime/otel-collector:test \
--tag ghcr.io/oneuptime/otel-collector:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
.
isolated-vm-docker-image-deploy:
needs: generate-build-number
@@ -577,17 +605,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./IsolatedVM/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
timeout_minutes: 30
max_attempts: 3
command: |
docker buildx build \
--file ./IsolatedVM/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--tag oneuptime/isolated-vm:test \
--tag oneuptime/isolated-vm:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--tag ghcr.io/oneuptime/isolated-vm:test \
--tag ghcr.io/oneuptime/isolated-vm:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
.
home-docker-image-deploy:
needs: generate-build-number
@@ -638,17 +671,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./Home/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
timeout_minutes: 30
max_attempts: 3
command: |
docker buildx build \
--file ./Home/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--tag oneuptime/home:test \
--tag oneuptime/home:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--tag ghcr.io/oneuptime/home:test \
--tag ghcr.io/oneuptime/home:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
.
@@ -701,17 +739,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./StatusPage/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
timeout_minutes: 30
max_attempts: 3
command: |
docker buildx build \
--file ./StatusPage/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--tag oneuptime/status-page:test \
--tag oneuptime/status-page:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--tag ghcr.io/oneuptime/status-page:test \
--tag ghcr.io/oneuptime/status-page:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
.
@@ -764,17 +807,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./Tests/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
timeout_minutes: 30
max_attempts: 3
command: |
docker buildx build \
--file ./Tests/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--tag oneuptime/test:test \
--tag oneuptime/test:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--tag ghcr.io/oneuptime/test:test \
--tag ghcr.io/oneuptime/test:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
.
probe-ingest-docker-image-deploy:
needs: generate-build-number
@@ -825,17 +873,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./ProbeIngest/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
timeout_minutes: 30
max_attempts: 3
command: |
docker buildx build \
--file ./ProbeIngest/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--tag oneuptime/probe-ingest:test \
--tag oneuptime/probe-ingest:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--tag ghcr.io/oneuptime/probe-ingest:test \
--tag ghcr.io/oneuptime/probe-ingest:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
.
@@ -888,17 +941,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./ServerMonitorIngest/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
timeout_minutes: 30
max_attempts: 3
command: |
docker buildx build \
--file ./ServerMonitorIngest/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--tag oneuptime/server-monitor-ingest:test \
--tag oneuptime/server-monitor-ingest:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--tag ghcr.io/oneuptime/server-monitor-ingest:test \
--tag ghcr.io/oneuptime/server-monitor-ingest:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
.
@@ -952,17 +1010,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./IncomingRequestIngest/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
timeout_minutes: 30
max_attempts: 3
command: |
docker buildx build \
--file ./IncomingRequestIngest/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--tag oneuptime/incoming-request-ingest:test \
--tag oneuptime/incoming-request-ingest:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--tag ghcr.io/oneuptime/incoming-request-ingest:test \
--tag ghcr.io/oneuptime/incoming-request-ingest:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
.
open-telemetry-ingest-docker-image-deploy:
needs: generate-build-number
@@ -1013,17 +1076,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./OpenTelemetryIngest/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
timeout_minutes: 30
max_attempts: 3
command: |
docker buildx build \
--file ./OpenTelemetryIngest/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--tag oneuptime/open-telemetry-ingest:test \
--tag oneuptime/open-telemetry-ingest:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--tag ghcr.io/oneuptime/open-telemetry-ingest:test \
--tag ghcr.io/oneuptime/open-telemetry-ingest:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
.
fluent-ingest-docker-image-deploy:
needs: generate-build-number
@@ -1074,17 +1142,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./FluentIngest/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
timeout_minutes: 30
max_attempts: 3
command: |
docker buildx build \
--file ./FluentIngest/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--tag oneuptime/fluent-ingest:test \
--tag oneuptime/fluent-ingest:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--tag ghcr.io/oneuptime/fluent-ingest:test \
--tag ghcr.io/oneuptime/fluent-ingest:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
.
probe-docker-image-deploy:
needs: generate-build-number
@@ -1135,17 +1208,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./Probe/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
timeout_minutes: 30
max_attempts: 3
command: |
docker buildx build \
--file ./Probe/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--tag oneuptime/probe:test \
--tag oneuptime/probe:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--tag ghcr.io/oneuptime/probe:test \
--tag ghcr.io/oneuptime/probe:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
.
dashboard-docker-image-deploy:
needs: generate-build-number
@@ -1196,17 +1274,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./Dashboard/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
timeout_minutes: 30
max_attempts: 3
command: |
docker buildx build \
--file ./Dashboard/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--tag oneuptime/dashboard:test \
--tag oneuptime/dashboard:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--tag ghcr.io/oneuptime/dashboard:test \
--tag ghcr.io/oneuptime/dashboard:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
.
admin-dashboard-docker-image-deploy:
needs: generate-build-number
@@ -1257,17 +1340,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./AdminDashboard/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
timeout_minutes: 30
max_attempts: 3
command: |
docker buildx build \
--file ./AdminDashboard/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--tag oneuptime/admin-dashboard:test \
--tag oneuptime/admin-dashboard:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--tag ghcr.io/oneuptime/admin-dashboard:test \
--tag ghcr.io/oneuptime/admin-dashboard:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
.
app-docker-image-deploy:
needs: generate-build-number
@@ -1318,17 +1406,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./App/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
timeout_minutes: 30
max_attempts: 3
command: |
docker buildx build \
--file ./App/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--tag oneuptime/app:test \
--tag oneuptime/app:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--tag ghcr.io/oneuptime/app:test \
--tag ghcr.io/oneuptime/app:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
.
@@ -1382,17 +1475,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./APIReference/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
timeout_minutes: 30
max_attempts: 3
command: |
docker buildx build \
--file ./APIReference/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--tag oneuptime/api-reference:test \
--tag oneuptime/api-reference:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--tag ghcr.io/oneuptime/api-reference:test \
--tag ghcr.io/oneuptime/api-reference:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
.
@@ -1445,17 +1543,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./Accounts/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
timeout_minutes: 30
max_attempts: 3
command: |
docker buildx build \
--file ./Accounts/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--tag oneuptime/accounts:test \
--tag oneuptime/accounts:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--tag ghcr.io/oneuptime/accounts:test \
--tag ghcr.io/oneuptime/accounts:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
.
worker-docker-image-deploy:
needs: generate-build-number
@@ -1506,17 +1609,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./Worker/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.copilot-docker-image-deploybuild_number}}
timeout_minutes: 30
max_attempts: 3
command: |
docker buildx build \
--file ./Worker/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--tag oneuptime/worker:test \
--tag oneuptime/worker:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--tag ghcr.io/oneuptime/worker:test \
--tag ghcr.io/oneuptime/worker:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
.
copilot-docker-image-deploy:
needs: generate-build-number
@@ -1567,17 +1675,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./Copilot/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
timeout_minutes: 30
max_attempts: 3
command: |
docker buildx build \
--file ./Copilot/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--tag oneuptime/copilot:test \
--tag oneuptime/copilot:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--tag ghcr.io/oneuptime/copilot:test \
--tag ghcr.io/oneuptime/copilot:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
.
workflow-docker-image-deploy:
@@ -1629,17 +1742,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./Workflow/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
timeout_minutes: 30
max_attempts: 3
command: |
docker buildx build \
--file ./Workflow/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--tag oneuptime/workflow:test \
--tag oneuptime/workflow:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--tag ghcr.io/oneuptime/workflow:test \
--tag ghcr.io/oneuptime/workflow:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
.
docs-docker-image-deploy:
@@ -1691,17 +1809,22 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
uses: nick-fields/retry@v3
with:
file: ./Docs/Dockerfile
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_SHA=${{ github.sha }}
APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}}
timeout_minutes: 30
max_attempts: 3
command: |
docker buildx build \
--file ./Docs/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--tag oneuptime/docs:test \
--tag oneuptime/docs:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--tag ghcr.io/oneuptime/docs:test \
--tag ghcr.io/oneuptime/docs:7.0.${{needs.generate-build-number.outputs.build_number}}-test \
--build-arg GIT_SHA=${{ github.sha }} \
--build-arg APP_VERSION=7.0.${{needs.generate-build-number.outputs.build_number}} \
.

View File

@@ -4,5 +4,7 @@
"ignore": [
"greenlock.d/*"
],
"exec": "node --inspect=0.0.0.0:9229 --require ts-node/register Index.ts"
"watchOptions": {"useFsEvents": false, "interval": 500},
"env": {"TS_NODE_TRANSPILE_ONLY": "1", "TS_NODE_FILES": "false"},
"exec": "node --inspect=0.0.0.0:9229 -r ts-node/register/transpile-only Index.ts"
}

View File

@@ -135,7 +135,6 @@
<link rel="apple-touch-icon-precomposed" href="/img/ou-wb.svg">
<link rel="icon" href="/img/ou-wb.svg">
<link rel="image_src" type="image/png" href="/img/hou-wb.svg">
<link rel="canonical" href="/">
<link rel="manifest" href="/manifest.json">
<meta property="og:title" content="OneUptime - One Complete Observability platform.">
<meta property="og:url" content="https://oneuptime.com">

View File

@@ -538,11 +538,6 @@ import WorkspaceSettingService, {
Service as WorkspaceSettingServiceType,
} from "Common/Server/Services/WorkspaceSettingService";
import ProjectUser from "Common/Models/DatabaseModels/ProjectUser";
import ProjectUserService, {
Service as ProjectUserServiceType,
} from "Common/Server/Services/ProjectUserService";
import MonitorFeed from "Common/Models/DatabaseModels/MonitorFeed";
import MonitorFeedService, {
Service as MonitorFeedServiceType,
@@ -736,14 +731,6 @@ const BaseAPIFeatureSet: FeatureSet = {
).getRouter(),
);
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,
new BaseAPI<ProjectUser, ProjectUserServiceType>(
ProjectUser,
ProjectUserService,
).getRouter(),
);
//service provider setting
app.use(
`/${APP_NAME.toLocaleLowerCase()}`,

View File

@@ -20,7 +20,6 @@ import NotFoundException from "Common/Types/Exception/NotFoundException";
import OneUptimeDate from "Common/Types/Date";
import LIMIT_MAX, { LIMIT_PER_PROJECT } from "Common/Types/Database/LimitMax";
import Query from "Common/Types/BaseDatabase/Query";
import ProjectUser from "Common/Models/DatabaseModels/ProjectUser";
import QueryHelper from "Common/Server/Types/Database/QueryHelper";
import User from "Common/Models/DatabaseModels/User";
import {
@@ -89,6 +88,8 @@ const handleUserTeamOperations: (
ignoreHooks: true,
},
});
logger.debug(`SCIM Team operations - user added to team: ${team.id}`);
} else {
logger.debug(
`SCIM Team operations - user already member of team: ${team.id}`,
@@ -168,7 +169,7 @@ router.get(
);
// Build query for team members in this project
const query: Query<ProjectUser> = {
const query: Query<TeamMember> = {
projectId: projectId,
};

View File

@@ -67,7 +67,6 @@
<link rel="apple-touch-icon-precomposed" href="/img/ou-wb.svg">
<link rel="icon" href="/img/ou-wb.svg">
<link rel="image_src" type="image/png" href="/img/hou-wb.svg">
<link rel="canonical" href="/">
<link rel="manifest" href="/manifest.json">
<meta property="og:title" content="OneUptime - One Complete Observability platform.">
<meta property="og:url" content="https://oneuptime.com">

View File

@@ -4,5 +4,13 @@
"ignore": [
"greenlock.d/*"
],
"exec": "node --inspect=0.0.0.0:9229 --require ts-node/register Index.ts"
"watchOptions": {
"useFsEvents": false,
"interval": 500
},
"env": {
"TS_NODE_TRANSPILE_ONLY": "1",
"TS_NODE_FILES": "false"
},
"exec": "node --inspect=0.0.0.0:9229 -r ts-node/register/transpile-only Index.ts"
}

View File

@@ -178,7 +178,7 @@ import WorkspaceUserAuthToken from "./WorkspaceUserAuthToken";
import WorkspaceProjectAuthToken from "./WorkspaceProjectAuthToken";
import WorkspaceSetting from "./WorkspaceSetting";
import WorkspaceNotificationRule from "./WorkspaceNotificationRule";
import ProjectUser from "./ProjectUser";
import OnCallDutyPolicyUserOverride from "./OnCallDutyPolicyUserOverride";
import MonitorFeed from "./MonitorFeed";
import MetricType from "./MetricType";
@@ -380,8 +380,6 @@ const AllModelTypes: Array<{
WorkspaceSetting,
WorkspaceNotificationRule,
ProjectUser,
MonitorFeed,
MetricType,

View File

@@ -248,6 +248,84 @@ export default class Project extends TenantModel {
})
public paymentProviderCustomerId?: string = undefined;
@ColumnAccessControl({
create: [Permission.ProjectOwner, Permission.ManageProjectBilling],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProject,
Permission.UnAuthorizedSsoUser,
Permission.ProjectUser,
],
update: [Permission.ProjectOwner, Permission.ManageProjectBilling],
})
@TableColumn({
type: TableColumnType.LongText,
title: "Business Details / Billing Address",
description:
"Business legal name, address and any tax information to appear on invoices.",
})
@Column({
type: ColumnType.LongText,
length: ColumnLength.LongText,
nullable: true,
unique: false,
})
public businessDetails?: string = undefined;
@ColumnAccessControl({
create: [Permission.ProjectOwner, Permission.ManageProjectBilling],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProject,
Permission.UnAuthorizedSsoUser,
Permission.ProjectUser,
],
update: [Permission.ProjectOwner, Permission.ManageProjectBilling],
})
@TableColumn({
type: TableColumnType.ShortText,
title: "Business Country (ISO Alpha-2)",
description:
"Two-letter ISO country code for billing address (e.g., US, GB, DE).",
})
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: true,
unique: false,
})
public businessDetailsCountry?: string = undefined;
@ColumnAccessControl({
create: [Permission.ProjectOwner, Permission.ManageProjectBilling],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProject,
Permission.UnAuthorizedSsoUser,
Permission.ProjectUser,
],
update: [Permission.ProjectOwner, Permission.ManageProjectBilling],
})
@TableColumn({
type: TableColumnType.Email,
title: "Finance / Accounting Email",
description:
"Invoices, receipts and billing related notifications will be sent to this email in addition to project owner.",
})
@Column({
type: ColumnType.Email,
length: ColumnLength.Email,
nullable: true,
unique: false,
})
public financeAccountingEmail?: string = undefined;
@ColumnAccessControl({
create: [],
read: [

View File

@@ -1,336 +0,0 @@
import Project from "./Project";
import Team from "./Team";
import User from "./User";
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
import Route from "../../Types/API/Route";
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl";
import AllowUserQueryWithoutTenant from "../../Types/Database/AllowUserQueryWithoutTenant";
import ColumnType from "../../Types/Database/ColumnType";
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
import CurrentUserCanAccessRecordBy from "../../Types/Database/CurrentUserCanAccessRecordBy";
import MultiTenentQueryAllowed from "../../Types/Database/MultiTenentQueryAllowed";
import TableColumn from "../../Types/Database/TableColumn";
import TableColumnType from "../../Types/Database/TableColumnType";
import TableMetadata from "../../Types/Database/TableMetadata";
import TenantColumn from "../../Types/Database/TenantColumn";
import IconProp from "../../Types/Icon/IconProp";
import ObjectID from "../../Types/ObjectID";
import Permission from "../../Types/Permission";
import {
Column,
Entity,
Index,
JoinColumn,
JoinTable,
ManyToMany,
ManyToOne,
} from "typeorm";
@TableAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectTeam,
Permission.CurrentUser,
],
delete: [],
update: [],
})
@MultiTenentQueryAllowed(true)
@AllowUserQueryWithoutTenant(true)
@CurrentUserCanAccessRecordBy("userId")
@TenantColumn("projectId")
@CrudApiEndpoint(new Route("/project-user"))
@Entity({
name: "ProjectUser",
})
@TableMetadata({
tableName: "ProjectUser",
singularName: "Project User",
pluralName: "Project Users",
icon: IconProp.User,
tableDescription:
"This model connects users and teams. This is an internal table. Its a view on TeamMembers table.",
})
export default class ProjectUser extends BaseModel {
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectTeam,
Permission.CurrentUser,
],
update: [],
})
@TableColumn({
required: false,
type: TableColumnType.EntityArray,
modelType: Team,
title: "Teams",
description: "Teams to which this user belongs.",
})
@ManyToMany(
() => {
return Team;
},
{ eager: false },
)
@JoinTable({
name: "ProjectUserAcceptedTeams",
inverseJoinColumn: {
name: "teamId",
referencedColumnName: "_id",
},
joinColumn: {
name: "projectUserId",
referencedColumnName: "_id",
},
})
public acceptedTeams?: Array<Team> = undefined; // user is accepted to these teams. This is a view on TeamMembers table.
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectTeam,
Permission.CurrentUser,
],
update: [],
})
@TableColumn({
required: false,
type: TableColumnType.EntityArray,
modelType: Team,
title: "Teams",
description: "Teams to which this user belongs.",
})
@ManyToMany(
() => {
return Team;
},
{ eager: false },
)
@JoinTable({
name: "ProjectUserInvitedTeams",
inverseJoinColumn: {
name: "teamId",
referencedColumnName: "_id",
},
joinColumn: {
name: "projectUserId",
referencedColumnName: "_id",
},
})
public invitedTeams?: Array<Team> = undefined; // user is invited to these teams.
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectTeam,
Permission.CurrentUser,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "projectId",
type: TableColumnType.Entity,
modelType: Project,
title: "Project",
description: "Relation to Project Resource in which this object belongs",
})
@ManyToOne(
() => {
return Project;
},
{
eager: false,
nullable: true,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "projectId" })
public project?: Project = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectTeam,
Permission.CurrentUser,
],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: true,
canReadOnRelationQuery: true,
title: "Project ID",
description: "ID of your OneUptime Project in which this object belongs",
})
@Column({
type: ColumnType.ObjectID,
nullable: false,
transformer: ObjectID.getDatabaseTransformer(),
})
public projectId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ReadProjectTeam,
Permission.ProjectMember,
],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "userId",
type: TableColumnType.Entity,
modelType: User,
title: "User",
description: "User who belongs to this team.",
})
@ManyToOne(
() => {
return User;
},
{
eager: false,
nullable: false,
onDelete: "CASCADE",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "userId" })
public user?: User = undefined;
@ColumnAccessControl({
create: [],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectTeam,
Permission.CurrentUser,
],
update: [],
})
@TableColumn({
type: TableColumnType.ObjectID,
required: true,
title: "User ID",
description: "ID of User who belongs to this team",
})
@Column({
type: ColumnType.ObjectID,
nullable: false,
transformer: ObjectID.getDatabaseTransformer(),
})
public userId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "createdByUserId",
type: TableColumnType.Entity,
modelType: User,
title: "Created by User",
description:
"Relation to User who created this object (if this object was created by a User)",
})
@ManyToOne(
() => {
return User;
},
{
eager: false,
nullable: true,
onDelete: "SET NULL",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "createdByUserId" })
public createdByUser?: User = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.ObjectID,
title: "Created by User ID",
description:
"User ID who created this object (if this object was created by a User)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public createdByUserId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
manyToOneRelationColumn: "deletedByUserId",
type: TableColumnType.Entity,
title: "Deleted by User",
modelType: User,
description:
"Relation to User who deleted this object (if this object was deleted by a User)",
})
@ManyToOne(
() => {
return User;
},
{
cascade: false,
eager: false,
nullable: true,
onDelete: "SET NULL",
orphanedRowAction: "nullify",
},
)
@JoinColumn({ name: "deletedByUserId" })
public deletedByUser?: User = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.ObjectID,
title: "Deleted by User ID",
description:
"User ID who deleted this object (if this object was deleted by a User)",
})
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public deletedByUserId?: ObjectID = undefined;
}

View File

@@ -1179,6 +1179,45 @@ export default class StatusPage extends BaseModel {
})
public enableSlackSubscribers?: boolean = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateProjectStatusPage,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadProjectStatusPage,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditProjectStatusPage,
],
})
@TableColumn({
isDefaultValueColumn: true,
type: TableColumnType.Boolean,
title: "Enable Microsoft Teams Subscribers",
description:
"Can Microsoft Teams subscribers subscribe to this Status Page?",
defaultValue: false,
})
@Column({
type: ColumnType.Boolean,
default: false,
})
@ColumnBillingAccessControl({
read: PlanType.Free,
update: PlanType.Scale,
create: PlanType.Free,
})
public enableMicrosoftTeamsSubscribers?: boolean = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,

View File

@@ -379,6 +379,65 @@ export default class StatusPageSubscriber extends BaseModel {
})
public slackWorkspaceName?: string = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateStatusPageSubscriber,
Permission.Public,
],
read: [],
update: [],
})
@TableColumn({
required: false,
type: TableColumnType.LongURL,
title: "Microsoft Teams Incoming Webhook URL",
description:
"Microsoft Teams incoming webhook URL to send notifications to Teams channel",
})
@Column({
nullable: true,
type: ColumnType.LongURL,
transformer: URL.getDatabaseTransformer(),
})
public microsoftTeamsIncomingWebhookUrl?: URL = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateStatusPageSubscriber,
Permission.Public,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ReadStatusPageSubscriber,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditStatusPageSubscriber,
],
})
@TableColumn({
required: false,
type: TableColumnType.VeryLongText,
title: "Microsoft Teams Workspace Name",
description:
"Name of the Microsoft Teams workspace for validation and identification",
})
@Column({
nullable: true,
type: ColumnType.VeryLongText,
})
public microsoftTeamsWorkspaceName?: string = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,

View File

@@ -502,6 +502,7 @@ export default class StatusPageAPI extends BaseAPI<
footerHTML: true,
enableEmailSubscribers: true,
enableSlackSubscribers: true,
enableMicrosoftTeamsSubscribers: true,
enableSmsSubscribers: true,
isPublicStatusPage: true,
allowSubscribersToChooseResources: true,
@@ -2146,6 +2147,7 @@ export default class StatusPageAPI extends BaseAPI<
projectId: true,
enableEmailSubscribers: true,
enableSlackSubscribers: true,
enableMicrosoftTeamsSubscribers: true,
enableSmsSubscribers: true,
allowSubscribersToChooseResources: true,
allowSubscribersToChooseEventTypes: true,
@@ -2419,6 +2421,7 @@ export default class StatusPageAPI extends BaseAPI<
enableEmailSubscribers: true,
enableSmsSubscribers: true,
enableSlackSubscribers: true,
enableMicrosoftTeamsSubscribers: true,
allowSubscribersToChooseResources: true,
allowSubscribersToChooseEventTypes: true,
showSubscriberPageOnStatusPage: true,
@@ -2480,15 +2483,28 @@ export default class StatusPageAPI extends BaseAPI<
}
if (
!req.body.data["subscriberEmail"] &&
!req.body.data["subscriberPhone"] &&
!req.body.data["slackWorkspaceName"]
req.body.data["microsoftTeamsWorkspaceName"] &&
!statusPage.enableMicrosoftTeamsSubscribers
) {
logger.debug(
`No email, phone, or slack workspace name provided for subscription to status page with ID: ${objectId}`,
`Microsoft Teams subscribers not enabled for status page with ID: ${objectId}`,
);
throw new BadDataException(
"Email, phone or slack workspace name is required to subscribe to this status page.",
"Microsoft Teams subscribers not enabled for this status page.",
);
}
if (
!req.body.data["subscriberEmail"] &&
!req.body.data["subscriberPhone"] &&
!req.body.data["slackWorkspaceName"] &&
!req.body.data["microsoftTeamsWorkspaceName"]
) {
logger.debug(
`No email, phone, slack workspace name, or Microsoft Teams workspace name provided for subscription to status page with ID: ${objectId}`,
);
throw new BadDataException(
"Email, phone, slack workspace name, or Microsoft Teams workspace name is required to subscribe to this status page.",
);
}
@@ -2512,6 +2528,18 @@ export default class StatusPageAPI extends BaseAPI<
? (req.body.data["slackWorkspaceName"] as string)
: undefined;
const microsoftTeamsIncomingWebhookUrl: string | undefined = req.body.data[
"microsoftTeamsIncomingWebhookUrl"
]
? (req.body.data["microsoftTeamsIncomingWebhookUrl"] as string)
: undefined;
const microsoftTeamsWorkspaceName: string | undefined = req.body.data[
"microsoftTeamsWorkspaceName"
]
? (req.body.data["microsoftTeamsWorkspaceName"] as string)
: undefined;
let statusPageSubscriber: StatusPageSubscriber | null = null;
let isUpdate: boolean = false;
@@ -2570,6 +2598,23 @@ export default class StatusPageAPI extends BaseAPI<
statusPageSubscriber.slackWorkspaceName = slackWorkspaceName;
}
if (microsoftTeamsIncomingWebhookUrl) {
logger.debug(
`Setting subscriber Microsoft Teams webhook: ${microsoftTeamsIncomingWebhookUrl}`,
);
statusPageSubscriber.microsoftTeamsIncomingWebhookUrl = URL.fromString(
microsoftTeamsIncomingWebhookUrl,
);
}
if (microsoftTeamsWorkspaceName) {
logger.debug(
`Setting subscriber Microsoft Teams workspace name: ${microsoftTeamsWorkspaceName}`,
);
statusPageSubscriber.microsoftTeamsWorkspaceName =
microsoftTeamsWorkspaceName;
}
if (
req.body.data["statusPageResources"] &&
!statusPage.allowSubscribersToChooseResources

View File

@@ -68,6 +68,28 @@ export class MigrationName1754671483948 implements MigrationInterface {
await queryRunner.query(
`ALTER TABLE "StatusPageAnnouncement" ADD "subscriberNotificationStatusMessage" text`,
);
// Set all existing rows' subscriber notification statuses to 'Success' since they were previously considered notified
await queryRunner.query(
`UPDATE "Incident" SET "subscriberNotificationStatusOnIncidentCreated"='Success'`,
);
await queryRunner.query(
`UPDATE "IncidentPublicNote" SET "subscriberNotificationStatusOnNoteCreated"='Success'`,
);
await queryRunner.query(
`UPDATE "IncidentStateTimeline" SET "subscriberNotificationStatus"='Success'`,
);
await queryRunner.query(
`UPDATE "ScheduledMaintenance" SET "subscriberNotificationStatusOnEventScheduled"='Success'`,
);
await queryRunner.query(
`UPDATE "ScheduledMaintenancePublicNote" SET "subscriberNotificationStatusOnNoteCreated"='Success'`,
);
await queryRunner.query(
`UPDATE "ScheduledMaintenanceStateTimeline" SET "subscriberNotificationStatus"='Success'`,
);
await queryRunner.query(
`UPDATE "StatusPageAnnouncement" SET "subscriberNotificationStatus"='Success'`,
);
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`,
);

View File

@@ -0,0 +1,29 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class MigrationName1755775040650 implements MigrationInterface {
public name = "MigrationName1755775040650";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "StatusPage" ADD "enableMicrosoftTeamsSubscribers" boolean NOT NULL DEFAULT false`,
);
await queryRunner.query(
`ALTER TABLE "StatusPageSubscriber" ADD "microsoftTeamsIncomingWebhookUrl" character varying`,
);
await queryRunner.query(
`ALTER TABLE "StatusPageSubscriber" ADD "microsoftTeamsWorkspaceName" character varying(100)`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "StatusPageSubscriber" DROP COLUMN "microsoftTeamsWorkspaceName"`,
);
await queryRunner.query(
`ALTER TABLE "StatusPageSubscriber" DROP COLUMN "microsoftTeamsIncomingWebhookUrl"`,
);
await queryRunner.query(
`ALTER TABLE "StatusPage" DROP COLUMN "enableMicrosoftTeamsSubscribers"`,
);
}
}

View File

@@ -0,0 +1,23 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class MigrationName1755778495455 implements MigrationInterface {
public name = "MigrationName1755778495455";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "StatusPageSubscriber" DROP COLUMN "microsoftTeamsWorkspaceName"`,
);
await queryRunner.query(
`ALTER TABLE "StatusPageSubscriber" ADD "microsoftTeamsWorkspaceName" text`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "StatusPageSubscriber" DROP COLUMN "microsoftTeamsWorkspaceName"`,
);
await queryRunner.query(
`ALTER TABLE "StatusPageSubscriber" ADD "microsoftTeamsWorkspaceName" character varying(100)`,
);
}
}

View File

@@ -0,0 +1,23 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class MigrationName1755778934927 implements MigrationInterface {
public name = "MigrationName1755778934927";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "StatusPageSubscriber" DROP COLUMN "microsoftTeamsIncomingWebhookUrl"`,
);
await queryRunner.query(
`ALTER TABLE "StatusPageSubscriber" ADD "microsoftTeamsIncomingWebhookUrl" text`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`,
);
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`,
);
}
}

View File

@@ -0,0 +1,17 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class MigrationName1756293325324 implements MigrationInterface {
public name = "MigrationName1756293325324";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "Project" ADD "businessDetails" character varying(500)`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "Project" DROP COLUMN "businessDetails"`,
);
}
}

View File

@@ -0,0 +1,29 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class MigrationName1756296282627 implements MigrationInterface {
public name = "MigrationName1756296282627";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "Project" ADD "businessDetailsCountry" character varying(100)`,
);
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`,
);
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`,
);
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`,
);
await queryRunner.query(
`ALTER TABLE "Project" DROP COLUMN "businessDetailsCountry"`,
);
}
}

View File

@@ -0,0 +1,17 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class MigrationName1756300358095 implements MigrationInterface {
public name = "MigrationName1756300358095";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "Project" ADD "financeAccountingEmail" character varying(100)`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "Project" DROP COLUMN "financeAccountingEmail"`,
);
}
}

View File

@@ -158,6 +158,12 @@ import { MigrationName1755088852971 } from "./1755088852971-MigrationName";
import { MigrationName1755093133870 } from "./1755093133870-MigrationName";
import { MigrationName1755109893911 } from "./1755109893911-MigrationName";
import { MigrationName1755110936888 } from "./1755110936888-MigrationName";
import { MigrationName1755775040650 } from "./1755775040650-MigrationName";
import { MigrationName1755778495455 } from "./1755778495455-MigrationName";
import { MigrationName1755778934927 } from "./1755778934927-MigrationName";
import { MigrationName1756293325324 } from "./1756293325324-MigrationName";
import { MigrationName1756296282627 } from "./1756296282627-MigrationName";
import { MigrationName1756300358095 } from "./1756300358095-MigrationName";
export default [
InitialMigration,
@@ -320,4 +326,10 @@ export default [
MigrationName1755093133870,
MigrationName1755109893911,
MigrationName1755110936888,
MigrationName1755775040650,
MigrationName1755778495455,
MigrationName1755778934927,
MigrationName1756293325324,
MigrationName1756296282627,
MigrationName1756300358095,
];

View File

@@ -14,7 +14,20 @@ export default class QueueWorker {
public static getWorker(
queueName: QueueName,
onJobInQueue: (job: QueueJob) => Promise<void>,
options: { concurrency: number },
options: {
concurrency: number;
/**
* How long (in ms) the worker will hold a lock on the job before it's considered stalled
* if the event loop is blocked and the lock cannot be extended in time.
* Defaults to BullMQ default (30s) if not provided.
*/
lockDuration?: number;
/**
* Maximum number of times a job can be re-processed due to stall detection
* before being moved to failed. Defaults to BullMQ default (1) if not provided.
*/
maxStalledCount?: number;
},
): Worker {
const worker: Worker = new Worker(queueName, onJobInQueue, {
connection: {
@@ -23,6 +36,11 @@ export default class QueueWorker {
password: RedisPassword,
},
concurrency: options.concurrency,
// Only set these values if provided so we do not override BullMQ defaults
...(options.lockDuration ? { lockDuration: options.lockDuration } : {}),
...(options.maxStalledCount !== undefined
? { maxStalledCount: options.maxStalledCount }
: {}),
});
process.on("SIGINT", async () => {

View File

@@ -80,6 +80,94 @@ export class BillingService extends BaseService {
await this.stripe.customers.update(id, { name: newName });
}
@CaptureSpan()
public async updateCustomerBusinessDetails(
id: string,
businessDetails: string,
countryCode?: string | null,
financeAccountingEmail?: string | null,
): Promise<void> {
if (!this.isBillingEnabled()) {
throw new BadDataException(Errors.BillingService.BILLING_NOT_ENABLED);
}
// Goal: Update Stripe Customer "Billing details" (address fields) rather than invoice footer.
// We only have a single free-form textarea. We'll map:
// First non-empty line -> address.line1
// Second non-empty line (if any) and remaining (joined, truncated) -> address.line2
// We also persist full text in metadata so we can reconstruct or improve parsing later.
// NOTE: Because Stripe requires structured address, any city/state/postal/country detection
// would be heuristic; we keep it simple unless we later add structured fields.
const lines: Array<string> = businessDetails
.split(/\r?\n/)
.map((l: string) => {
return l.trim();
})
.filter((l: string) => {
return l.length > 0;
});
let line1: string | undefined = undefined;
let line2: string | undefined = undefined;
if (lines && lines.length > 0) {
const first: string = lines[0]!; // non-null
line1 = first.substring(0, 200); // Stripe typical limit safeguard.
}
if (lines && lines.length > 1) {
const rest: string = lines.slice(1).join(", ");
line2 = rest.substring(0, 200);
}
const metadata: Record<string, string> = {
business_details_full: businessDetails.substring(0, 5000),
};
if (financeAccountingEmail) {
metadata["finance_accounting_email"] = financeAccountingEmail.substring(
0,
200,
);
} else {
// Remove if cleared
metadata["finance_accounting_email"] = "";
}
const updateParams: Stripe.CustomerUpdateParams = {
metadata,
address: {},
};
// If finance / accounting email provided, set it as the customer email so Stripe sends
// invoices / receipts there. (Stripe only supports a single email via API currently.)
if (financeAccountingEmail && financeAccountingEmail.trim().length > 0) {
updateParams.email = financeAccountingEmail.trim();
}
if (line1) {
updateParams.address = updateParams.address || {};
updateParams.address.line1 = line1;
}
if (line2) {
updateParams.address = updateParams.address || {};
updateParams.address.line2 = line2;
}
if (countryCode) {
updateParams.address = updateParams.address || {};
// Stripe expects uppercase 2-letter ISO code
updateParams.address.country = countryCode.toUpperCase();
}
if (!line1 && !line2 && !countryCode) {
// Clear address if empty details submitted.
updateParams.address = {
line1: "",
line2: "",
} as any;
}
await this.stripe.customers.update(id, updateParams);
}
@CaptureSpan()
public async deleteCustomer(id: string): Promise<void> {
if (!this.isBillingEnabled()) {

View File

@@ -67,6 +67,7 @@ import QueryOperator from "../../Types/BaseDatabase/QueryOperator";
import { FindWhere } from "../../Types/BaseDatabase/Query";
import logger from "../Utils/Logger";
import PushNotificationUtil from "../Utils/PushNotificationUtil";
import ExceptionMessages from "../../Types/Exception/ExceptionMessages";
export class Service extends DatabaseService<Model> {
public constructor() {
@@ -162,11 +163,11 @@ export class Service extends DatabaseService<Model> {
});
if (!monitor) {
throw new BadDataException("Monitor not found.");
throw new BadDataException(ExceptionMessages.MonitorNotFound);
}
if (!monitor.id) {
throw new BadDataException("Monitor id not found.");
throw new BadDataException(ExceptionMessages.MonitorNotFound);
}
projectId = monitor.projectId!;
@@ -1389,7 +1390,7 @@ ${createdItem.description?.trim() || "No description provided."}
});
if (!monitor) {
throw new BadDataException("Monitor not found.");
throw new BadDataException(ExceptionMessages.MonitorNotFound);
}
return (monitor.postUpdatesToWorkspaceChannels || []).filter(
@@ -1419,7 +1420,7 @@ ${createdItem.description?.trim() || "No description provided."}
});
if (!monitor) {
throw new BadDataException("Monitor not found.");
throw new BadDataException(ExceptionMessages.MonitorNotFound);
}
return monitor.name || "";

View File

@@ -128,18 +128,50 @@ export default class OTelIngestService {
Metric,
) as Metric;
// Handle start timestamp safely
if (datapoint["startTimeUnixNano"]) {
newDbMetric.startTimeUnixNano = datapoint["startTimeUnixNano"] as number;
newDbMetric.startTime = OneUptimeDate.fromUnixNano(
datapoint["startTimeUnixNano"] as number,
);
try {
let startTimeUnixNano: number;
if (typeof datapoint["startTimeUnixNano"] === "string") {
startTimeUnixNano = parseFloat(datapoint["startTimeUnixNano"]);
if (isNaN(startTimeUnixNano)) {
startTimeUnixNano = OneUptimeDate.getCurrentDateAsUnixNano();
}
} else {
startTimeUnixNano =
(datapoint["startTimeUnixNano"] as number) ||
OneUptimeDate.getCurrentDateAsUnixNano();
}
newDbMetric.startTimeUnixNano = startTimeUnixNano;
newDbMetric.startTime = OneUptimeDate.fromUnixNano(startTimeUnixNano);
} catch {
const currentNano: number = OneUptimeDate.getCurrentDateAsUnixNano();
newDbMetric.startTimeUnixNano = currentNano;
newDbMetric.startTime = OneUptimeDate.getCurrentDate();
}
}
// Handle end timestamp safely
if (datapoint["timeUnixNano"]) {
newDbMetric.timeUnixNano = datapoint["timeUnixNano"] as number;
newDbMetric.time = OneUptimeDate.fromUnixNano(
datapoint["timeUnixNano"] as number,
);
try {
let timeUnixNano: number;
if (typeof datapoint["timeUnixNano"] === "string") {
timeUnixNano = parseFloat(datapoint["timeUnixNano"]);
if (isNaN(timeUnixNano)) {
timeUnixNano = OneUptimeDate.getCurrentDateAsUnixNano();
}
} else {
timeUnixNano =
(datapoint["timeUnixNano"] as number) ||
OneUptimeDate.getCurrentDateAsUnixNano();
}
newDbMetric.timeUnixNano = timeUnixNano;
newDbMetric.time = OneUptimeDate.fromUnixNano(timeUnixNano);
} catch {
const currentNano: number = OneUptimeDate.getCurrentDateAsUnixNano();
newDbMetric.timeUnixNano = currentNano;
newDbMetric.time = OneUptimeDate.getCurrentDate();
}
}
if (Object.keys(datapoint).includes("asInt")) {
@@ -174,7 +206,7 @@ export default class OTelIngestService {
serviceName: data.telemetryServiceName,
}),
...TelemetryUtil.getAttributes({
items: datapoint["attributes"] as JSONArray,
items: (datapoint["attributes"] as JSONArray) || [],
prefixKeysWithString: "metricAttributes",
}),
};

View File

@@ -273,6 +273,38 @@ export class ProjectService extends DatabaseService<Model> {
updateBy: UpdateBy<Model>,
): Promise<OnUpdate<Model>> {
if (IsBillingEnabled) {
if (
updateBy.data.businessDetails ||
updateBy.data.businessDetailsCountry ||
updateBy.data.financeAccountingEmail
) {
// Sync to Stripe.
const project: Model | null = await this.findOneById({
id: new ObjectID(updateBy.query._id! as string),
select: {
paymentProviderCustomerId: true,
financeAccountingEmail: true,
},
props: { isRoot: true },
});
if (project?.paymentProviderCustomerId) {
try {
await BillingService.updateCustomerBusinessDetails(
project.paymentProviderCustomerId,
(updateBy.data.businessDetails as string) || "",
(updateBy.data.businessDetailsCountry as string) || null,
(updateBy.data.financeAccountingEmail as string) ||
(project as any).financeAccountingEmail ||
null,
);
} catch (err) {
logger.error(
"Failed to update Stripe customer business details: " + err,
);
}
}
}
if (updateBy.data.enableAutoRechargeSmsOrCallBalance) {
await NotificationService.rechargeIfBalanceIsLow(
new ObjectID(updateBy.query._id! as string),

View File

@@ -1,132 +0,0 @@
import TeamMember from "../../Models/DatabaseModels/TeamMember";
import LIMIT_MAX from "../../Types/Database/LimitMax";
import ObjectID from "../../Types/ObjectID";
import DatabaseService from "./DatabaseService";
import Model from "../../Models/DatabaseModels/ProjectUser";
import TeamMemberService from "./TeamMemberService";
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
export class Service extends DatabaseService<Model> {
public constructor() {
super(Model);
}
@CaptureSpan()
public async refreshProjectUsersByProject(data: {
projectId: ObjectID;
}): Promise<void> {
// get all team members by user
// first delete all project users by project id.
await this.deleteBy({
query: {
projectId: data.projectId,
},
limit: LIMIT_MAX,
skip: 0,
props: {
isRoot: true,
},
});
// get all team members by project.
const teamMembers: Array<TeamMember> = await TeamMemberService.findBy({
query: {
projectId: data.projectId,
},
limit: LIMIT_MAX,
skip: 0,
props: {
isRoot: true,
},
select: {
userId: true,
teamId: true,
team: {
_id: true,
},
hasAcceptedInvitation: true,
},
});
// create project users by team members.
const projectUsersToCreate: Array<Model> = [];
for (const teamMember of teamMembers) {
// check if the user already exists in the project users.
// if yes then add the team to the project user acceptedTeams, if the invitation is accepted.
// if no then create a new project user.
// if the user is not accepted the invitation then add the team to invitedTeams of the project user.
// if the user is accepted the invitation then add the team to acceptedTeams of the project user.
let doesProjectUserExist: boolean = false;
for (const item of projectUsersToCreate) {
if (item.userId?.toString() === teamMember.userId?.toString()) {
doesProjectUserExist = true;
break;
}
}
if (doesProjectUserExist) {
// add the team to the project user acceptedTeams, if the invitation is accepted.
if (teamMember.hasAcceptedInvitation) {
for (const projectUser of projectUsersToCreate) {
if (
projectUser.userId?.toString() === teamMember.userId?.toString()
) {
if (!projectUser.acceptedTeams) {
projectUser.acceptedTeams = [];
}
projectUser.acceptedTeams?.push(teamMember.team!);
}
}
} else {
for (const projectUser of projectUsersToCreate) {
if (
projectUser.userId?.toString() === teamMember.userId?.toString()
) {
if (!projectUser.invitedTeams) {
projectUser.invitedTeams = [];
}
projectUser.invitedTeams?.push(teamMember.team!);
}
}
}
} else {
// create a new project user.
const projectUser: Model = new Model();
projectUser.userId = teamMember.userId!;
projectUser.projectId = data.projectId;
if (teamMember.hasAcceptedInvitation) {
projectUser.acceptedTeams = [teamMember.team!];
} else {
projectUser.invitedTeams = [teamMember.team!];
}
projectUsersToCreate.push(projectUser);
}
}
// now create the project users.
for (const projectUser of projectUsersToCreate) {
await this.create({
data: projectUser,
props: {
isRoot: true,
},
});
}
}
}
export default new Service();

View File

@@ -340,10 +340,26 @@ export class Service extends DatabaseService<StatusPage> {
projectId: ObjectID,
statusPageId: ObjectID,
): Promise<URL> {
const dahboardUrl: URL = await DatabaseConfig.getDashboardUrl();
if (!projectId) {
throw new BadDataException(
"projectId is required to build status page dashboard link",
);
}
return URL.fromString(dahboardUrl.toString()).addRoute(
`/${projectId.toString()}/status-pages/${statusPageId.toString()}`,
if (!statusPageId) {
throw new BadDataException(
"statusPageId is required to build status page dashboard link",
);
}
const dashboardUrl: URL = await DatabaseConfig.getDashboardUrl();
// Defensive: ensure objects have toString
const projectIdStr: string = projectId.toString();
const statusPageIdStr: string = statusPageId.toString();
return URL.fromString(dashboardUrl.toString()).addRoute(
`/${projectIdStr}/status-pages/${statusPageIdStr}`,
);
}

View File

@@ -32,6 +32,7 @@ import PositiveNumber from "../../Types/PositiveNumber";
import StatusPageEventType from "../../Types/StatusPage/StatusPageEventType";
import NumberUtil from "../../Utils/Number";
import SlackUtil from "../Utils/Workspace/Slack/Slack";
import MicrosoftTeamsUtil from "../Utils/Workspace/MicrosoftTeams/MicrosoftTeams";
export class Service extends DatabaseService<Model> {
public constructor() {
@@ -221,6 +222,23 @@ export class Service extends DatabaseService<Model> {
}
}
// Validate Microsoft Teams webhook URL if provided
if (data.data.microsoftTeamsIncomingWebhookUrl) {
logger.debug(
`Microsoft Teams Incoming Webhook URL: ${data.data.microsoftTeamsIncomingWebhookUrl}`,
);
if (
!MicrosoftTeamsUtil.isValidMicrosoftTeamsIncomingWebhookUrl(
data.data.microsoftTeamsIncomingWebhookUrl,
)
) {
logger.debug("Invalid Microsoft Teams Incoming Webhook URL.");
throw new BadDataException(
"Invalid Microsoft Teams Incoming Webhook URL.",
);
}
}
data.data.subscriptionConfirmationToken = NumberUtil.getRandomNumber(
100000,
999999,
@@ -370,16 +388,55 @@ Stay informed about service availability! 🚀`;
logger.debug(`Slack Message: ${slackMessage}`);
try {
await SlackUtil.sendMessageToChannelViaIncomingWebhook({
url: URL.fromString(createdItem.slackIncomingWebhookUrl.toString()),
text: SlackUtil.convertMarkdownToSlackRichText(slackMessage),
SlackUtil.sendMessageToChannelViaIncomingWebhook({
url: URL.fromString(createdItem.slackIncomingWebhookUrl.toString()),
text: SlackUtil.convertMarkdownToSlackRichText(slackMessage),
})
.then(() => {
logger.debug("Slack notification sent successfully.");
})
.catch((err: Error) => {
logger.error("Error sending Slack notification:");
logger.error(err);
});
}
// if Microsoft Teams incoming webhook is provided and sendYouHaveSubscribedMessage is true, then send a message to the Teams channel.
if (
createdItem.microsoftTeamsIncomingWebhookUrl &&
createdItem.sendYouHaveSubscribedMessage
) {
logger.debug("Sending Microsoft Teams notification for new subscriber.");
const teamsMessage: string = `## 📢 New Subscription to ${statusPageName}
**You have successfully subscribed to receive status updates!**
🔗 **Status Page:** [${statusPageName}](${statusPageURL})
📧 **Manage Subscription:** [Update preferences or unsubscribe](${unsubscribeLink})
You will receive real-time notifications for:
• Incidents and outages
• Scheduled maintenance events
• Service announcements
• Status updates
Stay informed about service availability! 🚀`;
logger.debug(`Teams Message: ${teamsMessage}`);
MicrosoftTeamsUtil.sendMessageToChannelViaIncomingWebhook({
url: URL.fromString(
createdItem.microsoftTeamsIncomingWebhookUrl.toString(),
),
text: teamsMessage,
})
.then(() => {
logger.debug("Microsoft Teams notification sent successfully.");
})
.catch((err: Error) => {
logger.error("Error sending Microsoft Teams notification:");
logger.error(err);
});
logger.debug("Slack notification sent successfully.");
} catch (error) {
logger.error("Error sending Slack notification:");
logger.error(error);
}
}
logger.debug("onCreateSuccess completed.");
@@ -691,6 +748,7 @@ Stay informed about service availability! 🚀`;
subscriberPhone: true,
subscriberWebhook: true,
slackIncomingWebhookUrl: true,
microsoftTeamsIncomingWebhookUrl: true,
isSubscribedToAllResources: true,
statusPageResources: true,
isSubscribedToAllEventTypes: true,

View File

@@ -33,7 +33,6 @@ import PositiveNumber from "../../Types/PositiveNumber";
import Project from "../../Models/DatabaseModels/Project";
import TeamMember from "../../Models/DatabaseModels/TeamMember";
import User from "../../Models/DatabaseModels/User";
import ProjectUserService from "./ProjectUserService";
import OnCallDutyPolicyTimeLogService from "./OnCallDutyPolicyTimeLogService";
import OneUptimeDate from "../../Types/Date";
@@ -198,12 +197,6 @@ export class TeamMemberService extends DatabaseService<TeamMember> {
onCreate.createBy.data.projectId!,
);
ProjectUserService.refreshProjectUsersByProject({
projectId: onCreate.createBy.data.projectId!,
}).catch((err: Error) => {
logger.error(err);
});
return createdItem;
}
@@ -247,12 +240,6 @@ export class TeamMemberService extends DatabaseService<TeamMember> {
item.user?.email as Email,
);
}
ProjectUserService.refreshProjectUsersByProject({
projectId: item.projectId!,
}).catch((err: Error) => {
logger.error(err);
});
}
return { updateBy, carryForward: onUpdate.carryForward };
@@ -335,13 +322,6 @@ export class TeamMemberService extends DatabaseService<TeamMember> {
item.userId!,
item.projectId!,
);
// refresh project users.
ProjectUserService.refreshProjectUsersByProject({
projectId: item.projectId!,
}).catch((err: Error) => {
logger.error(err);
});
}
return onDelete;

View File

@@ -49,6 +49,7 @@ import WorkspaceNotificationLog from "../../Models/DatabaseModels/WorkspaceNotif
import WorkspaceNotificationLogService from "./WorkspaceNotificationLogService";
import WorkspaceNotificationStatus from "../../Types/Workspace/WorkspaceNotificationStatus";
import WorkspaceNotificationActionType from "../../Types/Workspace/WorkspaceNotificationActionType";
import ExceptionMessages from "../../Types/Exception/ExceptionMessages";
export interface MessageBlocksByWorkspaceType {
workspaceType: WorkspaceType;
@@ -1911,7 +1912,7 @@ export class Service extends DatabaseService<WorkspaceNotificationRule> {
if (!monitor) {
logger.debug("Monitor not found for ID:");
logger.debug(data.notificationFor.monitorId);
throw new BadDataException("Monitor ID not found");
throw new BadDataException(ExceptionMessages.MonitorNotFound);
}
const monitorLabels: Array<Label> = monitor?.labels || [];

View File

@@ -19,9 +19,8 @@ export default class Markdown {
markdown: string,
contentType: MarkdownContentType,
): Promise<string> {
// convert tags > and < to &gt; and &lt;
markdown = markdown.replace(/</g, "&lt;");
markdown = markdown.replace(/>/g, "&gt;");
// Basic sanitization: neutralize script tags but preserve markdown syntax like '>' for blockquotes.
markdown = markdown.replace(/<script/gi, "&lt;script");
let renderer: Renderer | null = null;
@@ -78,7 +77,7 @@ export default class Markdown {
};
renderer.code = function (code, language) {
return `<pre class="language-${language} rounded-md"><code class="language-${language} rounded-md">${code}</code></pre>`;
return `<pre class="language-${language} rounded-xl bg-slate-900/95 text-slate-100 p-5 overflow-x-auto text-sm shadow-md ring-1 ring-slate-900/10"><code class="language-${language}">${code}</code></pre>`;
};
renderer.heading = function (text, level) {
@@ -137,6 +136,102 @@ export default class Markdown {
return `<h6 class="my-5 tracking-tight font-bold text-gray-800">${text}</h6>`;
};
// Lists
renderer.list = function (body, ordered, start) {
const tag: string = ordered ? "ol" : "ul";
const cls: string = ordered
? "list-decimal pl-6 my-6 space-y-2 text-gray-700"
: "list-disc pl-6 my-6 space-y-2 text-gray-700";
const startAttr: string =
ordered && start !== 1 ? ` start="${start}"` : "";
return `<${tag}${startAttr} class="${cls}">${body}</${tag}>`;
};
renderer.listitem = function (text) {
return `<li class="leading-7">${text}</li>`;
};
// Tables
renderer.table = function (header, body) {
return `<div class="overflow-x-auto my-8"><table class="min-w-full border border-gray-200 text-sm text-left">
${header}${body}
</table></div>`;
};
renderer.tablerow = function (content) {
return `<tr class="border-b last:border-b-0">${content}</tr>`;
};
renderer.tablecell = function (content, flags) {
const type: string = flags.header ? "th" : "td";
const base: string = "px-4 py-2 border-r last:border-r-0 border-gray-200";
const align: string = flags.align ? ` text-${flags.align}` : "";
const weight: string = flags.header ? " font-semibold bg-gray-50" : "";
return `<${type} class="${base}${align}${weight}">${content}</${type}>`;
};
// Inline code
renderer.codespan = function (code) {
return `<code class="rounded-md bg-gray-100 px-1.5 py-0.5 text-sm text-pink-600">${code}</code>`;
};
// Horizontal rule
renderer.hr = function () {
return '<hr class="my-12 border-t border-gray-200" />';
};
// Emphasis / Strong / Strikethrough
renderer.strong = function (text) {
return `<strong class="font-semibold text-gray-800">${text}</strong>`;
};
renderer.em = function (text) {
return `<em class="italic text-gray-700">${text}</em>`;
};
renderer.del = function (text) {
return `<del class="line-through text-gray-400">${text}</del>`;
};
// Images
renderer.image = function (href, _title, text) {
return `<figure class="my-8"><img src="${href}" alt="${text}" class="rounded-xl shadow-sm border border-gray-200" loading="lazy"/><figcaption class="mt-2 text-center text-sm text-gray-500">${text || ""}</figcaption></figure>`;
};
// Links
// We explicitly add underline + color classes because Tailwind Typography (prose-*)
// styles may get overridden by surrounding utility classes or global resets.
// External links open in a new tab with proper rel attributes; internal links stay in-page.
renderer.link = function (href, title, text) {
// Guard: if no href, just return the text.
if (!href) {
return text as string;
}
const isHash: boolean = href.startsWith("#");
const isMailTo: boolean = href.startsWith("mailto:");
const isTel: boolean = href.startsWith("tel:");
const isInternal: boolean =
href.startsWith("/") ||
href.includes("oneuptime.com") ||
isHash ||
isMailTo ||
isTel;
const baseClasses: string = [
"font-semibold",
"text-indigo-600",
"underline",
"underline-offset-2",
"decoration-indigo-300",
"hover:decoration-indigo-500",
"hover:text-indigo-500",
"transition-colors",
].join(" ");
const titleAttr: string = title ? ` title="${title}"` : "";
const externalAttrs: string = isInternal
? ""
: ' target="_blank" rel="noopener noreferrer"';
return `<a href="${href}"${titleAttr} class="${baseClasses}"${externalAttrs}>${text}</a>`;
};
this.blogRenderer = renderer;
return renderer;

View File

@@ -58,6 +58,7 @@ import CaptureSpan from "../Telemetry/CaptureSpan";
import MetricType from "../../../Models/DatabaseModels/MetricType";
import MonitorLog from "../../../Models/AnalyticsModels/MonitorLog";
import MonitorLogService from "../../Services/MonitorLogService";
import ExceptionMessages from "../../../Types/Exception/ExceptionMessages";
export default class MonitorResourceUtil {
@CaptureSpan()
@@ -109,7 +110,7 @@ export default class MonitorResourceUtil {
if (!monitor) {
logger.debug(`${dataToProcess.monitorId.toString()} Monitor not found`);
throw new BadDataException("Monitor not found");
throw new BadDataException(ExceptionMessages.MonitorNotFound);
}
if (!monitor.projectId) {
@@ -126,9 +127,7 @@ export default class MonitorResourceUtil {
`${dataToProcess.monitorId.toString()} Monitor is disabled. Please enable it to start monitoring again.`,
);
throw new BadDataException(
"Monitor is disabled. Please enable it to start monitoring again.",
);
throw new BadDataException(ExceptionMessages.MonitorDisabled);
}
if (monitor.disableActiveMonitoringBecauseOfManualIncident) {

View File

@@ -3,414 +3,154 @@ import HTTPResponse from "../../../../Types/API/HTTPResponse";
import URL from "../../../../Types/API/URL";
import { JSONObject } from "../../../../Types/JSON";
import API from "../../../../Utils/API";
import WorkspaceMessagePayload from "../../../../Types/Workspace/WorkspaceMessagePayload";
import logger from "../../Logger";
import Dictionary from "../../../../Types/Dictionary";
import WorkspaceBase, {
WorkspaceChannel,
WorkspaceSendMessageResponse,
WorkspaceThread,
} from "../WorkspaceBase";
import WorkspaceType from "../../../../Types/Workspace/WorkspaceType";
import OneUptimeDate from "../../../../Types/Date";
import WorkspaceBase from "../WorkspaceBase";
import CaptureSpan from "../../Telemetry/CaptureSpan";
import BadDataException from "../../../../Types/Exception/BadDataException";
export default class MicrosoftTeams extends WorkspaceBase {
@CaptureSpan()
public static override async getAllWorkspaceChannels(data: {
authToken: string;
}): Promise<Dictionary<WorkspaceChannel>> {
logger.debug("Getting all workspace channels with data:");
logger.debug(data);
const channels: Dictionary<WorkspaceChannel> = {};
const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
await API.get<JSONObject>(
URL.fromString("https://graph.microsoft.com/v1.0/me/joinedTeams"),
{
Authorization: `Bearer ${data.authToken}`,
},
);
logger.debug("Response from Microsoft Graph API for getting all channels:");
logger.debug(JSON.stringify(response, null, 2));
if (response instanceof HTTPErrorResponse) {
logger.error("Error response from Microsoft Graph API:");
logger.error(response);
throw response;
}
for (const team of (response.jsonData as JSONObject)?.[
"value"
] as Array<JSONObject>) {
if (!team["id"] || !team["displayName"]) {
continue;
}
channels[team["displayName"].toString()] = {
id: team["id"] as string,
name: team["displayName"] as string,
workspaceType: WorkspaceType.MicrosoftTeams,
};
}
logger.debug("All workspace channels obtained:");
logger.debug(channels);
return channels;
}
@CaptureSpan()
public static override getDividerBlock(): JSONObject {
return {
type: "divider",
};
}
@CaptureSpan()
public static getValuesFromView(data: {
view: JSONObject;
}): Dictionary<string | number | Array<string | number> | Date> {
logger.debug("Getting values from view with data:");
logger.debug(JSON.stringify(data, null, 2));
const teamsView: JSONObject = data.view;
const values: Dictionary<string | number | Array<string | number> | Date> =
{};
if (!teamsView["state"] || !(teamsView["state"] as JSONObject)["values"]) {
return {};
}
for (const valueId in (teamsView["state"] as JSONObject)[
"values"
] as JSONObject) {
for (const blockId in (
(teamsView["state"] as JSONObject)["values"] as JSONObject
)[valueId] as JSONObject) {
const valueObject: JSONObject = (
(teamsView["state"] as JSONObject)["values"] as JSONObject
)[valueId] as JSONObject;
const value: JSONObject = valueObject[blockId] as JSONObject;
values[blockId] = value["value"] as string | number;
if ((value["selected_option"] as JSONObject)?.["value"]) {
values[blockId] = (value["selected_option"] as JSONObject)?.[
"value"
] as string;
}
if (Array.isArray(value["selected_options"])) {
values[blockId] = (
value["selected_options"] as Array<JSONObject>
).map((option: JSONObject) => {
return option["value"] as string | number;
});
}
// if date picker
if (value["selected_date_time"]) {
values[blockId] = OneUptimeDate.fromUnixTimestamp(
value["selected_date_time"] as number,
);
}
}
}
logger.debug("Values obtained from view:");
logger.debug(values);
return values;
}
@CaptureSpan()
public static override async inviteUserToChannelByChannelName(data: {
authToken: string;
channelName: string;
workspaceUserId: string;
}): Promise<void> {
logger.debug("Inviting user to channel with data:");
logger.debug(data);
const channelId: string = (
await this.getWorkspaceChannelFromChannelName({
authToken: data.authToken,
channelName: data.channelName,
private static buildMessageCardFromMarkdown(markdown: string): JSONObject {
// Teams MessageCard has limited markdown support. Headings like '##' are not supported
// and single newlines can collapse. Convert common patterns to a structured card.
const lines: Array<string> = markdown
.split("\n")
.map((l: string) => {
return l.trim();
})
).id;
return this.inviteUserToChannelByChannelId({
authToken: data.authToken,
channelId: channelId,
workspaceUserId: data.workspaceUserId,
});
}
@CaptureSpan()
public static override async createChannelsIfDoesNotExist(data: {
authToken: string;
channelNames: Array<string>;
}): Promise<Array<WorkspaceChannel>> {
logger.debug("Creating channels if they do not exist with data:");
logger.debug(data);
const workspaceChannels: Array<WorkspaceChannel> = [];
const existingWorkspaceChannels: Dictionary<WorkspaceChannel> =
await this.getAllWorkspaceChannels({
authToken: data.authToken,
.filter((l: string) => {
return l.length > 0;
});
logger.debug("Existing workspace channels:");
logger.debug(existingWorkspaceChannels);
let title: string = "";
const facts: Array<JSONObject> = [];
const actions: Array<JSONObject> = [];
const bodyTextParts: Array<string> = [];
for (let channelName of data.channelNames) {
// if channel name starts with #, remove it
if (channelName && channelName.startsWith("#")) {
channelName = channelName.substring(1);
}
// convert channel name to lowercase
channelName = channelName.toLowerCase();
// replace spaces with hyphens
channelName = channelName.replace(/\s+/g, "-");
if (existingWorkspaceChannels[channelName]) {
logger.debug(`Channel ${channelName} already exists.`);
workspaceChannels.push(existingWorkspaceChannels[channelName]!);
continue;
}
logger.debug(`Channel ${channelName} does not exist. Creating channel.`);
const channel: WorkspaceChannel = await this.createChannel({
authToken: data.authToken,
channelName: channelName,
});
if (channel) {
logger.debug(`Channel ${channelName} created successfully.`);
workspaceChannels.push(channel);
}
// Extract title from the first non-empty line and strip markdown heading markers
if (lines.length > 0) {
const firstLine: string = lines[0] ?? "";
title = firstLine
.replace(/^#+\s*/, "") // remove leading markdown headers like ##
.replace(/^\*\*|\*\*$/g, "") // remove stray bold markers if any
.trim();
lines.shift();
}
logger.debug("Channels created or found:");
logger.debug(workspaceChannels);
return workspaceChannels;
}
const linkRegex: RegExp = /\[([^\]]+)\]\(([^)]+)\)/g; // [text](url)
@CaptureSpan()
public static override async getWorkspaceChannelFromChannelName(data: {
authToken: string;
channelName: string;
}): Promise<WorkspaceChannel> {
logger.debug("Getting workspace channel ID from channel name with data:");
logger.debug(data);
const channels: Dictionary<WorkspaceChannel> =
await this.getAllWorkspaceChannels({
authToken: data.authToken,
});
logger.debug("All workspace channels:");
logger.debug(channels);
if (!channels[data.channelName]) {
logger.error("Channel not found.");
throw new BadDataException("Channel not found.");
}
logger.debug("Workspace channel ID obtained:");
logger.debug(channels[data.channelName]!.id);
return channels[data.channelName]!;
}
@CaptureSpan()
public static override async getWorkspaceChannelFromChannelId(data: {
authToken: string;
channelId: string;
}): Promise<WorkspaceChannel> {
logger.debug("Getting workspace channel from channel ID with data:");
logger.debug(data);
const response: HTTPErrorResponse | HTTPResponse<JSONObject> =
await API.get<JSONObject>(
URL.fromString(
`https://graph.microsoft.com/v1.0/teams/${data.channelId}`,
),
{
Authorization: `Bearer ${data.authToken}`,
},
);
logger.debug("Response from Microsoft Graph API for getting channel info:");
logger.debug(response);
if (response instanceof HTTPErrorResponse) {
logger.error("Error response from Microsoft Graph API:");
logger.error(response);
throw response;
}
if (!(response.jsonData as JSONObject)?.["displayName"]) {
logger.error("Invalid response from Microsoft Graph API:");
logger.error(response.jsonData);
throw new Error("Invalid response");
}
const channel: WorkspaceChannel = {
name: (response.jsonData as JSONObject)["displayName"] as string,
id: data.channelId,
workspaceType: WorkspaceType.MicrosoftTeams,
};
logger.debug("Workspace channel obtained:");
logger.debug(channel);
return channel;
}
@CaptureSpan()
public static override async doesChannelExist(data: {
authToken: string;
channelName: string;
}): Promise<boolean> {
// if channel name starts with #, remove it
if (data.channelName && data.channelName.startsWith("#")) {
data.channelName = data.channelName.substring(1);
}
// convert channel name to lowercase
data.channelName = data.channelName.toLowerCase();
// get channel id from channel name
const channels: Dictionary<WorkspaceChannel> =
await this.getAllWorkspaceChannels({
authToken: data.authToken,
});
// if this channel exists
if (channels[data.channelName]) {
return true;
}
return false;
}
@CaptureSpan()
public static override async sendMessage(data: {
workspaceMessagePayload: WorkspaceMessagePayload;
authToken: string; // which auth token should we use to send.
userId: string;
}): Promise<WorkspaceSendMessageResponse> {
logger.debug("Sending message to Microsoft Teams with data:");
logger.debug(data);
const blocks: Array<JSONObject> = this.getBlocksFromWorkspaceMessagePayload(
data.workspaceMessagePayload,
);
logger.debug("Blocks generated from workspace message payload:");
logger.debug(blocks);
const existingWorkspaceChannels: Dictionary<WorkspaceChannel> =
await this.getAllWorkspaceChannels({
authToken: data.authToken,
});
logger.debug("Existing workspace channels:");
logger.debug(existingWorkspaceChannels);
const workspaceChannelsToPostTo: Array<WorkspaceChannel> = [];
for (let channelName of data.workspaceMessagePayload.channelNames) {
if (channelName && channelName.startsWith("#")) {
// trim # from channel name
channelName = channelName.substring(1);
}
let channel: WorkspaceChannel | null = null;
if (existingWorkspaceChannels[channelName]) {
channel = existingWorkspaceChannels[channelName]!;
}
if (channel) {
workspaceChannelsToPostTo.push(channel);
} else {
logger.debug(`Channel ${channelName} does not exist.`);
}
}
// add channel ids.
for (const channelId of data.workspaceMessagePayload.channelIds) {
try {
// Get the channel info including name from channel ID
const channel: WorkspaceChannel =
await this.getWorkspaceChannelFromChannelId({
authToken: data.authToken,
channelId: channelId,
});
workspaceChannelsToPostTo.push(channel);
} catch (err) {
logger.error(`Error getting channel info for channel ID ${channelId}:`);
logger.error(err);
// Fallback: create channel object with empty name if API call fails
const channel: WorkspaceChannel = {
id: channelId,
name: channelId,
workspaceType: WorkspaceType.MicrosoftTeams,
};
workspaceChannelsToPostTo.push(channel);
}
}
logger.debug("Channel IDs to post to:");
logger.debug(workspaceChannelsToPostTo);
const workspaceMessageResponse: WorkspaceSendMessageResponse = {
threads: [],
workspaceType: WorkspaceType.MicrosoftTeams,
};
for (const channel of workspaceChannelsToPostTo) {
try {
// check if the user is in the channel.
const isUserInChannel: boolean = await this.isUserInChannel({
authToken: data.authToken,
channelId: channel.id,
userId: data.userId,
for (const line of lines) {
// Extract links to actions and strip them from text
let lineWithoutLinks: string = line;
let match: RegExpExecArray | null = null;
while ((match = linkRegex.exec(line))) {
const name: string = match[1] ?? "";
const url: string = match[2] ?? "";
actions.push({
["@type"]: "OpenUri",
name: name,
targets: [
{
os: "default",
uri: url,
},
],
});
lineWithoutLinks = lineWithoutLinks.replace(match[0], "").trim();
}
if (!isUserInChannel) {
// add user to the channel
await this.joinChannel({
authToken: data.authToken,
channelId: channel.id,
});
// Parse facts of the form **Label:** value
const factMatch: RegExpExecArray | null = new RegExp(
"\\*\\*(.*?):\\*\\*\\s*(.*)",
).exec(lineWithoutLinks);
if (factMatch) {
const name: string = (factMatch[1] ?? "").trim();
const value: string = (factMatch[2] ?? "").trim();
if (
name.toLowerCase() === "description" ||
name.toLowerCase() === "note"
) {
bodyTextParts.push(`**${name}:** ${value}`);
} else {
facts.push({ name: name, value: value });
}
const thread: WorkspaceThread = await this.sendPayloadBlocksToChannel({
authToken: data.authToken,
workspaceChannel: channel,
blocks: blocks,
});
workspaceMessageResponse.threads.push(thread);
logger.debug(`Message sent to channel ID ${channel.id} successfully.`);
} catch (e) {
logger.error(`Error sending message to channel ID ${channel.id}:`);
logger.error(e);
} else if (lineWithoutLinks) {
bodyTextParts.push(lineWithoutLinks);
}
}
logger.debug("Message sent successfully.");
logger.debug(workspaceMessageResponse);
const payload: JSONObject = {
["@type"]: "MessageCard",
["@context"]: "https://schema.org/extensions",
title: title,
summary: title,
};
return workspaceMessageResponse;
if (bodyTextParts.length > 0) {
payload["text"] = bodyTextParts.join("\n\n");
}
if (facts.length > 0) {
payload["sections"] = [
{
facts: facts,
},
];
}
if (actions.length > 0) {
payload["potentialAction"] = actions;
}
return payload;
}
@CaptureSpan()
public static override async sendMessageToChannelViaIncomingWebhook(data: {
url: URL;
text: string;
}): Promise<HTTPResponse<JSONObject> | HTTPErrorResponse> {
logger.debug("Sending message to Teams channel via incoming webhook:");
logger.debug(data);
// Build a structured MessageCard from markdown for better rendering in Teams
const payload: JSONObject = this.buildMessageCardFromMarkdown(data.text);
const apiResult: HTTPResponse<JSONObject> | HTTPErrorResponse | null =
await API.post(data.url, payload);
if (!apiResult) {
logger.error(
"Could not send message to Teams channel via incoming webhook.",
);
throw new Error(
"Could not send message to Teams channel via incoming webhook.",
);
}
if (apiResult instanceof HTTPErrorResponse) {
logger.error(
"Error sending message to Teams channel via incoming webhook:",
);
logger.error(apiResult);
throw apiResult;
}
logger.debug(
"Message sent to Teams channel via incoming webhook successfully:",
);
logger.debug(apiResult);
return apiResult;
}
public static isValidMicrosoftTeamsIncomingWebhookUrl(
incomingWebhookUrl: URL,
): boolean {
// Check if the URL contains outlook.office.com or office.com webhook pattern
const urlString: string = incomingWebhookUrl.toString();
return (
urlString.includes("outlook.office.com") ||
urlString.includes("office.com")
);
}
}

View File

@@ -67,7 +67,6 @@
<link rel="apple-touch-icon-precomposed" href="/img/ou-wb.svg">
<link rel="icon" href="/img/ou-wb.svg">
<link rel="image_src" type="image/png" href="/img/hou-wb.svg">
<link rel="canonical" href="/">
<link rel="manifest" href="/manifest.json">
<meta property="og:title" content="OneUptime - One Complete Observability platform.">
<meta property="og:url" content="https://oneuptime.com">

View File

@@ -28,21 +28,10 @@ import EmptyResponseData from "../../../Types/API/EmptyResponse";
jest.setTimeout(60000); // Increase test timeout to 60 seconds becuase GitHub runners are slow
jest.mock("../../../Server/Services/ProjectUserService", () => {
return {
refreshProjectUsersByProject: jest.fn().mockResolvedValue(undefined),
};
});
// Get the mocked module to access the mock functions
import ProjectUserService from "../../../Server/Services/ProjectUserService";
const mockProjectUserService = ProjectUserService as jest.Mocked<typeof ProjectUserService>;
describe("TeamMemberService", () => {
beforeEach(async () => {
jest.resetAllMocks();
// Re-setup the mock after resetAllMocks
mockProjectUserService.refreshProjectUsersByProject.mockResolvedValue(undefined);
await TestDatabaseMock.connectDbMock();
});

View File

@@ -0,0 +1,6 @@
enum ExceptionMessages {
MonitorNotFound = "Monitor not found.",
MonitorDisabled = "Monitor is disabled. Please enable it to start monitoring again.",
}
export default ExceptionMessages;

View File

@@ -420,9 +420,8 @@ const Detail: DetailFunction = <T extends GenericObject>(
)}
</div>
)}
{(data === null || data === undefined) && field.placeholder && (
<PlaceholderText text={field.placeholder} />
)}
{(data === null || data === undefined || data === "") &&
field.placeholder && <PlaceholderText text={field.placeholder} />}
</div>
</div>
);

View File

@@ -102,10 +102,9 @@ const MarkdownViewer: FunctionComponent<ComponentProps> = (
code: (props: any) => {
const { children, className, ...rest } = props;
// eslint-disable-next-line wrap-regex
const match: RegExpExecArray | null = /language-(\w+)/.exec(
className || "",
);
const match: RegExpExecArray | null = new RegExp(
"language-(\\w+)",
).exec(className || "");
const content: string = String(children as string).replace(
/\n$/,

View File

@@ -91,6 +91,10 @@ class BaseAPI extends API {
return defaultHeaders;
}
protected static logoutUser(): void {
return User.logout();
}
protected static override handleError(
error: HTTPErrorResponse | APIException,
): HTTPErrorResponse | APIException {
@@ -103,7 +107,7 @@ class BaseAPI extends API {
) {
const loginRoute: Route = this.getLoginRoute();
User.logout();
this.logoutUser();
if (Navigation.getQueryStringByName("token")) {
Navigation.navigate(loginRoute.addRouteParam("sso", "true"), {

View File

@@ -0,0 +1,259 @@
// ISO 3166-1 alpha-2 country codes and names.
// Limited to widely recognized sovereign states and territories supported by Stripe.
// If needed, expand or adjust for specific business logic.
export interface CountryOption {
value: string;
label: string;
}
export const Countries: Array<CountryOption> = [
{ value: "AF", label: "Afghanistan" },
{ value: "AL", label: "Albania" },
{ value: "DZ", label: "Algeria" },
{ value: "AS", label: "American Samoa" },
{ value: "AD", label: "Andorra" },
{ value: "AO", label: "Angola" },
{ value: "AI", label: "Anguilla" },
{ value: "AQ", label: "Antarctica" },
{ value: "AG", label: "Antigua and Barbuda" },
{ value: "AR", label: "Argentina" },
{ value: "AM", label: "Armenia" },
{ value: "AW", label: "Aruba" },
{ value: "AU", label: "Australia" },
{ value: "AT", label: "Austria" },
{ value: "AZ", label: "Azerbaijan" },
{ value: "BS", label: "Bahamas" },
{ value: "BH", label: "Bahrain" },
{ value: "BD", label: "Bangladesh" },
{ value: "BB", label: "Barbados" },
{ value: "BY", label: "Belarus" },
{ value: "BE", label: "Belgium" },
{ value: "BZ", label: "Belize" },
{ value: "BJ", label: "Benin" },
{ value: "BM", label: "Bermuda" },
{ value: "BT", label: "Bhutan" },
{ value: "BO", label: "Bolivia" },
{ value: "BQ", label: "Bonaire, Sint Eustatius and Saba" },
{ value: "BA", label: "Bosnia and Herzegovina" },
{ value: "BW", label: "Botswana" },
{ value: "BV", label: "Bouvet Island" },
{ value: "BR", label: "Brazil" },
{ value: "IO", label: "British Indian Ocean Territory" },
{ value: "BN", label: "Brunei Darussalam" },
{ value: "BG", label: "Bulgaria" },
{ value: "BF", label: "Burkina Faso" },
{ value: "BI", label: "Burundi" },
{ value: "KH", label: "Cambodia" },
{ value: "CM", label: "Cameroon" },
{ value: "CA", label: "Canada" },
{ value: "CV", label: "Cape Verde" },
{ value: "KY", label: "Cayman Islands" },
{ value: "CF", label: "Central African Republic" },
{ value: "TD", label: "Chad" },
{ value: "CL", label: "Chile" },
{ value: "CN", label: "China" },
{ value: "CX", label: "Christmas Island" },
{ value: "CC", label: "Cocos (Keeling) Islands" },
{ value: "CO", label: "Colombia" },
{ value: "KM", label: "Comoros" },
{ value: "CG", label: "Congo" },
{ value: "CD", label: "Congo, the Democratic Republic of the" },
{ value: "CK", label: "Cook Islands" },
{ value: "CR", label: "Costa Rica" },
{ value: "CI", label: "Côte d'Ivoire" },
{ value: "HR", label: "Croatia" },
{ value: "CU", label: "Cuba" },
{ value: "CW", label: "Curaçao" },
{ value: "CY", label: "Cyprus" },
{ value: "CZ", label: "Czech Republic" },
{ value: "DK", label: "Denmark" },
{ value: "DJ", label: "Djibouti" },
{ value: "DM", label: "Dominica" },
{ value: "DO", label: "Dominican Republic" },
{ value: "EC", label: "Ecuador" },
{ value: "EG", label: "Egypt" },
{ value: "SV", label: "El Salvador" },
{ value: "GQ", label: "Equatorial Guinea" },
{ value: "ER", label: "Eritrea" },
{ value: "EE", label: "Estonia" },
{ value: "ET", label: "Ethiopia" },
{ value: "FK", label: "Falkland Islands (Malvinas)" },
{ value: "FO", label: "Faroe Islands" },
{ value: "FJ", label: "Fiji" },
{ value: "FI", label: "Finland" },
{ value: "FR", label: "France" },
{ value: "GF", label: "French Guiana" },
{ value: "PF", label: "French Polynesia" },
{ value: "TF", label: "French Southern Territories" },
{ value: "GA", label: "Gabon" },
{ value: "GM", label: "Gambia" },
{ value: "GE", label: "Georgia" },
{ value: "DE", label: "Germany" },
{ value: "GH", label: "Ghana" },
{ value: "GI", label: "Gibraltar" },
{ value: "GR", label: "Greece" },
{ value: "GL", label: "Greenland" },
{ value: "GD", label: "Grenada" },
{ value: "GP", label: "Guadeloupe" },
{ value: "GU", label: "Guam" },
{ value: "GT", label: "Guatemala" },
{ value: "GG", label: "Guernsey" },
{ value: "GN", label: "Guinea" },
{ value: "GW", label: "Guinea-Bissau" },
{ value: "GY", label: "Guyana" },
{ value: "HT", label: "Haiti" },
{ value: "HM", label: "Heard Island and McDonald Islands" },
{ value: "HN", label: "Honduras" },
{ value: "HK", label: "Hong Kong" },
{ value: "HU", label: "Hungary" },
{ value: "IS", label: "Iceland" },
{ value: "IN", label: "India" },
{ value: "ID", label: "Indonesia" },
{ value: "IR", label: "Iran, Islamic Republic of" },
{ value: "IQ", label: "Iraq" },
{ value: "IE", label: "Ireland" },
{ value: "IM", label: "Isle of Man" },
{ value: "IL", label: "Israel" },
{ value: "IT", label: "Italy" },
{ value: "JM", label: "Jamaica" },
{ value: "JP", label: "Japan" },
{ value: "JE", label: "Jersey" },
{ value: "JO", label: "Jordan" },
{ value: "KZ", label: "Kazakhstan" },
{ value: "KE", label: "Kenya" },
{ value: "KI", label: "Kiribati" },
{ value: "KP", label: "Korea, Democratic People's Republic of" },
{ value: "KR", label: "Korea, Republic of" },
{ value: "KW", label: "Kuwait" },
{ value: "KG", label: "Kyrgyzstan" },
{ value: "LA", label: "Lao People's Democratic Republic" },
{ value: "LV", label: "Latvia" },
{ value: "LB", label: "Lebanon" },
{ value: "LS", label: "Lesotho" },
{ value: "LR", label: "Liberia" },
{ value: "LY", label: "Libya" },
{ value: "LI", label: "Liechtenstein" },
{ value: "LT", label: "Lithuania" },
{ value: "LU", label: "Luxembourg" },
{ value: "MO", label: "Macao" },
{ value: "MG", label: "Madagascar" },
{ value: "MW", label: "Malawi" },
{ value: "MY", label: "Malaysia" },
{ value: "MV", label: "Maldives" },
{ value: "ML", label: "Mali" },
{ value: "MT", label: "Malta" },
{ value: "MH", label: "Marshall Islands" },
{ value: "MQ", label: "Martinique" },
{ value: "MR", label: "Mauritania" },
{ value: "MU", label: "Mauritius" },
{ value: "YT", label: "Mayotte" },
{ value: "MX", label: "Mexico" },
{ value: "FM", label: "Micronesia, Federated States of" },
{ value: "MD", label: "Moldova, Republic of" },
{ value: "MC", label: "Monaco" },
{ value: "MN", label: "Mongolia" },
{ value: "ME", label: "Montenegro" },
{ value: "MS", label: "Montserrat" },
{ value: "MA", label: "Morocco" },
{ value: "MZ", label: "Mozambique" },
{ value: "MM", label: "Myanmar" },
{ value: "NA", label: "Namibia" },
{ value: "NR", label: "Nauru" },
{ value: "NP", label: "Nepal" },
{ value: "NL", label: "Netherlands" },
{ value: "NC", label: "New Caledonia" },
{ value: "NZ", label: "New Zealand" },
{ value: "NI", label: "Nicaragua" },
{ value: "NE", label: "Niger" },
{ value: "NG", label: "Nigeria" },
{ value: "NU", label: "Niue" },
{ value: "NF", label: "Norfolk Island" },
{ value: "MK", label: "North Macedonia" },
{ value: "MP", label: "Northern Mariana Islands" },
{ value: "NO", label: "Norway" },
{ value: "OM", label: "Oman" },
{ value: "PK", label: "Pakistan" },
{ value: "PW", label: "Palau" },
{ value: "PS", label: "Palestine, State of" },
{ value: "PA", label: "Panama" },
{ value: "PG", label: "Papua New Guinea" },
{ value: "PY", label: "Paraguay" },
{ value: "PE", label: "Peru" },
{ value: "PH", label: "Philippines" },
{ value: "PN", label: "Pitcairn" },
{ value: "PL", label: "Poland" },
{ value: "PT", label: "Portugal" },
{ value: "PR", label: "Puerto Rico" },
{ value: "QA", label: "Qatar" },
{ value: "RE", label: "Réunion" },
{ value: "RO", label: "Romania" },
{ value: "RU", label: "Russian Federation" },
{ value: "RW", label: "Rwanda" },
{ value: "BL", label: "Saint Barthélemy" },
{ value: "SH", label: "Saint Helena, Ascension and Tristan da Cunha" },
{ value: "KN", label: "Saint Kitts and Nevis" },
{ value: "LC", label: "Saint Lucia" },
{ value: "MF", label: "Saint Martin (French part)" },
{ value: "PM", label: "Saint Pierre and Miquelon" },
{ value: "VC", label: "Saint Vincent and the Grenadines" },
{ value: "WS", label: "Samoa" },
{ value: "SM", label: "San Marino" },
{ value: "ST", label: "Sao Tome and Principe" },
{ value: "SA", label: "Saudi Arabia" },
{ value: "SN", label: "Senegal" },
{ value: "RS", label: "Serbia" },
{ value: "SC", label: "Seychelles" },
{ value: "SL", label: "Sierra Leone" },
{ value: "SG", label: "Singapore" },
{ value: "SX", label: "Sint Maarten (Dutch part)" },
{ value: "SK", label: "Slovakia" },
{ value: "SI", label: "Slovenia" },
{ value: "SB", label: "Solomon Islands" },
{ value: "SO", label: "Somalia" },
{ value: "ZA", label: "South Africa" },
{ value: "GS", label: "South Georgia and the South Sandwich Islands" },
{ value: "SS", label: "South Sudan" },
{ value: "ES", label: "Spain" },
{ value: "LK", label: "Sri Lanka" },
{ value: "SD", label: "Sudan" },
{ value: "SR", label: "Suriname" },
{ value: "SJ", label: "Svalbard and Jan Mayen" },
{ value: "SZ", label: "Swaziland" },
{ value: "SE", label: "Sweden" },
{ value: "CH", label: "Switzerland" },
{ value: "SY", label: "Syrian Arab Republic" },
{ value: "TW", label: "Taiwan, Province of China" },
{ value: "TJ", label: "Tajikistan" },
{ value: "TZ", label: "Tanzania, United Republic of" },
{ value: "TH", label: "Thailand" },
{ value: "TL", label: "Timor-Leste" },
{ value: "TG", label: "Togo" },
{ value: "TK", label: "Tokelau" },
{ value: "TO", label: "Tonga" },
{ value: "TT", label: "Trinidad and Tobago" },
{ value: "TN", label: "Tunisia" },
{ value: "TR", label: "Turkey" },
{ value: "TM", label: "Turkmenistan" },
{ value: "TC", label: "Turks and Caicos Islands" },
{ value: "TV", label: "Tuvalu" },
{ value: "UG", label: "Uganda" },
{ value: "UA", label: "Ukraine" },
{ value: "AE", label: "United Arab Emirates" },
{ value: "GB", label: "United Kingdom" },
{ value: "US", label: "United States" },
{ value: "UM", label: "United States Minor Outlying Islands" },
{ value: "UY", label: "Uruguay" },
{ value: "UZ", label: "Uzbekistan" },
{ value: "VU", label: "Vanuatu" },
{ value: "VE", label: "Venezuela, Bolivarian Republic of" },
{ value: "VN", label: "Viet Nam" },
{ value: "VG", label: "Virgin Islands, British" },
{ value: "VI", label: "Virgin Islands, U.S." },
{ value: "WF", label: "Wallis and Futuna" },
{ value: "EH", label: "Western Sahara" },
{ value: "YE", label: "Yemen" },
{ value: "ZM", label: "Zambia" },
{ value: "ZW", label: "Zimbabwe" },
];
export default Countries;

View File

@@ -4,5 +4,7 @@
"../Common"
],
"ext": "ts,json,tsx,env,js,jsx,hbs",
"exec": "node --inspect=0.0.0.0:9229 --require ts-node/register Index.ts"
"watchOptions": {"useFsEvents": false, "interval": 500},
"env": {"TS_NODE_TRANSPILE_ONLY": "1", "TS_NODE_FILES": "false"},
"exec": "node --inspect=0.0.0.0:9229 -r ts-node/register/transpile-only Index.ts"
}

View File

@@ -12,6 +12,7 @@ import React, {
} from "react";
import MonitorStatusElement from "./MonitorStatusElement";
import Loader, { LoaderType } from "Common/UI/Components/Loader/Loader";
import ExceptionMessages from "Common/Types/Exception/ExceptionMessages";
export interface ComponentProps {
monitorId: ObjectID;
@@ -45,7 +46,7 @@ const GetMonitorStatusElement: FunctionComponent<ComponentProps> = (
if (!monitor) {
setIsLoading(false);
setError("Monitor not found");
setError(ExceptionMessages.MonitorNotFound);
return;
}

View File

@@ -27,6 +27,33 @@ const CustomProbeDocumentation: FunctionComponent<ComponentProps> = (
docker run --name oneuptime-probe --network host -e PROBE_KEY=${props.probeKey.toString()} -e PROBE_ID=${props.probeId.toString()} -e ONEUPTIME_URL=${host.toString()} -d oneuptime/probe:release
`}
/>
<div className="mt-4">
<h4 className="text-sm font-medium text-gray-700 mb-2">
With Proxy Configuration (Optional)
</h4>
<CodeBlock
language="bash"
code={`
# With HTTP/HTTPS proxy
docker run --name oneuptime-probe --network host \\
-e PROBE_KEY=${props.probeKey.toString()} \\
-e PROBE_ID=${props.probeId.toString()} \\
-e ONEUPTIME_URL=${host.toString()} \\
-e HTTP_PROXY_URL=http://proxy.example.com:8080 \\
-e HTTPS_PROXY_URL=http://proxy.example.com:8080 \\
-d oneuptime/probe:release
# With proxy authentication
docker run --name oneuptime-probe --network host \\
-e PROBE_KEY=${props.probeKey.toString()} \\
-e PROBE_ID=${props.probeId.toString()} \\
-e ONEUPTIME_URL=${host.toString()} \\
-e HTTP_PROXY_URL=http://username:password@proxy.example.com:8080 \\
-e HTTPS_PROXY_URL=http://username:password@proxy.example.com:8080 \\
-d oneuptime/probe:release
`}
/>
</div>
</div>
}
/>

View File

@@ -29,6 +29,7 @@ import React, {
ReactElement,
useState,
} from "react";
import ExceptionMessages from "Common/Types/Exception/ExceptionMessages";
import { useAsyncEffect } from "use-async-effect";
import MonitorTestForm from "../../../Components/Form/Monitor/MonitorTest";
import Probe from "Common/Models/DatabaseModels/Probe";
@@ -61,7 +62,7 @@ const MonitorCriteria: FunctionComponent<
});
if (!item) {
setError(`Monitor not found`);
setError(ExceptionMessages.MonitorNotFound);
return;
}

View File

@@ -17,6 +17,7 @@ import React, {
ReactElement,
useState,
} from "react";
import ExceptionMessages from "Common/Types/Exception/ExceptionMessages";
import useAsyncEffect from "use-async-effect";
const MonitorDocumentation: FunctionComponent<
@@ -55,7 +56,7 @@ const MonitorDocumentation: FunctionComponent<
setMonitor(item);
if (!item) {
setError(`Monitor not found`);
setError(ExceptionMessages.MonitorNotFound);
return;
}

View File

@@ -47,6 +47,7 @@ import React, {
ReactElement,
useState,
} from "react";
import ExceptionMessages from "Common/Types/Exception/ExceptionMessages";
import useAsyncEffect from "use-async-effect";
import RouteMap, { RouteUtil } from "../../../Utils/RouteMap";
import PageMap from "../../../Utils/PageMap";
@@ -215,7 +216,7 @@ const MonitorView: FunctionComponent<PageComponentProps> = (): ReactElement => {
);
if (!item) {
setError(`Monitor not found`);
setError(ExceptionMessages.MonitorNotFound);
return;
}

View File

@@ -22,6 +22,7 @@ import React, {
ReactElement,
useState,
} from "react";
import ExceptionMessages from "Common/Types/Exception/ExceptionMessages";
import useAsyncEffect from "use-async-effect";
const MonitorCriteria: FunctionComponent<
@@ -48,7 +49,7 @@ const MonitorCriteria: FunctionComponent<
});
if (!item) {
setError(`Monitor not found`);
setError(ExceptionMessages.MonitorNotFound);
return;
}

View File

@@ -11,6 +11,7 @@ import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
import Navigation from "Common/UI/Utils/Navigation";
import Monitor from "Common/Models/DatabaseModels/Monitor";
import React, { FunctionComponent, ReactElement, useState } from "react";
import ExceptionMessages from "Common/Types/Exception/ExceptionMessages";
import { Outlet, useParams } from "react-router-dom";
import useAsyncEffect from "use-async-effect";
@@ -43,7 +44,7 @@ const MonitorViewLayout: FunctionComponent = (): ReactElement => {
setMonitor(item);
if (!item) {
setError(`Monitor not found`);
setError(ExceptionMessages.MonitorNotFound);
return;
}

View File

@@ -23,6 +23,7 @@ import React, {
ReactElement,
useState,
} from "react";
import ExceptionMessages from "Common/Types/Exception/ExceptionMessages";
import useAsyncEffect from "use-async-effect";
import AnalyticsModelTable from "Common/UI/Components/ModelTable/AnalyticsModelTable";
import SummaryInfo from "../../../Components/Monitor/SummaryView/SummaryInfo";
@@ -55,7 +56,7 @@ const MonitorLogs: FunctionComponent<PageComponentProps> = (): ReactElement => {
});
if (!item) {
setError(`Monitor not found`);
setError(ExceptionMessages.MonitorNotFound);
return;
}

View File

@@ -33,6 +33,7 @@ import React, {
import useAsyncEffect from "use-async-effect";
import SummaryInfo from "../../../Components/Monitor/SummaryView/SummaryInfo";
import ProbeMonitorResponse from "Common/Types/Probe/ProbeMonitorResponse";
import ExceptionMessages from "Common/Types/Exception/ExceptionMessages";
const MonitorProbes: FunctionComponent<
PageComponentProps
@@ -61,7 +62,7 @@ const MonitorProbes: FunctionComponent<
});
if (!item) {
setError(`Monitor not found`);
setError(ExceptionMessages.MonitorNotFound);
return;
}

View File

@@ -25,6 +25,7 @@ import React, {
ReactElement,
useState,
} from "react";
import ExceptionMessages from "Common/Types/Exception/ExceptionMessages";
import useAsyncEffect from "use-async-effect";
import OneUptimeDate from "Common/Types/Date";
@@ -61,7 +62,7 @@ const MonitorCriteria: FunctionComponent<
});
if (!monitor) {
setError(`Monitor not found`);
setError(ExceptionMessages.MonitorNotFound);
return;
}

View File

@@ -48,6 +48,7 @@ import React, {
useState,
} from "react";
import useAsyncEffect from "use-async-effect";
import Countries from "Common/UI/Utils/Countries";
export type ComponentProps = PageComponentProps;
@@ -523,6 +524,89 @@ const Settings: FunctionComponent<ComponentProps> = (
<></>
)}
<CardModelDetail<Project>
name="Business Details"
cardProps={{
title: "Business Details / Billing Address",
description:
"Enter your business legal name, address and optional tax info. This will appear on your invoices.",
}}
isEditable={true}
editButtonText={"Update"}
formFields={[
{
field: {
businessDetails: true,
},
title: "Business Details / Billing Address",
description:
"This information will appear on invoices. Include company legal name, address, and tax / VAT ID if applicable.",
required: false,
fieldType: FormFieldSchemaType.LongText,
validation: {
maxLength: 10000,
},
},
{
field: {
businessDetailsCountry: true,
},
title: "Country",
description: "Required by Stripe. Select your billing country.",
required: false,
placeholder: "Select Country",
fieldType: FormFieldSchemaType.Dropdown,
dropdownOptions: Countries,
},
{
field: {
financeAccountingEmail: true,
},
title: "Finance / Accounting Email",
description:
"Invoices, receipts and billing notifications will be sent here (optional).",
required: false,
placeholder: "finance@yourcompany.com",
fieldType: FormFieldSchemaType.Email,
validation: {
minLength: 3,
maxLength: 200,
},
},
]}
modelDetailProps={{
modelType: Project,
id: "model-detail-project-business-details",
fields: [
{
field: {
businessDetails: true,
},
title: "Business Details / Billing Address",
placeholder: "No business details added yet.",
fieldType: FieldType.LongText,
},
{
field: {
businessDetailsCountry: true,
},
title: "Country",
placeholder: "No country details added yet.",
fieldType: FieldType.Text,
},
{
field: {
financeAccountingEmail: true,
},
title: "Finance / Accounting Email",
placeholder: "No finance / accounting email added yet.",
fieldType: FieldType.Email,
},
],
modelId: ProjectUtil.getCurrentProjectId()!,
}}
/>
{!reseller && (
<Card
title={`Cancel Plan`}

View File

@@ -1,12 +1,11 @@
import ProjectUtil from "Common/UI/Utils/Project";
import PageComponentProps from "../PageComponentProps";
import FieldType from "Common/UI/Components/Types/FieldType";
import ProjectUser from "Common/Models/DatabaseModels/ProjectUser";
import React, { Fragment, FunctionComponent, ReactElement } from "react";
import UserElement from "../../Components/User/User";
import ModelTable from "Common/UI/Components/ModelTable/ModelTable";
import Team from "Common/Models/DatabaseModels/Team";
import TeamsElement from "../../Components/Team/TeamsElement";
import TeamElement from "../../Components/Team/Team";
import Route from "Common/Types/API/Route";
import { RouteUtil } from "../../Utils/RouteMap";
import { ButtonStyleType } from "Common/UI/Components/Button/Button";
@@ -16,6 +15,8 @@ import TeamMember from "Common/Models/DatabaseModels/TeamMember";
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
import { FormType } from "Common/UI/Components/Forms/ModelForm";
import Navigation from "Common/UI/Utils/Navigation";
import Pill from "Common/UI/Components/Pill/Pill";
import { Green, Yellow } from "Common/Types/BrandColors";
const Teams: FunctionComponent<PageComponentProps> = (
props: PageComponentProps,
@@ -26,21 +27,22 @@ const Teams: FunctionComponent<PageComponentProps> = (
return (
<Fragment>
<ModelTable<ProjectUser>
modelType={ProjectUser}
<ModelTable<TeamMember>
modelType={TeamMember}
id="teams-table"
name="Settings > Users"
userPreferencesKey="users-table"
isDeleteable={false}
isDeleteable={true}
isEditable={false}
isCreateable={true}
isCreateable={false}
onFilterApplied={(isApplied: boolean) => {
setIsFilterApplied(isApplied);
}}
isViewable={true}
cardProps={{
title: "Users",
description: "Here is a list of all the users in this project.",
description:
"Here is a list of all the team members in this project.",
buttons: [
{
title: "Invite User",
@@ -61,7 +63,7 @@ const Teams: FunctionComponent<PageComponentProps> = (
projectId: ProjectUtil.getCurrentProjectId()!,
}}
showRefreshButton={true}
onViewPage={(item: ProjectUser) => {
onViewPage={(item: TeamMember) => {
const viewPageRoute: string =
RouteUtil.populateRouteParams(props.pageRoute).toString() +
"/" +
@@ -72,12 +74,12 @@ const Teams: FunctionComponent<PageComponentProps> = (
filters={[
{
field: {
acceptedTeams: {
team: {
name: true,
},
},
title: "Teams member of",
type: FieldType.EntityArray,
title: "Team",
type: FieldType.Entity,
filterEntityType: Team,
filterQuery: {
projectId: ProjectUtil.getCurrentProjectId()!,
@@ -89,20 +91,10 @@ const Teams: FunctionComponent<PageComponentProps> = (
},
{
field: {
invitedTeams: {
name: true,
},
},
title: "Teams invited to",
type: FieldType.EntityArray,
filterEntityType: Team,
filterQuery: {
projectId: ProjectUtil.getCurrentProjectId()!,
},
filterDropdownField: {
label: "name",
value: "_id",
hasAcceptedInvitation: true,
},
title: "Status",
type: FieldType.Boolean,
},
]}
columns={[
@@ -116,7 +108,7 @@ const Teams: FunctionComponent<PageComponentProps> = (
},
title: "User",
type: FieldType.Element,
getElement: (item: ProjectUser) => {
getElement: (item: TeamMember) => {
if (!item.user) {
return <p>User not found</p>;
}
@@ -125,26 +117,31 @@ const Teams: FunctionComponent<PageComponentProps> = (
},
{
field: {
acceptedTeams: {
team: {
name: true,
_id: true,
},
},
title: "Teams member of",
title: "Team",
type: FieldType.Element,
getElement: (item: ProjectUser) => {
return <TeamsElement teams={item.acceptedTeams || []} />;
getElement: (item: TeamMember) => {
if (!item.team) {
return <p>No team assigned</p>;
}
return <TeamElement team={item.team!} />;
},
},
{
field: {
invitedTeams: {
name: true,
},
hasAcceptedInvitation: true,
},
title: "Teams invited to",
title: "Status",
type: FieldType.Element,
getElement: (item: ProjectUser) => {
return <TeamsElement teams={item.invitedTeams || []} />;
getElement: (item: TeamMember) => {
if (item.hasAcceptedInvitation) {
return <Pill text="Member" color={Green} />;
}
return <Pill text="Invitation Sent" color={Yellow} />;
},
},
]}

View File

@@ -0,0 +1,393 @@
import PageComponentProps from "../../PageComponentProps";
import NotNull from "Common/Types/BaseDatabase/NotNull";
import { Green, Red } from "Common/Types/BrandColors";
import BadDataException from "Common/Types/Exception/BadDataException";
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import ObjectID from "Common/Types/ObjectID";
import Alert, { AlertType } from "Common/UI/Components/Alerts/Alert";
import { CategoryCheckboxOptionsAndCategories } from "Common/UI/Components/CategoryCheckbox/Index";
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
import { ModelField } from "Common/UI/Components/Forms/ModelForm";
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
import FormValues from "Common/UI/Components/Forms/Types/FormValues";
import PageLoader from "Common/UI/Components/Loader/PageLoader";
import ModelTable from "Common/UI/Components/ModelTable/ModelTable";
import Pill from "Common/UI/Components/Pill/Pill";
import FieldType from "Common/UI/Components/Types/FieldType";
import API from "Common/UI/Utils/API/API";
import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
import Navigation from "Common/UI/Utils/Navigation";
import SubscriberUtil from "Common/UI/Utils/StatusPage";
import StatusPage from "Common/Models/DatabaseModels/StatusPage";
import StatusPageSubscriber from "Common/Models/DatabaseModels/StatusPageSubscriber";
import React, {
Fragment,
FunctionComponent,
ReactElement,
useEffect,
useState,
} from "react";
import ProjectUtil from "Common/UI/Utils/Project";
const StatusPageMicrosoftTeamsSubscribers: FunctionComponent<
PageComponentProps
> = (props: PageComponentProps): ReactElement => {
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
const [
allowSubscribersToChooseResources,
setAllowSubscribersToChooseResources,
] = React.useState<boolean>(false);
const [
allowSubscribersToChooseEventTypes,
setAllowSubscribersToChooseEventTypes,
] = React.useState<boolean>(false);
const [
isMicrosoftTeamsSubscribersEnabled,
setIsMicrosoftTeamsSubscribersEnabled,
] = React.useState<boolean>(false);
const [isLoading, setIsLoading] = React.useState<boolean>(false);
const [error, setError] = React.useState<string>("");
const [
categoryCheckboxOptionsAndCategories,
setCategoryCheckboxOptionsAndCategories,
] = useState<CategoryCheckboxOptionsAndCategories>({
categories: [],
options: [],
});
const fetchCheckboxOptionsAndCategories: PromiseVoidFunction =
async (): Promise<void> => {
const result: CategoryCheckboxOptionsAndCategories =
await SubscriberUtil.getCategoryCheckboxPropsBasedOnResources(modelId);
setCategoryCheckboxOptionsAndCategories(result);
};
const fetchStatusPage: PromiseVoidFunction = async (): Promise<void> => {
try {
setIsLoading(true);
const statusPage: StatusPage | null = await ModelAPI.getItem({
modelType: StatusPage,
id: modelId,
select: {
allowSubscribersToChooseResources: true,
allowSubscribersToChooseEventTypes: true,
enableMicrosoftTeamsSubscribers: true,
},
});
if (statusPage && statusPage.allowSubscribersToChooseResources) {
setAllowSubscribersToChooseResources(
statusPage.allowSubscribersToChooseResources,
);
await fetchCheckboxOptionsAndCategories();
}
if (statusPage && statusPage.allowSubscribersToChooseEventTypes) {
setAllowSubscribersToChooseEventTypes(
statusPage.allowSubscribersToChooseEventTypes,
);
}
if (statusPage && statusPage.enableMicrosoftTeamsSubscribers) {
setIsMicrosoftTeamsSubscribersEnabled(
statusPage.enableMicrosoftTeamsSubscribers,
);
}
setIsLoading(false);
} catch (err) {
setError(API.getFriendlyMessage(err));
}
setIsLoading(false);
};
useEffect(() => {
fetchStatusPage().catch((err: Error) => {
setError(API.getFriendlyMessage(err));
});
}, []);
const [formFields, setFormFields] = React.useState<
Array<ModelField<StatusPageSubscriber>>
>([]);
useEffect(() => {
if (isLoading) {
return; // don't do anything if loading
}
const formFields: Array<ModelField<StatusPageSubscriber>> = [
{
field: {
microsoftTeamsWorkspaceName: true,
},
stepId: "subscriber-info",
title: "Microsoft Teams Workspace Name",
description:
"Name of the Microsoft Teams workspace for identification.",
fieldType: FormFieldSchemaType.Text,
required: true,
placeholder: "my-company-workspace",
},
{
field: {
microsoftTeamsIncomingWebhookUrl: true,
},
stepId: "subscriber-info",
title: "Microsoft Teams Incoming Webhook URL",
description: "Status page updates will be sent to this Teams channel.",
fieldType: FormFieldSchemaType.URL,
required: true,
placeholder: "https://xxxxx.office.com/webhook/...",
disableSpellCheck: true,
},
{
field: {
sendYouHaveSubscribedMessage: true,
},
title: "Send Subscription Notification",
stepId: "subscriber-info",
description:
"Send a notification to the Teams channel confirming the subscription.",
fieldType: FormFieldSchemaType.Toggle,
required: false,
doNotShowWhenEditing: true,
},
{
field: {
isUnsubscribed: true,
},
title: "Unsubscribe",
stepId: "subscriber-info",
description: "Unsubscribe this Teams channel from the status page.",
fieldType: FormFieldSchemaType.Toggle,
required: false,
doNotShowWhenCreating: true,
},
];
if (allowSubscribersToChooseResources) {
formFields.push({
field: {
isSubscribedToAllResources: true,
},
title: "Subscribe to All Resources",
stepId: "subscriber-info",
description: "Send notifications for all resources.",
fieldType: FormFieldSchemaType.Checkbox,
required: false,
defaultValue: true,
});
formFields.push({
field: {
statusPageResources: true,
},
title: "Select Resources to Subscribe",
description: "Please select the resources you want to subscribe to.",
stepId: "subscriber-info",
fieldType: FormFieldSchemaType.CategoryCheckbox,
required: false,
categoryCheckboxProps: categoryCheckboxOptionsAndCategories,
showIf: (model: FormValues<StatusPageSubscriber>) => {
return !model || !model.isSubscribedToAllResources;
},
});
}
if (allowSubscribersToChooseEventTypes) {
formFields.push({
field: {
isSubscribedToAllEventTypes: true,
},
title: "Subscribe to All Event Types",
stepId: "subscriber-info",
description:
"Select this option if you want to subscribe to all event types.",
fieldType: FormFieldSchemaType.Checkbox,
required: false,
defaultValue: true,
});
formFields.push({
field: {
statusPageEventTypes: true,
},
title: "Select Event Types to Subscribe",
stepId: "subscriber-info",
description: "Please select the event types you want to subscribe to.",
fieldType: FormFieldSchemaType.MultiSelectDropdown,
required: false,
dropdownOptions: SubscriberUtil.getDropdownPropsBasedOnEventTypes(),
showIf: (model: FormValues<StatusPageSubscriber>) => {
return !model || !model.isSubscribedToAllEventTypes;
},
});
}
// add internal note field
formFields.push({
field: {
internalNote: true,
},
title: "Internal Note",
stepId: "internal-info",
description:
"Internal note for the subscriber. This is for internal use only and is visible only to the team members.",
fieldType: FormFieldSchemaType.Markdown,
required: false,
});
setFormFields(formFields);
}, [isLoading]);
return (
<Fragment>
{isLoading ? <PageLoader isVisible={true} /> : <></>}
{error ? <ErrorMessage message={error} /> : <></>}
{!error && !isLoading ? (
<>
{!isMicrosoftTeamsSubscribersEnabled && (
<Alert
type={AlertType.DANGER}
title="Microsoft Teams subscribers are not enabled for this status page. Please enable it in Subscriber Settings"
/>
)}
<ModelTable<StatusPageSubscriber>
modelType={StatusPageSubscriber}
id="table-microsoft-teams-subscriber"
name="Status Page > Microsoft Teams Subscribers"
userPreferencesKey="status-page-microsoft-teams-subscribers-table"
isDeleteable={true}
showViewIdButton={true}
isCreateable={true}
isEditable={true}
isViewable={false}
selectMoreFields={{
isSubscriptionConfirmed: true,
}}
query={{
statusPageId: modelId,
projectId: ProjectUtil.getCurrentProjectId()!,
microsoftTeamsWorkspaceName: new NotNull(),
}}
onBeforeCreate={(
item: StatusPageSubscriber,
): Promise<StatusPageSubscriber> => {
if (!props.currentProject || !props.currentProject._id) {
throw new BadDataException("Project ID cannot be null");
}
item.statusPageId = modelId;
item.projectId = new ObjectID(props.currentProject._id);
return Promise.resolve(item);
}}
cardProps={{
title: "Microsoft Teams Subscribers",
description:
"Here are the list of Microsoft Teams channels that have subscribed to the status page.",
}}
noItemsMessage={"No Microsoft Teams subscribers found."}
formSteps={[
{
title: "Subscriber Info",
id: "subscriber-info",
},
{
title: "Internal Info",
id: "internal-info",
},
]}
formFields={formFields}
showRefreshButton={true}
viewPageRoute={Navigation.getCurrentRoute()}
filters={[
{
field: {
microsoftTeamsWorkspaceName: true,
},
title: "Teams Workspace Name",
type: FieldType.Text,
},
{
field: {
isUnsubscribed: true,
},
title: "Is Unsubscribed",
type: FieldType.Boolean,
},
{
field: {
isSubscriptionConfirmed: true,
},
title: "Subscription Confirmed?",
type: FieldType.Boolean,
},
{
field: {
createdAt: true,
},
title: "Subscribed At",
type: FieldType.DateTime,
},
]}
columns={[
{
field: {
microsoftTeamsWorkspaceName: true,
},
title: "Workspace Name",
type: FieldType.Text,
},
{
field: {
isUnsubscribed: true,
},
title: "Status",
type: FieldType.Text,
getElement: (item: StatusPageSubscriber): ReactElement => {
if (item["isUnsubscribed"]) {
return <Pill color={Red} text={"Unsubscribed"} />;
}
if (!item["isSubscriptionConfirmed"]) {
return (
<Pill
color={Red}
text={"Awaiting Confirmation"}
tooltip="Subscription not yet confirmed"
/>
);
}
return <Pill color={Green} text={"Subscribed"} />;
},
},
{
field: {
createdAt: true,
},
title: "Subscribed At",
type: FieldType.DateTime,
hideOnMobile: true,
},
]}
/>
</>
) : (
<></>
)}
</Fragment>
);
};
export default StatusPageMicrosoftTeamsSubscribers;

View File

@@ -111,6 +111,18 @@ const DashboardSideMenu: FunctionComponent<ComponentProps> = (
}}
icon={IconProp.Slack}
/>
<SideMenuItem
link={{
title: "MS Teams Subscribers",
to: RouteUtil.populateRouteParams(
RouteMap[
PageMap.STATUS_PAGE_VIEW_MICROSOFT_TEAMS_SUBSCRIBERS
] as Route,
{ modelId: props.modelId },
),
}}
icon={IconProp.MicrosoftTeams}
/>
{/* <SideMenuItem
link={{

View File

@@ -127,6 +127,43 @@ const StatusPageDelete: FunctionComponent<
}}
/>
<CardModelDetail<StatusPage>
name="Status Page > Branding > Subscriber > Microsoft Teams"
cardProps={{
title: "Microsoft Teams Subscribers",
description:
"Microsoft Teams subscriber settings for this status page.",
}}
isEditable={true}
formFields={[
{
field: {
enableMicrosoftTeamsSubscribers: true,
},
title: "Enable Microsoft Teams Subscribers",
fieldType: FormFieldSchemaType.Toggle,
required: false,
placeholder:
"Can Microsoft Teams subscribers subscribe to this status page?",
},
]}
modelDetailProps={{
showDetailsInNumberOfColumns: 1,
modelType: StatusPage,
id: "model-detail-microsoft-teams-subscribers",
fields: [
{
field: {
enableMicrosoftTeamsSubscribers: true,
},
fieldType: FieldType.Boolean,
title: "Enable Microsoft Teams Subscribers",
},
],
modelId: modelId,
}}
/>
<CardModelDetail<StatusPage>
name="Status Page > Branding > Subscriber > Advanced"
cardProps={{

View File

@@ -49,6 +49,11 @@ const StatusPagesViewSlackSubscribers: LazyExoticComponent<
> = lazy(() => {
return import("../Pages/StatusPages/View/SlackSubscribers");
});
const StatusPagesViewMicrosoftTeamsSubscribers: LazyExoticComponent<
FunctionComponent<ComponentProps>
> = lazy(() => {
return import("../Pages/StatusPages/View/MicrosoftTeamsSubscribers");
});
const StatusPagesViewWebhookSubscribers: LazyExoticComponent<
FunctionComponent<ComponentProps>
> = lazy(() => {
@@ -640,6 +645,24 @@ const StatusPagesRoutes: FunctionComponent<ComponentProps> = (
}
/>
<PageRoute
path={RouteUtil.getLastPathForKey(
PageMap.STATUS_PAGE_VIEW_MICROSOFT_TEAMS_SUBSCRIBERS,
)}
element={
<Suspense fallback={Loader}>
<StatusPagesViewMicrosoftTeamsSubscribers
{...props}
pageRoute={
RouteMap[
PageMap.STATUS_PAGE_VIEW_MICROSOFT_TEAMS_SUBSCRIBERS
] as Route
}
/>
</Suspense>
}
/>
<PageRoute
path={RouteUtil.getLastPathForKey(PageMap.STATUS_PAGE_VIEW_EMBEDDED)}
element={

View File

@@ -209,6 +209,7 @@ enum PageMap {
STATUS_PAGE_VIEW_EMAIL_SUBSCRIBERS = "STATUS_PAGE_VIEW_EMAIL_SUBSCRIBERS",
STATUS_PAGE_VIEW_SMS_SUBSCRIBERS = "STATUS_PAGE_VIEW_SMS_SUBSCRIBERS",
STATUS_PAGE_VIEW_SLACK_SUBSCRIBERS = "STATUS_PAGE_VIEW_SLACK_SUBSCRIBERS",
STATUS_PAGE_VIEW_MICROSOFT_TEAMS_SUBSCRIBERS = "STATUS_PAGE_VIEW_MICROSOFT_TEAMS_SUBSCRIBERS",
STATUS_PAGE_VIEW_WEBHOOK_SUBSCRIBERS = "STATUS_PAGE_VIEW_WEBHOOK_SUBSCRIBERS",
STATUS_PAGE_VIEW_RESOURCES = "STATUS_PAGE_VIEW_RESOURCES",
STATUS_PAGE_VIEW_ADVANCED_OPTIONS = "STATUS_PAGE_VIEW_ADVANCED_OPTIONS",

View File

@@ -128,6 +128,7 @@ export const StatusPagesRoutePath: Dictionary<string> = {
[PageMap.STATUS_PAGE_VIEW_EMAIL_SUBSCRIBERS]: `${RouteParams.ModelID}/email-subscribers`,
[PageMap.STATUS_PAGE_VIEW_SMS_SUBSCRIBERS]: `${RouteParams.ModelID}/sms-subscribers`,
[PageMap.STATUS_PAGE_VIEW_SLACK_SUBSCRIBERS]: `${RouteParams.ModelID}/slack-subscribers`,
[PageMap.STATUS_PAGE_VIEW_MICROSOFT_TEAMS_SUBSCRIBERS]: `${RouteParams.ModelID}/microsoft-teams-subscribers`,
[PageMap.STATUS_PAGE_VIEW_WEBHOOK_SUBSCRIBERS]: `${RouteParams.ModelID}/webhook-subscribers`,
[PageMap.STATUS_PAGE_VIEW_HEADER_STYLE]: `${RouteParams.ModelID}/header-style`,
[PageMap.STATUS_PAGE_VIEW_FOOTER_STYLE]: `${RouteParams.ModelID}/footer-style`,
@@ -1081,6 +1082,12 @@ const RouteMap: Dictionary<Route> = {
}`,
),
[PageMap.STATUS_PAGE_VIEW_MICROSOFT_TEAMS_SUBSCRIBERS]: new Route(
`/dashboard/${RouteParams.ProjectID}/status-pages/${
StatusPagesRoutePath[PageMap.STATUS_PAGE_VIEW_MICROSOFT_TEAMS_SUBSCRIBERS]
}`,
),
[PageMap.STATUS_PAGE_VIEW_WEBHOOK_SUBSCRIBERS]: new Route(
`/dashboard/${RouteParams.ProjectID}/status-pages/${
StatusPagesRoutePath[PageMap.STATUS_PAGE_VIEW_WEBHOOK_SUBSCRIBERS]

View File

@@ -92,7 +92,7 @@ The following example shows how to use a JavaScript expression to monitor an inc
// you can combine multiple expressions using logical operators
"{{requestBody.item}"} === "hello" && "{{requestHeaders.contentType}}" === "text/html"
"{{requestBody.item}}" === "hello" && "{{requestHeaders.contentType}}" === "text/html"
// you can use the following for arrays

View File

@@ -16,6 +16,37 @@ docker run --name oneuptime-probe --network host -e PROBE_KEY=<probe-key> -e PRO
If you are self hosting OneUptime, you can change `ONEUPTIME_URL` to your custom self hosted instance.
##### Proxy Configuration
If your probe needs to go through a proxy server to reach OneUptime or monitor external resources, you can configure proxy settings using these environment variables:
```
# For HTTP proxy
docker run --name oneuptime-probe --network host \
-e PROBE_KEY=<probe-key> \
-e PROBE_ID=<probe-id> \
-e ONEUPTIME_URL=https://oneuptime.com \
-e HTTP_PROXY_URL=http://proxy.example.com:8080 \
-d oneuptime/probe:release
# For HTTPS proxy
docker run --name oneuptime-probe --network host \
-e PROBE_KEY=<probe-key> \
-e PROBE_ID=<probe-id> \
-e ONEUPTIME_URL=https://oneuptime.com \
-e HTTPS_PROXY_URL=http://proxy.example.com:8080 \
-d oneuptime/probe:release
# With proxy authentication
docker run --name oneuptime-probe --network host \
-e PROBE_KEY=<probe-key> \
-e PROBE_ID=<probe-id> \
-e ONEUPTIME_URL=https://oneuptime.com \
-e HTTP_PROXY_URL=http://username:password@proxy.example.com:8080 \
-e HTTPS_PROXY_URL=http://username:password@proxy.example.com:8080 \
-d oneuptime/probe:release
```
#### Docker Compose
You can also run the probe using docker-compose. Create a `docker-compose.yml` file with the following content:
@@ -35,6 +66,31 @@ services:
restart: always
```
##### With Proxy Configuration
If you need to use a proxy server, you can add proxy environment variables:
```yaml
version: "3"
services:
oneuptime-probe:
image: oneuptime/probe:release
container_name: oneuptime-probe
environment:
- PROBE_KEY=<probe-key>
- PROBE_ID=<probe-id>
- ONEUPTIME_URL=https://oneuptime.com
# Proxy configuration (optional)
- HTTP_PROXY_URL=http://proxy.example.com:8080
- HTTPS_PROXY_URL=http://proxy.example.com:8080
# For proxy with authentication:
# - HTTP_PROXY_URL=http://username:password@proxy.example.com:8080
# - HTTPS_PROXY_URL=http://username:password@proxy.example.com:8080
network_mode: host
restart: always
```
Then run the following command:
```
@@ -56,20 +112,61 @@ spec:
selector:
matchLabels:
app: oneuptime-probe
template:
metadata:
labels:
app: oneuptime-probe
spec:
containers:
image: oneuptime/probe:release
env:
- name: PROBE_KEY
value: "<probe-key>"
- name: PROBE_ID
value: "<probe-id>"
- name: ONEUPTIME_URL
value: "https://oneuptime.com"
template:
metadata:
labels:
app: oneuptime-probe
spec:
containers:
- name: oneuptime-probe
image: oneuptime/probe:release
env:
- name: PROBE_KEY
value: "<probe-key>"
- name: PROBE_ID
value: "<probe-id>"
- name: ONEUPTIME_URL
value: "https://oneuptime.com"
```
##### With Proxy Configuration
If you need to use a proxy server, you can add proxy environment variables:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: oneuptime-probe
spec:
selector:
matchLabels:
app: oneuptime-probe
template:
metadata:
labels:
app: oneuptime-probe
spec:
containers:
- name: oneuptime-probe
image: oneuptime/probe:release
env:
- name: PROBE_KEY
value: "<probe-key>"
- name: PROBE_ID
value: "<probe-id>"
- name: ONEUPTIME_URL
value: "https://oneuptime.com"
# Proxy configuration (optional)
- name: HTTP_PROXY_URL
value: "http://proxy.example.com:8080"
- name: HTTPS_PROXY_URL
value: "http://proxy.example.com:8080"
# For proxy with authentication, use:
# - name: HTTP_PROXY_URL
# value: "http://username:password@proxy.example.com:8080"
# - name: HTTPS_PROXY_URL
# value: "http://username:password@proxy.example.com:8080"
```
Then run the following command:
@@ -80,6 +177,46 @@ kubectl apply -f oneuptime-probe.yaml
If you are self hosting OneUptime, you can change `ONEUPTIME_URL` to your custom self hosted instance.
### Environment Variables
The probe supports the following environment variables:
#### Required Variables
- `PROBE_KEY` - The probe key from your OneUptime dashboard
- `PROBE_ID` - The probe ID from your OneUptime dashboard
- `ONEUPTIME_URL` - The URL of your OneUptime instance (default: https://oneuptime.com)
#### Optional Variables
- `HTTP_PROXY_URL` - HTTP proxy server URL for HTTP requests
- `HTTPS_PROXY_URL` - HTTP proxy server URL for HTTPS requests
- `PROBE_NAME` - Custom name for the probe
- `PROBE_DESCRIPTION` - Description for the probe
- `PROBE_MONITORING_WORKERS` - Number of monitoring workers (default: 1)
- `PROBE_MONITOR_FETCH_LIMIT` - Number of monitors to fetch at once (default: 10)
- `PROBE_MONITOR_RETRY_LIMIT` - Number of retries for failed monitors (default: 3)
- `PROBE_SYNTHETIC_MONITOR_SCRIPT_TIMEOUT_IN_MS` - Timeout for synthetic monitor scripts in milliseconds (default: 60000)
- `PROBE_CUSTOM_CODE_MONITOR_SCRIPT_TIMEOUT_IN_MS` - Timeout for custom code monitor scripts in milliseconds (default: 60000)
#### Proxy Configuration
The probe supports both HTTP and HTTPS proxy servers. When configured, the probe will route all monitoring traffic through the specified proxy servers.
**Proxy URL Format:**
```
http://[username:password@]proxy.server.com:port
```
**Examples:**
- Basic proxy: `http://proxy.example.com:8080`
- With authentication: `http://username:password@proxy.example.com:8080`
**Supported Features:**
- HTTP and HTTPS proxy support
- Proxy authentication (username/password)
- Automatic fallback between HTTP and HTTPS proxies
- Works with all monitor types (Website, API, SSL, Synthetic, etc.)
**Note:** Both standard environment variables (`HTTP_PROXY_URL`, `HTTPS_PROXY_URL`) and lowercase variants (`http_proxy`, `https_proxy`) are supported for compatibility.
### Verify

View File

@@ -7,30 +7,79 @@
}) %>
</head>
<body class="flex min-h-full bg-white ">
<body class="flex min-h-full bg-white">
<div class="flex w-full flex-col">
<%- include('./Partials/Header.ejs') %>
<!-- Mobile top bar with menu button -->
<div class="flex items-center justify-between lg:hidden border-b border-slate-200 px-4 py-2 gap-2">
<button aria-label="Open navigation" data-mobile-menu-open class="inline-flex items-center gap-2 rounded-md border border-slate-300 bg-white px-3 py-2 text-sm font-medium text-slate-600 shadow-sm active:scale-[.97]">
<svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h16"/></svg>
Menu
</button>
<h1 class="text-sm font-semibold text-slate-700 truncate"><%- link.title %></h1>
</div>
<!-- Mobile nav overlay -->
<div data-mobile-menu-overlay class="fixed inset-0 z-40 bg-slate-900/40 lg:hidden hidden"></div>
<aside data-mobile-menu-drawer class="fixed inset-y-0 left-0 z-50 w-72 overflow-y-auto bg-white px-6 py-6 shadow-lg ring-1 ring-slate-900/10 lg:hidden transform transition ease-out duration-200 -translate-x-full hidden">
<div class="flex items-center justify-between mb-4">
<span class="text-sm font-semibold text-slate-700">Documentation</span>
<button aria-label="Close navigation" data-mobile-menu-close class="rounded-md p-2 text-slate-500 hover:text-slate-700 focus:outline-none focus:ring-2 focus:ring-sky-500"><svg class="h-5 w-5" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6L6 18M6 6l12 12"/></svg></button>
</div>
<%- include('./Partials/Nav.ejs') %>
</aside>
<div class="relative mx-auto flex w-full max-w-8xl flex-auto justify-center sm:px-2 lg:px-8 xl:px-12">
<div class="hidden lg:relative lg:block lg:flex-none">
<div class="absolute inset-y-0 right-0 w-[50vw] bg-slate-50 "></div>
<div
class="absolute bottom-0 right-0 top-16 hidden h-12 w-px bg-gradient-to-t from-slate-800 ">
</div>
<div class="absolute bottom-0 right-0 top-28 hidden w-px bg-slate-800 "></div>
<div
class="sticky top-[4.75rem] -ml-0.5 h-[calc(100vh-4.75rem)] w-64 overflow-y-auto overflow-x-hidden py-16 pl-0.5 pr-8 xl:w-72 xl:pr-16">
<%- include('./Partials/Nav.ejs') %>
<div class="absolute inset-y-0 right-0 w-[50vw] bg-slate-50"></div>
<div class="absolute bottom-0 right-0 top-16 hidden h-12 w-px bg-gradient-to-t from-slate-800"></div>
<div class="absolute bottom-0 right-0 top-28 hidden w-px bg-slate-800"></div>
<div class="sticky top-[4.75rem] -ml-0.5 h-[calc(100vh-4.75rem)] w-64 overflow-y-auto overflow-x-hidden py-16 pl-0.5 pr-8 xl:w-72 xl:pr-16">
<%- include('./Partials/Nav.ejs') %>
</div>
</div>
<div class="min-w-0 max-w-2xl flex-auto px-4 py-16 lg:max-w-none lg:pl-8 lg:pr-0 xl:px-16">
<div class="min-w-0 max-w-2xl flex-auto px-4 py-8 md:py-12 lg:py-16 lg:max-w-none lg:pl-8 lg:pr-0 xl:px-16">
<%- include('./Partials/Content.ejs', { category: category, link: link, content: content }) %>
<%- include('./Partials/OpenSourceCommitment.ejs', { githubPath: githubPath }) %>
</div>
</div>
</div>
<!-- Mobile menu toggle (vanilla JS) -->
<script>
(function(){
const openBtn = document.querySelector('[data-mobile-menu-open]');
const closeBtn = document.querySelector('[data-mobile-menu-close]');
const overlay = document.querySelector('[data-mobile-menu-overlay]');
const drawer = document.querySelector('[data-mobile-menu-drawer]');
if(!openBtn || !overlay || !drawer){ return; }
function openMenu(){
overlay.classList.remove('hidden');
drawer.classList.remove('hidden');
// trigger slide in
requestAnimationFrame(()=>{
drawer.classList.remove('-translate-x-full');
});
document.body.classList.add('overflow-hidden');
}
function closeMenu(){
drawer.classList.add('-translate-x-full');
overlay.classList.add('hidden');
document.body.classList.remove('overflow-hidden');
// after transition hide drawer
drawer.addEventListener('transitionend', function handler(e){
if(e.propertyName === 'transform'){
if(drawer.classList.contains('-translate-x-full')){
drawer.classList.add('hidden');
}
drawer.removeEventListener('transitionend', handler);
}
});
}
openBtn.addEventListener('click', openMenu);
overlay.addEventListener('click', closeMenu);
closeBtn && closeBtn.addEventListener('click', closeMenu);
// Close on escape
document.addEventListener('keydown', (e)=>{ if(e.key==='Escape'){ closeMenu(); }});
})();
</script>
</body>
</html>

View File

@@ -2,17 +2,17 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="preload" href="/docs/static/fonts/f1.woff2" as="font" crossorigin="" type="font/woff2">
<link rel="preload" href="/docs/static/fonts/f2.woff2" as="font" crossorigin="" type="font/woff2">
<link rel="stylesheet" href="/docs/static/css/style.css" crossorigin="" data-precedence="next">
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>
<link rel="preconnect" href="https://cdn.tailwindcss.com" crossorigin>
<link rel="stylesheet" href="/docs/static/css/style.css" crossorigin data-precedence="next">
<title>OneUptime Documentation</title>
<meta name="description"
content="Cache every single thing your app could ever do ahead of time, so your code never even has to run at all.">
<link rel="icon" href="/favicon.ico" type="image/x-icon" sizes="16x16">
<meta name="next-size-adjust">
<link rel="stylesheet" href="/docs/static/css/style.css" crossorigin="" data-precedence="next" />
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/a11y-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
<script src="https://cdn.tailwindcss.com" defer></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/a11y-dark.min.css" crossorigin>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js" onload="hljs.highlightAll();"></script>
<% if(typeof enableGoogleTagManager !== 'undefined' ? enableGoogleTagManager : false){ %>
<!-- Google Tag Manager -->

View File

@@ -1,5 +1,5 @@
<header
class="sticky top-0 z-50 flex flex-none flex-wrap items-center justify-between bg-white px-4 py-5 shadow-md shadow-slate-900/5 transition duration-500 sm:px-6 lg:px-8 ">
class="sticky top-0 z-50 flex flex-none flex-wrap items-center justify-between bg-white px-4 py-4 sm:py-5 border-b border-slate-200 sm:px-6 lg:px-8">
<div class="relative flex flex-grow basis-0 items-center"><a aria-label="Home page" href="/">
<img class="h-8 w-auto" src="/img/3-transparent.svg" alt="">

View File

@@ -1,4 +1,4 @@
<nav class="text-base lg:text-sm">
<nav class="text-base lg:text-sm" style="width:16rem;flex:0 0 16rem;" aria-label="Documentation navigation">
<ul role="list" class="space-y-9">
<% for(var i=0; i<nav.length; i++) {%>
<li>

View File

@@ -0,0 +1,50 @@
import { BASE_URL, IS_BILLING_ENABLED } from "../../Config";
import { Page, expect, test, Response } from "@playwright/test";
import URL from "Common/Types/API/URL";
// Helper to fetch sitemap XML via a normal page navigation.
async function fetchSitemap(page: Page): Promise<string> {
const response: Response | null = await page.goto(
URL.fromString(`${BASE_URL.toString()}sitemap.xml`).toString(),
{ waitUntil: "networkidle" },
);
expect(response, "sitemap.xml should respond").toBeTruthy();
expect(response?.status(), "sitemap.xml should return 200").toBe(200);
// Raw content (Playwright wraps XML in HTML view sometimes); extract text.
const body: Awaited<ReturnType<typeof page.$>> = await page.$("body");
const xml: string = (await body?.innerText()) || (await page.content());
return xml;
}
function extractLocs(xml: string): string[] {
const regex: RegExp = /<loc>(.*?)<\/loc>/g;
const locs: string[] = [];
let match: RegExpExecArray | null;
while ((match = regex.exec(xml)) !== null) {
if (match[1]) {
locs.push(match[1]);
}
}
return locs;
}
test.describe("Home: Sitemap", () => {
test("sitemap loads and has home first", async ({ page }: { page: Page }) => {
if (!IS_BILLING_ENABLED) {
return; // mirror existing pattern
}
page.setDefaultNavigationTimeout(120000);
const xml: string = await fetchSitemap(page);
expect(xml.includes("<urlset")).toBeTruthy();
const locs: string[] = extractLocs(xml);
expect(locs.length).toBeGreaterThan(0);
const first: string | undefined = locs[0];
expect(first, "First <loc> should exist").toBeTruthy();
expect(first!.endsWith("/")).toBeTruthy();
});
});

View File

@@ -36,7 +36,9 @@ const init: PromiseVoidFunction = async (): Promise<void> => {
serviceName: APP_NAME,
});
logger.info(`FluentIngest Service - Queue concurrency: ${FLUENT_INGEST_CONCURRENCY}`);
logger.info(
`FluentIngest Service - Queue concurrency: ${FLUENT_INGEST_CONCURRENCY}`,
);
// init the app
await App.init({

View File

@@ -1,5 +1,7 @@
{
"watch": ["./","../Common/Server", "../Common/Types", "../Common/Utils", "../Common/Models"],
"ext": "ts,json,tsx,env,js,jsx,hbs",
"exec": "node --inspect=0.0.0.0:9229 --require ts-node/register Index.ts"
"watchOptions": {"useFsEvents": false, "interval": 500},
"env": {"TS_NODE_TRANSPILE_ONLY": "1", "TS_NODE_FILES": "false"},
"exec": "node --inspect=0.0.0.0:9229 -r ts-node/register/transpile-only Index.ts"
}

View File

@@ -93,9 +93,40 @@ app.get(
async (req: ExpressRequest, res: ExpressResponse) => {
try {
const tagName: string = req.params["tagName"] as string;
const tagSlug: string = tagName; // original slug
const blogPosts: Array<BlogPostHeader> =
// Pagination params
const pageParam: string | undefined = req.query["page"] as
| string
| undefined;
const pageSizeParam: string | undefined = req.query["pageSize"] as
| string
| undefined;
let page: number = pageParam ? parseInt(pageParam, 10) : 1;
let pageSize: number = pageSizeParam ? parseInt(pageSizeParam, 10) : 24;
if (isNaN(page) || page < 1) {
page = 1;
}
if (isNaN(pageSize) || pageSize < 1) {
pageSize = 24;
}
if (pageSize > 100) {
pageSize = 100;
}
const allPosts: Array<BlogPostHeader> =
await BlogPostUtil.getBlogPostList(tagName);
const totalPosts: number = allPosts.length;
const totalPages: number = Math.ceil(totalPosts / pageSize) || 1;
if (page > totalPages) {
page = totalPages;
}
const start: number = (page - 1) * pageSize;
const paginatedPosts: Array<BlogPostHeader> = allPosts.slice(
start,
start + pageSize,
);
const allTags: Array<string> = await BlogPostUtil.getTags();
res.render(`${ViewsPath}/Blog/ListByTag`, {
support: false,
@@ -103,8 +134,15 @@ app.get(
cta: true,
blackLogo: false,
requestDemoCta: false,
blogPosts: blogPosts,
blogPosts: paginatedPosts,
tagName: Text.fromDashesToPascalCase(tagName),
tagSlug: tagSlug,
allTags: allTags,
page: page,
pageSize: pageSize,
totalPages: totalPages,
totalPosts: totalPosts,
basePath: `/blog/tag/${tagSlug}`,
enableGoogleTagManager: IsBillingEnabled,
});
} catch (e) {
@@ -117,8 +155,38 @@ app.get(
// main blog page
app.get("/blog", async (_req: ExpressRequest, res: ExpressResponse) => {
try {
const blogPosts: Array<BlogPostHeader> =
const req: ExpressRequest = _req; // alias for clarity
const pageParam: string | undefined = req.query["page"] as
| string
| undefined;
const pageSizeParam: string | undefined = req.query["pageSize"] as
| string
| undefined;
let page: number = pageParam ? parseInt(pageParam, 10) : 1;
let pageSize: number = pageSizeParam ? parseInt(pageSizeParam, 10) : 24;
if (isNaN(page) || page < 1) {
page = 1;
}
if (isNaN(pageSize) || pageSize < 1) {
pageSize = 24;
}
if (pageSize > 100) {
pageSize = 100;
}
const allPosts: Array<BlogPostHeader> =
await BlogPostUtil.getBlogPostList();
const totalPosts: number = allPosts.length;
const totalPages: number = Math.ceil(totalPosts / pageSize) || 1;
if (page > totalPages) {
page = totalPages;
}
const start: number = (page - 1) * pageSize;
const paginatedPosts: Array<BlogPostHeader> = allPosts.slice(
start,
start + pageSize,
);
const allTags: Array<string> = await BlogPostUtil.getTags();
res.render(`${ViewsPath}/Blog/List`, {
support: false,
@@ -126,7 +194,13 @@ app.get("/blog", async (_req: ExpressRequest, res: ExpressResponse) => {
cta: true,
blackLogo: false,
requestDemoCta: false,
blogPosts: blogPosts,
blogPosts: paginatedPosts,
page: page,
pageSize: pageSize,
totalPages: totalPages,
totalPosts: totalPosts,
basePath: `/blog`,
allTags: allTags,
enableGoogleTagManager: IsBillingEnabled,
});
} catch (e) {

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 DatabaseConfig from "Common/Server/DatabaseConfig";
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
import HTTPResponse from "Common/Types/API/HTTPResponse";
import URL from "Common/Types/API/URL";
@@ -25,12 +26,41 @@ import Reviews from "./Utils/Reviews";
// import jobs.
import "./Jobs/UpdateBlog";
import { IsBillingEnabled } from "Common/Server/EnvironmentConfig";
import LocalCache from "Common/Server/Infrastructure/LocalCache";
const HomeFeatureSet: FeatureSet = {
init: async (): Promise<void> => {
const app: ExpressApplication = Express.getExpressApp();
//Routes
// Middleware to inject baseUrl for templates (used for canonical links)
app.use(
async (_req: ExpressRequest, res: ExpressResponse, next: () => void) => {
if (!res.locals["homeUrl"]) {
try {
// Try to get cached home URL first.
let homeUrl: string | undefined = LocalCache.getString(
"home",
"url",
);
if (!homeUrl) {
homeUrl = (await DatabaseConfig.getHomeUrl())
.toString()
.replace(/\/$/, "");
LocalCache.setString("home", "url", homeUrl);
}
res.locals["homeUrl"] = homeUrl;
} catch {
// Fallback hard-coded production domain if env misconfigured
res.locals["homeUrl"] = "https://oneuptime.com";
}
}
next();
},
);
app.get("/", (_req: ExpressRequest, res: ExpressResponse) => {
const { reviewsList1, reviewsList2, reviewsList3 } = Reviews;
@@ -1331,18 +1361,21 @@ const HomeFeatureSet: FeatureSet = {
);
// Dynamic Sitemap
app.get("/sitemap.xml", async (_req: ExpressRequest, res: ExpressResponse) => {
try {
const xml: string = await generateSitemapXml();
res.setHeader("Content-Type", "text/xml");
res.send(xml);
} catch (err) {
// Fallback minimal static sitemap if dynamic generation fails
const fallback: string = `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n <url><loc>https://oneuptime.com/</loc></url>\n</urlset>`;
res.setHeader("Content-Type", "text/xml");
res.status(200).send(fallback);
}
});
app.get(
"/sitemap.xml",
async (_req: ExpressRequest, res: ExpressResponse) => {
try {
const xml: string = await generateSitemapXml();
res.setHeader("Content-Type", "text/xml");
res.send(xml);
} catch {
// Fallback minimal static sitemap if dynamic generation fails
const fallback: string = `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n <url><loc>https://oneuptime.com/</loc></url>\n</urlset>`;
res.setHeader("Content-Type", "text/xml");
res.status(200).send(fallback);
}
},
);
/*
* Cache policy for static contents

View File

@@ -1,14 +1,9 @@
import AnalyticsBaseModel from "Common/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
import BaseModel from "Common/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
import HTTPResponse from "Common/Types/API/HTTPResponse";
import URL from "Common/Types/API/URL";
import OneUptimeDate from "Common/Types/Date";
import BadDataException from "Common/Types/Exception/BadDataException";
import { JSONArray, JSONObject, JSONObjectOrArray } from "Common/Types/JSON";
import { JSONArray, JSONObject } from "Common/Types/JSON";
import JSONFunctions from "Common/Types/JSONFunctions";
import Text from "Common/Types/Text";
import API from "Common/Utils/API";
import Markdown, { MarkdownContentType } from "Common/Server/Types/Markdown";
import { BlogRootPath } from "./Config";
import LocalFile from "Common/Server/Utils/LocalFile";
@@ -19,6 +14,7 @@ export interface BlogPostAuthor {
githubUrl: string;
profileImageUrl: string;
name: string;
bio?: string | undefined; // optional bio from Authors.json
}
export interface BlogPostBaseProps {
@@ -44,6 +40,45 @@ export interface BlogPost extends BlogPostBaseProps {
}
export default class BlogPostUtil {
// Cache Blogs.json contents to avoid repeated disk reads and external calls.
private static blogsMetaCache: Array<JSONObject> | null = null;
// Cache Authors.json (keyed by github username)
private static authorsMetaCache: JSONObject | null = null;
private static async getBlogsMeta(): Promise<Array<JSONObject>> {
if (this.blogsMetaCache) {
return this.blogsMetaCache;
}
const filePath: string = `${BlogRootPath}/Blogs.json`;
let jsonContent: string | JSONArray = await LocalFile.read(filePath);
if (typeof jsonContent === "string") {
jsonContent = JSONFunctions.parseJSONArray(jsonContent);
}
const blogs: Array<JSONObject> = JSONFunctions.deserializeArray(
jsonContent as Array<JSONObject>,
);
this.blogsMetaCache = blogs;
return blogs;
}
private static async getAuthorsMeta(): Promise<JSONObject> {
if (this.authorsMetaCache) {
return this.authorsMetaCache;
}
const filePath: string = `${BlogRootPath}/Authors.json`;
try {
let jsonContent: string | JSONObject = await LocalFile.read(filePath);
if (typeof jsonContent === "string") {
jsonContent = JSONFunctions.parse(jsonContent) as JSONObject;
}
this.authorsMetaCache = jsonContent as JSONObject;
return this.authorsMetaCache || ({} as JSONObject);
} catch {
this.authorsMetaCache = {} as JSONObject;
return this.authorsMetaCache;
}
}
public static async getBlogPostList(
tagName?: string | undefined,
): Promise<BlogPostHeader[]> {
@@ -98,30 +133,6 @@ export default class BlogPostUtil {
return blogPost;
}
public static async getNameOfGitHubUser(username: string): Promise<string> {
const fileUrl: URL = URL.fromString(
`https://api.github.com/users/${username}`,
);
const fileData:
| HTTPResponse<
| JSONObjectOrArray
| BaseModel
| BaseModel[]
| AnalyticsBaseModel
| AnalyticsBaseModel[]
>
| HTTPErrorResponse = await API.get(fileUrl);
if (fileData.isFailure()) {
throw fileData as HTTPErrorResponse;
}
const name: string =
(fileData.data as JSONObject)?.["name"]?.toString() || "";
return name;
}
public static async getTags(): Promise<string[]> {
// check if tags are in cache
@@ -168,8 +179,43 @@ export default class BlogPostUtil {
let markdownContent: string = await LocalFile.read(filePath);
const blogPostAuthor: BlogPostAuthor | null =
await this.getAuthorFromFileContent(markdownContent);
// Resolve author WITHOUT hitting GitHub API. Use Blogs.json to get username, Authors.json for name/bio.
let blogPostAuthor: BlogPostAuthor | null = null;
try {
const blogsMeta: Array<JSONObject> = await this.getBlogsMeta();
const blogMeta: JSONObject | undefined = blogsMeta.find(
(b: JSONObject) => {
return (b["post"] as string) === fileName;
},
);
const username: string | undefined = blogMeta?.[
"authorGitHubUsername"
] as string | undefined;
if (username) {
const authorsMeta: JSONObject = await this.getAuthorsMeta();
const authorMeta: JSONObject | undefined = authorsMeta[username] as
| JSONObject
| undefined;
const authorName: string | undefined =
(authorMeta?.["authorName"] as string) || undefined;
const authorBio: string | undefined =
(authorMeta?.["authorBio"] as string) || undefined;
blogPostAuthor = {
username,
githubUrl: `https://github.com/${username}`,
profileImageUrl: `https://avatars.githubusercontent.com/${username}`,
name: authorName || username,
bio: authorBio,
};
}
} catch {
// ignore and fallback
}
// Fallback to parsing markdown (no network) if metadata missing.
if (!blogPostAuthor) {
blogPostAuthor = await this.getAuthorFromFileContent(markdownContent);
}
const title: string = this.getTitleFromFileContent(markdownContent);
const description: string =
@@ -346,7 +392,8 @@ export default class BlogPostUtil {
username: authorUsername,
githubUrl: authorGitHubUrl,
profileImageUrl: authorProfileImageUrl,
name: await this.getNameOfGitHubUser(authorUsername),
// Do NOT call GitHub; use username as name placeholder.
name: authorUsername,
};
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -15,130 +15,171 @@ interface CachedSitemap {
const TTL_MS: number = 10 * 60 * 1000;
let cache: CachedSitemap | null = null;
export const generateSitemapXml = async (): Promise<string> => {
const now: number = OneUptimeDate.getCurrentDate().getTime();
if (cache && now - cache.generatedAt < TTL_MS) {
return cache.xml;
}
export const generateSitemapXml: () => Promise<string> =
async (): Promise<string> => {
const now: number = OneUptimeDate.getCurrentDate().getTime();
if (cache && now - cache.generatedAt < TTL_MS) {
return cache.xml;
}
const baseUrl: URL = await BlogPostUtil.getHomeUrl();
const baseUrl: URL = await BlogPostUtil.getHomeUrl();
// Discover static (non-parameterized) routes from Express stack
const discoveredStaticPaths: Set<string> = new Set();
try {
const app: ExpressApplication = Express.getExpressApp();
const stack: any[] = (app as any)?._router?.stack || [];
for (const layer of stack) {
if (!layer) { continue; }
const route = layer.route || layer?.handle?.route;
if (!route) { continue; }
// Only include GET handlers
const methods: any = route.methods || {};
if (!methods.get) { continue; }
const path: string | string[] | undefined = route.path || route?.route?.path;
const rawPaths: Array<string | undefined> = Array.isArray(path) ? path : [path];
const paths: string[] = rawPaths.filter((p): p is string => !!p);
for (let p of paths) {
if (!p || typeof p !== "string") { continue; }
// Filters: skip parameterized, wildcard, api or file-serving or sitemap itself
if (p.includes(":") || p.includes("*") || p.includes("sitemap")) { continue; }
// Exclude script or installer endpoints
if (p.endsWith(".sh")) { continue; }
if (p.startsWith("/api") || p.startsWith("/blog/post")) { continue; }
// We'll add compare pages separately with real slugs; skip base compare param route
if (p.startsWith("/compare")) { continue; }
// Normalize slash
if (!p.startsWith("/")) { p = `/${p}`; }
discoveredStaticPaths.add(p);
// Discover static (non-parameterized) routes from Express stack
const discoveredStaticPaths: Set<string> = new Set();
try {
const app: ExpressApplication = Express.getExpressApp();
const stack: any[] = (app as any)?._router?.stack || [];
for (const layer of stack) {
if (!layer) {
continue;
}
const route: any = layer.route || layer?.handle?.route;
if (!route) {
continue;
}
// Only include GET handlers
const methods: any = route.methods || {};
if (!methods.get) {
continue;
}
const path: string | string[] | undefined =
route.path || route?.route?.path;
const rawPaths: Array<string | undefined> = Array.isArray(path)
? path
: [path];
const paths: string[] = rawPaths.filter(
(p: string | undefined): p is string => {
return Boolean(p);
},
);
for (let p of paths) {
if (!p || typeof p !== "string") {
continue;
}
// Filters: skip parameterized, wildcard, api or file-serving or sitemap itself
if (p.includes(":") || p.includes("*") || p.includes("sitemap")) {
continue;
}
// Exclude script or installer endpoints
if (p.endsWith(".sh")) {
continue;
}
if (p.startsWith("/api") || p.startsWith("/blog/post")) {
continue;
}
// We'll add compare pages separately with real slugs; skip base compare param route
if (p.startsWith("/compare")) {
continue;
}
// Normalize slash
if (!p.startsWith("/")) {
p = `/${p}`;
}
discoveredStaticPaths.add(p);
}
}
// Ensure root present
discoveredStaticPaths.add("/");
// Ensure docs main landing page present (may be served statically and not discoverable)
discoveredStaticPaths.add("/docs");
// add /reference
discoveredStaticPaths.add("/reference");
} catch {
// If introspection fails, fall back to minimal set
discoveredStaticPaths.add("/");
discoveredStaticPaths.add("/blog");
}
// Ensure root present
discoveredStaticPaths.add("/");
} catch {
// If introspection fails, fall back to minimal set
discoveredStaticPaths.add("/");
discoveredStaticPaths.add("/blog");
}
const staticPaths: string[] = Array.from(discoveredStaticPaths);
const staticPaths: string[] = Array.from(discoveredStaticPaths);
// Product compare pages
const productComparePaths: string[] = getProductCompareSlugs().map(
(slug: string) => `/compare/${slug}`,
);
// Product compare pages
const productComparePaths: string[] = getProductCompareSlugs().map(
(slug: string) => {
return `/compare/${slug}`;
},
);
// Blog posts
const blogPosts: Array<BlogPostHeader> = await BlogPostUtil.getBlogPostList();
const blogPostEntries = 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
: `${baseUrl.toString()}${post.blogUrl.replace(/^\//, "")}`;
return {
loc,
lastmod: new Date(post.postDate).toISOString(),
};
});
// Blog posts
const blogPosts: Array<BlogPostHeader> =
await BlogPostUtil.getBlogPostList();
const blogPostEntries: any[] = 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
: `${baseUrl.toString()}${post.blogUrl.replace(/^\//, "")}`;
return {
loc,
lastmod: new Date(post.postDate).toISOString(),
};
});
// Blog tags
const tags: string[] = await BlogPostUtil.getTags();
const tagEntries = tags.map((tag: string) => {
const tagSlug: string = tag
.toLowerCase()
.replace(/\s+/g, "-")
.trim();
return {
loc: `${baseUrl.toString()}blog/tag/${tagSlug}`,
lastmod: OneUptimeDate.getCurrentDate().toISOString(),
};
});
// Blog tags
const tags: string[] = await BlogPostUtil.getTags();
const tagEntries: any[] = tags.map((tag: string) => {
const tagSlug: string = tag.toLowerCase().replace(/\s+/g, "-").trim();
return {
loc: `${baseUrl.toString()}blog/tag/${tagSlug}`,
lastmod: OneUptimeDate.getCurrentDate().toISOString(),
};
});
const timestamp: string = OneUptimeDate.getCurrentDate().toISOString();
const timestamp: string = OneUptimeDate.getCurrentDate().toISOString();
interface Entry { loc: string; lastmod: string }
const entries: Entry[] = [
...staticPaths.map((p: string) => ({
loc: `${baseUrl.toString()}${p.replace(/^\//, "")}`,
lastmod: timestamp,
})),
...productComparePaths.map((p: string) => ({
loc: `${baseUrl.toString()}${p.replace(/^\//, "")}`,
lastmod: timestamp,
})),
...blogPostEntries,
...tagEntries,
];
// Remove duplicates (possible if overlap)
const dedupMap: Map<string, Entry> = new Map();
entries.forEach((e: Entry) => {
if (!dedupMap.has(e.loc)) {
dedupMap.set(e.loc, e);
interface Entry {
loc: string;
lastmod: string;
}
});
const entries: Entry[] = [
...staticPaths.map((p: string) => {
return {
loc: `${baseUrl.toString()}${p.replace(/^\//, "")}`,
lastmod: timestamp,
};
}),
...productComparePaths.map((p: string) => {
return {
loc: `${baseUrl.toString()}${p.replace(/^\//, "")}`,
lastmod: timestamp,
};
}),
...blogPostEntries,
...tagEntries,
];
const urlset: XMLBuilder = create().ele("urlset");
urlset.att("xmlns", "http://www.sitemaps.org/schemas/sitemap/0.9");
// Remove duplicates (possible if overlap)
const dedupMap: Map<string, Entry> = new Map();
entries.forEach((e: Entry) => {
if (!dedupMap.has(e.loc)) {
dedupMap.set(e.loc, e);
}
});
// Ensure home URL is first
const baseUrlString: string = baseUrl.toString();
const orderedEntries = Array.from(dedupMap.values());
orderedEntries.sort((a, b) => {
if (a.loc === baseUrlString) { return -1; }
if (b.loc === baseUrlString) { return 1; }
return 0; // preserve relative order otherwise
});
const urlset: XMLBuilder = create().ele("urlset");
urlset.att("xmlns", "http://www.sitemaps.org/schemas/sitemap/0.9");
for (const entry of orderedEntries) {
const urlEle: XMLBuilder = urlset.ele("url");
urlEle.ele("loc").txt(entry.loc);
urlEle.ele("lastmod").txt(entry.lastmod);
}
// Ensure home URL is first
const baseUrlString: string = baseUrl.toString();
const orderedEntries: any[] = Array.from(dedupMap.values());
orderedEntries.sort((a: any, b: any) => {
if (a.loc === baseUrlString) {
return -1;
}
if (b.loc === baseUrlString) {
return 1;
}
return 0; // preserve relative order otherwise
});
const xml: string = urlset.end({ prettyPrint: true });
for (const entry of orderedEntries) {
const urlEle: XMLBuilder = urlset.ele("url");
urlEle.ele("loc").txt(entry.loc);
urlEle.ele("lastmod").txt(entry.lastmod);
}
cache = { xml, generatedAt: now };
return xml;
};
const xml: string = urlset.end({ prettyPrint: true });
cache = { xml, generatedAt: now };
return xml;
};
export default generateSitemapXml;

View File

@@ -20,31 +20,55 @@
<div class="relative isolate overflow-hidden bg-white">
<div class="py-24 sm:py-32">
<%- include('./Partials/BlogTitleAndDescription', { title: 'Engineering Uptime', smallTitle: '- Blog by OneUptime', description: 'Latest posts on Observability, Monitoring, Reliability and more.' }) -%>
<div class="mx-auto max-w-7xl px-6 lg:px-8">
<div>
<%- include('./Partials/BlogTitleAndDescription', { title: 'Engineering Uptime', smallTitle: '- Blog by OneUptime', description: 'Latest posts on Observability, Monitoring, Reliability and more.' }) -%>
<div class="mt-4">
<% if(blogPosts.length> 0){ %>
<ul role="list" class="divide-y divide-gray-100 list-none">
<% for(var i=0; i<blogPosts.length; i++) {%>
<%- include('./Partials/ListItem', { blogPost: blogPosts[i] }) -%>
<% } %>
</ul>
<% } %>
<% const featured = blogPosts[0]; const rest = blogPosts.slice(1); %>
<!-- Featured Post -->
<div class="relative mb-14 rounded-3xl overflow-hidden border border-indigo-100 bg-gradient-to-br from-indigo-50 via-white to-indigo-100/40 p-8 md:p-12 shadow-sm ring-1 ring-indigo-100/60">
<div class="grid md:grid-cols-5 gap-10 items-center">
<div class="md:col-span-3">
<a href="<%- featured.blogUrl %>" class="group no-underline">
<p class="inline-flex items-center gap-2 text-xs font-medium uppercase tracking-wide text-indigo-700 bg-indigo-100/70 px-3 py-1 rounded-full ring-1 ring-indigo-200">Featured</p>
<h2 class="mt-5 text-3xl md:text-4xl font-bold tracking-tight text-gray-900 group-hover:text-indigo-700 transition-colors"><%- featured.title %></h2>
<p class="mt-5 text-base md:text-lg leading-relaxed text-gray-600 line-clamp-5"><%- featured.description %></p>
<div class="mt-6 text-xs text-gray-500 flex flex-wrap items-center gap-2">
<img class="h-8 w-8 rounded-full ring-2 ring-white shadow" src="https://avatars.githubusercontent.com/<%- featured.authorGitHubUsername -%>?s=80" alt="@<%- featured.authorGitHubUsername -%>" width="32" height="32" loading="lazy" decoding="async">
<span>@<%- featured.authorGitHubUsername -%></span>
<span class="select-none">•</span>
<span><%- featured.formattedPostDate -%></span>
</div>
<div class="mt-6">
<%- include('./Partials/Tags', { blogPost: featured }) -%>
</div>
<div class="mt-8 inline-flex items-center gap-2 text-sm font-semibold text-indigo-600 group-hover:text-indigo-500">Read article <svg class="w-4 h-4" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd" /></svg></div>
</a>
</div>
<div class="md:col-span-2 relative">
<div class="absolute inset-0 bg-[radial-gradient(circle_at_30%_30%,rgba(99,102,241,0.15),transparent_70%)] pointer-events-none"></div>
</div>
</div>
</div>
<!-- Posts Grid -->
<div id="blog-posts-grid" class="grid grid-cols-1 gap-10 sm:grid-cols-2 xl:grid-cols-3 items-stretch">
<% for(var i=0; i<rest.length; i++) { const post = rest[i]; %>
<div data-blog-card class="h-full" data-search="<%- (post.title + ' ' + post.description + ' ' + post.tags.join(' ')).toLowerCase() -%>">
<%- include('./Partials/ListItem', { blogPost: post }) -%>
</div>
<% } %>
</div>
<div id="no-search-results" class="hidden text-center text-gray-500 text-sm py-16">No posts match your search.</div>
<%- include('./Partials/Pagination', { page: page, pageSize: pageSize, totalPages: totalPages, totalPosts: totalPosts, basePath: basePath }) -%>
<% } else { %>
<div class="text-center text-gray-600 text-lg py-12">No blog posts found.</div>
<% } %>
<div class="mt-24">
<%- include('./Partials/OpenSourceCommitment', {blogPost: null}) -%>
</div>
</div>
</div>
</div>

View File

@@ -20,30 +20,23 @@
<div class="relative isolate overflow-hidden bg-white">
<div class="py-24 sm:py-32">
<%- include('./Partials/BlogTitleAndDescription', { title: 'Latest posts on '+tagName, smallTitle: "", description: 'Here are some of the latest posts on '+tagName }) -%>
<div class="mx-auto max-w-7xl px-6 lg:px-8">
<div>
<%- include('./Partials/BlogTitleAndDescription', { title: 'Latest posts on '+tagName, smallTitle: "", description: 'Here are some of the latest posts on '+tagName }) -%>
<div class="mt-16">
<% if(blogPosts.length> 0){ %>
<ul role="list" class="divide-y divide-gray-100 list-none">
<% for(var i=0; i<blogPosts.length; i++) {%>
<div class="grid grid-cols-1 gap-10 sm:grid-cols-2 xl:grid-cols-3">
<% for(var i=0; i<blogPosts.length; i++) { %>
<%- include('./Partials/ListItem', { blogPost: blogPosts[i] }) -%>
<% } %>
</ul>
<% } %>
<% } %>
</div>
<%- include('./Partials/Pagination', { page: page, pageSize: pageSize, totalPages: totalPages, totalPosts: totalPosts, basePath: basePath }) -%>
<% } else { %>
<div class="text-center text-gray-600 text-lg py-12">No posts found for this tag.</div>
<% } %>
<div class="mt-20">
<%- include('./Partials/OpenSourceCommitment', {blogPost: null}) -%>
</div>
</div>
<%- include('./Partials/OpenSourceCommitment', {blogPost: null}) -%>
</div>
</div>

View File

@@ -0,0 +1,78 @@
<% if(allTags && allTags.length){ %>
<div class="mb-14" id="tag-filter-root">
<div class="mb-4 flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div class="flex items-center gap-3">
<span class="text-sm font-semibold text-gray-700 flex items-center gap-1"><svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M7 7h.01M4 6a2 2 0 012-2h3.586a1 1 0 01.707.293l1.414 1.414A1 1 0 0012.414 6H18a2 2 0 012 2v2M4 8v8a2 2 0 002 2h2m12-8v4a2 2 0 01-2 2h-4m-6 0h6m-6 0a2 2 0 01-2-2v-2"/></svg> Tags</span>
<span class="hidden md:inline text-xs rounded-full bg-indigo-50 text-indigo-700 px-2 py-0.5 font-medium"> <%- allTags.length %> </span>
</div>
<div class="relative w-full md:w-72">
<input id="tag-filter-search" type="text" placeholder="Search tags..." class="w-full rounded-md border border-gray-200 bg-white py-2 pl-9 pr-3 text-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" aria-label="Search tags" />
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-2 text-gray-400">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd"/></svg>
</div>
</div>
</div>
<div class="relative group">
<div id="tag-filter-container" class="flex gap-2 overflow-x-auto pb-2 scrollbar-thin snap-x snap-mandatory" style="scrollbar-width: none; -ms-overflow-style: none;">
<style>
#tag-filter-container::-webkit-scrollbar{display:none;}
</style>
<a href="/blog" data-tag="all" class="tag-chip <%= !tagName ? 'tag-chip-active' : '' %>" aria-label="Show all posts">All</a>
<% for(var i=0;i<allTags.length;i++){ const t = allTags[i]; const slug = t.replaceAll(' ','-').toLowerCase(); const active = (tagName && tagName.toLowerCase()===t.toLowerCase()); %>
<a href="/blog/tag/<%- slug -%>" data-tag="<%- slug %>" class="tag-chip <%= active ? 'tag-chip-active' : '' %>" aria-label="Filter by <%- t %>"><%- t %></a>
<% } %>
<div id="no-tag-results" class="hidden px-3 py-1.5 text-xs text-gray-500">No tags match.</div>
</div>
<div class="pointer-events-none absolute top-0 right-0 h-full w-10 bg-gradient-to-l from-white to-transparent hidden md:block"></div>
</div>
<div class="mt-3 flex items-center justify-between text-[11px] text-gray-500">
<div id="tag-count-info">Showing <%- allTags.length %> tags</div>
<button id="toggle-more-tags" type="button" class="hidden text-indigo-600 hover:text-indigo-500 font-medium">Show more</button>
</div>
</div>
<script>
(function(){
if(window.__TAG_FILTER_INITIALIZED__) { return; }
window.__TAG_FILTER_INITIALIZED__ = true;
const searchInput = document.getElementById('tag-filter-search');
const container = document.getElementById('tag-filter-container');
const info = document.getElementById('tag-count-info');
const noResults = document.getElementById('no-tag-results');
if(!searchInput || !container) { return; }
const chips = Array.from(container.querySelectorAll('.tag-chip'));
function normalize(s){ return (s||'').toLowerCase(); }
function filter(){
const q = normalize(searchInput.value);
let visible = 0;
chips.forEach(chip => {
if(chip.dataset.tag === 'all'){ // always show All
chip.classList.remove('hidden');
return; }
const text = normalize(chip.textContent);
if(!q || text.includes(q)) { chip.classList.remove('hidden'); visible++; }
else { chip.classList.add('hidden'); }
});
if(visible === 0 && q){ noResults.classList.remove('hidden'); }
else { noResults.classList.add('hidden'); }
info && (info.textContent = q ? `Matched ${visible} tag${visible!==1?'s':''}` : `Showing ${chips.length-1} tags`);
}
searchInput.addEventListener('input', filter);
// Keyboard horizontal scroll convenience
searchInput.addEventListener('keydown', function(e){
if(e.key === 'ArrowRight'){ container.scrollBy({left:120,behavior:'smooth'}); }
if(e.key === 'ArrowLeft'){ container.scrollBy({left:-120,behavior:'smooth'}); }
});
})();
</script>
<style>
/* Inline fallback styling since Tailwind @apply isn't processed in EJS runtime */
.tag-chip { font-size: 0.75rem; font-weight:500; padding:0.375rem 0.75rem; border-radius:9999px; border:1px solid #e5e7eb; background:#ffffff; color:#4b5563; display:inline-flex; align-items:center; transition:all .15s; white-space:nowrap; text-decoration:none; }
.tag-chip:hover { color:#4f46e5; border-color:#818cf8; background:#eef2ff; }
.tag-chip-active { background:#4f46e5; border-color:#4f46e5; color:#ffffff; }
.tag-chip-active:hover { background:#4f46e5; color:#ffffff; }
</style>
<% } %>

View File

@@ -1,17 +1,17 @@
<div class="mb-24">
<div class="mx-auto flex justify-center text-center ">
<h1 class="max-w-5xl text-center text-6xl font-bold text-gray-900 leading-tight">
<div class="relative mb-16 md:mb-20">
<div class="mx-auto flex justify-center text-center">
<div>
<h1 class="max-w-5xl text-5xl md:text-6xl font-extrabold tracking-tight text-gray-900 leading-tight bg-clip-text">
<%= title %>
</h1>
</h1>
<% if(smallTitle){ %>
<h3 class="max-w-5xl mt-4 text-2xl font-semibold text-indigo-600/80 leading-tight">
<%= smallTitle %>
</h3>
<% } %>
<p class="max-w-3xl mx-auto text-lg md:text-2xl leading-8 text-gray-600 mt-6">
<%= description %>
</p>
</div>
<div class="mx-auto flex justify-center text-center ">
<% if(smallTitle){ %>
<h3 class="max-w-5xl mt-5 text-center text-2xl font-medium text-gray-900 leading-tight">
<%= smallTitle %>
</h3>
<% } %>
</div>
<p class="text-2xl text-center leading-8 text-gray-600 mt-8 leading-normal">
<%= description %>
</p>
</div>

View File

@@ -1,33 +1,30 @@
<li class="py-5">
<div class="min-w-0">
<a href="<%= blogPost.blogUrl %>">
<div class="min-w-0 flex-auto">
<p class="font-semibold text-2xl leading-6 text-gray-900">
<%= blogPost.title %>
</p>
<p class="mt-2 leading-5 text-gray-600">
<%= blogPost.description %>
</p>
<div class="group relative flex flex-col h-full rounded-2xl border border-gray-200/80 bg-white/70 backdrop-blur-sm p-6 shadow-sm ring-1 ring-gray-200/60 hover:shadow-lg hover:border-indigo-300 hover:ring-indigo-200 transition-all duration-300 focus-within:shadow-lg" tabindex="0" aria-label="Blog post card: <%- blogPost.title.replace(/\"/g,'') -%>">
<div class="absolute -inset-px rounded-2xl opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100 transition pointer-events-none bg-gradient-to-br from-indigo-500/5 via-transparent to-indigo-400/10"></div>
<div class="flex-1 flex flex-col relative z-10">
<div class="flex items-start gap-3">
<div class="flex-1">
<a href="<%= blogPost.blogUrl %>" class="no-underline focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 rounded-md">
<h2 class="text-xl font-semibold tracking-tight text-gray-900 group-hover:text-indigo-600 transition-colors line-clamp-2">
<%= blogPost.title %>
</h2>
</a>
<p class="mt-3 text-sm leading-6 text-gray-600 line-clamp-4"><%= blogPost.description %></p>
</div>
</a>
</div>
<div class="-mt-5">
<div class="mt-4">
<%- include('./Tags', { blogPost: blogPost }) -%>
</div>
<div class="mt-5">
<div class="">
<a href="https://github.com/<%- blogPost.authorGitHubUsername -%>" target="_blank">
<div class="flex items-center gap-x-6">
<p class="text-sm font-medium leading-7 tracking-tight text-gray-600">
<span class="text-gray-500">By</span> @<%- blogPost.authorGitHubUsername -%> <span
class="text-gray-500"> on
<%- blogPost.formattedPostDate -%></span>
</p>
</div>
</div>
<div class="mt-6 flex items-center justify-between">
<a href="https://github.com/<%- blogPost.authorGitHubUsername -%>" target="_blank" class="flex items-center gap-3 group/author">
<img loading="lazy" src="https://avatars.githubusercontent.com/<%- blogPost.authorGitHubUsername -%>?s=64" alt="@<%- blogPost.authorGitHubUsername -%>" class="h-10 w-10 rounded-full ring-2 ring-white shadow-sm group-hover/author:ring-indigo-200 transition" width="40" height="40" decoding="async">
<div class="text-[11px] leading-4 text-gray-600">
<span class="text-gray-500">By</span> @<%- blogPost.authorGitHubUsername -%><br/>
<span class="text-gray-400"><%- blogPost.formattedPostDate -%></span>
</div>
</a>
<a href="<%= blogPost.blogUrl %>" class="shrink-0 text-sm font-medium text-indigo-600 hover:text-indigo-500 inline-flex items-center gap-1 group/cta">Read
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="h-4 w-4 transition-transform group-hover/cta:translate-x-0.5"><path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd" /></svg>
</a>
</div>
</li>
</div>
</div>

View File

@@ -0,0 +1,37 @@
<% if(totalPages && totalPages > 1){ %>
<nav class="mt-16 flex items-center justify-between" aria-label="Pagination">
<div>
<% if(page > 1){ %>
<a href="<%- basePath %>?page=<%- page-1 %>&pageSize=<%- pageSize %>" class="inline-flex items-center rounded-md px-4 py-2 text-sm font-medium border border-gray-300 bg-white text-gray-700 hover:bg-gray-50">Previous</a>
<% } else { %>
<span class="inline-flex items-center rounded-md px-4 py-2 text-sm font-medium border border-gray-200 bg-gray-50 text-gray-300 cursor-not-allowed">Previous</span>
<% } %>
</div>
<div class="hidden md:flex items-center gap-1">
<% const windowSize = 5; let start = Math.max(1, page - Math.floor(windowSize/2)); let end = Math.min(totalPages, start + windowSize -1); if(end-start+1 < windowSize){ start = Math.max(1, end-windowSize+1);} %>
<% if(start > 1){ %>
<a href="<%- basePath %>?page=1&pageSize=<%- pageSize %>" class="px-3 py-2 text-sm font-medium rounded-md border border-gray-200 bg-white hover:bg-gray-50">1</a>
<% if(start > 2){ %><span class="px-2 text-gray-400">…</span><% } %>
<% } %>
<% for(let p = start; p<=end; p++){ %>
<% if(p === page){ %>
<span class="px-3 py-2 text-sm font-semibold rounded-md bg-indigo-600 text-white border border-indigo-600"><%- p %></span>
<% } else { %>
<a href="<%- basePath %>?page=<%- p %>&pageSize=<%- pageSize %>" class="px-3 py-2 text-sm font-medium rounded-md border border-gray-200 bg-white hover:bg-gray-50"><%- p %></a>
<% } %>
<% } %>
<% if(end < totalPages){ %>
<% if(end < totalPages-1){ %><span class="px-2 text-gray-400">…</span><% } %>
<a href="<%- basePath %>?page=<%- totalPages %>&pageSize=<%- pageSize %>" class="px-3 py-2 text-sm font-medium rounded-md border border-gray-200 bg-white hover:bg-gray-50"><%- totalPages %></a>
<% } %>
</div>
<div class="flex items-center gap-2">
<% if(page < totalPages){ %>
<a href="<%- basePath %>?page=<%- page+1 %>&pageSize=<%- pageSize %>" class="inline-flex items-center rounded-md px-4 py-2 text-sm font-medium border border-gray-300 bg-white text-gray-700 hover:bg-gray-50">Next</a>
<% } else { %>
<span class="inline-flex items-center rounded-md px-4 py-2 text-sm font-medium border border-gray-200 bg-gray-50 text-gray-300 cursor-not-allowed">Next</span>
<% } %>
</div>
</nav>
<p class="mt-4 text-center text-xs text-gray-500">Showing <%- ((page-1)*pageSize)+1 %> - <%- Math.min(page*pageSize, totalPosts) %> of <%- totalPosts %> posts</p>
<% } %>

View File

@@ -1,22 +1,12 @@
<% if(blogPost.tags.length> 0){ %>
<div class="flex mt-10">
<div class="space-x-1">
<!-- Loop over blogPost.tags and show them here-->
<% for(var i=0; i<blogPost.tags.length; i++) {%>
<a href="/blog/tag/<%- blogPost.tags[i].replaceAll(' ','-').toLowerCase() -%>">
<div
class="relative inline-flex items-center rounded-full border border-gray-300 px-3 py-0.5 text-sm">
<div class="absolute flex flex-shrink-0 items-center justify-center">
<div class="h-1.5 w-1.5 rounded-full bg-indigo-500" aria-hidden="true">
</div>
</div>
<div class="ml-3.5 font-medium text-gray-900">
<%- blogPost.tags[i] -%>
</div>
</div>
</a>
<% } %>
</div>
<div class="flex flex-wrap gap-2">
<% for(var i=0; i<blogPost.tags.length; i++) { %>
<a href="/blog/tag/<%- blogPost.tags[i].replaceAll(' ','-').toLowerCase() -%>" class="group/tag">
<span class="inline-flex items-center gap-1.5 rounded-full border border-gray-200 bg-gray-50 px-3 py-1 text-xs font-medium text-gray-700 shadow-sm hover:border-indigo-300 hover:bg-indigo-50 hover:text-indigo-700 transition">
<span class="h-1.5 w-1.5 rounded-full bg-indigo-500 group-hover/tag:bg-indigo-600"></span>
<%- blogPost.tags[i] -%>
</span>
</a>
<% } %>
</div>
<% } %>
<% } %>

View File

@@ -9,7 +9,7 @@
</title>
<meta name="description" content="<%= blogPost.description %>">
<%- include('../head-basic') -%>
<link rel="canonical" href="https://oneuptime.com/blog/post/<%= blogPost.fileName %>" />
<link rel="canonical" href="https://oneuptime.com/blog/post/<%= blogPost.fileName %>/view" />
<meta property="og:site_name" content="OneUptime | One Complete Observability platform.">
<meta property="og:type" content="article">
<meta property="og:title" content="<%= blogPost.title %>">
@@ -63,56 +63,131 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
<style>
/* Custom styles for code blocks in blog posts */
.blog-body pre {
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Consolas', 'Courier New', monospace !important;
font-size: 14px !important;
line-height: 1.5 !important;
font-weight: 400 !important;
}
.blog-body pre code {
font-family: inherit !important;
font-size: inherit !important;
line-height: inherit !important;
font-weight: inherit !important;
}
/* Ensure highlight.js doesn't override our font settings */
.blog-body .hljs {
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Consolas', 'Courier New', monospace !important;
font-size: 14px !important;
line-height: 1.5 !important;
font-weight: 400 !important;
}
</style>
</head>
<body>
<%- include('../nav') -%>
<div class="relative isolate overflow-hidden bg-white">
<div class="py-24 sm:py-32">
<div class="mx-auto max-w-7xl px-6 lg:px-8">
<%- include('./Partials/BlogTitleAndDescription', { title: blogPost.title , smallTitle: "", description:
blogPost.description }) -%>
<div class="blog-body">
<%- blogPost.htmlBody -%>
</div>
<div class="relative isolate overflow-hidden bg-gradient-to-b from-white via-white to-indigo-50/40">
<!-- Hero / Title Section -->
<div class="pt-20 pb-12 sm:pt-28 sm:pb-20">
<div class="mx-auto max-w-4xl px-6 lg:px-8 text-center">
<h1 class="text-4xl sm:text-5xl font-extrabold tracking-tight text-gray-900 leading-tight">
<%= blogPost.title %>
</h1>
<p class="mt-6 text-xl text-gray-600 leading-relaxed max-w-3xl mx-auto">
<%= blogPost.description %>
</p>
<div class="mt-8 flex flex-wrap items-center justify-center gap-4 text-sm text-gray-500">
<% if(blogPost.author){ %>
<a href="<%- blogPost.author.githubUrl -%>" target="_blank" class="flex items-center space-x-2 group">
<!-- Added explicit width & height to stabilize layout (CLS) -->
<img class="h-8 w-8 rounded-full ring-2 ring-indigo-200 group-hover:ring-indigo-400 transition" src="<%- blogPost.author.profileImageUrl -%>" alt="<%- blogPost.author.name -%>" width="32" height="32" decoding="async">
<span class="font-medium text-gray-700 group-hover:text-gray-900 transition">@<%- blogPost.author.username -%></span>
</a>
<span class="hidden sm:inline select-none">•</span>
<span><%- blogPost.formattedPostDate -%></span>
<span class="hidden sm:inline select-none">•</span>
<% } %>
<span id="reading-time" class="inline-flex items-center gap-1">
<svg class="w-4 h-4 text-indigo-500" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
<span class="sr-only">Reading time</span>
</span>
</div>
<div class="mt-8">
<%- include('./Partials/Tags', { blogPost: blogPost }) -%>
<div class="bg-white my-10 mt-20">
<div class="">
<ul role="list" class="">
<li>
<a href="<%- blogPost.author.githubUrl -%>" target="_blank">
<div class="flex items-center gap-x-6">
<img class="h-12 w-12 rounded-full"
src="<%- blogPost.author.profileImageUrl -%>" alt="">
<div class="-ml-4">
<h3
class="text-base font-medium leading-7 tracking-tight text-gray-600">
<span class="text-gray-500">By</span> <%-
blogPost.author.name -%> <span class="text-gray-500"> on
<%- blogPost.formattedPostDate -%></span>
</h3>
<p class="text-sm font-medium leading-6 text-indigo-500 -mt-1">
@<%- blogPost.author.username -%></p>
</div>
</div>
</a>
</li>
</ul>
</div>
</div>
<%- include('./Partials/OpenSourceCommitment', { blogPost: blogPost }) -%>
</div>
</div>
</div>
<!-- Content + Sidebar -->
<div class="pb-24">
<div class="mx-auto max-w-7xl px-6 lg:px-8">
<div class="grid grid-cols-1 lg:grid-cols-12 gap-12">
<!-- Main content -->
<article class="lg:col-span-8 xl:col-span-9">
<div class="blog-body prose prose-slate max-w-none prose-headings:scroll-mt-28 prose-headings:font-display prose-headings:font-semibold lg:prose-headings:scroll-mt-[8.5rem] prose-a:font-semibold prose-a:text-indigo-600 hover:prose-a:text-indigo-500 prose-img:rounded-xl prose-pre:rounded-xl prose-code:text-indigo-600">
<%- blogPost.htmlBody -%>
</div>
<!-- Share -->
<div class="mt-14 border-t border-gray-200/70 pt-8 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-6">
<div class="text-sm font-medium text-gray-600">Share this article</div>
<div class="flex gap-3">
<a title="Share on X" target="_blank" rel="noopener" class="group inline-flex items-center justify-center h-9 w-9 rounded-lg border border-gray-200 hover:border-indigo-400 hover:bg-indigo-50 transition" href="https://twitter.com/intent/tweet?text=<%- encodeURIComponent(blogPost.title) -%>&url=<%- encodeURIComponent(blogPost.blogUrl) -%>">
<svg class="w-4 h-4 text-gray-500 group-hover:text-indigo-600" viewBox="0 0 24 24" fill="currentColor"><path d="M17.53 3h3.77l-8.26 9.45L23 21h-6.17l-4.8-6.01L6.4 21H2.62l8.63-9.87L1 3h6.32l4.33 5.41L17.53 3Zm-1.33 15.62h2.09L7.94 4.29H5.71l10.49 14.33Z"/></svg>
</a>
<a title="Share on LinkedIn" target="_blank" rel="noopener" class="group inline-flex items-center justify-center h-9 w-9 rounded-lg border border-gray-200 hover:border-indigo-400 hover:bg-indigo-50 transition" href="https://www.linkedin.com/sharing/share-offsite/?url=<%- encodeURIComponent(blogPost.blogUrl) -%>">
<svg class="w-4 h-4 text-gray-500 group-hover:text-indigo-600" viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.049c.476-.9 1.637-1.85 3.37-1.85 3.601 0 4.266 2.37 4.266 5.455v6.286ZM5.337 7.433a2.062 2.062 0 1 1 0-4.124 2.062 2.062 0 0 1 0 4.124ZM7.119 20.452H3.553V9h3.566v11.452ZM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003Z"/></svg>
</a>
<a title="Discuss on Hacker News" target="_blank" rel="noopener" class="group inline-flex items-center justify-center h-9 w-9 rounded-lg border border-gray-200 hover:border-indigo-400 hover:bg-indigo-50 transition" href="https://news.ycombinator.com/submitlink?u=<%- encodeURIComponent(blogPost.blogUrl) -%>&t=<%- encodeURIComponent(blogPost.title) -%>">
<svg class="w-4 h-4 text-gray-500 group-hover:text-indigo-600" viewBox="0 0 24 24" fill="currentColor"><path d="M3 3v18h18V3H3Zm9.774 10.876L17 6h-2.117l-2.742 5.274L9.4 6H7l4.226 7.876V18h2.548v-4.124Z"/></svg>
</a>
</div>
</div>
<!-- Author -->
<% if(blogPost.author){ %>
<div class="mt-16 p-6 rounded-2xl bg-white/60 backdrop-blur border border-gray-200 shadow-sm flex gap-6 items-start">
<!-- Added explicit width/height + lazy loading for below-the-fold author bio image -->
<img class="h-16 w-16 rounded-full ring-2 ring-indigo-200" src="<%- blogPost.author.profileImageUrl -%>" alt="<%- blogPost.author.name -%>" width="64" height="64" loading="lazy" decoding="async">
<div>
<h3 class="text-lg font-semibold text-gray-900"><%- blogPost.author.name -%></h3>
<p class="text-sm text-gray-600 mb-2">@<%- blogPost.author.username -%> • <%- blogPost.formattedPostDate -%> • <span id="reading-time-inline"></span></p>
<div class="text-sm text-gray-700"><%- blogPost.author.bio || 'Building reliable software at OneUptime. Follow along for more on observability & reliability.' -%></div>
<div class="mt-3 flex gap-3">
<a href="<%- blogPost.author.githubUrl -%>" target="_blank" class="text-xs font-medium text-indigo-600 hover:text-indigo-500 inline-flex items-center gap-1">View GitHub
<svg class="w-3 h-3" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M13 5h6m0 0v6m0-6L10 14"/></svg>
</a>
</div>
</div>
</div>
<% } %>
<div class="mt-20">
<%- include('./Partials/OpenSourceCommitment', { blogPost: blogPost }) -%>
</div>
</article>
<!-- Sidebar -->
<aside class="hidden lg:block lg:col-span-4 xl:col-span-3">
<div class="sticky top-28 space-y-8">
<div class="rounded-2xl border border-gray-200 bg-white/70 backdrop-blur p-6 shadow-sm">
<h2 class="text-sm font-semibold tracking-wide text-gray-900 uppercase mb-4">On this page</h2>
<!-- Reserve initial space for TOC to reduce layout shift once populated -->
<nav id="toc" aria-label="Table of contents" class="text-sm leading-6 space-y-2 text-gray-700 min-h-[3rem]"></nav>
</div>
</div>
</aside>
</div>
</div>
</div>
</div>
@@ -120,6 +195,76 @@
<%- include('./Partials/BlogCta') -%>
<%- include('../footer') -%>
<script>
(function(){
// Reading time
try {
const container = document.querySelector('.blog-body');
if(container){
const text = container.textContent || '';
const words = text.trim().split(/\s+/).filter(Boolean).length;
const minutes = Math.max(1, Math.round(words / 200));
const rt = minutes + ' min read';
const el = document.getElementById('reading-time');
if(el){ el.insertAdjacentText('beforeend', rt); }
const el2 = document.getElementById('reading-time-inline');
if(el2){ el2.textContent = rt; }
}
} catch (e) {}
// TOC
try {
const container = document.querySelector('.blog-body');
if(!container){ return; }
const headings = container.querySelectorAll('h2, h3');
if(!headings.length){ return; }
const toc = document.getElementById('toc');
if(!toc){ return; }
headings.forEach(h => {
if(!h.id){
h.id = h.textContent.toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-+|-+$/g,'');
}
const a = document.createElement('a');
a.href = '#' + h.id;
a.textContent = h.textContent;
a.className = 'block hover:text-indigo-600 transition ' + (h.tagName === 'H3' ? 'pl-4 text-gray-500' : 'font-medium');
toc.appendChild(a);
});
} catch (e) {}
// Stabilize images inside blog body (add width/height if missing & lazy-load non-hero images)
try {
const imgs = document.querySelectorAll('.blog-body img');
imgs.forEach((img, index) => {
// If width/height already specified, skip dimension inference
if(!img.hasAttribute('width') || !img.hasAttribute('height')){
if(img.naturalWidth && img.naturalHeight){
img.setAttribute('width', img.naturalWidth);
img.setAttribute('height', img.naturalHeight);
} else {
// If not yet loaded, attach a one-time listener
img.addEventListener('load', function handler(){
if(!img.hasAttribute('width') && img.naturalWidth){ img.setAttribute('width', img.naturalWidth); }
if(!img.hasAttribute('height') && img.naturalHeight){ img.setAttribute('height', img.naturalHeight); }
img.removeEventListener('load', handler);
});
}
}
// Lazy-load images after the first one (often top/hero) for better LCP while reducing CLS
if(index > 0 && !img.hasAttribute('loading')){
img.setAttribute('loading', 'lazy');
}
if(!img.hasAttribute('decoding')){
img.setAttribute('decoding', 'async');
}
// Ensure display block for centered images without causing late shifts
if(!img.className.includes('inline') && !img.className.includes('block')){
img.classList.add('block');
}
});
} catch(e) {}
})();
</script>
</body>
</html>

View File

@@ -22,10 +22,10 @@
<div class="hidden sm:block">
<div class="border-b border-gray-200">
<nav class="-mb-px flex justify-center space-x-8" aria-label="Tabs">
<nav class="-mb-px flex justify-center space-x-8" aria-label="Product feature tabs" role="tablist">
<!-- Current: "border-indigo-500 text-indigo-600", Default: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700" -->
<a onclick="showTab(1)"
class="tab-1-button cursor-pointer border-indigo-500 text-indigo-600 group inline-flex items-center border-b-2 py-4 px-1 font-medium">
<button type="button" onclick="showTab('status-pages')" id="tab-status-pages" role="tab" aria-selected="true" aria-controls="panel-status-pages"
class="tab-status-pages-button tab-1-button cursor-pointer border-indigo-500 text-indigo-600 group inline-flex items-center border-b-2 py-4 px-1 font-medium focus:outline-none" tabindex="0" aria-label="Status Pages - communicates service availability to users">
<!-- Current: "text-indigo-500", Default: "text-gray-400 group-hover:text-gray-500" -->
<svg class="icon-tab-1 text-gray-400 text-indigo-500 -ml-0.5 mr-2 h-5 w-5"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
@@ -33,67 +33,60 @@
<path stroke-linecap="round" stroke-linejoin="round"
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>Status Pages</span>
</a>
<a onclick="showTab(2)"
class="tab-2-button cursor-pointer border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 group inline-flex items-center border-b-2 py-4 px-1 font-medium">
</button>
<button type="button" onclick="showTab('monitoring')" id="tab-monitoring" role="tab" aria-selected="false" aria-controls="panel-monitoring"
class="tab-monitoring-button tab-2-button cursor-pointer border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 group inline-flex items-center border-b-2 py-4 px-1 font-medium focus:outline-none" tabindex="-1" aria-label="Monitoring - uptime, performance and synthetic checks">
<svg class="icon-tab-2 text-gray-400 group-hover:text-gray-500 -ml-0.5 mr-2 h-5 w-5"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M12 21a9.004 9.004 0 008.716-6.747M12 21a9.004 9.004 0 01-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 017.843 4.582M12 3a8.997 8.997 0 00-7.843 4.582m15.686 0A11.953 11.953 0 0112 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0121 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0112 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 013 12c0-1.605.42-3.113 1.157-4.418" />
</svg>
<span>Monitoring</span>
</a>
<a onclick="showTab(3)"
class="tab-3-button cursor-pointer border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 group inline-flex items-center border-b-2 py-4 px-1 font-medium"
aria-current="page">
</button>
<button type="button" onclick="showTab('incidents')" id="tab-incidents" role="tab" aria-selected="false" aria-controls="panel-incidents"
class="tab-incidents-button tab-3-button cursor-pointer border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 group inline-flex items-center border-b-2 py-4 px-1 font-medium focus:outline-none" tabindex="-1" aria-label="Incidents - detect, respond and learn from outages">
<svg xmlns="http://www.w3.org/2000/svg"
class="icon-tab-3 text-gray-400 group-hover:text-gray-500 -ml-0.5 mr-2 h-5 w-5" fill="none"
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
</svg>
<span>Incidents</span>
</a>
<a onclick="showTab(4)"
class="tab-4-button cursor-pointer border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 group inline-flex items-center border-b-2 py-4 px-1 font-medium">
</button>
<button type="button" onclick="showTab('on-call')" id="tab-on-call" role="tab" aria-selected="false" aria-controls="panel-on-call"
class="tab-on-call-button tab-4-button cursor-pointer border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 group inline-flex items-center border-b-2 py-4 px-1 font-medium focus:outline-none" tabindex="-1" aria-label="On Call Alerts - intelligent alert routing and scheduling">
<svg class="icon-tab-4 text-gray-400 group-hover:text-gray-500 -ml-0.5 mr-2 h-5 w-5"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round"
d="M2.25 6.75c0 8.284 6.716 15 15 15h2.25a2.25 2.25 0 002.25-2.25v-1.372c0-.516-.351-.966-.852-1.091l-4.423-1.106c-.44-.11-.902.055-1.173.417l-.97 1.293c-.282.376-.769.542-1.21.38a12.035 12.035 0 01-7.143-7.143c-.162-.441.004-.928.38-1.21l1.293-.97c.363-.271.527-.734.417-1.173L6.963 3.102a1.125 1.125 0 00-1.091-.852H4.5A2.25 2.25 0 002.25 4.5v2.25z" />
</svg>
<span>On Call Alerts</span>
</a>
</button>
<a onclick="showTab(5)"
class="tab-5-button cursor-pointer border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 group inline-flex items-center border-b-2 py-4 px-1 font-medium">
<button type="button" onclick="showTab('logs')" id="tab-logs" role="tab" aria-selected="false" aria-controls="panel-logs"
class="tab-logs-button tab-5-button cursor-pointer border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 group inline-flex items-center border-b-2 py-4 px-1 font-medium focus:outline-none" tabindex="-1" aria-label="Logs - centralized log management and analytics">
<svg class="icon-tab-5 text-gray-400 group-hover:text-gray-500 -ml-0.5 mr-2 h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" >
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 12h16.5m-16.5 3.75h16.5M3.75 19.5h16.5M5.625 4.5h12.75a1.875 1.875 0 0 1 0 3.75H5.625a1.875 1.875 0 0 1 0-3.75Z" />
</svg>
<span>Logs</span>
</a>
</button>
<a onclick="showTab(6)"
class="tab-6-button cursor-pointer border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 group inline-flex items-center border-b-2 py-4 px-1 font-medium">
<button type="button" onclick="showTab('apm')" id="tab-apm" role="tab" aria-selected="false" aria-controls="panel-apm"
class="tab-apm-button tab-6-button cursor-pointer border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 group inline-flex items-center border-b-2 py-4 px-1 font-medium focus:outline-none" tabindex="-1" aria-label="APM - metrics, traces and error tracking for applications">
<svg class="icon-tab-6 text-gray-400 group-hover:text-gray-500 -ml-0.5 mr-2 h-5 w-5"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 0 1 3 19.875v-6.75ZM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V8.625ZM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V4.125Z" />
</svg>
<span>APM</span>
</a>
</button>
<a onclick="showTab(7)"
class="tab-7-button cursor-pointer border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 group inline-flex items-center border-b-2 py-4 px-1 font-medium">
<button type="button" onclick="showTab('workflows')" id="tab-workflows" role="tab" aria-selected="false" aria-controls="panel-workflows"
class="tab-workflows-button tab-7-button cursor-pointer border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 group inline-flex items-center border-b-2 py-4 px-1 font-medium focus:outline-none" tabindex="-1" aria-label="Workflows - automate reliability operations and responses">
<svg class="icon-tab-7 text-gray-400 group-hover:text-gray-500 -ml-0.5 mr-2 h-5 w-5"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
@@ -101,9 +94,8 @@
<path strokeLinecap="round" strokeLinejoin="round"
d="M2.25 7.125C2.25 6.504 2.754 6 3.375 6h6c.621 0 1.125.504 1.125 1.125v3.75c0 .621-.504 1.125-1.125 1.125h-6a1.125 1.125 0 01-1.125-1.125v-3.75zM14.25 8.625c0-.621.504-1.125 1.125-1.125h5.25c.621 0 1.125.504 1.125 1.125v8.25c0 .621-.504 1.125-1.125 1.125h-5.25a1.125 1.125 0 01-1.125-1.125v-8.25zM3.75 16.125c0-.621.504-1.125 1.125-1.125h5.25c.621 0 1.125.504 1.125 1.125v2.25c0 .621-.504 1.125-1.125 1.125h-5.25a1.125 1.125 0 01-1.125-1.125v-2.25z" />
</svg>
<span>Workflows</span>
</a>
</button>
</nav>
</div>
</div>
@@ -588,36 +580,97 @@
<script>
let currentActiveTab = 1;
// Semantic tab slugs in desired order
const TAB_ORDER = ['status-pages','monitoring','incidents','on-call','logs','apm','workflows'];
let currentActiveTab = 'status-pages';
function normalize(tab){
// allow old numeric references for backward compatibility
if(typeof tab === 'number' || /^\d+$/.test(tab)){ return TAB_ORDER[Number(tab)-1] || 'status-pages'; }
return tab;
}
function hideTab(tab) {
tab = normalize(tab);
const panel = document.querySelector(`.tab-${TAB_ORDER.indexOf(tab)+1}`) || document.querySelector(`.tab-${tab}`);
if (panel) { panel.style.display = 'none'; panel.setAttribute('aria-hidden','true'); }
document.querySelector(`.tab-${tab}`).style.display = 'none';
document.querySelector(`.tab-${tab}-button`).classList.remove('border-indigo-500', 'text-indigo-600');
document.querySelector(`.tab-${tab}-button`).classList.add('border-transparent', 'text-gray-500', 'hover:border-gray-300', 'hover:text-gray-700');
document.querySelector(`.icon-tab-${tab}`).classList.remove('text-indigo-500');
document.querySelector(`.icon-tab-${tab}`).classList.add('text-gray-400');
const button = document.querySelector(`.tab-${tab}-button`) || document.querySelector(`.tab-${TAB_ORDER.indexOf(tab)+1}-button`);
if (button) {
button.classList.remove('border-indigo-500', 'text-indigo-600');
button.classList.add('border-transparent', 'text-gray-500', 'hover:border-gray-300', 'hover:text-gray-700');
button.setAttribute('aria-selected','false');
button.setAttribute('tabindex','-1');
}
const icon = document.querySelector(`.icon-tab-${TAB_ORDER.indexOf(tab)+1}`) || document.querySelector(`.icon-tab-${tab}`);
if (icon) { icon.classList.remove('text-indigo-500'); icon.classList.add('text-gray-400'); }
}
function showTab(tab) {
tab = normalize(tab);
if (tab === currentActiveTab) { return; }
hideTab(currentActiveTab);
document.querySelector(`.tab-${tab}`).style.display = 'block';
const numericIndex = TAB_ORDER.indexOf(tab)+1;
const panel = document.querySelector(`.tab-${numericIndex}`) || document.querySelector(`.tab-${tab}`);
if (panel) { panel.style.display = 'block'; panel.setAttribute('aria-hidden','false'); }
document.querySelector(`.tab-${tab}-button`).classList.remove('border-transparent', 'text-gray-500', 'hover:border-gray-300', 'hover:text-gray-700');
document.querySelector(`.tab-${tab}-button`).classList.add('border-indigo-500', 'text-indigo-600');
const button = document.querySelector(`.tab-${tab}-button`) || document.querySelector(`.tab-${numericIndex}-button`);
if (button) {
button.classList.remove('border-transparent', 'text-gray-500', 'hover:border-gray-300', 'hover:text-gray-700');
button.classList.add('border-indigo-500', 'text-indigo-600');
button.setAttribute('aria-selected','true');
button.setAttribute('tabindex','0');
button.focus();
}
document.querySelector(`.icon-tab-${tab}`).classList.remove('text-gray-400');
document.querySelector(`.icon-tab-${tab}`).classList.add('text-indigo-500');
const icon = document.querySelector(`.icon-tab-${numericIndex}`) || document.querySelector(`.icon-tab-${tab}`);
if (icon) { icon.classList.remove('text-gray-400'); icon.classList.add('text-indigo-500'); }
currentActiveTab = tab;
if (history.replaceState) { history.replaceState(null, '', `#tab-${tab}`); }
}
// Initialize tabs accessibility + allow keyboard navigation (arrow keys)
document.addEventListener('DOMContentLoaded', () => {
// Mark all panels with role="tabpanel" and associate with buttons
TAB_ORDER.forEach((slug, idx) => {
const panel = document.querySelector(`.tab-${idx+1}`) || document.querySelector(`.tab-${slug}`);
if (panel) {
panel.setAttribute('role','tabpanel');
panel.setAttribute('id',`panel-${slug}`);
panel.setAttribute('aria-labelledby',`tab-${slug}`);
if (slug !== currentActiveTab) { panel.style.display='none'; panel.setAttribute('aria-hidden','true'); }
else { panel.setAttribute('aria-hidden','false'); }
}
});
// Support deep linking via hash like #tab-apm or legacy #tab-6
const hash = window.location.hash;
if (hash && /^#tab-/.test(hash)) {
const raw = hash.replace('#tab-','');
showTab(raw);
}
// Keyboard navigation
const tabButtons = Array.from(document.querySelectorAll('[role="tab"]'));
tabButtons.forEach(btn => {
btn.addEventListener('keydown', e => {
const idx = tabButtons.indexOf(e.currentTarget);
if (e.key === 'ArrowRight') {
e.preventDefault();
const next = tabButtons[(idx+1)%tabButtons.length];
const ident = next.id.replace('tab-',''); showTab(ident);
} else if (e.key === 'ArrowLeft') {
e.preventDefault();
const prev = tabButtons[(idx-1+tabButtons.length)%tabButtons.length];
const ident = prev.id.replace('tab-',''); showTab(ident);
}
});
});
});
</script>

View File

@@ -1,5 +1,8 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- Preconnect to external image/avatar domains to reduce layout shift & latency -->
<link rel="preconnect" href="https://avatars.githubusercontent.com" crossorigin>
<link rel="dns-prefetch" href="//avatars.githubusercontent.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap"
rel="stylesheet">
<style>
@@ -19,6 +22,8 @@
right: 0;
top: 0;
width: auto;
appearance: none; /* standard */
-webkit-appearance: none; /* vendor */
}
input[type="date"]::-webkit-calendar-picker-indicator {
@@ -32,6 +37,8 @@
right: 0;
top: 0;
width: auto;
appearance: none;
-webkit-appearance: none;
}
/*Chrome*/
@@ -161,5 +168,5 @@
<link rel="image_src" type="image/png" href="/img/hou-wb.svg">
<!-- Canonical and Manifest -->
<link rel="canonical" href="/">
<% /* Expect a homeUrl variable like https://oneuptime.com passed from server; fallback to production domain */ %>
<link rel="manifest" href="/manifest.json">

View File

@@ -41,7 +41,7 @@
</div>
</a>
</div>
<h1 class="text-4xl font-bold tracking-tight text-gray-900 sm:text-6xl">Build reliable software.</h1>
<h1 class="text-6xl font-bold tracking-tight text-gray-900 sm:text-6xl mt-8 mb-8">Complete Monitoring <br/> &amp; Observability Platform</h1>
<p class="mt-6 text-xl sm:text-2xl leading-8 text-gray-600">Monitor, Observe, Debug, Resolve. Everything you
need to build reliable software in one open source platform.</p>
<div class="mt-10 flex items-center justify-center gap-x-6">
@@ -117,15 +117,6 @@
});
}
// Mobile redirect helper - if JS is available and we detect mobile, redirect to dashboard
if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|Windows Phone|Opera Mini|IEMobile|Mobile Safari|Mobile/i.test(navigator.userAgent)) {
// Small delay to avoid redirect loops and let server handle first
setTimeout(() => {
if (window.location.pathname === '/' && !window.location.search.includes('no-mobile-redirect')) {
window.location.href = '/dashboard';
}
}, 100);
}
</script>
</body>

View File

@@ -4,5 +4,7 @@
"ignore": [
"greenlock.d/*"
],
"exec": "node --inspect=0.0.0.0:9229 --require ts-node/register Index.ts"
"watchOptions": {"useFsEvents": false, "interval": 500},
"env": {"TS_NODE_TRANSPILE_ONLY": "1", "TS_NODE_FILES": "false"},
"exec": "node --inspect=0.0.0.0:9229 -r ts-node/register/transpile-only Index.ts"
}

View File

@@ -37,7 +37,9 @@ const init: PromiseVoidFunction = async (): Promise<void> => {
serviceName: APP_NAME,
});
logger.info(`IncomingRequestIngest Service - Queue concurrency: ${INCOMING_REQUEST_INGEST_CONCURRENCY}`);
logger.info(
`IncomingRequestIngest Service - Queue concurrency: ${INCOMING_REQUEST_INGEST_CONCURRENCY}`,
);
// init the app
await App.init({

View File

@@ -6,6 +6,7 @@ import HTTPMethod from "Common/Types/API/HTTPMethod";
import OneUptimeDate from "Common/Types/Date";
import Dictionary from "Common/Types/Dictionary";
import BadDataException from "Common/Types/Exception/BadDataException";
import ExceptionMessages from "Common/Types/Exception/ExceptionMessages";
import { JSONObject } from "Common/Types/JSON";
import IncomingMonitorRequest from "Common/Types/Monitor/IncomingMonitor/IncomingMonitorRequest";
import MonitorType from "Common/Types/Monitor/MonitorType";
@@ -31,9 +32,20 @@ QueueWorker.getWorker(
`Successfully processed incoming request ingestion job: ${job.name}`,
);
} catch (error) {
// Certain BadDataException cases are expected / non-actionable and should not fail the job.
// These include disabled monitors (manual, maintenance, explicitly disabled) and missing monitors
// (e.g. secret key referencing a deleted monitor). Retrying provides no value and only creates noise.
if (
error instanceof BadDataException &&
(error.message === ExceptionMessages.MonitorNotFound ||
error.message === ExceptionMessages.MonitorDisabled)
) {
return;
}
logger.error(`Error processing incoming request ingestion job:`);
logger.error(error);
throw error;
throw error; // rethrow other errors so they are visible and retried if needed.
}
},
{ concurrency: INCOMING_REQUEST_INGEST_CONCURRENCY }, // Configurable via env, defaults to 100
@@ -78,7 +90,7 @@ async function processIncomingRequestFromQueue(
});
if (!monitor || !monitor._id) {
throw new BadDataException("Monitor not found");
throw new BadDataException(ExceptionMessages.MonitorNotFound);
}
if (!monitor.projectId) {

View File

@@ -1,5 +1,7 @@
{
"watch": ["./","../Common/Server", "../Common/Types", "../Common/Utils", "../Common/Models"],
"ext": "ts,json,tsx,env,js,jsx,hbs",
"exec": "node --inspect=0.0.0.0:9229 --require ts-node/register Index.ts"
"watchOptions": {"useFsEvents": false, "interval": 500},
"env": {"TS_NODE_TRANSPILE_ONLY": "1", "TS_NODE_FILES": "false"},
"exec": "node --inspect=0.0.0.0:9229 -r ts-node/register/transpile-only Index.ts"
}

View File

@@ -1,5 +1,7 @@
{
"watch": ["./","../Common"],
"ext": "ts,json,tsx,env,js,jsx,hbs",
"exec": "node --inspect=0.0.0.0:9229 --require ts-node/register Index.ts"
"watchOptions": {"useFsEvents": false, "interval": 500},
"env": {"TS_NODE_TRANSPILE_ONLY": "1", "TS_NODE_FILES": "false"},
"exec": "node --inspect=0.0.0.0:9229 -r ts-node/register/transpile-only Index.ts"
}

View File

@@ -7,3 +7,9 @@ if (typeof concurrency === "string") {
}
export const OPEN_TELEMETRY_INGEST_CONCURRENCY: number = concurrency as number;
// Some telemetry batches can be large and take >30s (BullMQ default lock) to process.
// Allow configuring a longer lock duration (in ms) to avoid premature stall detection.
// 10 minutes.
export const OPEN_TELEMETRY_INGEST_LOCK_DURATION_MS: number = 10 * 60 * 1000;

View File

@@ -37,7 +37,9 @@ const init: PromiseVoidFunction = async (): Promise<void> => {
serviceName: APP_NAME,
});
logger.info(`OpenTelemetryIngest Service - Queue concurrency: ${OPEN_TELEMETRY_INGEST_CONCURRENCY}`);
logger.info(
`OpenTelemetryIngest Service - Queue concurrency: ${OPEN_TELEMETRY_INGEST_CONCURRENCY}`,
);
// init the app
await App.init({

View File

@@ -8,7 +8,10 @@ import logger from "Common/Server/Utils/Logger";
import { QueueJob, QueueName } from "Common/Server/Infrastructure/Queue";
import QueueWorker from "Common/Server/Infrastructure/QueueWorker";
import ObjectID from "Common/Types/ObjectID";
import { OPEN_TELEMETRY_INGEST_CONCURRENCY } from "../../Config";
import {
OPEN_TELEMETRY_INGEST_CONCURRENCY,
OPEN_TELEMETRY_INGEST_LOCK_DURATION_MS,
} from "../../Config";
// Set up the unified worker for processing telemetry queue
QueueWorker.getWorker(
@@ -59,7 +62,12 @@ QueueWorker.getWorker(
throw error;
}
},
{ concurrency: OPEN_TELEMETRY_INGEST_CONCURRENCY },
{
concurrency: OPEN_TELEMETRY_INGEST_CONCURRENCY,
lockDuration: OPEN_TELEMETRY_INGEST_LOCK_DURATION_MS,
// allow a couple of stall recoveries before marking failed if genuinely stuck
maxStalledCount: 2,
},
);
logger.debug("Unified telemetry worker initialized");

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,7 @@
{
"watch": ["./","../Common/Server", "../Common/Types", "../Common/Utils", "../Common/Models"],
"ext": "ts,json,tsx,env,js,jsx,hbs",
"exec": "node --inspect=0.0.0.0:9229 --require ts-node/register Index.ts"
"watchOptions": {"useFsEvents": false, "interval": 500},
"env": {"TS_NODE_TRANSPILE_ONLY": "1", "TS_NODE_FILES": "false"},
"exec": "node --inspect=0.0.0.0:9229 -r ts-node/register/transpile-only Index.ts"
}

View File

@@ -84,3 +84,16 @@ export const PROBE_MONITOR_RETRY_LIMIT: number = process.env[
export const PORT: Port = new Port(
process.env["PORT"] ? parseInt(process.env["PORT"]) : 3874,
);
// Proxy configuration for all HTTP/HTTPS requests made by the probe
// HTTP_PROXY_URL: Proxy for HTTP requests
// Format: http://[username:password@]proxy.example.com:port
// Example: http://proxy.example.com:8080
// Example with auth: http://user:pass@proxy.example.com:8080
export const HTTP_PROXY_URL: string | null = process.env["HTTP_PROXY_URL"] || process.env["http_proxy"] || null;
// HTTPS_PROXY_URL: Proxy for HTTPS requests
// Format: http://[username:password@]proxy.example.com:port
// Example: http://proxy.example.com:8080
// Example with auth: http://user:pass@proxy.example.com:8080
export const HTTPS_PROXY_URL: string | null = process.env["HTTPS_PROXY_URL"] || process.env["https_proxy"] || null;

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