Compare commits

...

134 Commits

Author SHA1 Message Date
github-actions[bot] 36f2ae1f69 chore(release): bump version to 4.0.18 2026-03-17 06:14:16 +00:00
Raj Nandan Sharma dcd830eb82 implement v4.0.18 changelog with new features, improvements, and bug fixes 2026-03-17 09:12:14 +05:30
Raj Nandan Sharma 80637fd4aa Merge pull request #663 from rajnandan1/implement/662
Implements #662
2026-03-17 07:35:13 +05:30
Raj Nandan Sharma a7f0072f32 implement monitoring data deletion with optional tag and date range 2026-03-16 23:05:46 +05:30
Raj Nandan Sharma a39e676a23 implement monitoring data deletion functionality with date range support 2026-03-16 22:50:40 +05:30
Raj Nandan Sharma fbc926036e Merge pull request #661 from rajnandan1/add-incident-delete
Add incident delete
2026-03-16 17:24:14 +05:30
Raj Nandan Sharma 5b508d19bc implement incident deletion functionality and associated database updates 2026-03-16 17:20:23 +05:30
Raj Nandan Sharma 589281ce13 implement incident deletion functionality and associated database updates 2026-03-16 17:19:59 +05:30
Raj Nandan Sharma 36aec8c519 Merge pull request #659 from rajnandan1/translations-status-add
add missing translations
2026-03-16 10:59:53 +05:30
Raj Nandan Sharma 978bfb05f2 Merge pull request #660 from rajnandan1/fix-timezone-sw
refactor: update timestamp handling to respect selected timezone
2026-03-16 10:59:32 +05:30
Raj Nandan Sharma 3ffd0538a1 refactor: improve timezone handling by including UTC in available timezones and adjusting timestamp conversion 2026-03-16 10:58:44 +05:30
Raj Nandan Sharma 932a05a9ee refactor: update timestamp handling to respect selected timezone 2026-03-16 10:55:24 +05:30
Raj Nandan Sharma 15c62fa40f add missing translations 2026-03-16 09:59:49 +05:30
github-actions[bot] a5afd38520 chore(release): bump version to 4.0.17 2026-03-15 13:27:35 +00:00
Raj Nandan Sharma 5138f7fb6e chore: update documentation for v4.0.17 release with new features and improvements 2026-03-15 18:32:01 +05:30
Raj Nandan Sharma 1bed0538db refactor: remove unused Breadcrumb import and enhance date/time format suggestions 2026-03-15 16:37:54 +05:30
Raj Nandan Sharma bd582eaad3 document: include "Deploy to Render" button in README and relevant documentation 2026-03-15 13:57:19 +05:30
Raj Nandan Sharma 5803ccadca refactor: remove dockerCommand and redefine ORIGIN env variable sourcing 2026-03-15 13:50:57 +05:30
Raj Nandan Sharma 4f16b06a0f chore: update dockerCommand in render.yaml and remove unused ORIGIN env variable 2026-03-15 13:44:09 +05:30
Raj Nandan Sharma fad23e2a01 Merge pull request #657 from rajnandan1/implement/issue-620
Implement/issue 620
2026-03-15 13:23:11 +05:30
Raj Nandan Sharma 4b84e47d89 create render.yaml for service and database configuration 2026-03-15 13:22:17 +05:30
Raj Nandan Sharma 9e020c10f4 document: include pre-built image instructions for deployment under /status base path 2026-03-15 12:56:50 +05:30
Raj Nandan Sharma 6fd47963ec refactor: improve formatting consistency in internationalization documentation 2026-03-15 12:39:16 +05:30
Raj Nandan Sharma df9b36b242 refactor: remove AllMaintenanceMonitorGrid component and update date formatting across various components to use dynamic date and time formats 2026-03-15 12:39:03 +05:30
Raj Nandan Sharma f9fe74ef94 refactor: streamline documentation layout and enhance styling 2026-03-14 10:08:14 +05:30
Raj Nandan Sharma 823ea6eeb7 Merge pull request #656 from rajnandan1/fix/group-timeout
Fix/group timeout
2026-03-13 19:21:00 +05:30
Raj Nandan Sharma 6c7606d3b0 fix group call through APIS 2026-03-13 17:24:26 +05:30
Raj Nandan Sharma d2e9437cfa fix group call through APIS 2026-03-13 17:23:59 +05:30
github-actions[bot] a549eb5d1b chore(release): bump version to 4.0.16 2026-03-13 07:59:48 +00:00
Raj Nandan Sharma 648f8180e7 feat: include changelog for version 4.0.16 with new features, improvements, and bug fixes 2026-03-13 11:29:18 +05:30
Raj Nandan Sharma 9afb8947ac feat: implement gRPC monitor functionality and update related documentation, implements #505 2026-03-13 10:29:05 +05:30
Raj Nandan Sharma 0c8338e2a5 Merge pull request #655 from rajnandan1/order-m-pages
feat: implement position management for page monitors and update rela…
2026-03-13 09:42:40 +05:30
Raj Nandan Sharma df94755c6b feat: implement position management for page monitors and update related functionality 2026-03-13 09:40:54 +05:30
Raj Nandan Sharma 204419fccb Merge pull request #654 from TobiX/fix-json-api
fix(api): Stringify existing objects before inserting into database
2026-03-13 09:02:08 +05:30
Tobias Gruetzmacher a973494bc8 fix(api): Stringify existing objects before inserting into database 2026-03-12 22:33:33 +01:00
Raj Nandan Sharma ba459e61ad Merge pull request #653 from rajnandan1/fix/csrf-origin
Fix/csrf origin
2026-03-12 23:06:22 +05:30
Raj Nandan Sharma f92fc8ff73 refactor: streamline CSRF handler method checks for request types 2026-03-12 23:03:14 +05:30
Raj Nandan Sharma 91cb4850ca validate request origin in CSRF handler to prevent null origins 2026-03-12 23:03:02 +05:30
Raj Nandan Sharma fb7939a4dc implement CSRF protection with origin validation in API routes, fixes #570 2026-03-12 22:54:39 +05:30
Raj Nandan Sharma 1f352591a4 chore: update README to include DeepWiki badge 2026-03-12 20:22:45 +05:30
Raj Nandan Sharma 0085621900 Merge pull request #648 from pan93412/locales/zh-tw
feat(locales): add zh-TW translation
2026-03-12 14:50:42 +05:30
Raj Nandan Sharma 0eb789d89a Merge pull request #652 from rajnandan1/zeabur
chore: integrate Zeabur deployment options in documentation and templ…
2026-03-12 14:49:35 +05:30
Raj Nandan Sharma 7f21b27bb8 chore: integrate Zeabur deployment options in documentation and templates 2026-03-12 14:39:45 +05:30
github-actions[bot] 7abd6788de chore(release): bump version to 4.0.15 2026-03-12 09:00:01 +00:00
Raj Nandan Sharma 681b116e09 bug fix 2026-03-12 14:03:19 +05:30
github-actions[bot] 04b1d0716b chore(release): bump version to 4.0.14 2026-03-12 05:28:52 +00:00
Raj Nandan Sharma 668f12a0eb Merge pull request #646 from rajnandan1/release/4.0.14.1
chore(locales): Update translations for various languages to include …
2026-03-12 09:49:18 +05:30
Raj Nandan Sharma 394149a3d6 chore: update changelog for v4.0.14 with scoring model changes, new features, bug fixes, and improvements 2026-03-12 09:47:45 +05:30
Raj Nandan Sharma 57c8ff28ab chore: update user-agent header to include dynamic version from version.js 2026-03-12 09:40:34 +05:30
Raj Nandan Sharma 5b29af9e74 chore: enhance webhook processing by including webhookURL in environment secrets and update alert incident URL formatting in template. fixes #647 2026-03-12 09:36:57 +05:30
Raj Nandan Sharma 9f5b90cb93 added svg logo 2026-03-12 09:06:10 +05:30
Yi-Jyun Pan e4f001acf7 feat(locales): add zh-TW translation 2026-03-12 11:18:53 +08:00
Raj Nandan Sharma 604dbccaea chore: refactor button and input components for better readability 2026-03-11 23:01:16 +05:30
Raj Nandan Sharma 6b7d0335b7 chore: implement search functionality and filter options for monitors 2026-03-11 23:00:04 +05:30
Raj Nandan Sharma b55dd29f02 chore: update notification duration display to include ongoing status 2026-03-11 22:32:00 +05:30
Raj Nandan Sharma dcd7918a08 chore(locales): Update translations for various languages to include new status terms
feat(dashboard): Refactor notification payload to include event start and end times

refactor(controllers): Modify group call scoring model to use normalized scores for status

docs(changelog): Document changes in group monitor scoring model in v4.0.14 release notes

feat(badges): Implement locale selection for status badges in management interface

refactor(i18n): Create server-side i18n helper for translating strings outside of Svelte components
2026-03-11 11:45:59 +05:30
Raj Nandan Sharma 38e3a538ef chore: enhance page navigation and update deployment documentation 2026-03-10 11:58:18 +05:30
Raj Nandan Sharma 19c2275a7b chore: remove unused dependencies and update documentation for showcase 2026-03-10 10:00:22 +05:30
github-actions[bot] a53251454e chore(release): bump version to 4.0.13 2026-03-09 17:32:07 +00:00
Raj Nandan Sharma 8f39a0f649 Merge pull request #639 from otherwiseGG/main
Fix German Translation in de.json
2026-03-09 22:23:37 +05:30
Benjamin Thiele 674c310b67 Merge branch 'main' into main 2026-03-09 07:04:59 +01:00
Raj Nandan Sharma a550afca60 Merge pull request #644 from rajnandan1/fixes/4.0.13
Fixes/4.0.13
2026-03-09 11:22:22 +05:30
Raj Nandan Sharma e64d955ecb Translate status and incident labels to Hindi in locale files and update MaintenanceItem component for proper translation usage 2026-03-09 11:21:18 +05:30
Raj Nandan Sharma 57fda67ed7 Updated Translations 2026-03-09 11:16:59 +05:30
Raj Nandan Sharma 6d5a42aeb2 Implement cache deletion functionality and update documentation 2026-03-09 10:40:27 +05:30
Benjamin Thiele 5d9160c36d Fix Misspelled or incorrect German in de.json 2026-03-05 15:36:17 +01:00
github-actions[bot] b465f48c6e chore(release): bump version to 4.0.12 2026-03-05 11:20:13 +00:00
Raj Nandan Sharma 8c16d99f55 Merge pull request #632 from danynocz/czech-slovakia-update
Add missing Czech and Slovak translations and fix minor typos in cs.json and sk.json
2026-03-05 16:48:37 +05:30
Raj Nandan Sharma fdedbb78cf Merge pull request #638 from rajnandan1/fix/issue-636
Update default value initialization and button label for DNS monitor
2026-03-05 16:47:51 +05:30
Raj Nandan Sharma 416beca1a6 Update default value initialization and button label for DNS monitor 2026-03-05 16:45:03 +05:30
_DANYNO_ e261d3620e Update Slovak translations in sk.json 2026-03-03 11:52:28 +01:00
_DANYNO_ 1089133b97 Update Czech translations for COMPLETED and SCHEDULED 2026-03-03 11:44:47 +01:00
_DANYNO_ 90de4ae4cb Update Czech translations in cs.json 2026-03-03 11:42:03 +01:00
_DANYNO_ 4e263284c8 Add missing Slovak translations and fix minor typos in sk.json 2026-02-28 14:59:02 +01:00
_DANYNO_ 343368631f Add missing Czech translations and fix minor typos in cs.json 2026-02-28 14:56:11 +01:00
Raj Nandan Sharma f5f40e1438 Merge pull request #631 from danynocz/czech-slovakia-update
Update Czech and Slovak Language
2026-02-28 10:13:51 +05:30
_DANYNO_ 74f6601f5d Update Slovak Language 2026-02-27 22:02:21 +01:00
_DANYNO_ c4e03c1a83 Update Czech Language 2026-02-27 21:59:54 +01:00
Raj Nandan Sharma bb9124db0d Merge pull request #630 from danynocz/add-translation-labels 2026-02-28 00:35:10 +05:30
_DANYNO_ ce52506c95 Translate labels to use $t function 2026-02-27 18:47:54 +01:00
github-actions[bot] f20b74cb36 chore(release): bump version to 4.0.11 2026-02-26 18:43:37 +00:00
Raj Nandan Sharma f3a5839aae Merge pull request #627 from rajnandan1/features-n-fixes/4.0.11
Features n fixes/4.0.11
2026-02-26 23:08:52 +05:30
Raj Nandan Sharma 0896aed91e refactor: Update ManualUpdateUserData to include user ID and validate roles 2026-02-26 22:14:58 +05:30
Raj Nandan Sharma d8ae54da53 refactor: Implement email and name normalization and validation in user creation process 2026-02-26 21:57:22 +05:30
Raj Nandan Sharma 66bf012c87 refactor: Enhance user management with is_owner attribute and update permissions logic, implements #624 2026-02-26 21:35:56 +05:30
Raj Nandan Sharma 08fc54d2ea refactor: Sort page monitors by creation date in getPageMonitors methods. implements #625 2026-02-26 20:41:55 +05:30
Raj Nandan Sharma b36e68f7c8 refactor: Update redirect logic in signin load function and change sidebar link to site configurations, fixes #623 2026-02-26 20:16:54 +05:30
github-actions[bot] 045f32b7ce chore(release): bump version to 4.0.10 2026-02-26 11:25:52 +00:00
Raj Nandan Sharma 7ae869d98a Merge pull request #619 from rajnandan1/fix/v-4010
refactor: Improve environment variable handling and documentation for…
2026-02-26 16:55:12 +05:30
Raj Nandan Sharma efb04a4238 refactor: Improve environment variable handling and documentation for SMTP and Redis configurations. Fixes: #616, #617, #615, #614, #612 2026-02-26 16:40:50 +05:30
github-actions[bot] e6e586f6de chore(release): bump version to 4.0.9 2026-02-26 06:36:24 +00:00
Raj Nandan Sharma e13d7fb1f4 refactor: Implement monitor tag validation for URL-friendliness in monitor creation and update 2026-02-26 12:02:39 +05:30
Raj Nandan Sharma 23b0bae018 Merge pull request #611 from rajnandan1/feature/pages-ordering
Feature/pages ordering
2026-02-26 10:49:02 +05:30
Raj Nandan Sharma e581346f84 refactor: Enhance documentation for page ordering functionality in customizations 2026-02-26 10:45:44 +05:30
Raj Nandan Sharma 555fd3a8a2 Refactor: Implement monitor reordering functionality with up and down buttons 2026-02-26 10:42:59 +05:30
Raj Nandan Sharma 7a29d2f2ca refactor: Implement page ordering functionality and enhance page fetching logic 2026-02-26 09:45:34 +05:30
Raj Nandan Sharma 7a3fe20083 refactor: Simplify specification path determination in GET handler 2026-02-25 23:24:13 +05:30
Raj Nandan Sharma c16c119d65 Merge pull request #608 from rajnandan1/fix/mysql-indexname-long
refactor: Update site configuration documentation and enhance global …
2026-02-25 22:54:37 +05:30
Raj Nandan Sharma 048a12899d refactor: Enhance documentation guidelines for architecture knowledge preservation 2026-02-25 22:41:06 +05:30
Raj Nandan Sharma 5fd0e691b3 refactor: Improve down migration logic for global column removal 2026-02-25 22:37:37 +05:30
Raj Nandan Sharma 28932f8df5 refactor: Update event page navigation to use dynamic page path 2026-02-25 22:33:53 +05:30
Raj Nandan Sharma 4da29ac4e1 Merge branch 'main' into fix/mysql-indexname-long 2026-02-25 19:36:31 +05:30
Raj Nandan Sharma 396fc5e3c3 refactor: Update site configuration documentation and enhance global page visibility settings
- Revise site configuration documentation to clarify page visibility behavior.
- Introduce global page visibility settings with detailed descriptions and functionality.
- Modify API server to dynamically select the correct specification path based on environment.
- Streamline event fetching logic in event pages to improve performance and maintainability.
- Remove unused vault secret management code from the manage API.
- Enhance customizations page to support global page visibility settings.
- Create new guides for adding custom fonts and custom JavaScript/CSS.
- Implement server-side logic for handling events by month with improved date validation.
2026-02-25 19:33:34 +05:30
Raj Nandan Sharma af8daf98f6 Merge pull request #602 from theoludwig/docs/typo-github
docs: typo Github => GitHub
2026-02-25 15:47:56 +05:30
Raj Nandan Sharma 2e91f90057 Merge pull request #603 from theoludwig/docs/link-404-migration-to-v4
docs: fix link to migration guide v3 => v4
2026-02-25 15:47:17 +05:30
Théo LUDWIG 3213ab8efa docs: fix link to migration guide v3 => v4 2026-02-25 10:22:36 +01:00
Théo LUDWIG f2308d9bd1 docs: typo Github => GitHub 2026-02-25 10:21:01 +01:00
github-actions[bot] 623465ff50 chore(release): bump version to 4.0.8 2026-02-24 19:17:23 +00:00
Raj Nandan Sharma 7c224b3886 refactor: restructure monitor_alerts_v2 table creation and enforce foreign key constraints 2026-02-25 00:46:30 +05:30
github-actions[bot] 166073fbd4 chore(release): bump version to 4.0.7 2026-02-24 18:47:54 +00:00
Raj Nandan Sharma 373cbd5917 refactor: update notification and incident handling to include site URL in context 2026-02-25 00:17:12 +05:30
github-actions[bot] 2c27859197 chore(release): bump version to 4.0.6 2026-02-24 18:35:29 +00:00
Raj Nandan Sharma bb5d58b675 Merge pull request #601 from rajnandan1/fix/privilage-1
ensure inactive users cannot log in and restrict actions for non-admi…
2026-02-25 00:03:08 +05:30
Raj Nandan Sharma caca29d354 ensure inactive users cannot log in and restrict actions for non-admin roles. Fixes #600 2026-02-25 00:01:49 +05:30
github-actions[bot] 35f0adb235 chore(release): bump version to 4.0.5 2026-02-24 18:18:40 +00:00
Raj Nandan Sharma 8bcff45d87 fix: #599 migration db 2026-02-24 23:46:15 +05:30
github-actions[bot] 3ad256a626 chore(release): bump version to 4.0.4 2026-02-24 17:27:46 +00:00
Raj Nandan Sharma c4b9181a0a refactor: update migration scripts to rename .js entries to .ts in knex_migrations table 2026-02-24 22:55:19 +05:30
Raj Nandan Sharma 1ba40fadf6 refactor: remove upcoming version details and clean up README content 2026-02-24 21:23:15 +05:30
Raj Nandan Sharma 20f1481a01 refactor: include Mattermost Webhook Trigger guide in documentation 2026-02-24 20:27:13 +05:30
Raj Nandan Sharma 828747876b refactor: update v4.0.0 changelog to include categories and monitor ordering management through pages 2026-02-24 20:22:19 +05:30
Raj Nandan Sharma cbe0ea683f refactor: update SKILL.md for code-architecture and tailwindcss; enhance button component in docs page; implement API spec retrieval in v4 server 2026-02-24 19:54:03 +05:30
Raj Nandan Sharma 561864c625 Fix Railway deployment link in README
Updated the Railway deployment link in the README.
2026-02-24 18:51:35 +05:30
github-actions[bot] 3abdb5c17a chore(release): bump version to 4.0.3 2026-02-24 12:54:46 +00:00
Raj Nandan Sharma f2ef19e3d8 fix release workflow 2026-02-24 18:24:06 +05:30
github-actions[bot] 78b18a59ec chore(release): bump version to 4.0.2 2026-02-24 12:27:09 +00:00
Raj Nandan Sharma c28581b51d removed docs from main build 2026-02-24 17:56:10 +05:30
github-actions[bot] f9486927de chore(release): bump version to 4.0.1 2026-02-24 12:09:00 +00:00
Raj Nandan Sharma 86645d9ea3 Merge pull request #597 from rajnandan1/chore/docs-and-locales
added llms.txt and fixed locales
2026-02-24 17:38:19 +05:30
Raj Nandan Sharma feea1d76cd added llms.txt and fixed locales 2026-02-24 17:37:55 +05:30
Raj Nandan Sharma 2e95f31d93 Merge pull request #596 from rajnandan1/chore/docs-and-locales
added llms.txt and fixed locales
2026-02-24 17:35:49 +05:30
Raj Nandan Sharma 18cf8f51b7 added llms.txt and fixed locales 2026-02-24 17:33:22 +05:30
Raj Nandan Sharma 9af842ebc0 fix docker file 2026-02-24 13:30:30 +05:30
Raj Nandan Sharma 3d157f1c20 Merge pull request #595 from rajnandan1/chore/docs-4
docs: Include changelog for v4.0.0 release with breaking changes and …
2026-02-24 11:55:47 +05:30
204 changed files with 7115 additions and 2438 deletions
+5
View File
@@ -0,0 +1,5 @@
{
"enabledPlugins": {
"frontend-design@claude-plugins-official": true
}
}
+250
View File
@@ -0,0 +1,250 @@
---
name: code-context
description: Persistent code architecture documentation via a `.codecontext/` folder.
user-invokable: false
metadata:
category: architecture
---
# Code Architecture Documentation Skill
Use this skill to **read architecture docs before work** and **document architecture after work** using the `.codecontext/` folder.
`.codecontext/` is a living architecture reference — it helps new developers onboard and coding agents pick up where previous sessions left off. It is **NOT** a session log, changelog, or task diary.
---
## What Belongs in `.codecontext/`
Only document **architecture-level knowledge** that would take significant effort to rediscover by reading code alone.
### Include
- **Code architecture** — how modules/components are structured, layered, and why
- **Code flow** — request lifecycle, data flow between layers, event/cron pipelines
- **Component relationships** — which modules depend on each other, call chains, shared state
- **Edge cases and gotchas** — non-obvious behaviors, race conditions, ordering constraints
- **Design decisions and rationale** — why a pattern was chosen over alternatives
- **Integration points** — how external services, databases, queues connect
- **Invariants and constraints** — rules that must hold (e.g., "timestamps are always UTC seconds", "all DB access goes through db singleton")
- **Error handling patterns** — how errors propagate, retry logic, fallback behavior
- **Key file map** — which files own which responsibilities (only when non-obvious)
### Exclude
- Session logs, changelogs, or diary-style entries
- What files were changed in a specific task
- Raw terminal output or build logs
- Code snippets (reference file paths + line ranges instead)
- Obvious facts that can be inferred from reading one file
- Task status, TODO lists, or progress tracking
- Anything already covered in README, AGENTS.md, or inline comments
---
## Trigger Conditions
Run this skill at the **start and end** of any coding task that touches architecture:
- Feature implementations spanning multiple files/modules
- Refactors that change module boundaries or data flow
- Bug fixes that reveal non-obvious system behavior
- New integrations or service connections
- Discovery of undocumented edge cases or invariants
- **After any agent run** — if the agent explored, read, or traced code to understand how part of the codebase works, that understanding must be captured (see Phase C)
**Skip** for trivial changes (typo fixes, single-line edits, style-only changes).
---
## Phase A — Read Architecture Docs (Before Acting)
### A1) Discover docs
```bash
ls .codecontext/
```
If `.codecontext/` does not exist, continue the task and create it in Phase B.
### A2) Find relevant docs
```bash
grep -ril "<domain keyword>" .codecontext/
```
Use keywords from the feature area you are working on (e.g., "alerting", "auth", "monitors", "cron").
### A3) Read and apply
Read only relevant files. Extract:
- Architecture constraints that affect your implementation
- Code flow you need to hook into or extend
- Edge cases to preserve or handle
- Integration points to respect
If existing docs conflict with current code, trust the code — update docs in Phase B.
---
## Phase B — Document Architecture (Before Ending)
Only write/update docs if the task revealed architecture knowledge worth preserving.
### B1) Decide what to document
Ask: _"Would a new developer or future agent need to re-discover this to work in this area?"_
If yes, proceed. If no, skip Phase B entirely.
Then apply this filter to **every sentence** before writing:
> "Does this sentence describe how the code is structured, a design decision, or a constraint that would change how someone writes future code in this area?"
If no → cut it. This is the line between architecture documentation and a session diary.
### B2) Write architecture documentation
Structure each doc as a **reference document**, not a session diary.
Template (use only the sections that apply):
```markdown
# <Domain/Feature Area>
## Overview
Brief description of what this area does and its role in the system.
## Architecture
How the components are structured, key abstractions, layers.
## Code Flow
Step-by-step flow for the primary operations (e.g., "How a monitor check executes").
## Key Files
| File | Responsibility |
| -------------------- | -------------- |
| `src/lib/server/...` | Does X |
## Edge Cases and Gotchas
- Non-obvious behavior 1
- Constraint that must be preserved
## Design Decisions
- Why X was chosen over Y (if non-obvious)
```
Not all sections are required — include only what is relevant. Keep each doc under **300 lines**.
### B3) Pick target file
```bash
ls .codecontext/
grep -ril "<topic keyword>" .codecontext/
```
| Condition | Action |
| ------------------------------- | -------------------------------- |
| Existing doc covers this domain | Update/rewrite relevant sections |
| Different domain | Create new file |
| No match | Create new file |
When updating, **replace outdated sections** rather than appending session entries. The doc should always read as a clean, current architecture reference.
### B4) Persist
```bash
mkdir -p .codecontext
```
Create or overwrite the file so it reads as a standalone reference:
```bash
cat > .codecontext/<domain>.md
```
---
## Phase C — Capture Agent Understanding (After Any Agent Run)
After completing any task (coding, debugging, research, exploration), review what you learned about the codebase during the session and persist anything not already documented.
### C1) Identify new understanding
Reflect on what you discovered during this session:
- How does a feature/module actually work? (code flow, data transformations, call chains)
- What patterns or conventions did you observe across multiple files?
- What dependencies or relationships between modules did you trace?
- What surprised you or was non-obvious? (hidden side effects, implicit ordering, shared state)
- What constraints or invariants did you discover that aren't documented anywhere?
### C2) Check if already documented
```bash
ls .codecontext/
grep -ril "<keyword>" .codecontext/
```
Read matching files. If the understanding is already captured accurately, skip. If partially captured, update the relevant sections.
### C3) Write or update docs
Apply the same quality filters from Phase B (B1 architecture filter). Then:
- If the understanding maps to an existing `.codecontext/` file, update the relevant sections
- If it covers a new domain area, create a new file following the B2 template and naming rules
- Merge your new understanding with existing content — do not duplicate or contradict
### C4) Scope
This phase applies even when:
- The task was **read-only** (research, exploration, answering questions about code)
- The task was a **bug investigation** that didn't result in a fix
- The agent **traced code flow** to understand behavior before making changes
- The agent **read multiple files** to understand how a feature works
This phase does **NOT** apply when:
- The agent only touched a single file and learned nothing non-obvious
- The understanding is already fully captured in existing `.codecontext/` docs
- The session was trivial (formatting, typo fix, config change)
---
## Naming Rules
- Name by domain/feature area: `alerting.md`, `auth.md`, `monitor-execution.md`, `incident-lifecycle.md`
- Use kebab-case for multi-word topics
- Never use generic names: `notes.md`, `misc.md`, `context.md`, `session-1.md`
- One file per bounded domain — split if a file exceeds ~300 lines
---
## Fast Checklist
Before coding:
- [ ] Checked `.codecontext/` for relevant architecture docs
- [ ] Applied constraints and patterns from existing docs
Before finishing:
- [ ] Every sentence passed the B1 architecture filter
- [ ] Documented any new architecture knowledge discovered
- [ ] Updated outdated docs if current code contradicts them
- [ ] Doc reads as a clean architecture reference, not a session log
After any agent run:
- [ ] Reviewed what was learned about the codebase during this session
- [ ] Checked if that understanding is already in `.codecontext/`
- [ ] Persisted any new architectural knowledge (even from read-only/research sessions)
+3 -10
View File
@@ -1,8 +1,9 @@
---
name: tailwindcss
displayName: Tailwind CSS
description: Tailwind CSS v4 utility-first styling patterns including responsive design, dark mode, and custom configuration. Use when styling with Tailwind, adding utility classes, configuring Tailwind, setting up dark mode, or customizing the theme.
version: 1.0.0
user-invokable: false
metadata:
category: styling
---
# Tailwind CSS v4 Development Guidelines
@@ -358,11 +359,3 @@ Tailwind v4 delivers 3.5x faster full builds (~100ms) compared to v3 using moder
6. **Enable Dark Mode**: Plan for dark mode from the start
7. **Use Plugins**: Leverage official plugins for common needs
8. **Optimize Production**: Ensure purge is configured correctly
## Additional Resources
For detailed information, see:
- [Utility Patterns](resources/utility-patterns.md)
- [Component Library](resources/component-library.md)
- [Configuration Guide](resources/configuration.md)
+46
View File
@@ -0,0 +1,46 @@
# Database & Migrations
## Overview
Kener uses **Knex.js** as a database abstraction layer supporting three engines: **SQLite** (default, via `better-sqlite3`), **PostgreSQL** (`pg`), and **MySQL** (`mysql2`). The engine is selected at runtime from the `DATABASE_URL` environment variable prefix (`sqlite://`, `postgresql://`, `mysql://`).
## Architecture
### Connection & Config
| File | Responsibility |
| --------------------------------- | ---------------------------------------------------------- |
| `knexfile.ts` | Parses `DATABASE_URL`, selects client, exports Knex config |
| `src/lib/server/db/db.ts` | Singleton Knex instance — all app code imports from here |
| `src/lib/server/db/dbimpl.js` | High-level DB methods (wraps repositories) |
| `src/lib/server/db/repositories/` | Per-domain query classes extending `BaseRepository` |
### Repository Pattern
Each repository lives in `src/lib/server/db/repositories/<domain>.ts`, extends `BaseRepository` (which receives the Knex instance), and exposes typed async methods. Queries use Knex query builder — never raw SQL (except index creation wrapped in try/catch).
### Migrations
Migrations live in `migrations/` as TypeScript files with naming convention `YYYYMMDDHHMMSS_<description>.ts`.
Key patterns observed across all existing migrations:
- **Idempotency guards**: `knex.schema.hasTable` / `knex.schema.hasColumn` before `createTable` / `alterTable`.
- **Column types**: Knex abstractions only (`.string()`, `.integer()`, `.text()`, `.float()`). No raw DDL.
- **Defaults**: `.defaultTo()` + `.notNullable()` for YES/NO string flags (e.g., `is_hidden`, `is_owner`).
- **Data seeding in migrations**: Standard Knex query builder (`.orderBy().first()`, `.update()`) — works on all three engines.
- **Index creation**: Wrapped in `try/catch` because `CREATE INDEX IF NOT EXISTS` isn't portable.
- **PostgreSQL insert returning**: Some repos branch on `GetDbType() === "postgresql"` to use `.returning("*")`, with fallback to re-read by inserted ID for SQLite/MySQL.
## Edge Cases and Gotchas
- SQLite requires `useNullAsDefault: true` in Knex config.
- PostgreSQL `INSERT ... RETURNING *` is not supported by SQLite/MySQL — branch on `GetDbType()`.
- `knex.fn.now()` is the portable way to set timestamps; never use `NOW()` or `datetime('now')`.
- String-based YES/NO flags (not booleans) are the project convention for flag columns.
## Design Decisions
- **YES/NO strings over booleans**: Consistent with existing `is_hidden`, `include_degraded_in_downtime`, etc. Avoids SQLite boolean quirks.
- **hasColumn guard in migrations**: Allows re-running migrations safely without failure on already-applied columns.
- **Owner flag (`is_owner`)**: Set during migration on the first user by `id ASC`. Only one user should be owner; enforced at application level, not DB constraint.
+39
View File
@@ -0,0 +1,39 @@
# Group Monitor Cache Flow
## Overview
Group monitors aggregate the status of child monitors using weighted scores. The execution involves a cache layer (Redis) that must stay in sync with the database for correct status propagation.
## Execution Flow
1. **Cron fires**`appScheduler` schedules each monitor via BullMQ job scheduler (`monitorSchedulers.ts`)
2. **Worker calls** `Minuter(monitor)` in `cron-minute.ts`
3. For GROUP monitors, `Minuter` adds a BullMQ **delay** of `executionDelay` ms so child monitors finish first
4. **`monitorExecuteQueue`** worker calls `Service.execute()``GroupCall.execute()`
5. `GroupCall.execute()` reads each child's latest status from **Redis cache** via `GetLastMonitoringValue(tag, fetcher)`
## Cache Behaviour
- **Key format:** `kener:cache:{tag}:last_status`
- **TTL:** 86400 seconds (1 day)
- `getCache()` returns cached value if present — **fetcher is only called on cache miss**
- Cache is updated in two places:
- `monitorResponseQueue.ts` — after cron-driven monitor execution completes
- API PATCH endpoints (`api/v4/monitors/[tag]/data/`) — after external status updates
## Key Invariant
Any code path that writes monitoring data to the DB **must** also update the `last_status` cache entry. Otherwise `GroupCall` (and other cache consumers like badge endpoints) will serve stale data.
## `executionDelay` Purpose
`executionDelay` is NOT used inside `groupCall.ts`. It is applied in `cron-minute.ts` as BullMQ's `delay` option, deferring the group job so child monitors have time to complete and update the cache before the group aggregates.
## Key Files
- `src/lib/server/services/groupCall.ts` — weighted status aggregation
- `src/lib/server/cron-minute.ts` — applies `executionDelay` as BullMQ delay
- `src/lib/server/cache/setGet.ts``SetLastMonitoringValue` / `GetLastMonitoringValue`
- `src/lib/server/queues/monitorResponseQueue.ts` — cache update after cron execution
- `src/routes/(api)/api/v4/monitors/[monitor_tag]/data/+server.ts` — range update API
- `src/routes/(api)/api/v4/monitors/[monitor_tag]/data/[timestamp]/+server.ts` — single-point update API
+19 -1
View File
@@ -119,4 +119,22 @@ Always use `import type { ... }` when importing types to avoid accidental runtim
# Other skills
Read files in .claude/skills for more instructions on specific tasks or file types.
Read files in .claude/skills for more instructions on specific tasks or file types.
## Code Architecture Documentation (MUST)
For every coding task that touches architecture (multi-file features, refactors, new integrations):
1. **Before edits**
- Read and apply `.claude/skills/code-context/SKILL.md`.
- Load relevant architecture docs from `.codecontext/` when present.
2. **Before finishing the response**
- If the task revealed new architecture knowledge (code flow, edge cases, component relationships, design decisions), write/update a `.codecontext/*.md` entry as a clean reference doc.
- Skip if the task was trivial (typo fixes, single-line edits).
3. **Final response contract**
- Include a short line: `Context loaded: ...`
- Include a short line: `Context updated: ...`
`.codecontext/` documents **code architecture only** — not session logs, changelogs, or task summaries.
+3 -2
View File
@@ -1,4 +1,4 @@
name: Create Release (Deterministic)
name: Create Release
on:
workflow_dispatch:
@@ -53,7 +53,7 @@ jobs:
- name: Bump package version
run: |
VERSION="${{ inputs.version }}"
CURRENT_VERSION="$(node -p \"require('./package.json').version\")"
CURRENT_VERSION=$(node -p 'require("./package.json").version')
if [ "$CURRENT_VERSION" != "$VERSION" ]; then
npm version "$VERSION" --no-git-tag-version --allow-same-version
@@ -95,3 +95,4 @@ jobs:
generate_release_notes: true
make_latest: ${{ inputs.make_latest && 'true' || 'false' }}
prerelease: ${{ inputs.prerelease }}
token: ${{ secrets.RELEASE_TOKEN }}
+5 -2
View File
@@ -40,7 +40,7 @@ jobs:
run: |
TAG="${{ github.event.release.tag_name || github.ref_name }}"
EXPECTED_VERSION="${TAG#v}"
PACKAGE_VERSION="$(node -p \"require('./package.json').version\")"
PACKAGE_VERSION=$(node -p 'require("./package.json").version')
if [ "$PACKAGE_VERSION" != "$EXPECTED_VERSION" ]; then
echo "package.json version mismatch"
@@ -83,6 +83,8 @@ jobs:
BASE_SUFFIX=""
fi
WITH_DOCS="false"
if [ "${{ matrix.variant }}" = "alpine" ]; then
VARIANT_SUFFIX="-alpine"
else
@@ -94,6 +96,7 @@ jobs:
echo "release_tag=${TAG}${FULL_SUFFIX}" >> "$GITHUB_OUTPUT"
echo "release_norm_tag=${NORM_TAG}${FULL_SUFFIX}" >> "$GITHUB_OUTPUT"
echo "latest_tag=latest${FULL_SUFFIX}" >> "$GITHUB_OUTPUT"
echo "with_docs=${WITH_DOCS}" >> "$GITHUB_OUTPUT"
if [ "${{ matrix.variant }}" = "debian" ]; then
echo "release_tag_debian_alias=${TAG}${BASE_SUFFIX}-debian" >> "$GITHUB_OUTPUT"
@@ -129,7 +132,7 @@ jobs:
labels: ${{ steps.meta.outputs.labels }}
build-args: |
VARIANT=${{ matrix.variant }}
WITH_DOCS=true
WITH_DOCS=${{ steps.vars.outputs.with_docs }}
KENER_BASE_PATH=${{ matrix.base_path }}
platforms: linux/amd64,linux/arm64
cache-from: type=gha
+29
View File
@@ -22,6 +22,15 @@ You MUST use this tool whenever writing Svelte code before sending it to the use
Generates a Svelte Playground link with the provided code.
After completing the code, ask the user if they want a playground link. Only call this tool after user confirmation and NEVER if code was written to files in their project.
## Database compatibility rule
All database operations — migrations, queries, and repository functions — **MUST** work across all three supported databases: **SQLite**, **PostgreSQL**, and **MySQL**. Use Knex.js schema builder and query builder abstractions; avoid raw SQL unless wrapped in dialect-safe helpers or guarded with `try/catch`. When writing migrations:
- Use `knex.schema.hasColumn` / `knex.schema.hasTable` guards for idempotency.
- Use Knex column types (`.string()`, `.integer()`, `.text()`, etc.) — never raw `ALTER TABLE` unless necessary.
- For data-seeding inside migrations, use standard Knex query builder (`.insert()`, `.update()`, `.orderBy()`, `.first()`).
- Test that `defaultTo()` values and `notNullable()` constraints work on all three engines.
## Documentation writing skill
When the user asks to write or edit documentation, follow the skill file:
@@ -29,3 +38,23 @@ When the user asks to write or edit documentation, follow the skill file:
- `.claude/skills/documentation-writer/SKILL.md`
This is mandatory for docs-related tasks. Prioritize short, clear, action-oriented docs and avoid bloat.
## Code architecture docs skill - Important for all tasks
Always try to use the code-context skill at the start and end of coding sessions:
- `.claude/skills/code-context/SKILL.md`
## Code architecture enforcement (mandatory)
The code-context skill is not optional. Agents MUST do both:
1. **Before coding**: load relevant architecture docs from `.codecontext/`.
2. **Before final response**: if the task revealed new architecture knowledge (code flow, edge cases, component relationships), update or create a `.codecontext/*.md` entry. Skip if the task was trivial.
Required output evidence in the final response:
- `Context loaded:` list of `.codecontext` files read (or `none found`).
- `Context updated:` exact `.codecontext` file path written (or `skipped — no architecture changes`).
The `.codecontext/` folder documents **code architecture only** — not session logs, changelogs, or task summaries.
+124
View File
@@ -0,0 +1,124 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## What is Kener?
Kener is an open-source status page application built with **SvelteKit 2.x (Svelte 5)** and **Node.js/Express**. It provides real-time monitoring, uptime tracking, incident management, and customizable dashboards. The codebase is migrating to **TypeScript-first**.
## Development Commands
```bash
npm run dev # Start dev server (SvelteKit + cron scheduler in parallel)
npm run build # Production build (SvelteKit then esbuild server bundle)
npm run start # Run production build (node build/main.js)
npm run check # Svelte + TypeScript type checking
npm run prettify # Format all files with Prettier
npm run migrate # Run database migrations via Knex
npm run seed # Run database seeds (migrations run automatically first)
```
## Architecture
### Dual Process Model
In development, `npm run dev` runs two parallel processes:
1. **SvelteKit dev server** (`vite dev`) - serves the frontend
2. **Cron scheduler** (`vite-node src/lib/server/startup.ts`) - runs monitor checks, maintenance scheduler, daily cleanup
In production, `scripts/main.ts` is the single entry point: Express server + SvelteKit handler + migrations + seeds + scheduler startup.
### SvelteKit Route Groups
- **`(kener)/`** - Public status page
- **`(manage)/`** - Admin dashboard (authenticated)
- **`(embed)/`** - Embeddable widgets
- **`(docs)/`** - Documentation pages
- **`(api)/`** - SvelteKit API routes; also `src/lib/server/api-server/` for Express-side API handlers (file-based routing: `./action/method.ts`)
- **`(account)/`** - Account/auth pages
- **`(ext)/`** - External integrations
- **`(assets)/`** - Asset serving
### Database
- **Knex.js** for query building and migrations. Supports SQLite (default), PostgreSQL, MySQL
- Connection configured via `DATABASE_URL` env var: `sqlite://./path`, `postgresql://...`, `mysql://...`
- Migrations in `/migrations/`, seeds in `/seeds/`
- Always use the db singleton: `import db from "$lib/server/db/db"`
### Monitor Services
Each monitor type has a dedicated implementation in `src/lib/server/services/`:
- Types: API, Ping, TCP, DNS, SSL, SQL, Heartbeat, GameDig, Group
- Scheduled via `src/lib/server/schedulers/` using `croner`
- Job queues managed with **BullMQ** + **Redis** (`src/lib/server/queues/`)
### Build System
`npm run build` is a two-step process:
1. `scripts/build-sveltekit.js` - Vite build of SvelteKit app (optionally with `--with-docs`)
2. `scripts/build-server.js` - esbuild bundles `scripts/main.ts` into `build/main.js`
## Key Conventions
### Svelte 5 + TypeScript
- Use **TypeScript** for new/modified code
- Use **Svelte 5 runes** (`$state`, `$derived`, `$effect`, `$props()`) in new components
- Use generated `$types` for SvelteKit route typing (`import type { PageServerLoad } from './$types'`)
- Use `import type { ... }` for type imports
### UI Components
- **shadcn-svelte** components in `src/lib/components/ui/`
- Import: `import { Button } from "$lib/components/ui/button"`
- Styling: **Tailwind CSS v4** with HSL CSS variables for theming
### Timestamps
All timestamps are **UTC seconds** (not milliseconds). Use helpers from `src/lib/server/tool.ts`.
### Status Constants
#### When svelte code
```typescript
import { UP, DOWN, DEGRADED, MAINTENANCE, NO_DATA } from "$lib/server/global-constants.ts"
```
#### When server code use directory traversal
```typescript
import { UP, DOWN, DEGRADED, MAINTENANCE, NO_DATA } from "./global-constants.ts"
```
### API Authentication
APIs use Bearer token auth: `import { VerifyAPIKey } from "$lib/server/controllers/controller.js"`
### Types Location
- `src/lib/types/` - Shared types (client + server)
- `src/lib/server/types/` - Server-only types
- `src/lib/client/types/` - Client-only types
### i18n
Locale files in `src/lib/locales/`. Add translations by creating `{code}.json` and updating `locales.json`.
## Environment Variables
Required: `KENER_SECRET_KEY`, `ORIGIN`, `DATABASE_URL`
Optional: `KENER_BASE_PATH`, `PORT` (default 3000), `RESEND_API_KEY`, `RESEND_SENDER_EMAIL`
## Skills
Read `.claude/skills/` for specialized instructions on:
- **svelte-code-writer** - Svelte component creation/editing
- **documentation-writer** - Editing docs in `src/routes/(docs)/docs/content/`
- **tailwindcss** - Tailwind CSS v4 patterns
- **code-context** - Architecture documentation in `.codecontext/`
+3
View File
@@ -164,6 +164,9 @@ COPY --chown=node:node --from=builder /app/src/lib/server/db/seedMonitorData.ts
COPY --chown=node:node --from=builder /app/src/lib/server/db/seedPagesData.ts ./src/lib/server/db/seedPagesData.ts
COPY --chown=node:node --from=builder /app/src/lib/server/templates/general ./src/lib/server/templates/general
# Locale JSON files (read at runtime by server-side i18n)
COPY --chown=node:node --from=builder /app/src/lib/locales ./src/lib/locales
# Build output (SvelteKit client/server + esbuild main.js) — changes most often
COPY --chown=node:node --from=builder /app/build ./build
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Raj Nandan Sharma
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+12 -18
View File
@@ -1,10 +1,5 @@
# Kener - Stunning Status Pages
<details>
<summary>Upcoming Version 4.0.0</summary>
Currently we are working on updating kener to the latest svelte version with typescript
</details>
<p align="center">
<img src="https://kener.ing/og.jpg?v=1" width="100%" height="auto" class="rounded-lg shadow-lg" alt="kener example illustration">
</p>
@@ -25,6 +20,7 @@
<a href="https://github.com/rajnandan1/kener/actions/workflows/publish-images.yml"><img alt="GitHub Workflow Status" src="https://img.shields.io/github/actions/workflow/status/rajnandan1/kener/publish-images.yml" /></a>
<a href="https://github.com/rajnandan1/kener/commit/HEAD"><img src="https://img.shields.io/github/last-commit/rajnandan1/kener/main" alt="" /></a>
<a href="https://github.com/rajnandan1/kener/issues"><img alt="GitHub issues" src="https://img.shields.io/github/issues/rajnandan1/kener.svg" /></a>
<a href="https://deepwiki.com/rajnandan1/kener"><img alt="Ask DeepWiki" src="https://deepwiki.com/badge.svg" /></a>
</p>
<p align="center">
@@ -51,6 +47,14 @@
| [🌍 Live Server](https://kener.ing) | [🎉 Quick Start](https://kener.ing/docs/v4/getting-started/quick-start) | [🗄 Documentation](https://kener.ing/docs/v4/getting-started/introduction) |
| ----------------------------------- | ----------------------------------------------------------------------- | -------------------------------------------------------------------------- |
<p align="center">
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/spSvic?referralCode=1Pn7vs&utm_medium=integration&utm_source=template&utm_campaign=generic)
[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/templates/1YRTMI?referralCode=rajnandan1)
[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https%3A%2F%2Fgithub.com%2Frajnandan1%2Fkener)
</p>
## What is Kener?
**Kener** is a sleek and lightweight status page system built with **SvelteKit** and **NodeJS**. Its not here to replace heavyweights like Datadog or Atlassian but rather to offer a simple, modern, and hassle-free way to set up a great-looking status page with minimal effort.
@@ -173,11 +177,7 @@ PORT=3000
For the full quick start (including local Docker builds and dev mode), see the docs:
- https://kener.ing/docs/quick-start
## One Click Deployment
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/template/spSvic?referralCode=1Pn7vs)
- https://kener.ing/docs/v4/getting-started/quick-start
## Features
@@ -207,10 +207,6 @@ Kener combines public status page essentials with advanced admin workflows.
- Integrate analytics providers like GA, Plausible, Mixpanel, Umami, and Clarity
- Access the full REST API for incidents, monitors, and reporting
<div align="left">
<img alt="Visitor Stats" src="https://widgetbite.com/stats/rajnandan"/>
</div>
## Technologies Used
- [SvelteKit](https://kit.svelte.dev/)
@@ -220,11 +216,9 @@ Kener combines public status page essentials with advanced admin workflows.
If youre enjoying Kener and want to support its development, consider sponsoring me on GitHub or treating me to a coffee. Your support helps keep the project growing! 🚀
[Sponsor Me Using Github](https://github.com/sponsors/rajnandan1)
- [Sponsor Me Using GitHub](https://github.com/sponsors/rajnandan1)
☕ &nbsp;[Buy Me a Coffee](https://www.buymeacoffee.com/rajnandan1)
![image](https://badges.pufler.dev/visits/rajnandan1/kener)
- [Buy Me a Coffee](https://www.buymeacoffee.com/rajnandan1)
## Contributing
+55 -23
View File
@@ -1,17 +1,19 @@
import type { Knex } from "knex";
export async function up(knex: Knex): Promise<void> {
await knex.schema
.createTable("monitoring_data", (table) => {
if (!(await knex.schema.hasTable("monitoring_data"))) {
await knex.schema.createTable("monitoring_data", (table) => {
table.string("monitor_tag", 255).notNullable();
table.integer("timestamp").notNullable();
table.text("status");
table.float("latency", 8, 2);
table.text("type");
table.primary(["monitor_tag", "timestamp"]);
})
// Create monitor_alerts table
.createTable("monitor_alerts", (table) => {
});
}
if (!(await knex.schema.hasTable("monitor_alerts"))) {
await knex.schema.createTable("monitor_alerts", (table) => {
table.increments("id").primary();
table.string("monitor_tag", 255).notNullable();
table.string("monitor_status", 255).notNullable();
@@ -20,18 +22,29 @@ export async function up(knex: Knex): Promise<void> {
table.integer("incident_number").defaultTo(0);
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
})
// Add index to monitor_alerts table
.raw("CREATE INDEX idx_monitor_tag_created_at ON monitor_alerts (monitor_tag, created_at)")
.createTable("site_data", (table) => {
});
}
// Add index (IF NOT EXISTS not supported by all DBs, so use try/catch)
try {
await knex.schema.raw("CREATE INDEX idx_monitor_tag_created_at ON monitor_alerts (monitor_tag, created_at)");
} catch (_e) {
// Index already exists
}
if (!(await knex.schema.hasTable("site_data"))) {
await knex.schema.createTable("site_data", (table) => {
table.increments("id").primary();
table.string("key", 255).notNullable().unique();
table.text("value").notNullable();
table.string("data_type", 255).notNullable();
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
})
.createTable("monitors", (table) => {
});
}
if (!(await knex.schema.hasTable("monitors"))) {
await knex.schema.createTable("monitors", (table) => {
table.increments("id").primary();
table.string("tag", 255).notNullable().unique();
table.string("name", 255).notNullable().unique();
@@ -50,8 +63,11 @@ export async function up(knex: Knex): Promise<void> {
table.string("include_degraded_in_downtime", 255).defaultTo("NO");
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
})
.createTable("triggers", (table) => {
});
}
if (!(await knex.schema.hasTable("triggers"))) {
await knex.schema.createTable("triggers", (table) => {
table.increments("id").primary();
table.string("name", 255).notNullable().unique();
table.string("trigger_type", 255);
@@ -60,8 +76,11 @@ export async function up(knex: Knex): Promise<void> {
table.text("trigger_meta");
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
})
.createTable("users", (table) => {
});
}
if (!(await knex.schema.hasTable("users"))) {
await knex.schema.createTable("users", (table) => {
table.increments("id").primary();
table.string("email", 255).notNullable().unique();
table.string("name", 255).notNullable();
@@ -71,8 +90,11 @@ export async function up(knex: Knex): Promise<void> {
table.string("role", 255).defaultTo("user");
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
})
.createTable("api_keys", (table) => {
});
}
if (!(await knex.schema.hasTable("api_keys"))) {
await knex.schema.createTable("api_keys", (table) => {
table.increments("id").primary();
table.string("name", 255).notNullable().unique();
table.string("hashed_key", 255).notNullable().unique();
@@ -80,8 +102,11 @@ export async function up(knex: Knex): Promise<void> {
table.string("status", 255).defaultTo("ACTIVE");
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
})
.createTable("incidents", (table) => {
});
}
if (!(await knex.schema.hasTable("incidents"))) {
await knex.schema.createTable("incidents", (table) => {
table.increments("id").primary();
table.string("title", 255).notNullable();
table.integer("start_date_time").notNullable();
@@ -90,8 +115,11 @@ export async function up(knex: Knex): Promise<void> {
table.timestamp("updated_at").defaultTo(knex.fn.now());
table.string("status", 255).defaultTo("ACTIVE");
table.string("state", 255).defaultTo("INVESTIGATING");
})
.createTable("incident_monitors", (table) => {
});
}
if (!(await knex.schema.hasTable("incident_monitors"))) {
await knex.schema.createTable("incident_monitors", (table) => {
table.increments("id").primary();
table.string("monitor_tag", 255).notNullable();
table.string("monitor_impact", 255);
@@ -99,8 +127,11 @@ export async function up(knex: Knex): Promise<void> {
table.timestamp("updated_at").defaultTo(knex.fn.now());
table.integer("incident_id").notNullable();
table.unique(["monitor_tag", "incident_id"]);
})
.createTable("incident_comments", (table) => {
});
}
if (!(await knex.schema.hasTable("incident_comments"))) {
await knex.schema.createTable("incident_comments", (table) => {
table.increments("id").primary();
table.text("comment").notNullable();
table.integer("incident_id").notNullable();
@@ -110,6 +141,7 @@ export async function up(knex: Knex): Promise<void> {
table.string("status", 255).defaultTo("ACTIVE");
table.string("state", 255).defaultTo("INVESTIGATING");
});
}
}
export async function down(knex: Knex): Promise<void> {
@@ -1,9 +1,12 @@
import type { Knex } from "knex";
export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable("incidents", function (table) {
table.text("incident_type").defaultTo("INCIDENT");
});
const hasCol = await knex.schema.hasColumn("incidents", "incident_type");
if (!hasCol) {
await knex.schema.alterTable("incidents", function (table) {
table.text("incident_type").defaultTo("INCIDENT");
});
}
}
export async function down(knex: Knex): Promise<void> {
@@ -1,10 +1,12 @@
import type { Knex } from "knex";
export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable("incidents", function (table) {
table.text("incident_source").defaultTo("DASHBOARD");
});
const hasCol = await knex.schema.hasColumn("incidents", "incident_source");
if (!hasCol) {
await knex.schema.alterTable("incidents", function (table) {
table.text("incident_source").defaultTo("DASHBOARD");
});
}
}
export async function down(knex: Knex): Promise<void> {
@@ -1,6 +1,8 @@
import type { Knex } from "knex";
export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasTable("invitations")) return;
await knex.schema.createTable("invitations", (table) => {
// Primary key
table.increments("id").primary();
@@ -1,8 +1,8 @@
import type { Knex } from "knex";
export async function up(knex: Knex): Promise<void> {
await knex.schema
.createTable("subscribers", (table) => {
if (!(await knex.schema.hasTable("subscribers"))) {
await knex.schema.createTable("subscribers", (table) => {
table.increments("id").primary();
table.string("subscriber_send").notNullable();
table.text("subscriber_meta").nullable();
@@ -16,8 +16,11 @@ export async function up(knex: Knex): Promise<void> {
// Add index on subscriber_send for better query performance
table.index(["subscriber_send"]);
})
.createTable("subscriptions", (table) => {
});
}
if (!(await knex.schema.hasTable("subscriptions"))) {
await knex.schema.createTable("subscriptions", (table) => {
table.increments("id").primary();
table.integer("subscriber_id").unsigned().notNullable();
table.string("subscriptions_status").notNullable();
@@ -32,8 +35,11 @@ export async function up(knex: Knex): Promise<void> {
// Add index to optimize queries filtering by status and monitors
table.index(["subscriptions_status", "subscriptions_monitors"]);
})
.createTable("subscription_triggers", (table) => {
});
}
if (!(await knex.schema.hasTable("subscription_triggers"))) {
await knex.schema.createTable("subscription_triggers", (table) => {
table.increments("id").primary();
table.string("subscription_trigger_type").notNullable().unique();
table.string("subscription_trigger_status").notNullable();
@@ -41,6 +47,7 @@ export async function up(knex: Knex): Promise<void> {
table.datetime("created_at").defaultTo(knex.fn.now());
table.datetime("updated_at").defaultTo(knex.fn.now());
});
}
}
export async function down(knex: Knex): Promise<void> {
await knex.schema
@@ -1,6 +1,8 @@
import type { Knex } from "knex";
export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasTable("images")) return;
await knex.schema.createTable("images", (table) => {
table.string("id", 32).primary(); // nanoid generated ID with prefix
table.text("data").notNullable(); // base64 encoded image data
+38 -26
View File
@@ -2,37 +2,49 @@ import type { Knex } from "knex";
export async function up(knex: Knex): Promise<void> {
// Create pages table
await knex.schema.createTable("pages", (table) => {
table.increments("id").primary();
table.string("page_path", 255).notNullable().unique(); // e.g., "/", "/api", "/infrastructure"
table.string("page_title", 255).notNullable();
table.string("page_header", 255);
table.string("page_subheader", 255);
table.string("page_logo", 255);
table.text("page_settings_json"); // JSON settings for the page
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
});
if (!(await knex.schema.hasTable("pages"))) {
await knex.schema.createTable("pages", (table) => {
table.increments("id").primary();
table.string("page_path", 255).notNullable().unique(); // e.g., "/", "/api", "/infrastructure"
table.string("page_title", 255).notNullable();
table.string("page_header", 255);
table.string("page_subheader", 255);
table.string("page_logo", 255);
table.text("page_settings_json"); // JSON settings for the page
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
});
}
// Create pages_monitors junction table
await knex.schema.createTable("pages_monitors", (table) => {
table.integer("page_id").unsigned().notNullable();
table.string("monitor_tag", 255).notNullable();
table.text("monitor_settings_json"); // JSON settings for monitor on this page (e.g., order, visibility)
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
if (!(await knex.schema.hasTable("pages_monitors"))) {
await knex.schema.createTable("pages_monitors", (table) => {
table.integer("page_id").unsigned().notNullable();
table.string("monitor_tag", 255).notNullable();
table.text("monitor_settings_json"); // JSON settings for monitor on this page (e.g., order, visibility)
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
// Composite primary key
table.primary(["page_id", "monitor_tag"]);
// Composite primary key
table.primary(["page_id", "monitor_tag"]);
// Foreign key constraints
table.foreign("page_id").references("id").inTable("pages").onDelete("CASCADE");
table.foreign("monitor_tag").references("tag").inTable("monitors").onDelete("CASCADE");
});
// Foreign key constraints
table.foreign("page_id").references("id").inTable("pages").onDelete("CASCADE");
table.foreign("monitor_tag").references("tag").inTable("monitors").onDelete("CASCADE");
});
}
// Add index for faster lookups
await knex.schema.raw("CREATE INDEX idx_pages_monitors_page_id ON pages_monitors (page_id)");
await knex.schema.raw("CREATE INDEX idx_pages_monitors_monitor_tag ON pages_monitors (monitor_tag)");
// Add indexes (safe to fail if they already exist)
try {
await knex.schema.raw("CREATE INDEX idx_pages_monitors_page_id ON pages_monitors (page_id)");
} catch (_e) {
/* index already exists */
}
try {
await knex.schema.raw("CREATE INDEX idx_pages_monitors_monitor_tag ON pages_monitors (monitor_tag)");
} catch (_e) {
/* index already exists */
}
}
export async function down(knex: Knex): Promise<void> {
@@ -1,64 +1,69 @@
import type { Knex } from "knex";
export async function up(knex: Knex): Promise<void> {
// Create maintenances table - defines maintenance schedules using iCalendar RRULE
// RRULE examples:
// - ONE_TIME: FREQ=MINUTELY;COUNT=1 (single occurrence)
// - RECURRING: FREQ=WEEKLY;BYDAY=SU;BYHOUR=2;BYMINUTE=0 (every Sunday at 2 AM)
// Reference: http://www.kanzaki.com/docs/ical/rrule.html
await knex.schema.createTable("maintenances", (table) => {
table.increments("id").primary();
table.string("title", 255).notNullable();
table.text("description").nullable(); // Maintenance details/description
table.integer("start_date_time").notNullable(); // Unix timestamp - when the first occurrence starts
table.string("rrule", 500).notNullable(); // iCalendar RRULE string (e.g., FREQ=WEEKLY;BYDAY=SU)
table.integer("duration_seconds").notNullable(); // Duration of each maintenance window in seconds
table.string("status", 50).notNullable().defaultTo("ACTIVE"); // ACTIVE or INACTIVE
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
});
if (!(await knex.schema.hasTable("maintenances"))) {
await knex.schema.createTable("maintenances", (table) => {
table.increments("id").primary();
table.string("title", 255).notNullable();
table.text("description").nullable();
table.integer("start_date_time").notNullable();
table.string("rrule", 500).notNullable();
table.integer("duration_seconds").notNullable();
table.string("status", 50).notNullable().defaultTo("ACTIVE");
table.string("is_global", 15).notNullable().defaultTo("YES");
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
});
}
// Create maintenance_monitors junction table - links monitors to maintenance schedules
await knex.schema.createTable("maintenance_monitors", (table) => {
table.increments("id").primary();
table.integer("maintenance_id").unsigned().notNullable();
table.string("monitor_tag", 255).notNullable();
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
if (!(await knex.schema.hasTable("maintenance_monitors"))) {
await knex.schema.createTable("maintenance_monitors", (table) => {
table.increments("id").primary();
table.integer("maintenance_id").unsigned().notNullable();
table.string("monitor_tag", 255).notNullable();
table.string("monitor_impact").defaultTo("MAINTENANCE").notNullable();
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
// Foreign key constraints
table.foreign("maintenance_id").references("id").inTable("maintenances").onDelete("CASCADE");
table.foreign("monitor_tag").references("tag").inTable("monitors").onDelete("CASCADE");
table.foreign("maintenance_id").references("id").inTable("maintenances").onDelete("CASCADE");
table.foreign("monitor_tag").references("tag").inTable("monitors").onDelete("CASCADE");
// Unique constraint to prevent duplicate monitor assignments
table.unique(["maintenance_id", "monitor_tag"]);
});
table.unique(["maintenance_id", "monitor_tag"]);
});
}
// Create maintenances_events table - actual maintenance occurrences (generated by job)
await knex.schema.createTable("maintenances_events", (table) => {
table.increments("id").primary();
table.integer("maintenance_id").unsigned().notNullable();
table.integer("start_date_time").notNullable(); // Unix timestamp
table.integer("end_date_time").notNullable(); // Unix timestamp
table.string("status", 50).notNullable().defaultTo("SCHEDULED"); // SCHEDULED, IN_PROGRESS, COMPLETED, CANCELLED
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
if (!(await knex.schema.hasTable("maintenances_events"))) {
await knex.schema.createTable("maintenances_events", (table) => {
table.increments("id").primary();
table.integer("maintenance_id").unsigned().notNullable();
table.integer("start_date_time").notNullable();
table.integer("end_date_time").notNullable();
table.string("status", 50).notNullable().defaultTo("SCHEDULED");
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
// Foreign key constraint
table.foreign("maintenance_id").references("id").inTable("maintenances").onDelete("CASCADE");
});
table.foreign("maintenance_id").references("id").inTable("maintenances").onDelete("CASCADE");
});
}
// Add indexes for faster lookups
await knex.schema.raw("CREATE INDEX idx_maintenances_status ON maintenances (status)");
await knex.schema.raw("CREATE INDEX idx_maintenances_start_time ON maintenances (start_date_time)");
await knex.schema.raw(
// Add indexes (safe to fail if they already exist)
const indexes = [
"CREATE INDEX idx_maintenances_status ON maintenances (status)",
"CREATE INDEX idx_maintenances_start_time ON maintenances (start_date_time)",
"CREATE INDEX idx_maintenance_monitors_maintenance_id ON maintenance_monitors (maintenance_id)",
);
await knex.schema.raw("CREATE INDEX idx_maintenance_monitors_monitor_tag ON maintenance_monitors (monitor_tag)");
await knex.schema.raw("CREATE INDEX idx_maintenances_events_maintenance_id ON maintenances_events (maintenance_id)");
await knex.schema.raw("CREATE INDEX idx_maintenances_events_status ON maintenances_events (status)");
await knex.schema.raw("CREATE INDEX idx_maintenances_events_start_time ON maintenances_events (start_date_time)");
await knex.schema.raw("CREATE INDEX idx_maintenances_events_end_time ON maintenances_events (end_date_time)");
"CREATE INDEX idx_maintenance_monitors_monitor_tag ON maintenance_monitors (monitor_tag)",
"CREATE INDEX idx_maintenances_events_maintenance_id ON maintenances_events (maintenance_id)",
"CREATE INDEX idx_maintenances_events_status ON maintenances_events (status)",
"CREATE INDEX idx_maintenances_events_start_time ON maintenances_events (start_date_time)",
"CREATE INDEX idx_maintenances_events_end_time ON maintenances_events (end_date_time)",
];
for (const sql of indexes) {
try {
await knex.schema.raw(sql);
} catch (_e) {
/* index already exists */
}
}
}
export async function down(knex: Knex): Promise<void> {
@@ -1,10 +1,14 @@
import type { Knex } from "knex";
export async function up(knex: Knex): Promise<void> {
// Remove unique constraint from monitors.name
await knex.schema.alterTable("monitors", (table) => {
table.dropUnique(["name"]);
});
// Remove unique constraint from monitors.name (safe to fail if already dropped)
try {
await knex.schema.alterTable("monitors", (table) => {
table.dropUnique(["name"]);
});
} catch (_e) {
// Constraint already removed
}
}
export async function down(knex: Knex): Promise<void> {
@@ -1,9 +1,12 @@
import type { Knex } from "knex";
export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable("monitors", (table) => {
table.string("is_hidden").defaultTo("NO").notNullable();
});
const hasCol = await knex.schema.hasColumn("monitors", "is_hidden");
if (!hasCol) {
await knex.schema.alterTable("monitors", (table) => {
table.string("is_hidden").defaultTo("NO").notNullable();
});
}
}
export async function down(knex: Knex): Promise<void> {
@@ -1,9 +1,12 @@
import type { Knex } from "knex";
export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable("monitors", (table) => {
table.text("monitor_settings_json").nullable();
});
const hasCol = await knex.schema.hasColumn("monitors", "monitor_settings_json");
if (!hasCol) {
await knex.schema.alterTable("monitors", (table) => {
table.text("monitor_settings_json").nullable();
});
}
}
export async function down(knex: Knex): Promise<void> {
@@ -1,12 +1,17 @@
import type { Knex } from "knex";
export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable("maintenance_monitors", (table) => {
table.string("monitor_impact").defaultTo("MAINTENANCE").notNullable();
});
if (!(await knex.schema.hasTable("maintenance_monitors"))) return;
const hasCol = await knex.schema.hasColumn("maintenance_monitors", "monitor_impact");
if (!hasCol) {
await knex.schema.alterTable("maintenance_monitors", (table) => {
table.string("monitor_impact").defaultTo("MAINTENANCE").notNullable();
});
}
}
export async function down(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable("maintenance_monitors"))) return;
await knex.schema.alterTable("maintenance_monitors", (table) => {
table.dropColumn("monitor_impact");
});
@@ -2,42 +2,54 @@ import type { Knex } from "knex";
export async function up(knex: Knex): Promise<void> {
// Create monitor_alerts_config table
await knex.schema.createTable("monitor_alerts_config", (table) => {
table.increments("id").primary();
table.string("monitor_tag", 255).notNullable();
table.string("alert_for", 50).notNullable(); // STATUS, LATENCY, UPTIME
table.string("alert_value", 255).notNullable(); // DOWN, DEGRADED, or numeric value like "1000" or "99"
table.integer("failure_threshold").notNullable().defaultTo(1);
table.integer("success_threshold").notNullable().defaultTo(1);
table.text("alert_description");
table.string("create_incident", 10).notNullable().defaultTo("NO"); // YES or NO
table.string("is_active", 10).notNullable().defaultTo("YES"); // YES or NO
table.string("severity", 50).notNullable().defaultTo("WARNING"); // CRITICAL or WARNING
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
if (!(await knex.schema.hasTable("monitor_alerts_config"))) {
await knex.schema.createTable("monitor_alerts_config", (table) => {
table.increments("id").primary();
table.string("monitor_tag", 255).notNullable();
table.string("alert_for", 50).notNullable(); // STATUS, LATENCY, UPTIME
table.string("alert_value", 255).notNullable(); // DOWN, DEGRADED, or numeric value like "1000" or "99"
table.integer("failure_threshold").notNullable().defaultTo(1);
table.integer("success_threshold").notNullable().defaultTo(1);
table.text("alert_description");
table.string("create_incident", 10).notNullable().defaultTo("NO"); // YES or NO
table.string("is_active", 10).notNullable().defaultTo("YES"); // YES or NO
table.string("severity", 50).notNullable().defaultTo("WARNING"); // CRITICAL or WARNING
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
// Foreign key to monitors table
table.foreign("monitor_tag").references("tag").inTable("monitors").onDelete("CASCADE");
});
// Foreign key to monitors table
table.foreign("monitor_tag").references("tag").inTable("monitors").onDelete("CASCADE");
});
}
// Create index for faster lookups
await knex.raw("CREATE INDEX idx_monitor_alerts_config_monitor_tag ON monitor_alerts_config (monitor_tag)");
await knex.raw("CREATE INDEX idx_monitor_alerts_config_is_active ON monitor_alerts_config (is_active)");
// Create indexes (safe to fail if they already exist)
try {
await knex.raw("CREATE INDEX idx_monitor_alerts_config_monitor_tag ON monitor_alerts_config (monitor_tag)");
} catch (_e) {
/* index already exists */
}
try {
await knex.raw("CREATE INDEX idx_monitor_alerts_config_is_active ON monitor_alerts_config (is_active)");
} catch (_e) {
/* index already exists */
}
// Create monitor_alerts_config_triggers junction table
await knex.schema.createTable("monitor_alerts_config_triggers", (table) => {
table.integer("monitor_alerts_id").unsigned().notNullable();
table.integer("trigger_id").unsigned().notNullable();
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
if (!(await knex.schema.hasTable("monitor_alerts_config_triggers"))) {
await knex.schema.createTable("monitor_alerts_config_triggers", (table) => {
table.integer("monitor_alerts_id").unsigned().notNullable();
table.integer("trigger_id").unsigned().notNullable();
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
// Composite primary key
table.primary(["monitor_alerts_id", "trigger_id"]);
// Composite primary key
table.primary(["monitor_alerts_id", "trigger_id"]);
// Foreign keys
table.foreign("monitor_alerts_id").references("id").inTable("monitor_alerts_config").onDelete("CASCADE");
table.foreign("trigger_id").references("id").inTable("triggers").onDelete("CASCADE");
});
// Foreign keys
table.foreign("monitor_alerts_id").references("id").inTable("monitor_alerts_config").onDelete("CASCADE");
table.foreign("trigger_id").references("id").inTable("triggers").onDelete("CASCADE");
});
}
}
export async function down(knex: Knex): Promise<void> {
@@ -1,19 +1,56 @@
import type { Knex } from "knex";
export async function up(knex: Knex): Promise<void> {
await knex.schema.createTable("monitor_alerts_v2", (table) => {
table.increments("id").primary();
table.integer("config_id").references("id").inTable("monitor_alerts_config").notNullable().onDelete("CASCADE");
table.integer("incident_id").references("id").inTable("incidents").nullable().onDelete("SET NULL");
table.string("alert_status", 255).notNullable();
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
if (!(await knex.schema.hasTable("monitor_alerts_v2"))) {
await knex.schema.createTable("monitor_alerts_v2", (table) => {
table.increments("id").primary();
table.integer("config_id").unsigned().notNullable();
table.integer("incident_id").unsigned().nullable();
table.string("alert_status", 255).notNullable();
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
// Add index for faster queries on config_id and alert_status
table.index(["config_id", "alert_status"]);
});
// Add index for faster queries on config_id and alert_status
table.index(["config_id", "alert_status"]);
});
}
// Ensure config_id is unsigned (fix for MySQL users who had signed int from a prior failed run)
try {
await knex.schema.alterTable("monitor_alerts_v2", (table) => {
table.integer("config_id").unsigned().notNullable().alter();
});
} catch (_e) {
/* column may already be correct */
}
// Ensure incident_id is unsigned
try {
await knex.schema.alterTable("monitor_alerts_v2", (table) => {
table.integer("incident_id").unsigned().nullable().alter();
});
} catch (_e) {
/* column may already be correct */
}
// Add foreign key constraints (skip if they already exist)
try {
await knex.schema.alterTable("monitor_alerts_v2", (table) => {
table.foreign("config_id").references("id").inTable("monitor_alerts_config").onDelete("CASCADE");
});
} catch (_e) {
/* foreign key may already exist */
}
try {
await knex.schema.alterTable("monitor_alerts_v2", (table) => {
table.foreign("incident_id").references("id").inTable("incidents").onDelete("SET NULL");
});
} catch (_e) {
/* foreign key may already exist */
}
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTable("monitor_alerts_v2");
await knex.schema.dropTableIfExists("monitor_alerts_v2");
}
@@ -2,93 +2,138 @@ import type { Knex } from "knex";
export async function up(knex: Knex): Promise<void> {
// 1. Create subscriber_users table - the actual user identity
await knex.schema.createTable("subscriber_users", (table) => {
table.increments("id").primary();
if (!(await knex.schema.hasTable("subscriber_users"))) {
await knex.schema.createTable("subscriber_users", (table) => {
table.increments("id").primary();
table.string("email", 255).notNullable().unique();
table.string("status", 20).notNullable().defaultTo("PENDING");
table.string("verification_code", 10).nullable();
table.timestamp("verification_expires_at").nullable();
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
table.index(["status"]);
table.index(["email"]);
});
}
// Email is the primary identifier for users
table.string("email", 255).notNullable().unique();
// 2. Create subscriber_methods table
if (!(await knex.schema.hasTable("subscriber_methods"))) {
await knex.schema.createTable("subscriber_methods", (table) => {
table.increments("id").primary();
table.integer("subscriber_user_id").unsigned().notNullable();
table.string("method_type", 50).notNullable();
table.string("method_value", 500).notNullable();
table.string("status", 20).notNullable().defaultTo("ACTIVE");
table.text("meta").nullable();
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
});
}
// User status: PENDING (awaiting verification), ACTIVE, INACTIVE
table.string("status", 20).notNullable().defaultTo("PENDING");
// Add indexes, unique constraints, and foreign keys for subscriber_methods (idempotent)
try {
await knex.schema.alterTable("subscriber_methods", (table) => {
table.index(["subscriber_user_id"]);
});
} catch (_e) {
/* index already exists */
}
try {
await knex.schema.alterTable("subscriber_methods", (table) => {
table.index(["method_type"]);
});
} catch (_e) {
/* index already exists */
}
try {
await knex.schema.alterTable("subscriber_methods", (table) => {
table.index(["status"]);
});
} catch (_e) {
/* index already exists */
}
try {
await knex.schema.alterTable("subscriber_methods", (table) => {
table.unique(["subscriber_user_id", "method_type", "method_value"], {
indexName: "sub_methods_user_type_value_unique",
});
});
} catch (_e) {
/* unique constraint already exists */
}
try {
await knex.schema.alterTable("subscriber_methods", (table) => {
table.foreign("subscriber_user_id").references("id").inTable("subscriber_users").onDelete("CASCADE");
});
} catch (_e) {
/* foreign key already exists */
}
// Verification code for email verification (6 digit)
table.string("verification_code", 10).nullable();
table.timestamp("verification_expires_at").nullable();
// 3. Create user_subscriptions_v2 table
if (!(await knex.schema.hasTable("user_subscriptions_v2"))) {
await knex.schema.createTable("user_subscriptions_v2", (table) => {
table.increments("id").primary();
table.integer("subscriber_user_id").unsigned().notNullable();
table.integer("subscriber_method_id").unsigned().notNullable();
table.string("event_type", 50).notNullable();
table.string("status", 20).notNullable().defaultTo("ACTIVE");
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
});
}
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
// Indexes
table.index(["status"]);
table.index(["email"]);
});
// 2. Create subscriber_methods table - methods a user has configured
await knex.schema.createTable("subscriber_methods", (table) => {
table.increments("id").primary();
// Link to subscriber_user
table.integer("subscriber_user_id").unsigned().notNullable();
// Method type: email, webhook, slack, discord
table.string("method_type", 50).notNullable();
// Method value: email address, webhook URL, slack webhook, discord webhook
table.string("method_value", 500).notNullable();
// Status: ACTIVE, INACTIVE
table.string("status", 20).notNullable().defaultTo("ACTIVE");
// For webhook methods, we might want to store additional config
table.text("meta").nullable(); // JSON for extra config like headers
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
// Indexes
table.index(["subscriber_user_id"]);
table.index(["method_type"]);
table.index(["status"]);
// Unique: one method type per value per user (can't have same webhook twice)
table.unique(["subscriber_user_id", "method_type", "method_value"]);
// Foreign key
table.foreign("subscriber_user_id").references("id").inTable("subscriber_users").onDelete("CASCADE");
});
// 3. Create user_subscriptions_v2 table - what a user subscribes to
await knex.schema.createTable("user_subscriptions_v2", (table) => {
table.increments("id").primary();
// Link to subscriber_user
table.integer("subscriber_user_id").unsigned().notNullable();
// Link to subscriber_method (which method to use for this subscription)
table.integer("subscriber_method_id").unsigned().notNullable();
// What event type: incidents, maintenance
table.string("event_type", 50).notNullable();
// Status: ACTIVE, INACTIVE
table.string("status", 20).notNullable().defaultTo("ACTIVE");
table.timestamp("created_at").defaultTo(knex.fn.now());
table.timestamp("updated_at").defaultTo(knex.fn.now());
// Indexes
table.index(["subscriber_user_id"]);
table.index(["subscriber_method_id"]);
table.index(["event_type"]);
table.index(["status"]);
// Unique: one subscription per user-method-event-entity
table.unique(["subscriber_user_id", "subscriber_method_id", "event_type"]);
// Foreign keys
table.foreign("subscriber_user_id").references("id").inTable("subscriber_users").onDelete("CASCADE");
table.foreign("subscriber_method_id").references("id").inTable("subscriber_methods").onDelete("CASCADE");
});
// Add indexes, unique constraints, and foreign keys for user_subscriptions_v2 (idempotent)
try {
await knex.schema.alterTable("user_subscriptions_v2", (table) => {
table.index(["subscriber_user_id"]);
});
} catch (_e) {
/* index already exists */
}
try {
await knex.schema.alterTable("user_subscriptions_v2", (table) => {
table.index(["subscriber_method_id"]);
});
} catch (_e) {
/* index already exists */
}
try {
await knex.schema.alterTable("user_subscriptions_v2", (table) => {
table.index(["event_type"]);
});
} catch (_e) {
/* index already exists */
}
try {
await knex.schema.alterTable("user_subscriptions_v2", (table) => {
table.index(["status"]);
});
} catch (_e) {
/* index already exists */
}
try {
await knex.schema.alterTable("user_subscriptions_v2", (table) => {
table.unique(["subscriber_user_id", "subscriber_method_id", "event_type"], {
indexName: "sub_v2_user_method_event_unique",
});
});
} catch (_e) {
/* unique constraint already exists */
}
try {
await knex.schema.alterTable("user_subscriptions_v2", (table) => {
table.foreign("subscriber_user_id").references("id").inTable("subscriber_users").onDelete("CASCADE");
});
} catch (_e) {
/* foreign key already exists */
}
try {
await knex.schema.alterTable("user_subscriptions_v2", (table) => {
table.foreign("subscriber_method_id").references("id").inTable("subscriber_methods").onDelete("CASCADE");
});
} catch (_e) {
/* foreign key already exists */
}
}
export async function down(knex: Knex): Promise<void> {
@@ -1,6 +1,8 @@
import type { Knex } from "knex";
export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasTable("general_email_templates")) return;
await knex.schema.createTable("general_email_templates", (table) => {
table.string("template_id").primary();
table.string("template_subject");
@@ -1,12 +1,16 @@
import type { Knex } from "knex";
export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable("monitors", (table) => {
table.text("external_url").nullable();
});
await knex.schema.alterTable("monitoring_data", (table) => {
table.text("error_message").nullable();
});
if (!(await knex.schema.hasColumn("monitors", "external_url"))) {
await knex.schema.alterTable("monitors", (table) => {
table.text("external_url").nullable();
});
}
if (!(await knex.schema.hasColumn("monitoring_data", "error_message"))) {
await knex.schema.alterTable("monitoring_data", (table) => {
table.text("error_message").nullable();
});
}
}
export async function down(knex: Knex): Promise<void> {
@@ -1,10 +1,20 @@
import type { Knex } from "knex";
export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable("monitoring_data", (table) => {
table.index(["timestamp"], "idx_monitoring_data_timestamp");
table.index(["monitor_tag", "type", "timestamp"], "idx_monitoring_data_monitor_tag_type_timestamp");
});
try {
await knex.schema.alterTable("monitoring_data", (table) => {
table.index(["timestamp"], "idx_monitoring_data_timestamp");
});
} catch (_e) {
/* index already exists */
}
try {
await knex.schema.alterTable("monitoring_data", (table) => {
table.index(["monitor_tag", "type", "timestamp"], "idx_monitoring_data_monitor_tag_type_timestamp");
});
} catch (_e) {
/* index already exists */
}
}
export async function down(knex: Knex): Promise<void> {
@@ -1,19 +1,27 @@
import type { Knex } from "knex";
export async function up(knex: Knex): Promise<void> {
await knex.schema.table("incidents", (table) => {
table.string("is_global", 15).notNullable().defaultTo("YES");
});
await knex.schema.table("maintenances", (table) => {
table.string("is_global", 15).notNullable().defaultTo("YES");
});
if (!(await knex.schema.hasColumn("incidents", "is_global"))) {
await knex.schema.table("incidents", (table) => {
table.string("is_global", 15).notNullable().defaultTo("YES");
});
}
if ((await knex.schema.hasTable("maintenances")) && !(await knex.schema.hasColumn("maintenances", "is_global"))) {
await knex.schema.table("maintenances", (table) => {
table.string("is_global", 15).notNullable().defaultTo("YES");
});
}
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.table("incidents", (table) => {
table.dropColumn("is_global");
});
await knex.schema.table("maintenances", (table) => {
table.dropColumn("is_global");
});
if (await knex.schema.hasColumn("incidents", "is_global")) {
await knex.schema.table("incidents", (table) => {
table.dropColumn("is_global");
});
}
if ((await knex.schema.hasTable("maintenances")) && (await knex.schema.hasColumn("maintenances", "is_global"))) {
await knex.schema.table("maintenances", (table) => {
table.dropColumn("is_global");
});
}
}
@@ -0,0 +1,27 @@
import type { Knex } from "knex";
export async function up(knex: Knex): Promise<void> {
const hasCol = await knex.schema.hasColumn("users", "is_owner");
if (!hasCol) {
await knex.schema.alterTable("users", (table) => {
table.string("is_owner").defaultTo("NO").notNullable();
});
// Set the first user (by id) as owner, if any users exist.
// This only runs when the column has just been added to avoid
// overwriting an existing owner on migration re-run.
const firstUser = await knex("users").orderBy("id", "asc").first();
if (firstUser) {
await knex("users").where("id", firstUser.id).update({ is_owner: "YES" });
}
}
}
export async function down(knex: Knex): Promise<void> {
const hasCol = await knex.schema.hasColumn("users", "is_owner");
if (hasCol) {
await knex.schema.alterTable("users", (table) => {
table.dropColumn("is_owner");
});
}
}
@@ -0,0 +1,13 @@
import type { Knex } from "knex";
export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable("pages", (table) => {
table.text("page_subheader").alter();
});
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable("pages", (table) => {
table.string("page_subheader", 255).alter();
});
}
@@ -0,0 +1,19 @@
import type { Knex } from "knex";
export async function up(knex: Knex): Promise<void> {
const hasColumn = await knex.schema.hasColumn("pages_monitors", "position");
if (!hasColumn) {
await knex.schema.alterTable("pages_monitors", (table) => {
table.integer("position").unsigned().notNullable().defaultTo(0);
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasColumn = await knex.schema.hasColumn("pages_monitors", "position");
if (hasColumn) {
await knex.schema.alterTable("pages_monitors", (table) => {
table.dropColumn("position");
});
}
}
+139 -76
View File
@@ -1,12 +1,12 @@
{
"name": "kener",
"version": "4.0.0",
"version": "4.0.18",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "kener",
"version": "4.0.0",
"version": "4.0.18",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.28.4",
@@ -24,6 +24,8 @@
"@codemirror/state": "^6.5.4",
"@codemirror/view": "^6.39.11",
"@formkit/auto-animate": "^0.9.0",
"@grpc/grpc-js": "^1.14.3",
"@grpc/proto-loader": "^0.8.0",
"@humanspeak/svelte-purify": "^0.0.6",
"@number-flow/svelte": "^0.3.9",
"@scalar/express-api-reference": "^0.8.28",
@@ -47,7 +49,6 @@
"figlet": "^1.9.4",
"flexsearch": "^0.8.212",
"front-matter": "^4.0.2",
"fs-extra": "^11.3.2",
"gamedig": "^5.3.2",
"glob": "^13.0.6",
"highlight.js": "^11.11.1",
@@ -96,7 +97,6 @@
"@types/d3-shape": "^3.1.8",
"@types/dns2": "^2.0.10",
"@types/express": "^5.0.6",
"@types/fs-extra": "^11.0.4",
"@types/jsonwebtoken": "^9.0.10",
"@types/mustache": "^4.2.6",
"@types/node": "^25.0.3",
@@ -804,6 +804,37 @@
"integrity": "sha512-VhP4zEAacXS3dfTpJpJ88QdLqMTcabMg0jwpOSxZ/VzfQVfl3GkZSCZThhGC5uhq/TxPHPzW0dzr4H9Bb1OgKA==",
"license": "MIT"
},
"node_modules/@grpc/grpc-js": {
"version": "1.14.3",
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz",
"integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==",
"license": "Apache-2.0",
"dependencies": {
"@grpc/proto-loader": "^0.8.0",
"@js-sdsl/ordered-map": "^4.4.2"
},
"engines": {
"node": ">=12.10.0"
}
},
"node_modules/@grpc/proto-loader": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz",
"integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==",
"license": "Apache-2.0",
"dependencies": {
"lodash.camelcase": "^4.3.0",
"long": "^5.0.0",
"protobufjs": "^7.5.3",
"yargs": "^17.7.2"
},
"bin": {
"proto-loader-gen-types": "build/bin/proto-loader-gen-types.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@humanspeak/svelte-purify": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@humanspeak/svelte-purify/-/svelte-purify-0.0.6.tgz",
@@ -1348,6 +1379,16 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@js-sdsl/ordered-map": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz",
"integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/js-sdsl"
}
},
"node_modules/@layerstack/svelte-actions": {
"version": "1.0.1-next.14",
"resolved": "https://registry.npmjs.org/@layerstack/svelte-actions/-/svelte-actions-1.0.1-next.14.tgz",
@@ -1588,6 +1629,70 @@
"integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
"license": "MIT"
},
"node_modules/@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/base64": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/codegen": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/eventemitter": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/fetch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
"license": "BSD-3-Clause",
"dependencies": {
"@protobufjs/aspromise": "^1.1.1",
"@protobufjs/inquire": "^1.1.0"
}
},
"node_modules/@protobufjs/float": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/inquire": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/path": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/pool": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/utf8": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
"license": "BSD-3-Clause"
},
"node_modules/@rollup/plugin-commonjs": {
"version": "29.0.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-29.0.0.tgz",
@@ -2651,17 +2756,6 @@
"@types/send": "*"
}
},
"node_modules/@types/fs-extra": {
"version": "11.0.4",
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz",
"integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/jsonfile": "*",
"@types/node": "*"
}
},
"node_modules/@types/http-cache-semantics": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
@@ -2675,16 +2769,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/jsonfile": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz",
"integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/jsonwebtoken": {
"version": "9.0.10",
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz",
@@ -2714,7 +2798,6 @@
"version": "25.2.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.2.tgz",
"integrity": "sha512-BkmoP5/FhRYek5izySdkOneRyXYN35I860MFAGupTdebyE66uZaR+bXLHq8k4DirE5DwQi3NuhvRU1jqTVwUrQ==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.16.0"
@@ -2859,7 +2942,6 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -2869,7 +2951,6 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
@@ -3553,7 +3634,6 @@
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"dev": true,
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
@@ -3611,7 +3691,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
@@ -4585,7 +4664,6 @@
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT"
},
"node_modules/encodeurl": {
@@ -5247,20 +5325,6 @@
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"license": "MIT"
},
"node_modules/fs-extra": {
"version": "11.3.3",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz",
"integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==",
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=14.14"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -5389,7 +5453,6 @@
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true,
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
@@ -6085,7 +6148,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -6361,18 +6423,6 @@
"integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==",
"license": "MIT"
},
"node_modules/jsonfile": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
"license": "MIT",
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/jsonwebtoken": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
@@ -6912,6 +6962,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/lodash.camelcase": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
"license": "MIT"
},
"node_modules/lodash.defaults": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
@@ -8493,6 +8549,30 @@
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/protobufjs": {
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
"integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==",
"hasInstallScript": true,
"license": "BSD-3-Clause",
"dependencies": {
"@protobufjs/aspromise": "^1.1.2",
"@protobufjs/base64": "^1.1.2",
"@protobufjs/codegen": "^2.0.4",
"@protobufjs/eventemitter": "^1.1.0",
"@protobufjs/fetch": "^1.1.0",
"@protobufjs/float": "^1.0.2",
"@protobufjs/inquire": "^1.1.0",
"@protobufjs/path": "^1.1.2",
"@protobufjs/pool": "^1.1.0",
"@protobufjs/utf8": "^1.1.0",
"@types/node": ">=13.7.0",
"long": "^5.0.0"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -8756,7 +8836,6 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -9590,7 +9669,6 @@
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
@@ -9679,7 +9757,6 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
@@ -10324,18 +10401,8 @@
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"devOptional": true,
"license": "MIT"
},
"node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"license": "MIT",
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -10752,7 +10819,6 @@
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
@@ -10808,7 +10874,6 @@
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=10"
@@ -10818,7 +10883,6 @@
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"cliui": "^8.0.1",
@@ -10837,7 +10901,6 @@
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=12"
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "kener",
"version": "4.0.0",
"version": "4.0.18",
"type": "module",
"private": false,
"license": "MIT",
@@ -40,10 +40,10 @@
"devschedule": "vite-node src/lib/server/startup.ts",
"generate-readme": "node scripts/generate-readme.js",
"index-docs": "vite-node scripts/index-docs.ts",
"migrate": "npx knex migrate:latest",
"migrate": "vite-node scripts/fix-migration-ext.ts && npx knex migrate:latest",
"predev": "npm run seed",
"prepare": "svelte-kit sync || echo ''",
"preseed": "npx knex migrate:latest",
"preseed": "vite-node scripts/fix-migration-ext.ts && npx knex migrate:latest",
"prettify": "prettier --write .",
"preview": "vite preview",
"schedule": "vite-node src/lib/server/startup.ts",
@@ -64,7 +64,6 @@
"@types/d3-shape": "^3.1.8",
"@types/dns2": "^2.0.10",
"@types/express": "^5.0.6",
"@types/fs-extra": "^11.0.4",
"@types/jsonwebtoken": "^9.0.10",
"@types/mustache": "^4.2.6",
"@types/node": "^25.0.3",
@@ -113,6 +112,8 @@
"@codemirror/state": "^6.5.4",
"@codemirror/view": "^6.39.11",
"@formkit/auto-animate": "^0.9.0",
"@grpc/grpc-js": "^1.14.3",
"@grpc/proto-loader": "^0.8.0",
"@humanspeak/svelte-purify": "^0.0.6",
"@number-flow/svelte": "^0.3.9",
"@scalar/express-api-reference": "^0.8.28",
@@ -136,7 +137,6 @@
"figlet": "^1.9.4",
"flexsearch": "^0.8.212",
"front-matter": "^4.0.2",
"fs-extra": "^11.3.2",
"gamedig": "^5.3.2",
"glob": "^13.0.6",
"highlight.js": "^11.11.1",
+35
View File
@@ -0,0 +1,35 @@
services:
- type: web
name: kener
runtime: image
image:
url: docker.io/rajnandan1/kener:latest
envVars:
- key: DATABASE_URL
fromDatabase:
name: kener-db
property: connectionString
- key: KENER_SECRET_KEY
generateValue: true
- key: ORIGIN
fromService:
type: web
name: kener
envVarKey: RENDER_EXTERNAL_URL
- key: REDIS_URL
fromService:
name: kener-redis
type: keyvalue
property: connectionString
- type: keyvalue
name: kener-redis
plan: starter
ipAllowList:
- source: 0.0.0.0/0
description: everywhere
maxmemoryPolicy: allkeys-lru
databases:
- name: kener-db
plan: basic-256mb
databaseName: kener
-247
View File
@@ -1,247 +0,0 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { globSync } from "glob";
import yaml from "js-yaml";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const projectRoot = path.resolve(__dirname, "..");
const localesDir = path.join(projectRoot, "src", "lib", "locales");
const WHITELISTED_DYNAMIC_KEYS = new Set([
"All Systems Operational",
"Degraded Performance",
"Partial Degraded Performance",
"Partial System Outage",
"Major System Outage",
"No Status Available",
]);
function fail(message) {
throw new Error(message);
}
function isPlainObject(value) {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
function parseArgs(argv) {
let reportPath;
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
if (arg === "--report") {
const next = argv[i + 1];
if (!next || next.startsWith("--")) {
fail("Missing value for --report. Usage: --report <path>");
}
reportPath = next;
i += 1;
continue;
}
if (arg.startsWith("--report=")) {
reportPath = arg.slice("--report=".length);
if (!reportPath) {
fail("Missing value for --report. Usage: --report <path>");
}
continue;
}
fail(`Unknown argument: ${arg}`);
}
return { reportPath };
}
function detectReportPath(overridePath) {
if (overridePath) {
const resolved = path.resolve(projectRoot, overridePath);
if (!fs.existsSync(resolved)) {
fail(`Report file not found: ${resolved}`);
}
return resolved;
}
const jsonPath = path.join(projectRoot, "translation-report.json");
const yamlPath = path.join(projectRoot, "translation-report.yaml");
if (fs.existsSync(jsonPath)) return jsonPath;
if (fs.existsSync(yamlPath)) return yamlPath;
fail(
"Could not find translation report. Expected translation-report.json or translation-report.yaml in project root, or use --report <path>.",
);
}
function loadReport(reportPath) {
const ext = path.extname(reportPath).toLowerCase();
const raw = fs.readFileSync(reportPath, "utf8");
let parsed;
try {
if (ext === ".json") {
parsed = JSON.parse(raw);
} else if (ext === ".yaml" || ext === ".yml") {
parsed = yaml.load(raw);
} else {
fail(`Unsupported report file extension: ${ext}. Use .json, .yaml, or .yml.`);
}
} catch (error) {
fail(`Failed to parse report at ${reportPath}: ${error instanceof Error ? error.message : String(error)}`);
}
if (!isPlainObject(parsed)) {
fail("Invalid report format: expected a top-level object.");
}
if (!isPlainObject(parsed.locales)) {
fail("Invalid report format: expected report.locales to be an object.");
}
return parsed;
}
function getUnusedKeysForLocale(report, localeFileName) {
const localeReport = report.locales[localeFileName];
if (localeReport === undefined) return [];
if (!isPlainObject(localeReport)) {
fail(`Invalid report format for locales.${localeFileName}: expected an object.`);
}
const { unused } = localeReport;
if (unused === undefined) return [];
if (!Array.isArray(unused)) {
fail(`Invalid report format for locales.${localeFileName}.unused: expected an array.`);
}
const nonStrings = unused.filter((key) => typeof key !== "string");
if (nonStrings.length > 0) {
fail(`Invalid report format for locales.${localeFileName}.unused: all entries must be strings.`);
}
return unused;
}
function loadLocaleJson(localePath, localeFileName) {
const raw = fs.readFileSync(localePath, "utf8");
let parsed;
try {
parsed = JSON.parse(raw);
} catch (error) {
fail(`Invalid JSON in ${localeFileName}: ${error instanceof Error ? error.message : String(error)}`);
}
if (!isPlainObject(parsed)) {
fail(`Invalid locale file ${localeFileName}: expected a top-level object.`);
}
if (!isPlainObject(parsed.mappings)) {
fail(`Invalid locale file ${localeFileName}: expected \"mappings\" to be an object.`);
}
return parsed;
}
function sortObjectKeysAscending(input) {
const keys = Object.keys(input).sort((a, b) => a.localeCompare(b));
const result = {};
for (const key of keys) {
result[key] = input[key];
}
return result;
}
function replaceMappingsPreserveTopLevelOrder(localeData, sortedMappings) {
const next = {};
let sawMappings = false;
for (const key of Object.keys(localeData)) {
if (key === "mappings") {
next[key] = sortedMappings;
sawMappings = true;
} else {
next[key] = localeData[key];
}
}
if (!sawMappings) {
next.mappings = sortedMappings;
}
return next;
}
function cleanTranslations(report) {
if (!fs.existsSync(localesDir)) {
fail(`Locales directory not found: ${localesDir}`);
}
const localeFiles = globSync("*.json", {
cwd: localesDir,
nodir: true,
}).sort((a, b) => a.localeCompare(b));
if (localeFiles.length === 0) {
fail(`No locale files found in ${localesDir}`);
}
let totalRemoved = 0;
const perFile = [];
for (const localeFileName of localeFiles) {
const localePath = path.join(localesDir, localeFileName);
const localeData = loadLocaleJson(localePath, localeFileName);
const unusedKeys = getUnusedKeysForLocale(report, localeFileName);
const unusedSet = new Set(unusedKeys.filter((key) => !WHITELISTED_DYNAMIC_KEYS.has(key)));
const currentMappings = localeData.mappings;
const cleanedMappings = {};
let removedCount = 0;
for (const [key, value] of Object.entries(currentMappings)) {
if (unusedSet.has(key)) {
removedCount += 1;
} else {
cleanedMappings[key] = value;
}
}
const sortedMappings = sortObjectKeysAscending(cleanedMappings);
const nextLocaleData = replaceMappingsPreserveTopLevelOrder(localeData, sortedMappings);
fs.writeFileSync(localePath, `${JSON.stringify(nextLocaleData, null, 2)}\n`, "utf8");
totalRemoved += removedCount;
perFile.push({ file: localeFileName, removed: removedCount });
}
for (const item of perFile) {
console.log(`${item.file}: removed ${item.removed} key${item.removed === 1 ? "" : "s"}`);
}
console.log(`Total removed: ${totalRemoved}`);
}
function main() {
const { reportPath: reportArg } = parseArgs(process.argv.slice(2));
const reportPath = detectReportPath(reportArg);
const report = loadReport(reportPath);
console.log(`Using report: ${path.relative(projectRoot, reportPath)}`);
cleanTranslations(report);
}
try {
main();
} catch (error) {
console.error("Failed to clean translations.");
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
}
+42
View File
@@ -0,0 +1,42 @@
/**
* Renames .js migration entries to .ts in the knex_migrations table.
* This is needed because migration files were renamed from .js to .ts,
* but existing databases still reference the old .js filenames.
*
* Idempotent — safe to run multiple times.
*/
import knex from "knex";
import knexOb from "../knexfile.js";
const db = knex(knexOb);
async function fixMigrationExtensions() {
try {
const hasTable = await db.schema.hasTable("knex_migrations");
if (!hasTable) {
console.log("No knex_migrations table found, skipping.");
return;
}
const oldJsMigrations = await db("knex_migrations").where("name", "like", "%.js");
if (oldJsMigrations.length === 0) {
console.log("No .js migration entries found, nothing to rename.");
return;
}
for (const row of oldJsMigrations) {
const newName = row.name.replace(/\.js$/, ".ts");
await db("knex_migrations").where("id", row.id).update({ name: newName });
console.log(`Renamed: ${row.name} -> ${newName}`);
}
console.log(`Fixed ${oldJsMigrations.length} migration record(s).`);
} catch (err) {
console.error("Error fixing migration extensions:", err);
process.exit(1);
} finally {
await db.destroy();
}
}
fixMigrationExtensions();
+9 -6
View File
@@ -47,7 +47,8 @@ interface DocsSidebarGroup {
interface DocsNavTab {
name: string;
sidebar: DocsSidebarGroup[];
url?: string;
sidebar?: DocsSidebarGroup[];
}
interface DocsVersion {
@@ -164,14 +165,16 @@ async function main(): Promise<void> {
process.exit(1);
}
const primaryTabSidebar = latestVersion.content.navigation?.tabs?.[0]?.sidebar ?? [];
const sidebar = normalizeSidebar(primaryTabSidebar);
const tabs = latestVersion.content.navigation?.tabs ?? [];
const documents: DocsSearchDocument[] = [];
// Collect all pages from sidebar
// Collect all pages from all tabs' sidebars
const allPages: Array<{ page: DocsPageSource; group: string }> = [];
for (const sidebarGroup of sidebar) {
collectPages(sidebarGroup.pages, sidebarGroup.group, allPages);
for (const tab of tabs) {
const sidebar = normalizeSidebar(tab.sidebar ?? []);
for (const sidebarGroup of sidebar) {
collectPages(sidebarGroup.pages, sidebarGroup.group, allPages);
}
}
console.log(`[index-docs] Indexing version ${latestVersion.slug}`);
+12
View File
@@ -24,6 +24,18 @@ app.use(handler);
//migrations
async function runMigrations() {
try {
// Rename old .js migration entries to .ts in the knex_migrations table
// so Knex can find the renamed files on disk
const hasTable = await db.schema.hasTable("knex_migrations");
if (hasTable) {
const oldJsMigrations = await db("knex_migrations").where("name", "like", "%.js");
for (const row of oldJsMigrations) {
const newName = row.name.replace(/\.js$/, ".ts");
await db("knex_migrations").where("id", row.id).update({ name: newName });
console.log(`Renamed migration record: ${row.name} -> ${newName}`);
}
}
console.log("Running migrations...");
await db.migrate.latest(); // Runs migrations to the latest state
console.log("Migrations completed successfully!");
+2
View File
@@ -30,6 +30,7 @@ export async function seed(knex: Knex): Promise<void> {
page_id: pageId,
monitor_tag: "earth",
monitor_settings_json: "",
position: 0,
created_at: knex.fn.now(),
updated_at: knex.fn.now(),
});
@@ -42,6 +43,7 @@ export async function seed(knex: Knex): Promise<void> {
page_id: pageId,
monitor_tag: "kener",
monitor_settings_json: "",
position: 1,
created_at: knex.fn.now(),
updated_at: knex.fn.now(),
});
+35 -1
View File
@@ -1,4 +1,5 @@
import { json, type Handle } from "@sveltejs/kit";
import { sequence } from "@sveltejs/kit/hooks";
import { VerifyAPIKey } from "$lib/server/controllers/apiController";
import db from "$lib/server/db/db";
import type { UnauthorizedResponse, NotFoundResponse } from "$lib/types/api";
@@ -58,7 +59,38 @@ function extractPagePath(pathname: string): string | null {
return match ? decodeURIComponent(match[1]) : null;
}
export const handle: Handle = async ({ event, resolve }) => {
// Content types that indicate a form submission (mirrors SvelteKit's internal CSRF check scope)
const FORM_CONTENT_TYPES = ["application/x-www-form-urlencoded", "multipart/form-data", "text/plain"];
function isFormContentType(request: Request): boolean {
const type = request.headers.get("content-type")?.split(";", 1)[0].trim()?.toLowerCase() ?? "";
return FORM_CONTENT_TYPES.includes(type);
}
// Custom CSRF handler: validates Origin when present, allows requests when absent.
// When Origin is absent (e.g. Referrer-Policy: no-referrer), security relies on
// SameSite=Lax cookies which prevent cross-site POST from carrying auth cookies.
const csrfHandle: Handle = async ({ event, resolve }) => {
const { request } = event;
if (
isFormContentType(request) &&
(request.method === "POST" || request.method === "PUT" || request.method === "PATCH" || request.method === "DELETE")
) {
const requestOrigin = request.headers.get("origin");
if (requestOrigin && requestOrigin !== "null") {
const requestHost = new URL(requestOrigin).host;
const expectedHost = event.url.host;
if (requestHost !== expectedHost) {
return new Response(`Cross-site ${request.method} form submissions are forbidden`, { status: 403 });
}
}
}
return resolve(event);
};
const apiAuthHandle: Handle = async ({ event, resolve }) => {
const { pathname } = event.url;
// Check if this is an API route that requires authentication
@@ -160,3 +192,5 @@ export const handle: Handle = async ({ event, resolve }) => {
response.headers.delete("Link");
return response;
};
export const handle = sequence(csrfHandle, apiAuthHandle);
@@ -1,188 +0,0 @@
<script lang="ts">
import { Badge } from "$lib/components/ui/badge/index.js";
import Clock from "@lucide/svelte/icons/clock";
import CalendarClock from "@lucide/svelte/icons/calendar-clock";
import Timer from "@lucide/svelte/icons/timer";
import { t } from "$lib/stores/i18n";
import { formatDate, formatDuration } from "$lib/stores/datetime";
import { resolve } from "$app/paths";
import clientResolver from "$lib/client/resolver.js";
interface Maintenance {
id: number;
title: string;
description?: string | null;
start_date_time: number;
end_date_time: number;
monitor_tag?: string;
}
interface Props {
ongoingMaintenances?: Maintenance[];
upcomingMaintenances?: Maintenance[];
pastMaintenances?: Maintenance[];
class?: string;
}
let {
ongoingMaintenances = [],
upcomingMaintenances = [],
pastMaintenances = [],
class: className = ""
}: Props = $props();
// Deduplicate maintenances by id (can have duplicates due to multiple monitors)
function deduplicateMaintenances(maintenances: Maintenance[]): Maintenance[] {
const seen = new Set<number>();
return maintenances.filter((m) => {
if (seen.has(m.id)) return false;
seen.add(m.id);
return true;
});
}
// Deduplicated arrays
let uniqueOngoing = $derived(deduplicateMaintenances(ongoingMaintenances));
let uniqueUpcoming = $derived(deduplicateMaintenances(upcomingMaintenances));
let uniquePast = $derived(deduplicateMaintenances(pastMaintenances));
// Check if there's any maintenance data
let hasAnyData = $derived(uniqueOngoing.length > 0 || uniqueUpcoming.length > 0 || uniquePast.length > 0);
</script>
{#if hasAnyData}
<div class="bg-background rounded-3xl border {className}">
<div class=" flex items-center justify-between p-4">
<Badge variant="secondary" class="gap-1">{$t("Maintenances")}</Badge>
</div>
<div class="grid grid-cols-1 gap-0 lg:grid-cols-3">
<!-- Ongoing Maintenances -->
<div class="flex flex-col lg:border-r">
<div class="text-muted-foreground bg-secondary flex items-center justify-between gap-2 p-4 text-sm font-medium">
<div class="flex items-center gap-2">
<div class="bg-maintenance h-2 w-2 rounded-full"></div>
{$t("Ongoing")}
</div>
<div class="text-maintenance">
<span>{uniqueOngoing.length}</span>
</div>
</div>
<div class="scrollbar-hidden max-h-64 overflow-y-auto">
{#if uniqueOngoing.length === 0}
<p class="text-muted-foreground py-4 text-center text-xs">{$t("No ongoing maintenances")}</p>
{:else}
{#each uniqueOngoing as maintenance (maintenance.id)}
<a
href={clientResolver(resolve, `/maintenances/${maintenance.id}`)}
class="hover:bg-muted/50 block border-b p-3 transition-colors last:border-0"
>
<h4 class="line-clamp-2 text-sm leading-tight font-medium">{maintenance.title}</h4>
{#if maintenance.description}
<p class="text-muted-foreground mt-1 line-clamp-3 text-xs leading-relaxed">
{maintenance.description}
</p>
{/if}
<div class="text-muted-foreground mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs">
<span class="flex items-center gap-1">
<Clock class="h-3 w-3" />
{$formatDate(maintenance.start_date_time, "MMM d, HH:mm")}
</span>
<span class="flex items-center gap-1">
<Timer class="h-3 w-3" />
{$formatDuration(maintenance.start_date_time, maintenance.end_date_time)}
</span>
</div>
</a>
{/each}
{/if}
</div>
</div>
<!-- Upcoming Maintenances -->
<div class="flex flex-col lg:border-r">
<div class="text-muted-foreground bg-secondary flex items-center justify-between gap-2 p-4 text-sm font-medium">
<div class="flex items-center gap-2">
<div class="bg-primary h-2 w-2 rounded-full"></div>
{$t("Upcoming")}
</div>
<div>
{uniqueUpcoming.length}
</div>
</div>
<div class="scrollbar-hidden max-h-64 overflow-y-auto">
{#if uniqueUpcoming.length === 0}
<p class="text-muted-foreground py-4 text-center text-xs">{$t("No upcoming maintenances")}</p>
{:else}
{#each uniqueUpcoming as maintenance (maintenance.id)}
<a
href={clientResolver(resolve, `/maintenances/${maintenance.id}`)}
class="hover:bg-muted/50 block border-b p-3 transition-colors last:border-0"
>
<h4 class="line-clamp-2 text-sm leading-tight font-medium">{maintenance.title}</h4>
{#if maintenance.description}
<p class="text-muted-foreground mt-1 line-clamp-3 text-xs leading-relaxed">
{maintenance.description}
</p>
{/if}
<div class="text-muted-foreground mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs">
<span class="flex items-center gap-1">
<CalendarClock class="h-3 w-3" />
{$formatDate(maintenance.start_date_time, "MMM d, HH:mm")}
</span>
<span class="flex items-center gap-1">
<Timer class="h-3 w-3" />
{$formatDuration(maintenance.start_date_time, maintenance.end_date_time)}
</span>
</div>
</a>
{/each}
{/if}
</div>
</div>
<!-- Past Maintenances -->
<div class="flex flex-col">
<div class="text-muted-foreground bg-secondary flex items-center justify-between gap-2 p-4 text-sm font-medium">
<div class="flex items-center gap-2">
<div class="bg-muted-foreground h-2 w-2 rounded-full"></div>
{$t("Past")}
</div>
<div>
{uniquePast.length}
</div>
</div>
<div class="scrollbar-hidden max-h-64 overflow-y-auto">
{#if uniquePast.length === 0}
<p class="text-muted-foreground py-4 text-center text-xs">{$t("No past maintenances")}</p>
{:else}
{#each uniquePast as maintenance (maintenance.id)}
<a
href={clientResolver(resolve, `/maintenances/${maintenance.id}`)}
class="hover:bg-muted/50 block border-b p-3 transition-colors last:border-0"
>
<h4 class="line-clamp-2 text-sm leading-tight font-medium">{maintenance.title}</h4>
{#if maintenance.description}
<p class="text-muted-foreground mt-1 line-clamp-3 text-xs leading-relaxed">
{maintenance.description}
</p>
{/if}
<div class="text-muted-foreground mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs">
<span class="flex items-center gap-1">
<Clock class="h-3 w-3" />
{$formatDate(maintenance.start_date_time, "MMM d, HH:mm")}
</span>
<span class="flex items-center gap-1">
<Timer class="h-3 w-3" />
{$formatDuration(maintenance.start_date_time, maintenance.end_date_time)}
</span>
</div>
</a>
{/each}
{/if}
</div>
</div>
</div>
</div>
{/if}
+8 -8
View File
@@ -43,7 +43,7 @@
<Item.Root class="items-start p-0 {className} sm:items-center">
<Item.Content class="min-w-0 flex-1">
<div class="flex flex-col items-start justify-start gap-0.5">
<span class="text-xs font-medium text-{incident.state.toLowerCase()}">{incident.state}</span>
<span class="text-xs font-medium text-{incident.state.toLowerCase()}">{$t(incident.state)}</span>
<Item.Title class="min-w-0 text-base wrap-break-word break-all">
<a {target} class="hover:underline" href={clientResolver(resolve, `/incidents/${incident.id}`)}>
{incident.title}
@@ -105,7 +105,7 @@
class="mt-2 flex w-full flex-col gap-2 text-xs font-medium sm:flex-row sm:items-center sm:justify-between"
>
<span class="max-w-full rounded-full border px-3 py-2 wrap-break-word">
{$formatDate(incident.start_date_time, "PPp")}
{$formatDate(incident.start_date_time, page.data.dateAndTimeFormat.datePlusTime)}
</span>
<span class="relative w-full text-center sm:flex-1">
<span
@@ -117,7 +117,7 @@
</span>
{#if incident.end_date_time}
<span class="max-w-full rounded-full border px-3 py-2 wrap-break-word">
{$formatDate(incident.end_date_time, "PPp")}
{$formatDate(incident.end_date_time, page.data.dateAndTimeFormat.datePlusTime)}
</span>
{:else}
<span class="max-w-full rounded-full border px-3 py-2 wrap-break-word">
@@ -128,14 +128,14 @@
{#if showSummary}
<div class="my-2 grid grid-cols-1 gap-4 text-xs font-medium sm:grid-cols-3">
<div class="text-muted-foreground bg-secondary flex items-center justify-between rounded-full border p-2 px-4">
<span>Last Updated</span>
<span>{$formatDate(incident.updated_at, "PPp")}</span>
<span>{$t("Last Updated")}</span>
<span>{$formatDate(incident.updated_at, page.data.dateAndTimeFormat.datePlusTime)}</span>
</div>
<div class="text-muted-foreground bg-secondary flex items-center justify-between rounded-full border p-2 px-4">
<span>Status</span>
<span>{$t("Status")}</span>
<div class="flex items-center gap-2">
<span class="text-{incident.state.toLowerCase()}">
{incident.state}
{$t(incident.state)}
</span>
</div>
</div>
@@ -167,7 +167,7 @@
{$t(comment.state)}
</Badge>
<span class="text-muted-foreground text-xs">
{$formatDate(comment.commented_at, "PPp")}
{$formatDate(comment.commented_at, page.data.dateAndTimeFormat.datePlusTime)}
</span>
</div>
<div
+10 -2
View File
@@ -11,7 +11,15 @@
let { data } = page;
const navItems: { name: string; url: string; iconURL: string }[] = data.navItems || [];
const { siteName, siteUrl, logo } = data;
const { siteName, logo, globalPageVisibilitySettings } = data;
const brandPath = $derived.by(() => {
if (globalPageVisibilitySettings?.forceExclusivity) {
const currentPagePath = page.params?.page_path?.trim();
return currentPagePath ? `/${currentPagePath}` : "/";
}
return "/";
});
function trackBrandClick() {
trackEvent("nav_brand_clicked", { name: siteName });
@@ -29,7 +37,7 @@
>
<!-- Brand -->
<a
href={clientResolver(resolve, siteUrl)}
href={clientResolver(resolve, brandPath)}
class="{navigationMenuTriggerStyle()} hover:border-border border border-transparent bg-transparent text-xs hover:bg-transparent"
style="border-radius: var(--radius-3xl)"
onclick={trackBrandClick}
+4 -1
View File
@@ -6,6 +6,7 @@
import { t } from "$lib/stores/i18n";
import { ParseLatency } from "$lib/clientTools";
import { formatDate } from "$lib/stores/datetime";
import { page } from "$app/state";
interface ChartPoint {
date: Date;
@@ -82,7 +83,9 @@
></div>
<div class="flex flex-1 flex-col items-start justify-between gap-1 leading-none">
<span class="text-muted-foreground text-xs"
>{item.payload?.date ? $formatDate(item.payload.date, "MMM d") : ""}</span
>{item.payload?.date
? $formatDate(item.payload.date, page.data.dateAndTimeFormat.dateOnly)
: ""}</span
>
<div class="flex items-center gap-2">
<span class="text-foreground font-mono font-medium tabular-nums">
+3 -3
View File
@@ -33,7 +33,7 @@
<Item.Root class="items-start p-0 {className} sm:items-center">
<Item.Content class="min-w-0 flex-1">
<div class="flex flex-col items-start justify-start gap-0.5">
<span class="text-xs font-medium text-{maintenance.status.toLowerCase()}">{maintenance.status}</span>
<span class="text-xs font-medium text-{maintenance.status.toLowerCase()}">{$t(maintenance.status)}</span>
<Item.Title class="min-w-0 text-base wrap-break-word break-all">
<a {target} class="hover:underline" href={clientResolver(resolve, `/maintenances/${maintenance.id}`)}
>{maintenance.title}</a
@@ -97,7 +97,7 @@
class="mt-2 flex w-full flex-col gap-2 text-xs font-medium sm:flex-row sm:items-center sm:justify-between"
>
<span class="max-w-full rounded-full border px-3 py-2 wrap-break-word">
{$formatDate(maintenance.start_date_time, "PPp")}
{$formatDate(maintenance.start_date_time, page.data.dateAndTimeFormat.datePlusTime)}
</span>
<span class="relative w-full text-center sm:flex-1">
<span
@@ -108,7 +108,7 @@
</span>
</span>
<span class="max-w-full rounded-full border px-3 py-2 wrap-break-word">
{$formatDate(maintenance.end_date_time, "PPp")}
{$formatDate(maintenance.end_date_time, page.data.dateAndTimeFormat.datePlusTime)}
</span>
</Item.Description>
</Item.Content>
+7 -2
View File
@@ -5,6 +5,8 @@
import TrendingUp from "@lucide/svelte/icons/trending-up";
import { t } from "$lib/stores/i18n";
import { formatDate } from "$lib/stores/datetime";
import { selectedTimezone } from "$lib/stores/timezone";
import { toZonedTime } from "date-fns-tz";
import * as Tooltip from "$lib/components/ui/tooltip/index.js";
interface MinuteData {
@@ -74,7 +76,7 @@
const minutesByHour: Map<number, MinuteData[]> = new Map();
for (const minute of minutes) {
const date = new Date(minute.timestamp * 1000);
const date = toZonedTime(minute.timestamp * 1000, $selectedTimezone);
const hour = date.getHours();
if (!minutesByHour.has(hour)) {
@@ -319,7 +321,10 @@
style={tooltipStyle}
>
<span class="text-{hoveredMinute.data.status.toLowerCase()}">
{$t(hoveredMinute.data.status)} @ {$formatDate(hoveredMinute.data.timestamp, "HH:mm")}
{$t(hoveredMinute.data.status)} @ {$formatDate(
hoveredMinute.data.timestamp,
page.data.dateAndTimeFormat.timeOnly
)}
</span>
</div>
{/if}
+3 -3
View File
@@ -1,6 +1,5 @@
<script lang="ts">
import * as Item from "$lib/components/ui/item/index.js";
import { Button } from "$lib/components/ui/button/index.js";
import { Skeleton } from "$lib/components/ui/skeleton/index.js";
import * as Avatar from "$lib/components/ui/avatar/index.js";
import ICONS from "$lib/icons";
@@ -12,6 +11,7 @@
import { GetInitials } from "$lib/clientTools.js";
import GroupMonitorPopover from "./GroupMonitorPopover.svelte";
import { t } from "$lib/stores/i18n";
import { page } from "$app/state";
interface Props {
tag: string;
@@ -136,11 +136,11 @@
<StatusBarCalendar data={data.uptimeData} monitorTag={tag} barHeight={40} radius={8} />
<div class="flex min-w-0 justify-between gap-3">
<p class="text-muted-foreground min-w-0 truncate text-xs font-medium">
{$formatDate(new Date(data.fromTimeStamp * 1000), "MMM d, yyyy")}
{$formatDate(new Date(data.fromTimeStamp * 1000), page.data.dateAndTimeFormat.dateOnly)}
</p>
<p class="text-muted-foreground min-w-0 truncate text-right text-xs font-medium">
{$formatDate(new Date(data.toTimeStamp * 1000), "MMM d, yyyy")}
{$formatDate(new Date(data.toTimeStamp * 1000), page.data.dateAndTimeFormat.dateOnly)}
</p>
</div>
</div>
+8 -11
View File
@@ -3,24 +3,20 @@
import * as Tooltip from "$lib/components/ui/tooltip/index.js";
import { resolve } from "$app/paths";
import TrendingUp from "@lucide/svelte/icons/trending-up";
import Clock from "@lucide/svelte/icons/clock";
import Activity from "@lucide/svelte/icons/activity";
import { Button } from "$lib/components/ui/button/index.js";
import * as Dialog from "$lib/components/ui/dialog/index.js";
import { Badge } from "$lib/components/ui/badge/index.js";
import { Skeleton } from "$lib/components/ui/skeleton/index.js";
import LoaderBoxes from "$lib/components/loaderbox.svelte";
import constants from "$lib/global-constants.js";
import * as Tabs from "$lib/components/ui/tabs/index.js";
import { page } from "$app/state";
import { AreaChart, Area, LinearGradient } from "layerchart";
import { curveCatmullRom } from "d3-shape";
import { scaleOrdinal, scaleSequential, scaleTime } from "d3-scale";
import { scaleTime } from "d3-scale";
import IncidentItem from "$lib/components/IncidentItem.svelte";
import MaintenanceItem from "$lib/components/MaintenanceItem.svelte";
import MinuteGrid from "$lib/components/MinuteGrid.svelte";
import clientResolver from "$lib/client/resolver.js";
import { ParseLatency } from "$lib/clientTools";
import * as Chart from "$lib/components/ui/chart/index.js";
import type { IncidentForMonitorListWithComments, MaintenanceEventsMonitorList } from "$lib/server/types/db";
@@ -213,9 +209,8 @@
<Dialog.Content class="max-h-[90vh] overflow-y-auto rounded-3xl p-4 sm:max-w-[46.5rem] sm:p-6">
<Dialog.Header>
<Dialog.Title class="flex items-center gap-2 text-base sm:text-lg">
<Activity class="h-4 w-4 shrink-0 sm:h-5 sm:w-5" />
<span class="truncate">
{selectedDay ? $formatDate(new Date(selectedDay.timestamp * 1000), "EEEE, MMMM do, yyyy") : ""}
{selectedDay ? $formatDate(new Date(selectedDay.timestamp * 1000), page.data.dateAndTimeFormat.dateOnly) : ""}
</span>
</Dialog.Title>
<Dialog.Description class="text-xs sm:text-sm"
@@ -316,7 +311,7 @@
line: { class: "stroke-1" }
},
xAxis: {
format: (d: Date) => $formatDate(d, "HH:mm")
format: (d: Date) => $formatDate(d, page.data.dateAndTimeFormat.timeOnly)
}
}}
>
@@ -342,11 +337,13 @@
></div>
<div class="flex flex-1 flex-col items-start justify-between gap-1 leading-none">
<span class="text-muted-foreground text-xs">
{item.payload?.date ? $formatDate(item.payload.date, "HH:mm") : ""}
{item.payload?.date
? $formatDate(item.payload.date, page.data.dateAndTimeFormat.timeOnly)
: ""}
</span>
<div class="flex items-center gap-2">
<span class="text-foreground font-mono font-medium tabular-nums">
{Math.round(Number(value))} ms
{ParseLatency(Math.round(Number(value)))}
</span>
</div>
</div>
+3 -3
View File
@@ -2,7 +2,6 @@
import { onMount, untrack } from "svelte";
import * as Card from "$lib/components/ui/card/index.js";
import { Skeleton } from "$lib/components/ui/skeleton/index.js";
import { Badge } from "$lib/components/ui/badge/index.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
import StatusBarCalendar from "$lib/components/StatusBarCalendar.svelte";
import LatencyTrendChart from "$lib/components/LatencyTrendChart.svelte";
@@ -18,6 +17,7 @@
import * as Popover from "$lib/components/ui/popover/index.js";
import * as ToggleGroup from "$lib/components/ui/toggle-group/index.js";
import GroupMonitorPopover from "$lib/components/GroupMonitorPopover.svelte";
import { page } from "$app/state";
interface Props {
monitorTag: string;
@@ -205,12 +205,12 @@
<div class="flex justify-between">
<p class="text-muted-foreground text-xs font-medium">
{#if displayData.length > 0}
{$formatDate(displayData[0].ts, "d MMM yyyy")}
{$formatDate(displayData[0].ts, page.data.dateAndTimeFormat.dateOnly)}
{/if}
</p>
<p class="text-muted-foreground text-xs font-medium">
{#if displayData.length > 0}
{$formatDate(displayData[displayData.length - 1].ts, "d MMM yyyy")}
{$formatDate(displayData[displayData.length - 1].ts, page.data.dateAndTimeFormat.dateOnly)}
{/if}
</p>
</div>
+19 -6
View File
@@ -1,14 +1,16 @@
<script lang="ts">
import { resolve } from "$app/paths";
import { page } from "$app/state";
import { Button } from "$lib/components/ui/button/index.js";
import * as Popover from "$lib/components/ui/popover/index.js";
import { Spinner } from "$lib/components/ui/spinner/index.js";
import ICONS from "$lib/icons";
import clientResolver from "$lib/client/resolver.js";
import { formatDate } from "$lib/stores/datetime";
import { formatDate, formatDuration } from "$lib/stores/datetime";
import { t } from "$lib/stores/i18n";
import type { NotificationEvent } from "$lib/server/controllers/dashboardController.js";
import Calendar from "@lucide/svelte/icons/calendar-1";
import { format } from "date-fns";
import { onMount } from "svelte";
interface Props {
@@ -22,6 +24,17 @@
let notifications = $state<NotificationEvent[]>([]);
let loading = $state(false);
const defaultEventsPath = $derived(`/events/${format(new Date(), "MMMM-yyyy")}`);
const resolvedEventsPath = $derived.by(() => {
const finalEventsPath = eventsPath || defaultEventsPath;
if (page.data?.globalPageVisibilitySettings?.forceExclusivity) {
const currentPagePath = page.params?.page_path?.trim();
return currentPagePath ? `/${currentPagePath}${finalEventsPath}` : finalEventsPath;
}
return finalEventsPath;
});
async function fetchNotifications() {
loading = true;
try {
@@ -76,7 +89,7 @@
>
<div class="flex items-center justify-between border-b px-4 py-3">
<h4 class="text-sm font-semibold">{$t("Events")}</h4>
<Button variant="outline" href={clientResolver(resolve, eventsPath)} size="icon-sm" class="rounded-btn">
<Button variant="outline" href={clientResolver(resolve, resolvedEventsPath)} size="icon-sm" class="rounded-btn">
<Calendar class="size-4" />
</Button>
</div>
@@ -93,17 +106,17 @@
class="hover:bg-muted/60 block border-b px-4 py-3 last:border-b-0"
>
<div class="my-0.5 flex items-center justify-between gap-2 text-xs">
<span class="text-muted-foreground text-[11px] uppercase">{item.eventType}</span>
<span class="text-{item.eventStatus.toLowerCase()}">{item.eventStatus}</span>
<span class="text-muted-foreground text-[11px] uppercase">{$t(item.eventType)}</span>
<span class="text-{item.eventStatus.toLowerCase()}">{$t(item.eventStatus)}</span>
</div>
<div class="flex items-start justify-between gap-2">
<p class="line-clamp-2 text-sm">{item.eventTitle}</p>
</div>
<div class="text-muted-foreground mt-1 flex flex-wrap items-center gap-2 text-xs">
<span>{$formatDate(item.eventDate, "PPp")}</span>
<span>{$formatDate(item.eventDate, page.data.dateAndTimeFormat.datePlusTime)}</span>
<span></span>
<span>{item.eventDuration}</span>
<span>{$formatDuration(item.eventStartDateTime, item.eventEndDateTime, $t("Ongoing"))}</span>
</div>
</a>
{/each}
+83
View File
@@ -0,0 +1,83 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button/index.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
import { Spinner } from "$lib/components/ui/spinner/index.js";
import { resolve } from "$app/paths";
import clientResolver from "$lib/client/resolver.js";
import { onMount } from "svelte";
import type { PageNavItem } from "$lib/server/controllers/dashboardController.js";
import { page } from "$app/state";
import * as Item from "$lib/components/ui/item/index.js";
import ChevronRight from "@lucide/svelte/icons/chevron-right";
let currentPath = $derived(page.params.page_path);
let pages = $state<PageNavItem[]>([]);
let pagesLoading = $state(false);
const defaultHomePage = $derived(pages.find((p) => p.page_path == ""));
const currentPage = $derived(pages.find((p) => p.page_path === currentPath) || defaultHomePage);
async function fetchPages() {
pagesLoading = true;
try {
const response = await fetch(clientResolver(resolve, "/dashboard-apis/pages"));
if (response.ok) {
pages = await response.json();
console.log(">>>>>>---- PageList:25 ", pages);
}
} catch {
// silently fail, pages dropdown will just not show
} finally {
pagesLoading = false;
}
}
onMount(() => {
fetchPages();
});
</script>
<div class="flex shrink-0 items-center gap-2">
{#if pagesLoading}
<Button
variant="outline"
size="sm"
class="bg-background/80 dark:bg-background/70 border-foreground/10 flex items-center justify-center rounded-full border text-xs shadow-none backdrop-blur-md"
disabled
>
<Spinner class="h-4 w-4" />
</Button>
{:else if pages.length > 0}
<!-- loop through pages -->
<div class="flex w-full flex-col gap-2">
{#each pages as page}
<Item.Root variant="outline" class="rounded-3xl">
{#snippet child({ props })}
<a href={clientResolver(resolve, `/${page.page_path}`)} {...props}>
{#if page.page_logo}
<Item.Media variant="image">
<img
src={page.page_logo}
alt={page.page_title}
width="32"
height="32"
class="size-8 rounded object-cover"
/>
</Item.Media>
{/if}
<Item.Content>
<Item.Title class="line-clamp-1">
{page.page_title}
</Item.Title>
<Item.Description>{page.page_header}</Item.Description>
</Item.Content>
<Item.Actions>
<ChevronRight class="size-4" />
</Item.Actions>
</a>
{/snippet}
</Item.Root>
{/each}
</div>
{/if}
</div>
+2 -2
View File
@@ -13,8 +13,8 @@
let pages = $state<PageNavItem[]>([]);
let pagesLoading = $state(false);
const currentPage = $derived(pages.find((p) => p.page_path === currentPath) || pages[0]);
const defaultHomePage = $derived(pages.find((p) => p.page_path == ""));
const currentPage = $derived(pages.find((p) => p.page_path === currentPath) || defaultHomePage);
async function fetchPages() {
pagesLoading = true;
+1 -1
View File
@@ -375,7 +375,7 @@
>
<span class={getStatusColor(hoveredBar.data)}>{$t(GetStatusSummary(hoveredBar.data))}</span>
<span class="text-muted-foreground">@</span>
{$formatDate(hoveredBar.data.ts, "d MMM yyyy")}
{$formatDate(hoveredBar.data.ts, page.data.dateAndTimeFormat.dateOnly)}
{#if hoveredBar.data.avgLatency > 0}
<span class="text-muted-foreground ml-1">|</span>
<span class="ml-1">{ParseLatency(hoveredBar.data.avgLatency)}</span>
+9 -6
View File
@@ -10,7 +10,6 @@
import Sun from "@lucide/svelte/icons/sun";
import Moon from "@lucide/svelte/icons/moon";
import Share from "@lucide/svelte/icons/share-2";
import ChevronLeft from "@lucide/svelte/icons/chevron-left";
import { format } from "date-fns";
import SubscribeMenu from "$lib/components/SubscribeMenu.svelte";
import CopyButton from "$lib/components/CopyButton.svelte";
@@ -41,22 +40,22 @@
if (page.route.id === "/(kener)/monitors/[monitor_tag]") {
return {
label: "Edit Monitor",
label: $t("Edit Monitor"),
url: clientResolver(resolve, "/manage/app/monitors/" + page.params.monitor_tag)
};
} else if (page.route.id === "/(kener)/incidents/[incident_id]") {
return {
label: "Update Incident",
label: $t("Update Incident"),
url: clientResolver(resolve, "/manage/app/incidents/" + page.params.incident_id)
};
} else if (page.route.id === "/(kener)/maintenances/[maintenance_id]") {
return {
label: "Update Maintenance",
label: $t("Update Maintenance"),
url: clientResolver(resolve, "/manage/app/maintenances/" + page.data.maintenance.id)
};
} else {
return {
label: "Manage Site",
label: $t("Manage Site"),
url: clientResolver(resolve, "/manage/app/site-configurations")
};
}
@@ -79,7 +78,10 @@
</script>
<div class="theme-plus-bar scrollbar-hidden sticky top-18 z-20 flex w-full items-center gap-2 rounded py-2">
<PageSelector />
<!-- Show the switcher if not forced exclusivity and switcher is enabled -->
{#if !!!page.data.globalPageVisibilitySettings.forceExclusivity && page.data.globalPageVisibilitySettings.showSwitcher}
<PageSelector />
{/if}
<div class="ml-auto flex shrink-0 items-center gap-2">
{#if page.data.isSubsEnabled && page.data.canSendEmail}
<ButtonGroup.Root class="hidden shrink-0 sm:flex">
@@ -168,6 +170,7 @@
{/if}
</div>
</div>
{#if !!page.data.announcement && !!page.data.announcement.title && !!page.data.announcement.message}
<SiteBanner announcement={page.data.announcement} />
{/if}
+64 -23
View File
@@ -2,21 +2,34 @@
"name": "Čeština",
"mappings": {
"%latency %metric latency": "%latency %metric latence",
"24 Hours": "24 hodin",
"30 Days": "30 dní",
"7 Days": "7 dní",
"90 Days": "90 dní",
"Affected Monitors (%count)": "Zasažené monitory (%count)",
"All Systems Operational": "Všechny systémy jsou v provozu",
"average": "průměrná",
"Average Latency": "Průměrná latence",
"Avg Latency": "Prům. latence",
"Back": "Zpět",
"Badges": "Odznaky",
"CANCELLED": "ZRUŠENO",
"COMPLETED": "DOKONČENO",
"Continue": "Pokračovat",
"Copied": "Zkopírováno",
"Current": "Aktuální",
"Dark": "Tmavý",
"Day": "Den",
"Day Uptime": "Denní dostupnost",
"Days": "Dny",
"Days": "Dní",
"Degraded": "Zhoršený",
"DEGRADED": "ZHORŠENÝ",
"Degraded Performance": "Zhoršený výkon",
"Didn't receive the code? Resend": "Nepřišel vám kód? Odeslat znovu",
"Down": "Nedostupný",
"DOWN": "VÝPADEK",
"Duration": "Trvání",
"Edit Monitor": "Upravit monitor",
"Email address": "E-mailová adresa",
"Embed Monitor": "Vložit monitor",
"Embed this monitor in your website or app": "Vložte tento monitor na svůj web nebo do aplikace",
@@ -28,15 +41,21 @@
"Failed to load status data for this day": "Nepodařilo se načíst data stavu pro tento den",
"Failed to send verification code": "Nepodařilo se odeslat ověřovací kód",
"Failed to update preference": "Nepodařilo se aktualizovat nastavení",
"Format": "Formát",
"Get badges for this monitor": "Získat odznaky pro tento monitor",
"Get notified about incidents and scheduled maintenance.": "Dostávejte upozornění na incidenty a plánované údržby.",
"Get notified about incidents and scheduled maintenance.": "Dostávejte upozornění na incidenty a plánovanou údržbu.",
"Get notified about incidents updates": "Dostávejte upozornění na aktualizace incidentů",
"Get notified about scheduled maintenance": "Dostávejte upozornění na plánovanou údržbu",
"Home": "Domů",
"IDENTIFIED": "IDENTIFIKOVÁNO",
"iFrame": "iFrame",
"Impact": "Dopad",
"incident": "Incident",
"Incident": "Incident",
"Incident Updates": "Aktualizace incidentů",
"Incidents": "Incidenty",
"Included Monitors": "Included Monitors",
"Included Monitors": "Monitorů zahrnuto",
"INVESTIGATING": "VYŠETŘOVÁNÍ",
"Last Updated": "Naposledy aktualizováno",
"Latency": "Latence",
"Latency Embed": "Vložená latence",
@@ -45,34 +64,44 @@
"Latest Latency": "Poslední latence",
"Latest Status": "Poslední stav",
"Light": "Světlý",
"Live Status": "Živý stav",
"Live Status": "Aktuální stav",
"Loading your preferences...": "Načítám vaše nastavení...",
"maintenance": "Údržba",
"Maintenance": "Údržba",
"MAINTENANCE": "ÚDRŽBA",
"Maintenance Updates": "Aktualizace údržby",
"Maintenances": "Údržby",
"Maintenances": "Údrždy",
"Major System Outage": "Závažný výpadek systému",
"Manage Site": "Spravovat stránku",
"Manage your notification preferences.": "Spravujte svá nastavení oznámení.",
"Max Latency": "Max. latence",
"Maximum Latency": "Max. latence",
"maximum": "maximální",
"Maximum Latency": "Maximální latence",
"Min Latency": "Min. latence",
"Minimum Latency": "Min. latence",
"minimum": "minimální",
"Minimum Latency": "Minimální latence",
"Minute-by-minute status data for this day": "Minutová data stavu pro tento den",
"MONITORING": "MONITOROVÁNÍ",
"Network error. Please try again.": "Chyba sítě. Zkuste to prosím znovu.",
"No Events in %currentMonth": "V měsíci %currentMonth nejsou žádné události",
"No events to show": "No events to show",
"No incidents for this day": "Pro tento den nejsou žádné incidenty",
"No Events in %currentMonth": "V měsíci %currentMonth nejsou plánované žádné události",
"No events to show": "Žádné události k zobrazení",
"No incidents for this day": "Pro tento den nejsou evidovány žádné incidenty",
"No latency data available for this day": "Pro tento den nejsou k dispozici data latence",
"No maintenances for this day": "Pro tento den nejsou naplánované žádné údržby",
"No maintenances for this day": "Pro tento den ne naplánovaná žádná údržba",
"No monitors affected": "Žádné zasažené monitory",
"No monitors available.": "No monitors available.",
"No ongoing maintenances": "Žádné probíhající údržby",
"No past maintenances": "Žádné minulé údržby",
"No monitors available.": "Žádné dostupné monitory.",
"No ongoing maintenances": "Žádná probíhající údržba",
"No past maintenances": "Žádná minulá údržba",
"No Status Available": "Stav není k dispozici",
"No upcoming maintenances": "Žádné nadcházející údržby",
"No Updates": "No Updates",
"No upcoming maintenances": "Žádná nadcházející údržba",
"No Updates": "Žádné aktualizace",
"No updates yet": "Zatím bez aktualizací",
"Notifications": "Notifications",
"NO_DATA": "Žádná data",
"Notifications": "Oznámení",
"One-time": "Jednorázově",
"Ongoing": "Probíhající",
"Ongoing Maintenances": "Probíhající údržby",
"Operational": "V provozu",
"Partial Degraded Performance": "Částečně zhoršený výkon",
"Partial System Outage": "Částečný výpadek systému",
"Past": "Minulé",
@@ -82,28 +111,40 @@
"Please enter the 6-digit verification code": "Zadejte prosím 6místný ověřovací kód",
"Read less": "Zobrazit méně",
"Read more": "Zobrazit více",
"Recurring": "Recurring",
"READY": "PŘIPRAVENO",
"Recent Incidents": "Nedávné incidenty",
"Recurring": "Opakující se",
"RESOLVED": "VYŘEŠENO",
"SCHEDULED": "NAPLÁNOVÁNO",
"Scheduled Events (%count)": "Plánované události (%count)",
"Scheduled Windows": "Naplánované úlohy",
"Script": "Skript",
"Select Language": "Vyberte jazyk",
"Select latency metric to display": "Select latency metric to display",
"Select latency metric to display": "Vyberte metriku latence k zobrazení",
"Select Range": "Vyberte rozsah",
"Sending...": "Odesílání...",
"Standard": "Standardní",
"Start Time": "Začátek",
"Status": "Stav",
"Status Badge": "Odznak stavu",
"Status Embed": "Vložený stav",
"Status Embed": "Status pro vložení",
"Status history and latency trend": "Historie stavu a trend latence",
"Subscribe": "Odebírat",
"Subscribe to Updates": "Odebírat aktualizace",
"There are no incidents or maintenances scheduled for this month.": "Na tento měsíc nejsou naplánované žádné incidenty ani údržby.",
"There are no ongoing incidents or maintenance events.": "There are no ongoing incidents or maintenance events.",
"Theme": "Motiv",
"There are no incidents or maintenances scheduled for this month.": "Na tento měsíc nejsou naplánované incidenty ani údržba.",
"There are no ongoing incidents or maintenance events.": "V tuto chvíli neprobíhají žádné incidenty ani údržba.",
"Timeline": "Časová osa",
"Total Incidents": "Celkem incidentů",
"Total Maintenances": "Celkem údržeb",
"Under Maintenance": "Probíhá údržba",
"Unknown impact": "Neznámý dopad",
"UP": "V PROVOZU",
"Upcoming": "Nadcházející",
"Updates": "Updates",
"Upcoming Maintenances": "Nadcházející údržby",
"Update Incident": "Aktualizovat incident",
"Update Maintenance": "Aktualizovat údržbu",
"Updates": "Aktualizace",
"Updates (%count)": "Aktualizace (%count)",
"Uptime": "Dostupnost",
"Uptime Badge": "Odznak dostupnosti",
+33 -11
View File
@@ -8,14 +8,21 @@
"Avg Latency": "Durchschnittliche Latenz",
"Back": "Zurück",
"Badges": "Abzeichen",
"CANCELLED": "ABGESAGT",
"COMPLETED": "ABGESCHLOSSEN",
"Continue": "Weitermachen",
"Copied": "Kopiert",
"Current": "Aktuell",
"Dark": "Dunkel",
"Day": "Tag",
"Day Uptime": "Tagesverfügbarkeit",
"Days": "Tage",
"Degraded": "Beeinträchtigt",
"DEGRADED": "BEEINTRÄCHTIGT",
"Degraded Performance": "Beeinträchtigte Leistung",
"Didn't receive the code? Resend": "Sie haben den Code nicht erhalten? ",
"Down": "Ausgefallen",
"DOWN": "AUSGEFALLEN",
"Duration": "Dauer",
"Email address": "E-Mail-Adresse",
"Embed Monitor": "Monitor einbetten",
@@ -32,11 +39,14 @@
"Get notified about incidents and scheduled maintenance.": "Lassen Sie sich über Vorfälle und geplante Wartungsarbeiten benachrichtigen.",
"Get notified about incidents updates": "Lassen Sie sich über Aktualisierungen von Vorfällen benachrichtigen",
"Get notified about scheduled maintenance": "Lassen Sie sich über geplante Wartungsarbeiten benachrichtigen",
"IDENTIFIED": "IDENTIFIZIERT",
"iFrame": "iFrame",
"Impact": "Auswirkungen",
"incident": "Vorfall",
"Incident Updates": "Vorfallaktualisierungen",
"Incidents": "Vorfälle",
"Included Monitors": "Included Monitors",
"Included Monitors": "Enthaltene Monitore",
"INVESTIGATING": "WIRD UNTERSUCHT",
"Last Updated": "Zuletzt aktualisiert",
"Latency": "Latenz",
"Latency Embed": "Latenz-Einbettung",
@@ -44,35 +54,40 @@
"Latency Trend": "Latenztrend",
"Latest Latency": "Neueste Latenz",
"Latest Status": "Neuester Stand",
"Light": "Licht",
"Light": "Hell",
"Live Status": "Live-Status",
"Loading your preferences...": "Deine Einstellungen werden geladen...",
"maintenance": "Wartung",
"MAINTENANCE": "WARTUNG",
"Maintenance Updates": "Wartungsaktualisierungen",
"Maintenances": "Wartungen",
"Major System Outage": "Schwerwiegender Systemausfall",
"Manage Site": "Seite verwalten",
"Manage your notification preferences.": "Verwalten Sie Ihre Benachrichtigungseinstellungen.",
"Max Latency": "Maximale Latenz",
"Maximum Latency": "Maximale Latenz",
"Min Latency": "Min. Latenz",
"Minimum Latency": "Min. Latenz",
"Minute-by-minute status data for this day": "Minutenweise Statusdaten für diesen Tag",
"MONITORING": "WIRD ÜBERWACHT",
"Network error. Please try again.": "Netzwerkfehler. ",
"No Events in %currentMonth": "Keine Ereignisse in %currentMonth",
"No events to show": "No events to show",
"No events to show": "Keine Ereignisse zum Anzeigen",
"No incidents for this day": "Keine Vorfälle für diesen Tag",
"No latency data available for this day": "Für diesen Tag sind keine Latenzdaten verfügbar",
"No maintenances for this day": "An diesem Tag finden keine Wartungsarbeiten statt",
"No monitors affected": "Keine Monitore betroffen",
"No monitors available.": "No monitors available.",
"No monitors available.": "Keine Monitore verfügbar.",
"No ongoing maintenances": "Keine laufenden Wartungsarbeiten",
"No past maintenances": "Keine früheren Wartungsarbeiten",
"No Status Available": "Kein Status verfügbar",
"No upcoming maintenances": "Keine bevorstehenden Wartungsarbeiten",
"No Updates": "No Updates",
"No Updates": "Keine Aktualisierungen",
"No updates yet": "Noch keine Updates",
"Notifications": "Notifications",
"Notifications": "Benachrichtigungen",
"One-time": "Einmalig",
"Ongoing": "Laufend",
"Operational": "Betriebsbereit",
"Partial Degraded Performance": "Teilweise beeinträchtigte Leistung",
"Partial System Outage": "Teilweiser Systemausfall",
"Past": "Vergangenheit",
@@ -82,10 +97,14 @@
"Please enter the 6-digit verification code": "Bitte geben Sie den 6-stelligen Bestätigungscode ein",
"Read less": "Weniger lesen",
"Read more": "Mehr lesen",
"Recurring": "Recurring",
"READY": "BEREIT",
"Recurring": "Wiederkehrend",
"RESOLVED": "BEHOBEN",
"SCHEDULED": "GEPLANT",
"Scheduled Events (%count)": "Geplante Ereignisse (%count)",
"Script": "Skript",
"Select Language": "Wählen Sie Sprache aus",
"Select latency metric to display": "Select latency metric to display",
"Select latency metric to display": "Latenzmetrik zur Anzeige auswählen",
"Select Range": "Wählen Sie Bereich aus",
"Sending...": "Senden...",
"Standard": "Standard",
@@ -95,15 +114,18 @@
"Status Embed": "Status einbetten",
"Status history and latency trend": "Statusverlauf und Latenztrend",
"Subscribe": "Abonnieren",
"Subscribe to Updates": "Abonnieren Sie Updates",
"Subscribe to Updates": "Benachrichtigungen erhalten",
"There are no incidents or maintenances scheduled for this month.": "Für diesen Monat sind keine Vorfälle oder Wartungsarbeiten geplant.",
"There are no ongoing incidents or maintenance events.": "There are no ongoing incidents or maintenance events.",
"There are no ongoing incidents or maintenance events.": "Es gibt keine laufenden Vorfälle oder Wartungsereignisse.",
"Total Incidents": "Gesamtzahl der Vorfälle",
"Total Maintenances": "Gesamtwartungen",
"Under Maintenance": "Unter Wartung",
"Unknown impact": "Unbekannte Auswirkung",
"UP": "AKTIV",
"Upcoming": "Demnächst",
"Updates": "Updates",
"Update Incident": "Vorfall aktualisieren",
"Update Maintenance": "Wartung aktualisieren",
"Updates": "Aktualisierungen",
"Updates (%count)": "Aktualisierungen (%count)",
"Uptime": "Betriebszeit",
"Uptime Badge": "Verfügbarkeitsabzeichen",
+30 -8
View File
@@ -8,14 +8,21 @@
"Avg Latency": "Gns. latens",
"Back": "Tilbage",
"Badges": "Badges",
"CANCELLED": "ANNULLERET",
"COMPLETED": "AFSLUTTET",
"Continue": "Fortsætte",
"Copied": "Kopieret",
"Current": "Aktuel",
"Dark": "Mørk",
"Day": "Dag",
"Day Uptime": "Dag oppetid",
"Days": "dage",
"Degraded": "Forringet",
"DEGRADED": "FORRINGET",
"Degraded Performance": "Nedsat ydeevne",
"Didn't receive the code? Resend": "Modtog du ikke koden? ",
"Down": "Nede",
"DOWN": "NEDE",
"Duration": "Varighed",
"Email address": "E-mailadresse",
"Embed Monitor": "Integrer skærm",
@@ -32,11 +39,14 @@
"Get notified about incidents and scheduled maintenance.": "Få besked om hændelser og planlagt vedligeholdelse.",
"Get notified about incidents updates": "Få besked om opdateringer om hændelser",
"Get notified about scheduled maintenance": "Få besked om planlagt vedligeholdelse",
"IDENTIFIED": "IDENTIFIED",
"iFrame": "iFrame",
"Impact": "Indvirkning",
"incident": "Hændelse",
"Incident Updates": "Hændelsesopdateringer",
"Incidents": "Hændelser",
"Included Monitors": "Included Monitors",
"INVESTIGATING": "INVESTIGATING",
"Last Updated": "Sidst opdateret",
"Latency": "Latency",
"Latency Embed": "Latency Embed",
@@ -47,32 +57,37 @@
"Light": "Lys",
"Live Status": "Live status",
"Loading your preferences...": "Indlæser dine præferencer...",
"maintenance": "Vedligeholdelse",
"MAINTENANCE": "VEDLIGEHOLDELSE",
"Maintenance Updates": "Vedligeholdelsesopdateringer",
"Maintenances": "Vedligeholdelse",
"Major System Outage": "Større systemnedbrud",
"Manage Site": "Administrer side",
"Manage your notification preferences.": "Administrer dine meddelelsespræferencer.",
"Max Latency": "Max latens",
"Maximum Latency": "Max latens",
"Min Latency": "Min latens",
"Minimum Latency": "Min latens",
"Minute-by-minute status data for this day": "Minut for minut statusdata for denne dag",
"MONITORING": "MONITORING",
"Network error. Please try again.": "Netværksfejl. ",
"No Events in %currentMonth": "Ingen begivenheder i %currentMonth",
"No events to show": "No events to show",
"No events to show": "Ingen hændelser at vise",
"No incidents for this day": "Ingen hændelser denne dag",
"No latency data available for this day": "Ingen forsinkelsesdata tilgængelige for denne dag",
"No maintenances for this day": "Ingen vedligeholdelse denne dag",
"No monitors affected": "Ingen skærme påvirket",
"No monitors available.": "No monitors available.",
"No monitors available.": "Ingen monitorer tilgængelige.",
"No ongoing maintenances": "Ingen løbende vedligeholdelse",
"No past maintenances": "Ingen tidligere vedligeholdelse",
"No Status Available": "Ingen status tilgængelig",
"No upcoming maintenances": "Ingen kommende vedligeholdelse",
"No Updates": "No Updates",
"No Updates": "Ingen opdateringer",
"No updates yet": "Ingen opdateringer endnu",
"Notifications": "Notifications",
"Notifications": "Notifikationer",
"One-time": "Engangs",
"Ongoing": "Løbende",
"Operational": "Operationel",
"Partial Degraded Performance": "Delvist nedsat ydeevne",
"Partial System Outage": "Delvist systemnedbrud",
"Past": "Forbi",
@@ -82,10 +97,14 @@
"Please enter the 6-digit verification code": "Indtast venligst den 6-cifrede bekræftelseskode",
"Read less": "Læs mindre",
"Read more": "Læs mere",
"Recurring": "Recurring",
"READY": "KLAR",
"Recurring": "Tilbagevendende",
"RESOLVED": "RESOLVED",
"SCHEDULED": "PLANLAGT",
"Scheduled Events (%count)": "Planlagte begivenheder (%count)",
"Script": "Manuskript",
"Select Language": "Vælg sprog",
"Select latency metric to display": "Select latency metric to display",
"Select latency metric to display": "Vælg latenstidsmåling, der skal vises",
"Select Range": "Vælg Område",
"Sending...": "Sender...",
"Standard": "Standard",
@@ -97,13 +116,16 @@
"Subscribe": "Abonner",
"Subscribe to Updates": "Abonner på opdateringer",
"There are no incidents or maintenances scheduled for this month.": "Der er ingen hændelser eller vedligeholdelse planlagt i denne måned.",
"There are no ongoing incidents or maintenance events.": "There are no ongoing incidents or maintenance events.",
"There are no ongoing incidents or maintenance events.": "Der er ingen igangværende hændelser eller vedligeholdelsesaktiviteter.",
"Total Incidents": "Samlede hændelser",
"Total Maintenances": "Samlet vedligeholdelse",
"Under Maintenance": "Under Vedligeholdelse",
"Unknown impact": "Ukendt påvirkning",
"UP": "OPPE",
"Upcoming": "Kommende",
"Updates": "Updates",
"Update Incident": "Opdater hændelse",
"Update Maintenance": "Opdater vedligeholdelse",
"Updates": "Opdateringer",
"Updates (%count)": "Opdateringer (%count)",
"Uptime": "Oppetid",
"Uptime Badge": "Oppetidsmærke",
+22
View File
@@ -8,14 +8,21 @@
"Avg Latency": "Avg Latency",
"Back": "Back",
"Badges": "Badges",
"CANCELLED": "CANCELLED",
"COMPLETED": "COMPLETED",
"Continue": "Continue",
"Copied": "Copied",
"Current": "Current",
"Dark": "Dark",
"Day": "Day",
"Day Uptime": "Day Uptime",
"Days": "Days",
"Degraded": "Degraded",
"DEGRADED": "DEGRADED",
"Degraded Performance": "Degraded Performance",
"Didn't receive the code? Resend": "Didn't receive the code? Resend",
"Down": "Down",
"DOWN": "DOWN",
"Duration": "Duration",
"Email address": "Email address",
"Embed Monitor": "Embed Monitor",
@@ -32,11 +39,14 @@
"Get notified about incidents and scheduled maintenance.": "Get notified about incidents and scheduled maintenance.",
"Get notified about incidents updates": "Get notified about incidents updates",
"Get notified about scheduled maintenance": "Get notified about scheduled maintenance",
"IDENTIFIED": "IDENTIFIED",
"iFrame": "iFrame",
"Impact": "Impact",
"incident": "Incident",
"Incident Updates": "Incident Updates",
"Incidents": "Incidents",
"Included Monitors": "Included Monitors",
"INVESTIGATING": "INVESTIGATING",
"Last Updated": "Last Updated",
"Latency": "Latency",
"Latency Embed": "Latency Embed",
@@ -47,15 +57,19 @@
"Light": "Light",
"Live Status": "Live Status",
"Loading your preferences...": "Loading your preferences...",
"maintenance": "Maintenance",
"MAINTENANCE": "MAINTENANCE",
"Maintenance Updates": "Maintenance Updates",
"Maintenances": "Maintenances",
"Major System Outage": "Major System Outage",
"Manage Site": "Manage Site",
"Manage your notification preferences.": "Manage your notification preferences.",
"Max Latency": "Max Latency",
"Maximum Latency": "Max Latency",
"Min Latency": "Min Latency",
"Minimum Latency": "Min Latency",
"Minute-by-minute status data for this day": "Minute-by-minute status data for this day",
"MONITORING": "MONITORING",
"Network error. Please try again.": "Network error. Please try again.",
"No Events in %currentMonth": "No Events in %currentMonth",
"No events to show": "No events to show",
@@ -73,6 +87,7 @@
"Notifications": "Notifications",
"One-time": "One-time",
"Ongoing": "Ongoing",
"Operational": "Operational",
"Partial Degraded Performance": "Partial Degraded Performance",
"Partial System Outage": "Partial System Outage",
"Past": "Past",
@@ -82,7 +97,11 @@
"Please enter the 6-digit verification code": "Please enter the 6-digit verification code",
"Read less": "Read less",
"Read more": "Read more",
"READY": "READY",
"Recurring": "Recurring",
"RESOLVED": "RESOLVED",
"SCHEDULED": "SCHEDULED",
"Scheduled Events (%count)": "Scheduled Events (%count)",
"Script": "Script",
"Select Language": "Select Language",
"Select latency metric to display": "Select latency metric to display",
@@ -102,7 +121,10 @@
"Total Maintenances": "Total Maintenances",
"Under Maintenance": "Under Maintenance",
"Unknown impact": "Unknown impact",
"UP": "UP",
"Upcoming": "Upcoming",
"Update Incident": "Update Incident",
"Update Maintenance": "Update Maintenance",
"Updates": "Updates",
"Updates (%count)": "Updates (%count)",
"Uptime": "Uptime",
+30 -8
View File
@@ -8,14 +8,21 @@
"Avg Latency": "Latencia promedio",
"Back": "Atrás",
"Badges": "Insignias",
"CANCELLED": "CANCELADO",
"COMPLETED": "COMPLETADO",
"Continue": "Continuar",
"Copied": "Copiado",
"Current": "Actual",
"Dark": "Oscuro",
"Day": "Día",
"Day Uptime": "Tiempo de actividad del día",
"Days": "Días",
"Degraded": "Degradado",
"DEGRADED": "DEGRADADO",
"Degraded Performance": "Rendimiento degradado",
"Didn't receive the code? Resend": "¿No recibiste el código? ",
"Down": "Caído",
"DOWN": "CAÍDO",
"Duration": "Duración",
"Email address": "Dirección de correo electrónico",
"Embed Monitor": "Monitor integrado",
@@ -32,11 +39,14 @@
"Get notified about incidents and scheduled maintenance.": "Recibe notificaciones sobre incidencias y mantenimientos programados.",
"Get notified about incidents updates": "Recibir notificaciones sobre actualizaciones de incidentes",
"Get notified about scheduled maintenance": "Recibir notificaciones sobre el mantenimiento programado",
"IDENTIFIED": "IDENTIFIED",
"iFrame": "marco flotante",
"Impact": "Impacto",
"incident": "Incidente",
"Incident Updates": "Actualizaciones de incidentes",
"Incidents": "Incidentes",
"Included Monitors": "Included Monitors",
"INVESTIGATING": "INVESTIGATING",
"Last Updated": "Última actualización",
"Latency": "Estado latente",
"Latency Embed": "Incrustación de latencia",
@@ -47,32 +57,37 @@
"Light": "Luz",
"Live Status": "Estado en vivo",
"Loading your preferences...": "Cargando tus preferencias...",
"maintenance": "Mantenimiento",
"MAINTENANCE": "MANTENIMIENTO",
"Maintenance Updates": "Actualizaciones de mantenimiento",
"Maintenances": "Mantenimientos",
"Major System Outage": "Interrupción importante del sistema",
"Manage Site": "Gestionar sitio",
"Manage your notification preferences.": "Administre sus preferencias de notificación.",
"Max Latency": "Latencia máxima",
"Maximum Latency": "Latencia máxima",
"Min Latency": "Latencia mínima",
"Minimum Latency": "Latencia mínima",
"Minute-by-minute status data for this day": "Datos del estado minuto a minuto de este día",
"MONITORING": "MONITORING",
"Network error. Please try again.": "Error de red. ",
"No Events in %currentMonth": "No hay eventos en %currentMonth",
"No events to show": "No events to show",
"No events to show": "No hay eventos para mostrar",
"No incidents for this day": "No hay incidencias para este día",
"No latency data available for this day": "No hay datos de latencia disponibles para este día",
"No maintenances for this day": "No hay mantenimientos para este día.",
"No monitors affected": "Ningún monitor afectado",
"No monitors available.": "No monitors available.",
"No monitors available.": "No hay monitores disponibles.",
"No ongoing maintenances": "Sin mantenimientos continuos",
"No past maintenances": "Sin mantenimientos pasados",
"No Status Available": "Estado no disponible",
"No upcoming maintenances": "No hay mantenimientos próximos",
"No Updates": "No Updates",
"No Updates": "Sin actualizaciones",
"No updates yet": "Aún no hay actualizaciones",
"Notifications": "Notifications",
"Notifications": "Notificaciones",
"One-time": "una sola vez",
"Ongoing": "En curso",
"Operational": "Operativo",
"Partial Degraded Performance": "Rendimiento parcialmente degradado",
"Partial System Outage": "Interrupción parcial del sistema",
"Past": "Pasado",
@@ -82,10 +97,14 @@
"Please enter the 6-digit verification code": "Por favor ingresa el código de verificación de 6 dígitos",
"Read less": "Leer menos",
"Read more": "Leer más",
"Recurring": "Recurring",
"READY": "LISTO",
"Recurring": "Recurrente",
"RESOLVED": "RESOLVED",
"SCHEDULED": "PROGRAMADO",
"Scheduled Events (%count)": "Eventos programados (%count)",
"Script": "Guion",
"Select Language": "Seleccionar idioma",
"Select latency metric to display": "Select latency metric to display",
"Select latency metric to display": "Selecciona la métrica de latencia para mostrar",
"Select Range": "Seleccionar rango",
"Sending...": "Envío...",
"Standard": "Estándar",
@@ -97,13 +116,16 @@
"Subscribe": "Suscribir",
"Subscribe to Updates": "Suscríbete a las actualizaciones",
"There are no incidents or maintenances scheduled for this month.": "No hay incidencias ni mantenimientos programados para este mes.",
"There are no ongoing incidents or maintenance events.": "There are no ongoing incidents or maintenance events.",
"There are no ongoing incidents or maintenance events.": "No hay incidentes ni mantenimientos en curso.",
"Total Incidents": "Incidentes totales",
"Total Maintenances": "Mantenimientos totales",
"Under Maintenance": "En mantenimiento",
"Unknown impact": "Impacto desconocido",
"UP": "ACTIVO",
"Upcoming": "Próximo",
"Updates": "Updates",
"Update Incident": "Actualizar incidente",
"Update Maintenance": "Actualizar mantenimiento",
"Updates": "Actualizaciones",
"Updates (%count)": "Actualizaciones (%count)",
"Uptime": "tiempo de actividad",
"Uptime Badge": "Insignia de tiempo de actividad",
+30 -8
View File
@@ -8,14 +8,21 @@
"Avg Latency": "تأخیر متوسط",
"Back": "برگشت",
"Badges": "نشان ها",
"CANCELLED": "لغو شده",
"COMPLETED": "تکمیل شده",
"Continue": "ادامه دهید",
"Copied": "کپی شده",
"Current": "فعلی",
"Dark": "تاریک",
"Day": "روز",
"Day Uptime": "تایم روز",
"Days": "روزها",
"Degraded": "کاهش یافته",
"DEGRADED": "کاهش یافته",
"Degraded Performance": "عملکرد کاهش‌یافته",
"Didn't receive the code? Resend": "کد را دریافت نکردید؟ ",
"Down": "از کار افتاده",
"DOWN": "قطع",
"Duration": "مدت",
"Email address": "آدرس ایمیل",
"Embed Monitor": "تعبیه مانیتور",
@@ -32,11 +39,14 @@
"Get notified about incidents and scheduled maintenance.": "در مورد حوادث و تعمیر و نگهداری برنامه ریزی شده مطلع شوید.",
"Get notified about incidents updates": "در مورد به روز رسانی حوادث مطلع شوید",
"Get notified about scheduled maintenance": "در مورد تعمیر و نگهداری برنامه ریزی شده مطلع شوید",
"IDENTIFIED": "IDENTIFIED",
"iFrame": "آی فریم",
"Impact": "تاثیر",
"incident": "رخداد",
"Incident Updates": "به روز رسانی حادثه",
"Incidents": "حوادث",
"Included Monitors": "Included Monitors",
"INVESTIGATING": "INVESTIGATING",
"Last Updated": "آخرین به روز رسانی",
"Latency": "تأخیر",
"Latency Embed": "تعبیه تاخیر",
@@ -47,32 +57,37 @@
"Light": "نور",
"Live Status": "وضعیت زنده",
"Loading your preferences...": "در حال بارگیری تنظیمات برگزیده شما...",
"maintenance": "نگهداری",
"MAINTENANCE": "تعمیر و نگهداری",
"Maintenance Updates": "به روز رسانی های تعمیر و نگهداری",
"Maintenances": "تعمیر و نگهداری",
"Major System Outage": "قطعی عمده سیستم",
"Manage Site": "مدیریت سایت",
"Manage your notification preferences.": "تنظیمات برگزیده اعلان خود را مدیریت کنید.",
"Max Latency": "حداکثر تاخیر",
"Maximum Latency": "حداکثر تاخیر",
"Min Latency": "حداقل تاخیر",
"Minimum Latency": "حداقل تاخیر",
"Minute-by-minute status data for this day": "داده های وضعیت دقیقه به دقیقه برای این روز",
"MONITORING": "MONITORING",
"Network error. Please try again.": "خطای شبکه ",
"No Events in %currentMonth": "هیچ رویدادی در %currentMonth وجود ندارد",
"No events to show": "No events to show",
"No events to show": "رویدادی برای نمایش وجود ندارد",
"No incidents for this day": "هیچ حادثه ای برای این روز وجود ندارد",
"No latency data available for this day": "هیچ داده تاخیری برای این روز در دسترس نیست",
"No maintenances for this day": "بدون تعمیر و نگهداری برای این روز",
"No monitors affected": "هیچ مانیتوری تحت تأثیر قرار نگرفت",
"No monitors available.": "No monitors available.",
"No monitors available.": "هیچ مانیتوری در دسترس نیست.",
"No ongoing maintenances": "بدون تعمیر و نگهداری مداوم",
"No past maintenances": "بدون تعمیر و نگهداری قبلی",
"No Status Available": "وضعیتی در دسترس نیست",
"No upcoming maintenances": "بدون تعمیر و نگهداری آینده",
"No Updates": "No Updates",
"No Updates": "بدون به‌روزرسانی",
"No updates yet": "هنوز به روز رسانی نشده است",
"Notifications": "Notifications",
"Notifications": "اعلان‌ها",
"One-time": "یک بار",
"Ongoing": "در حال انجام است",
"Operational": "عملیاتی",
"Partial Degraded Performance": "کاهش عملکرد جزئی",
"Partial System Outage": "قطعی جزئی سیستم",
"Past": "گذشته",
@@ -82,10 +97,14 @@
"Please enter the 6-digit verification code": "لطفا کد تایید 6 رقمی را وارد کنید",
"Read less": "کمتر بخوانید",
"Read more": "ادامه مطلب",
"Recurring": "Recurring",
"READY": "آماده",
"Recurring": "دوره‌ای",
"RESOLVED": "RESOLVED",
"SCHEDULED": "زمان‌بندی شده",
"Scheduled Events (%count)": "رویدادهای زمان‌بندی شده (%count)",
"Script": "اسکریپت",
"Select Language": "زبان را انتخاب کنید",
"Select latency metric to display": "Select latency metric to display",
"Select latency metric to display": "معیار تأخیر برای نمایش را انتخاب کنید",
"Select Range": "Range را انتخاب کنید",
"Sending...": "ارسال...",
"Standard": "استاندارد",
@@ -97,13 +116,16 @@
"Subscribe": "مشترک شوید",
"Subscribe to Updates": "مشترک شدن در به روز رسانی",
"There are no incidents or maintenances scheduled for this month.": "هیچ حادثه یا تعمیراتی برای این ماه برنامه ریزی نشده است.",
"There are no ongoing incidents or maintenance events.": "There are no ongoing incidents or maintenance events.",
"There are no ongoing incidents or maintenance events.": "هیچ رخداد یا رویداد نگهداری فعالی وجود ندارد.",
"Total Incidents": "مجموع حوادث",
"Total Maintenances": "کل تعمیر و نگهداری",
"Under Maintenance": "تحت تعمیر و نگهداری",
"Unknown impact": "تاثیر نامعلوم",
"UP": "فعال",
"Upcoming": "آینده",
"Updates": "Updates",
"Update Incident": "به‌روزرسانی رویداد",
"Update Maintenance": "به‌روزرسانی نگهداری",
"Updates": "به‌روزرسانی‌ها",
"Updates (%count)": "به‌روزرسانی‌ها (%count)",
"Uptime": "آپتایم",
"Uptime Badge": "نشان Uptime",
+30 -8
View File
@@ -8,14 +8,21 @@
"Avg Latency": "Latence moyenne",
"Back": "Dos",
"Badges": "Insignes",
"CANCELLED": "ANNULÉ",
"COMPLETED": "TERMINÉ",
"Continue": "Continuer",
"Copied": "Copié",
"Current": "En cours",
"Dark": "Sombre",
"Day": "Jour",
"Day Uptime": "Disponibilité journalière",
"Days": "Jours",
"Degraded": "Dégradé",
"DEGRADED": "DÉGRADÉ",
"Degraded Performance": "Performance dégradée",
"Didn't receive the code? Resend": "Vous n'avez pas reçu le code ? ",
"Down": "Hors service",
"DOWN": "EN PANNE",
"Duration": "Durée",
"Email address": "Adresse email",
"Embed Monitor": "Intégrer le moniteur",
@@ -32,11 +39,14 @@
"Get notified about incidents and scheduled maintenance.": "Soyez informé des incidents et des maintenances planifiées.",
"Get notified about incidents updates": "Soyez informé des mises à jour des incidents",
"Get notified about scheduled maintenance": "Soyez informé de la maintenance planifiée",
"IDENTIFIED": "IDENTIFIED",
"iFrame": "iFrame",
"Impact": "Impact",
"incident": "Incident",
"Incident Updates": "Mises à jour des incidents",
"Incidents": "Incidents",
"Included Monitors": "Included Monitors",
"INVESTIGATING": "INVESTIGATING",
"Last Updated": "Dernière mise à jour",
"Latency": "Latence",
"Latency Embed": "Latence intégrée",
@@ -47,32 +57,37 @@
"Light": "Lumière",
"Live Status": "Statut en direct",
"Loading your preferences...": "Chargement de vos préférences...",
"maintenance": "Maintenance",
"MAINTENANCE": "MAINTENANCE",
"Maintenance Updates": "Mises à jour de maintenance",
"Maintenances": "Entretiens",
"Major System Outage": "Panne majeure du système",
"Manage Site": "Gérer le site",
"Manage your notification preferences.": "Gérez vos préférences de notification.",
"Max Latency": "Latence maximale",
"Maximum Latency": "Latence maximale",
"Min Latency": "Latence minimale",
"Minimum Latency": "Latence minimale",
"Minute-by-minute status data for this day": "Données d'état minute par minute pour cette journée",
"MONITORING": "MONITORING",
"Network error. Please try again.": "Erreur réseau. ",
"No Events in %currentMonth": "Aucun événement dans %currentMonth",
"No events to show": "No events to show",
"No events to show": "Aucun événement à afficher",
"No incidents for this day": "Aucun incident pour cette journée",
"No latency data available for this day": "Aucune donnée de latence disponible pour ce jour",
"No maintenances for this day": "Aucune maintenance pour cette journée",
"No monitors affected": "Aucun moniteur affecté",
"No monitors available.": "No monitors available.",
"No monitors available.": "Aucun moniteur disponible.",
"No ongoing maintenances": "Aucune maintenance en cours",
"No past maintenances": "Aucune maintenance passée",
"No Status Available": "Aucun statut disponible",
"No upcoming maintenances": "Aucune maintenance à venir",
"No Updates": "No Updates",
"No Updates": "Aucune mise à jour",
"No updates yet": "Aucune mise à jour pour l'instant",
"Notifications": "Notifications",
"Notifications": "Alertes",
"One-time": "Une fois",
"Ongoing": "En cours",
"Operational": "Opérationnel",
"Partial Degraded Performance": "Performance partiellement dégradée",
"Partial System Outage": "Panne partielle du système",
"Past": "Passé",
@@ -82,10 +97,14 @@
"Please enter the 6-digit verification code": "Veuillez saisir le code de vérification à 6 chiffres",
"Read less": "Lire moins",
"Read more": "En savoir plus",
"Recurring": "Recurring",
"READY": "PRÊT",
"Recurring": "Récurrent",
"RESOLVED": "RESOLVED",
"SCHEDULED": "PLANIFIÉ",
"Scheduled Events (%count)": "Événements planifiés (%count)",
"Script": "Scénario",
"Select Language": "Sélectionnez la langue",
"Select latency metric to display": "Select latency metric to display",
"Select latency metric to display": "Sélectionnez la métrique de latence à afficher",
"Select Range": "Sélectionner une plage",
"Sending...": "Envoi...",
"Standard": "Standard",
@@ -97,13 +116,16 @@
"Subscribe": "S'abonner",
"Subscribe to Updates": "Abonnez-vous aux mises à jour",
"There are no incidents or maintenances scheduled for this month.": "Il ny a aucun incident ou maintenance prévu pour ce mois.",
"There are no ongoing incidents or maintenance events.": "There are no ongoing incidents or maintenance events.",
"There are no ongoing incidents or maintenance events.": "Il ny a aucun incident ni aucune maintenance en cours.",
"Total Incidents": "Nombre total d'incidents",
"Total Maintenances": "Entretiens totaux",
"Under Maintenance": "En maintenance",
"Unknown impact": "Impact inconnu",
"UP": "OPÉRATIONNEL",
"Upcoming": "Prochain",
"Updates": "Updates",
"Update Incident": "Mettre à jour l'incident",
"Update Maintenance": "Mettre à jour la maintenance",
"Updates": "Mises à jour",
"Updates (%count)": "Mises à jour (%count)",
"Uptime": "Temps de disponibilité",
"Uptime Badge": "Badge de disponibilité",
+39 -17
View File
@@ -8,14 +8,21 @@
"Avg Latency": "औसत लेटेंसी",
"Back": "वापस",
"Badges": "बैज",
"CANCELLED": "रद्द",
"COMPLETED": "पूर्ण",
"Continue": "जारी रखें",
"Copied": "कॉपी किया गया",
"Current": "वर्तमान",
"Dark": "डार्क",
"Day": "दिन",
"Day Uptime": "दिन का अपटाइम",
"Days": "दिन",
"Degraded": "अवनत",
"DEGRADED": "अवक्रमित",
"Degraded Performance": "प्रदर्शन में गिरावट",
"Didn't receive the code? Resend": "कोड नहीं मिला? फिर से भेजें",
"Down": "बंद",
"DOWN": "बंद",
"Duration": "अवधि",
"Email address": "ईमेल पता",
"Embed Monitor": "मॉनिटर एम्बेड करें",
@@ -32,11 +39,14 @@
"Get notified about incidents and scheduled maintenance.": "घटनाओं और निर्धारित रखरखाव के बारे में सूचित हों।",
"Get notified about incidents updates": "घटना अपडेट के बारे में सूचित हों",
"Get notified about scheduled maintenance": "निर्धारित रखरखाव के बारे में सूचित हों",
"IDENTIFIED": "पहचाना गया",
"iFrame": "iFrame",
"Impact": "प्रभाव",
"incident": "घटना",
"Incident Updates": "घटना अपडेट",
"Incidents": "घटनाएँ",
"Included Monitors": "Included Monitors",
"Included Monitors": "शामिल मॉनिटर",
"INVESTIGATING": "जाँच कर रहे हैं",
"Last Updated": "अंतिम अपडेट",
"Latency": "लेटेंसी",
"Latency Embed": "लेटेंसी एम्बेड",
@@ -47,32 +57,37 @@
"Light": "लाइट",
"Live Status": "लाइव स्थिति",
"Loading your preferences...": "आपकी प्राथमिकताएँ लोड हो रही हैं...",
"maintenance": "रखरखाव",
"MAINTENANCE": "रखरखाव",
"Maintenance Updates": "रखरखाव अपडेट",
"Maintenances": "रखरखाव",
"Major System Outage": "सिस्टम का बड़ा आउटेज",
"Manage Site": "साइट प्रबंधित करें",
"Manage your notification preferences.": "अपनी सूचना प्राथमिकताएँ प्रबंधित करें।",
"Max Latency": "Max Latency",
"Maximum Latency": "Max Latency",
"Min Latency": "Min Latency",
"Minimum Latency": "Min Latency",
"Max Latency": "अधिकतम लेटेंसी",
"Maximum Latency": "अधिकतम लेटेंसी",
"Min Latency": "न्यूनतम लेटेंसी",
"Minimum Latency": "न्यूनतम लेटेंसी",
"Minute-by-minute status data for this day": "इस दिन के लिए मिनट-दर-मिनट स्थिति डेटा",
"MONITORING": "मॉनिटरिंग",
"Network error. Please try again.": "नेटवर्क त्रुटि। कृपया पुनः प्रयास करें।",
"No Events in %currentMonth": "%currentMonth में कोई घटना नहीं",
"No events to show": "No events to show",
"No events to show": "दिखाने के लिए कोई इवेंट नहीं",
"No incidents for this day": "इस दिन के लिए कोई घटना नहीं",
"No latency data available for this day": "इस दिन के लिए कोई लेटेंसी डेटा उपलब्ध नहीं",
"No maintenances for this day": "इस दिन के लिए कोई रखरखाव नहीं",
"No monitors affected": "कोई मॉनिटर प्रभावित नहीं",
"No monitors available.": "No monitors available.",
"No monitors available.": "कोई मॉनिटर उपलब्ध नहीं है।",
"No ongoing maintenances": "कोई चल रहा रखरखाव नहीं",
"No past maintenances": "कोई बीता हुआ रखरखाव नहीं",
"No Status Available": "स्थिति उपलब्ध नहीं है",
"No upcoming maintenances": "कोई आने वाला रखरखाव नहीं",
"No Updates": "No Updates",
"No Updates": "कोई अपडेट नहीं",
"No updates yet": "अभी तक कोई अपडेट नहीं",
"Notifications": "Notifications",
"Notifications": "सूचनाएँ",
"One-time": "एक बार",
"Ongoing": "चल रहा है",
"Operational": "चालू",
"Partial Degraded Performance": "आंशिक प्रदर्शन गिरावट",
"Partial System Outage": "सिस्टम का आंशिक आउटेज.",
"Past": "बीता हुआ",
@@ -82,10 +97,14 @@
"Please enter the 6-digit verification code": "कृपया 6 अंकों का सत्यापन कोड दर्ज करें",
"Read less": "कम पढ़ें",
"Read more": "और पढ़ें",
"Recurring": "Recurring",
"READY": "तैयार",
"Recurring": "आवर्ती",
"RESOLVED": "सुलझा हुआ",
"SCHEDULED": "अनुसूचित",
"Scheduled Events (%count)": "अनुसूचित कार्यक्रम (%count)",
"Script": "स्क्रिप्ट",
"Select Language": "भाषा चुनें",
"Select latency metric to display": "Select latency metric to display",
"Select latency metric to display": "दिखाने के लिए विलंबता मेट्रिक चुनें",
"Select Range": "रेंज चुनें",
"Sending...": "भेजा जा रहा है...",
"Standard": "मानक",
@@ -97,19 +116,22 @@
"Subscribe": "सब्सक्राइब करें",
"Subscribe to Updates": "अपडेट के लिए सब्सक्राइब करें",
"There are no incidents or maintenances scheduled for this month.": "इस महीने के लिए कोई घटना या रखरखाव निर्धारित नहीं है।",
"There are no ongoing incidents or maintenance events.": "There are no ongoing incidents or maintenance events.",
"Total Incidents": "Total Incidents",
"Total Maintenances": "Total Maintenances",
"There are no ongoing incidents or maintenance events.": "कोई चल रही घटना या रखरखाव इवेंट नहीं है।",
"Total Incidents": "कुल घटनाएँ",
"Total Maintenances": "कुल रखरखाव",
"Under Maintenance": "रखरखाव जारी है",
"Unknown impact": "अज्ञात प्रभाव",
"UP": "चालू",
"Upcoming": "आने वाला",
"Updates": "Updates",
"Update Incident": "घटना अपडेट करें",
"Update Maintenance": "रखरखाव अपडेट करें",
"Updates": "अपडेट्स",
"Updates (%count)": "अपडेट (%count)",
"Uptime": "अपटाइम",
"Uptime Badge": "अपटाइम बैज",
"Verification failed": "सत्यापन विफल रहा",
"Verify": "Verify",
"Verifying": "Verifying",
"Verify": "सत्यापित करें",
"Verifying": "सत्यापित हो रहा है",
"We sent a 6-digit code to": "हमने 6 अंकों का कोड भेजा है"
}
}
+30 -8
View File
@@ -8,14 +8,21 @@
"Avg Latency": "Latenza media",
"Back": "Indietro",
"Badges": "Distintivi",
"CANCELLED": "ANNULLATO",
"COMPLETED": "COMPLETATO",
"Continue": "Continuare",
"Copied": "Copiato",
"Current": "Corrente",
"Dark": "Buio",
"Day": "Giorno",
"Day Uptime": "Tempo di attività giornaliero",
"Days": "Giorni",
"Degraded": "Degradato",
"DEGRADED": "DEGRADATO",
"Degraded Performance": "Prestazioni degradate",
"Didn't receive the code? Resend": "Non hai ricevuto il codice? ",
"Down": "Non disponibile",
"DOWN": "NON DISPONIBILE",
"Duration": "Durata",
"Email address": "Indirizzo e-mail",
"Embed Monitor": "Incorpora monitoraggio",
@@ -32,11 +39,14 @@
"Get notified about incidents and scheduled maintenance.": "Ricevi notifiche sugli incidenti e sulla manutenzione programmata.",
"Get notified about incidents updates": "Ricevi notifiche sugli aggiornamenti degli incidenti",
"Get notified about scheduled maintenance": "Ricevi notifiche sulla manutenzione programmata",
"IDENTIFIED": "IDENTIFIED",
"iFrame": "iFrame",
"Impact": "Impatto",
"incident": "Incidente",
"Incident Updates": "Aggiornamenti sugli incidenti",
"Incidents": "Incidenti",
"Included Monitors": "Included Monitors",
"INVESTIGATING": "INVESTIGATING",
"Last Updated": "Ultimo aggiornamento",
"Latency": "Latenza",
"Latency Embed": "Incorporamento latenza",
@@ -47,32 +57,37 @@
"Light": "Leggero",
"Live Status": "Stato in tempo reale",
"Loading your preferences...": "Caricamento delle tue preferenze...",
"maintenance": "Manutenzione",
"MAINTENANCE": "MANUTENZIONE",
"Maintenance Updates": "Aggiornamenti sulla manutenzione",
"Maintenances": "Manutenzioni",
"Major System Outage": "Interruzione grave del sistema",
"Manage Site": "Gestisci sito",
"Manage your notification preferences.": "Gestisci le tue preferenze di notifica.",
"Max Latency": "Latenza massima",
"Maximum Latency": "Latenza massima",
"Min Latency": "Latenza minima",
"Minimum Latency": "Latenza minima",
"Minute-by-minute status data for this day": "Dati sullo stato minuto per minuto per questo giorno",
"MONITORING": "MONITORING",
"Network error. Please try again.": "Errore di rete. ",
"No Events in %currentMonth": "Nessun evento in %currentMonth",
"No events to show": "No events to show",
"No events to show": "Nessun evento da mostrare",
"No incidents for this day": "Nessun incidente per questa giornata",
"No latency data available for this day": "Nessun dato sulla latenza disponibile per questo giorno",
"No maintenances for this day": "Nessuna manutenzione per oggi",
"No monitors affected": "Nessun monitor interessato",
"No monitors available.": "No monitors available.",
"No monitors available.": "Nessun monitor disponibile.",
"No ongoing maintenances": "Nessuna manutenzione continua",
"No past maintenances": "Nessuna manutenzione passata",
"No Status Available": "Nessuno stato disponibile",
"No upcoming maintenances": "Nessuna manutenzione imminente",
"No Updates": "No Updates",
"No Updates": "Nessun aggiornamento",
"No updates yet": "Nessun aggiornamento ancora",
"Notifications": "Notifications",
"Notifications": "Notifiche",
"One-time": "Una volta",
"Ongoing": "In corso",
"Operational": "Operativo",
"Partial Degraded Performance": "Prestazioni parzialmente degradate",
"Partial System Outage": "Interruzione parziale del sistema",
"Past": "Passato",
@@ -82,10 +97,14 @@
"Please enter the 6-digit verification code": "Inserisci il codice di verifica di 6 cifre",
"Read less": "Leggi di meno",
"Read more": "Per saperne di più",
"Recurring": "Recurring",
"READY": "PRONTO",
"Recurring": "Ricorrente",
"RESOLVED": "RESOLVED",
"SCHEDULED": "PROGRAMMATO",
"Scheduled Events (%count)": "Eventi programmati (%count)",
"Script": "Copione",
"Select Language": "Seleziona lingua",
"Select latency metric to display": "Select latency metric to display",
"Select latency metric to display": "Seleziona la metrica di latenza da visualizzare",
"Select Range": "Seleziona Intervallo",
"Sending...": "Invio...",
"Standard": "Standard",
@@ -97,13 +116,16 @@
"Subscribe": "Iscriviti",
"Subscribe to Updates": "Iscriviti agli aggiornamenti",
"There are no incidents or maintenances scheduled for this month.": "Non ci sono incidenti o manutenzioni programmate per questo mese.",
"There are no ongoing incidents or maintenance events.": "There are no ongoing incidents or maintenance events.",
"There are no ongoing incidents or maintenance events.": "Non ci sono incidenti o manutenzioni in corso.",
"Total Incidents": "Incidenti totali",
"Total Maintenances": "Manutenzioni totali",
"Under Maintenance": "In manutenzione",
"Unknown impact": "Impatto sconosciuto",
"UP": "ATTIVO",
"Upcoming": "Prossimamente",
"Updates": "Updates",
"Update Incident": "Aggiorna incidente",
"Update Maintenance": "Aggiorna manutenzione",
"Updates": "Aggiornamenti",
"Updates (%count)": "Aggiornamenti (%count)",
"Uptime": "Tempo di attività",
"Uptime Badge": "Badge di operatività",
+30 -8
View File
@@ -8,14 +8,21 @@
"Avg Latency": "平均レイテンシー",
"Back": "戻る",
"Badges": "バッジ",
"CANCELLED": "キャンセル",
"COMPLETED": "完了",
"Continue": "続く",
"Copied": "コピーされました",
"Current": "現在",
"Dark": "暗い",
"Day": "日",
"Day Uptime": "一日の稼働時間",
"Days": "日数",
"Degraded": "低下",
"DEGRADED": "劣化",
"Degraded Performance": "パフォーマンスの低下",
"Didn't receive the code? Resend": "コードを受け取っていませんか?",
"Down": "停止",
"DOWN": "障害",
"Duration": "間隔",
"Email address": "電子メールアドレス",
"Embed Monitor": "埋め込みモニター",
@@ -32,11 +39,14 @@
"Get notified about incidents and scheduled maintenance.": "インシデントや定期メンテナンスに関する通知を受け取ります。",
"Get notified about incidents updates": "インシデントの更新に関する通知を受け取る",
"Get notified about scheduled maintenance": "定期メンテナンスに関する通知を受け取る",
"IDENTIFIED": "IDENTIFIED",
"iFrame": "iFrame",
"Impact": "インパクト",
"incident": "インシデント",
"Incident Updates": "インシデントの最新情報",
"Incidents": "事件",
"Included Monitors": "Included Monitors",
"INVESTIGATING": "INVESTIGATING",
"Last Updated": "最終更新日",
"Latency": "レイテンシー",
"Latency Embed": "レイテンシの埋め込み",
@@ -47,32 +57,37 @@
"Light": "ライト",
"Live Status": "ライブステータス",
"Loading your preferences...": "設定を読み込んでいます...",
"maintenance": "メンテナンス",
"MAINTENANCE": "メンテナンス",
"Maintenance Updates": "メンテナンスアップデート",
"Maintenances": "メンテナンス",
"Major System Outage": "大規模なシステム障害",
"Manage Site": "サイト管理",
"Manage your notification preferences.": "通知設定を管理します。",
"Max Latency": "最大レイテンシー",
"Maximum Latency": "最大レイテンシー",
"Min Latency": "最小レイテンシ",
"Minimum Latency": "最小レイテンシ",
"Minute-by-minute status data for this day": "この日の分単位のステータスデータ",
"MONITORING": "MONITORING",
"Network error. Please try again.": "ネットワークエラー。",
"No Events in %currentMonth": "%currentMonth にはイベントがありません",
"No events to show": "No events to show",
"No events to show": "表示するイベントはありません",
"No incidents for this day": "この日は事件はありませんでした",
"No latency data available for this day": "この日のレイテンシ データはありません",
"No maintenances for this day": "この日はメンテナンスはありません",
"No monitors affected": "影響を受けるモニターはありません",
"No monitors available.": "No monitors available.",
"No monitors available.": "利用可能なモニターはありません。",
"No ongoing maintenances": "継続的なメンテナンスはありません",
"No past maintenances": "過去のメンテナンスはありません",
"No Status Available": "ステータス情報はありません",
"No upcoming maintenances": "今後のメンテナンスはありません",
"No Updates": "No Updates",
"No Updates": "更新はありません",
"No updates yet": "まだ更新はありません",
"Notifications": "Notifications",
"Notifications": "通知",
"One-time": "一度",
"Ongoing": "進行中",
"Operational": "稼働中",
"Partial Degraded Performance": "部分的なパフォーマンス低下",
"Partial System Outage": "部分的なシステム障害。",
"Past": "過去",
@@ -82,10 +97,14 @@
"Please enter the 6-digit verification code": "6桁の認証コードを入力してください",
"Read less": "読む量を減らす",
"Read more": "続きを読む",
"Recurring": "Recurring",
"READY": "準備完了",
"Recurring": "繰り返し",
"RESOLVED": "RESOLVED",
"SCHEDULED": "予定済み",
"Scheduled Events (%count)": "予定イベント (%count)",
"Script": "スクリプト",
"Select Language": "言語の選択",
"Select latency metric to display": "Select latency metric to display",
"Select latency metric to display": "表示するレイテンシ指標を選択",
"Select Range": "範囲の選択",
"Sending...": "送信中...",
"Standard": "標準",
@@ -97,13 +116,16 @@
"Subscribe": "購読する",
"Subscribe to Updates": "アップデートを購読する",
"There are no incidents or maintenances scheduled for this month.": "今月は予定されているインシデントやメンテナンスはありません。",
"There are no ongoing incidents or maintenance events.": "There are no ongoing incidents or maintenance events.",
"There are no ongoing incidents or maintenance events.": "進行中のインシデントまたはメンテナンスイベントはありません。",
"Total Incidents": "総インシデント数",
"Total Maintenances": "トータルメンテナンス",
"Under Maintenance": "メンテナンス中",
"Unknown impact": "未知の影響",
"UP": "正常",
"Upcoming": "今後の予定",
"Updates": "Updates",
"Update Incident": "インシデントを更新",
"Update Maintenance": "メンテナンスを更新",
"Updates": "更新",
"Updates (%count)": "アップデート (%count)",
"Uptime": "稼働時間",
"Uptime Badge": "稼働時間バッジ",
+30 -8
View File
@@ -8,14 +8,21 @@
"Avg Latency": "평균 지연 시간",
"Back": "뒤쪽에",
"Badges": "배지",
"CANCELLED": "취소됨",
"COMPLETED": "완료됨",
"Continue": "계속하다",
"Copied": "복사됨",
"Current": "현재",
"Dark": "어두운",
"Day": "낮",
"Day Uptime": "일일 가동 시간",
"Days": "날",
"Degraded": "저하",
"DEGRADED": "성능 저하",
"Degraded Performance": "성능 저하",
"Didn't receive the code? Resend": "코드를 받지 못하셨나요? 재전송",
"Down": "중단",
"DOWN": "장애",
"Duration": "지속",
"Email address": "이메일 주소",
"Embed Monitor": "모니터 내장",
@@ -32,11 +39,14 @@
"Get notified about incidents and scheduled maintenance.": "사고 및 예정된 유지 관리에 대한 알림을 받으세요.",
"Get notified about incidents updates": "사건 업데이트에 대한 알림 받기",
"Get notified about scheduled maintenance": "예정된 유지 관리에 대한 알림 받기",
"IDENTIFIED": "IDENTIFIED",
"iFrame": "아이프레임",
"Impact": "영향",
"incident": "인시던트",
"Incident Updates": "사고 업데이트",
"Incidents": "사건",
"Included Monitors": "Included Monitors",
"INVESTIGATING": "INVESTIGATING",
"Last Updated": "마지막 업데이트",
"Latency": "숨어 있음",
"Latency Embed": "지연 시간 포함",
@@ -47,32 +57,37 @@
"Light": "빛",
"Live Status": "실시간 현황",
"Loading your preferences...": "환경설정 로드 중...",
"maintenance": "유지보수",
"MAINTENANCE": "점검 중",
"Maintenance Updates": "유지보수 업데이트",
"Maintenances": "유지보수",
"Major System Outage": "대규모 시스템 장애",
"Manage Site": "사이트 관리",
"Manage your notification preferences.": "알림 기본 설정을 관리하세요.",
"Max Latency": "최대 대기 시간",
"Maximum Latency": "최대 대기 시간",
"Min Latency": "최소 지연 시간",
"Minimum Latency": "최소 지연 시간",
"Minute-by-minute status data for this day": "오늘의 분별 상태 데이터",
"MONITORING": "MONITORING",
"Network error. Please try again.": "네트워크 오류입니다. 다시 시도해 주세요.",
"No Events in %currentMonth": "%currentMonth에 이벤트가 없습니다.",
"No events to show": "No events to show",
"No events to show": "표시할 이벤트가 없습니다",
"No incidents for this day": "오늘은 사건이 없습니다.",
"No latency data available for this day": "이 날에는 지연 시간 데이터가 없습니다.",
"No maintenances for this day": "이날은 유지보수가 없습니다",
"No monitors affected": "영향을 받는 모니터 없음",
"No monitors available.": "No monitors available.",
"No monitors available.": "사용 가능한 모니터가 없습니다.",
"No ongoing maintenances": "지속적인 유지 관리가 필요하지 않습니다.",
"No past maintenances": "과거 유지보수 없음",
"No Status Available": "사용 가능한 상태 정보 없음",
"No upcoming maintenances": "예정된 유지 관리가 없습니다.",
"No Updates": "No Updates",
"No Updates": "업데이트 없음",
"No updates yet": "아직 업데이트가 없습니다",
"Notifications": "Notifications",
"Notifications": "알림",
"One-time": "일회성",
"Ongoing": "전진",
"Operational": "정상",
"Partial Degraded Performance": "부분적 성능 저하",
"Partial System Outage": "부분적 시스템 장애.",
"Past": "과거",
@@ -82,10 +97,14 @@
"Please enter the 6-digit verification code": "인증번호 6자리를 입력해주세요",
"Read less": "덜 읽으세요",
"Read more": "더 읽어보세요",
"Recurring": "Recurring",
"READY": "준비됨",
"Recurring": "반복",
"RESOLVED": "RESOLVED",
"SCHEDULED": "예정됨",
"Scheduled Events (%count)": "예정된 이벤트 (%count)",
"Script": "스크립트",
"Select Language": "언어 선택",
"Select latency metric to display": "Select latency metric to display",
"Select latency metric to display": "표시할 지연 시간 지표를 선택하세요",
"Select Range": "범위 선택",
"Sending...": "배상...",
"Standard": "기준",
@@ -97,13 +116,16 @@
"Subscribe": "구독하다",
"Subscribe to Updates": "업데이트 구독",
"There are no incidents or maintenances scheduled for this month.": "이번 달에는 예정된 사고나 점검이 없습니다.",
"There are no ongoing incidents or maintenance events.": "There are no ongoing incidents or maintenance events.",
"There are no ongoing incidents or maintenance events.": "진행 중인 인시던트 또는 유지보수 이벤트가 없습니다.",
"Total Incidents": "총 사고",
"Total Maintenances": "총 유지보수",
"Under Maintenance": "유지보수 중",
"Unknown impact": "알 수 없는 영향",
"UP": "정상",
"Upcoming": "예정",
"Updates": "Updates",
"Update Incident": "인시던트 업데이트",
"Update Maintenance": "유지보수 업데이트",
"Updates": "업데이트",
"Updates (%count)": "업데이트(%count)",
"Uptime": "가동 시간",
"Uptime Badge": "가동 시간 배지",
+30 -8
View File
@@ -8,14 +8,21 @@
"Avg Latency": "Gj.sn. ventetid",
"Back": "Tilbake",
"Badges": "Merker",
"CANCELLED": "AVLYST",
"COMPLETED": "FULLFØRT",
"Continue": "Fortsette",
"Copied": "Kopiert",
"Current": "Nåværende",
"Dark": "Mørk",
"Day": "Dag",
"Day Uptime": "Dag oppetid",
"Days": "Dager",
"Degraded": "Degradert",
"DEGRADED": "FORRINGET",
"Degraded Performance": "Redusert ytelse",
"Didn't receive the code? Resend": "Fikk du ikke koden? Send på nytt",
"Down": "Nede",
"DOWN": "NEDE",
"Duration": "Varighet",
"Email address": "E-postadresse",
"Embed Monitor": "Bygg inn monitor",
@@ -32,11 +39,14 @@
"Get notified about incidents and scheduled maintenance.": "Bli varslet om hendelser og planlagt vedlikehold.",
"Get notified about incidents updates": "Bli varslet om hendelsesoppdateringer",
"Get notified about scheduled maintenance": "Bli varslet om planlagt vedlikehold",
"IDENTIFIED": "IDENTIFIED",
"iFrame": "iFrame",
"Impact": "Påvirkning",
"incident": "Hendelse",
"Incident Updates": "Hendelsesoppdateringer",
"Incidents": "Hendelser",
"Included Monitors": "Included Monitors",
"INVESTIGATING": "INVESTIGATING",
"Last Updated": "Sist oppdatert",
"Latency": "Latens",
"Latency Embed": "Innebygging av ventetid",
@@ -47,32 +57,37 @@
"Light": "Lys",
"Live Status": "Live-status",
"Loading your preferences...": "Laster inn innstillingene dine ...",
"maintenance": "Vedlikehold",
"MAINTENANCE": "VEDLIKEHOLD",
"Maintenance Updates": "Vedlikeholdsoppdateringer",
"Maintenances": "Vedlikehold",
"Major System Outage": "Større systemutfall",
"Manage Site": "Administrer nettsted",
"Manage your notification preferences.": "Administrer varslingspreferansene dine.",
"Max Latency": "Maks ventetid",
"Maximum Latency": "Maks ventetid",
"Min Latency": "Min latens",
"Minimum Latency": "Min latens",
"Minute-by-minute status data for this day": "Statusdata fra minutt for minutt for denne dagen",
"MONITORING": "MONITORING",
"Network error. Please try again.": "Nettverksfeil. Vennligst prøv igjen.",
"No Events in %currentMonth": "Ingen hendelser i %currentMonth",
"No events to show": "No events to show",
"No events to show": "Ingen hendelser å vise",
"No incidents for this day": "Ingen hendelser denne dagen",
"No latency data available for this day": "Ingen forsinkelsesdata tilgjengelig for denne dagen",
"No maintenances for this day": "Ingen vedlikehold denne dagen",
"No monitors affected": "Ingen monitorer er berørt",
"No monitors available.": "No monitors available.",
"No monitors available.": "Ingen monitorer tilgjengelige.",
"No ongoing maintenances": "Ingen løpende vedlikehold",
"No past maintenances": "Ingen tidligere vedlikehold",
"No Status Available": "Ingen status tilgjengelig",
"No upcoming maintenances": "Ingen kommende vedlikehold",
"No Updates": "No Updates",
"No Updates": "Ingen oppdateringer",
"No updates yet": "Ingen oppdateringer ennå",
"Notifications": "Notifications",
"Notifications": "Varsler",
"One-time": "En gang",
"Ongoing": "Pågående",
"Operational": "Operativ",
"Partial Degraded Performance": "Delvis redusert ytelse",
"Partial System Outage": "Delvis systemutfall",
"Past": "Forbi",
@@ -82,10 +97,14 @@
"Please enter the 6-digit verification code": "Vennligst skriv inn den 6-sifrede bekreftelseskoden",
"Read less": "Les mindre",
"Read more": "Les mer",
"Recurring": "Recurring",
"READY": "KLAR",
"Recurring": "Gjentakende",
"RESOLVED": "RESOLVED",
"SCHEDULED": "PLANLAGT",
"Scheduled Events (%count)": "Planlagte hendelser (%count)",
"Script": "Manus",
"Select Language": "Velg Språk",
"Select latency metric to display": "Select latency metric to display",
"Select latency metric to display": "Velg latensmåling som skal vises",
"Select Range": "Velg Område",
"Sending...": "Sender...",
"Standard": "Standard",
@@ -97,13 +116,16 @@
"Subscribe": "Abonner",
"Subscribe to Updates": "Abonner på oppdateringer",
"There are no incidents or maintenances scheduled for this month.": "Det er ingen hendelser eller vedlikehold planlagt denne måneden.",
"There are no ongoing incidents or maintenance events.": "There are no ongoing incidents or maintenance events.",
"There are no ongoing incidents or maintenance events.": "Det er ingen pågående hendelser eller vedlikeholdsaktiviteter.",
"Total Incidents": "Totale hendelser",
"Total Maintenances": "Totalt vedlikehold",
"Under Maintenance": "Under Vedlikehold",
"Unknown impact": "Ukjent påvirkning",
"UP": "OPPE",
"Upcoming": "Kommende",
"Updates": "Updates",
"Update Incident": "Oppdater hendelse",
"Update Maintenance": "Oppdater vedlikehold",
"Updates": "Oppdateringer",
"Updates (%count)": "Oppdateringer (%count)",
"Uptime": "Oppetid",
"Uptime Badge": "Oppetidsmerke",
+30 -8
View File
@@ -8,14 +8,21 @@
"Avg Latency": "Gem. latentie",
"Back": "Rug",
"Badges": "Insignes",
"CANCELLED": "GEANNULEERD",
"COMPLETED": "VOLTOOID",
"Continue": "Doorgaan",
"Copied": "Gekopieerd",
"Current": "Huidig",
"Dark": "Donker",
"Day": "Dag",
"Day Uptime": "Dag uptime",
"Days": "Dagen",
"Degraded": "Verstoord",
"DEGRADED": "VERSLECHTERD",
"Degraded Performance": "Verminderde prestaties",
"Didn't receive the code? Resend": "Heb je de code niet ontvangen? ",
"Down": "Niet bereikbaar",
"DOWN": "OFFLINE",
"Duration": "Duur",
"Email address": "E-mailadres",
"Embed Monitor": "Monitor insluiten",
@@ -32,11 +39,14 @@
"Get notified about incidents and scheduled maintenance.": "Ontvang meldingen over incidenten en gepland onderhoud.",
"Get notified about incidents updates": "Ontvang meldingen over incidentupdates",
"Get notified about scheduled maintenance": "Ontvang meldingen over gepland onderhoud",
"IDENTIFIED": "IDENTIFIED",
"iFrame": "iFrame",
"Impact": "Invloed",
"incident": "Incident",
"Incident Updates": "Incidentupdates",
"Incidents": "Incidenten",
"Included Monitors": "Included Monitors",
"INVESTIGATING": "INVESTIGATING",
"Last Updated": "Laatst bijgewerkt",
"Latency": "Latentie",
"Latency Embed": "Latentie insluiten",
@@ -47,32 +57,37 @@
"Light": "Licht",
"Live Status": "Live-status",
"Loading your preferences...": "Uw voorkeuren laden...",
"maintenance": "Onderhoud",
"MAINTENANCE": "ONDERHOUD",
"Maintenance Updates": "Onderhoudsupdates",
"Maintenances": "Onderhouden",
"Major System Outage": "Grote systeemstoring",
"Manage Site": "Site beheren",
"Manage your notification preferences.": "Beheer uw meldingsvoorkeuren.",
"Max Latency": "Maximale latentie",
"Maximum Latency": "Maximale latentie",
"Min Latency": "Minimale latentie",
"Minimum Latency": "Minimale latentie",
"Minute-by-minute status data for this day": "Statusgegevens van minuut tot minuut voor deze dag",
"MONITORING": "MONITORING",
"Network error. Please try again.": "Netwerkfout. ",
"No Events in %currentMonth": "Geen evenementen in %currentMonth",
"No events to show": "No events to show",
"No events to show": "Geen gebeurtenissen om weer te geven",
"No incidents for this day": "Geen incidenten deze dag",
"No latency data available for this day": "Er zijn geen latentiegegevens beschikbaar voor deze dag",
"No maintenances for this day": "Geen onderhoud voor deze dag",
"No monitors affected": "Er zijn geen monitoren getroffen",
"No monitors available.": "No monitors available.",
"No monitors available.": "Geen monitoren beschikbaar.",
"No ongoing maintenances": "Geen doorlopend onderhoud",
"No past maintenances": "Geen onderhoudsbeurten uit het verleden",
"No Status Available": "Geen status beschikbaar",
"No upcoming maintenances": "Geen aankomend onderhoud",
"No Updates": "No Updates",
"No Updates": "Geen actualisaties",
"No updates yet": "Nog geen updates",
"Notifications": "Notifications",
"Notifications": "Meldingen",
"One-time": "Eenmalig",
"Ongoing": "Lopend",
"Operational": "Operationeel",
"Partial Degraded Performance": "Gedeeltelijk verminderde prestaties",
"Partial System Outage": "Gedeeltelijke systeemstoring",
"Past": "Verleden",
@@ -82,10 +97,14 @@
"Please enter the 6-digit verification code": "Voer de 6-cijferige verificatiecode in",
"Read less": "Lees minder",
"Read more": "Lees meer",
"Recurring": "Recurring",
"READY": "GEREED",
"Recurring": "Terugkerend",
"RESOLVED": "RESOLVED",
"SCHEDULED": "GEPLAND",
"Scheduled Events (%count)": "Geplande evenementen (%count)",
"Script": "Script",
"Select Language": "Selecteer Taal",
"Select latency metric to display": "Select latency metric to display",
"Select latency metric to display": "Selecteer de latentie-metriek die moet worden weergegeven",
"Select Range": "Selecteer Bereik",
"Sending...": "Verzenden...",
"Standard": "Standaard",
@@ -97,13 +116,16 @@
"Subscribe": "Abonneren",
"Subscribe to Updates": "Abonneer u op updates",
"There are no incidents or maintenances scheduled for this month.": "Er staan deze maand geen incidenten of onderhoud gepland.",
"There are no ongoing incidents or maintenance events.": "There are no ongoing incidents or maintenance events.",
"There are no ongoing incidents or maintenance events.": "Er zijn geen lopende incidenten of onderhoudsgebeurtenissen.",
"Total Incidents": "Totaal aantal incidenten",
"Total Maintenances": "Totaal onderhoud",
"Under Maintenance": "Onder Onderhoud",
"Unknown impact": "Onbekende impact",
"UP": "ACTIEF",
"Upcoming": "Aankomend",
"Updates": "Updates",
"Update Incident": "Incident bijwerken",
"Update Maintenance": "Onderhoud bijwerken",
"Updates": "Actualisaties",
"Updates (%count)": "Updates (%count)",
"Uptime": "Uptime",
"Uptime Badge": "Uptime-badge",
+30 -8
View File
@@ -8,14 +8,21 @@
"Avg Latency": "Średnie opóźnienie",
"Back": "Z powrotem",
"Badges": "Odznaki",
"CANCELLED": "ANULOWANE",
"COMPLETED": "ZAKOŃCZONE",
"Continue": "Kontynuować",
"Copied": "Skopiowano",
"Current": "Bieżące",
"Dark": "Ciemny",
"Day": "Dzień",
"Day Uptime": "Dzień sprawności",
"Days": "Dni",
"Degraded": "Pogorszony",
"DEGRADED": "POGORSZONY",
"Degraded Performance": "Obniżona wydajność",
"Didn't receive the code? Resend": "Nie otrzymałeś kodu? ",
"Down": "Niedostępny",
"DOWN": "AWARIA",
"Duration": "Czas trwania",
"Email address": "Adres e-mail",
"Embed Monitor": "Wstaw monitor",
@@ -32,11 +39,14 @@
"Get notified about incidents and scheduled maintenance.": "Otrzymuj powiadomienia o incydentach i zaplanowanych konserwacjach.",
"Get notified about incidents updates": "Otrzymuj powiadomienia o aktualizacjach incydentów",
"Get notified about scheduled maintenance": "Otrzymuj powiadomienia o zaplanowanej konserwacji",
"IDENTIFIED": "IDENTIFIED",
"iFrame": "iFramka",
"Impact": "Uderzenie",
"incident": "Incydent",
"Incident Updates": "Aktualizacje incydentów",
"Incidents": "Incydenty",
"Included Monitors": "Included Monitors",
"INVESTIGATING": "INVESTIGATING",
"Last Updated": "Ostatnia aktualizacja",
"Latency": "Utajenie",
"Latency Embed": "Osadzanie opóźnień",
@@ -47,32 +57,37 @@
"Light": "Światło",
"Live Status": "Stan na żywo",
"Loading your preferences...": "Ładowanie Twoich preferencji...",
"maintenance": "Konserwacja",
"MAINTENANCE": "KONSERWACJA",
"Maintenance Updates": "Aktualizacje konserwacyjne",
"Maintenances": "Konserwacje",
"Major System Outage": "Poważna awaria systemu",
"Manage Site": "Zarządzaj stroną",
"Manage your notification preferences.": "Zarządzaj preferencjami powiadomień.",
"Max Latency": "Maksymalne opóźnienie",
"Maximum Latency": "Maksymalne opóźnienie",
"Min Latency": "Minimalne opóźnienie",
"Minimum Latency": "Minimalne opóźnienie",
"Minute-by-minute status data for this day": "Dane o stanie minuta po minucie na ten dzień",
"MONITORING": "MONITORING",
"Network error. Please try again.": "Błąd sieci. ",
"No Events in %currentMonth": "Brak wydarzeń w %currentMonth",
"No events to show": "No events to show",
"No events to show": "Brak zdarzeń do wyświetlenia",
"No incidents for this day": "Żadnych incydentów na ten dzień",
"No latency data available for this day": "Brak danych dotyczących opóźnień dla tego dnia",
"No maintenances for this day": "Brak konserwacji na ten dzień",
"No monitors affected": "Nie dotyczy to żadnych monitorów",
"No monitors available.": "No monitors available.",
"No monitors available.": "Brak dostępnych monitorów.",
"No ongoing maintenances": "Brak bieżących konserwacji",
"No past maintenances": "Brak wcześniejszych konserwacji",
"No Status Available": "Brak dostępnego statusu",
"No upcoming maintenances": "Brak nadchodzących prac konserwacyjnych",
"No Updates": "No Updates",
"No Updates": "Brak aktualizacji",
"No updates yet": "Nie ma jeszcze żadnych aktualizacji",
"Notifications": "Notifications",
"Notifications": "Powiadomienia",
"One-time": "Jednorazowy",
"Ongoing": "Bieżący",
"Operational": "Operacyjny",
"Partial Degraded Performance": "Częściowo obniżona wydajność",
"Partial System Outage": "Częściowa awaria systemu",
"Past": "Przeszłość",
@@ -82,10 +97,14 @@
"Please enter the 6-digit verification code": "Wprowadź 6-cyfrowy kod weryfikacyjny",
"Read less": "Czytaj mniej",
"Read more": "Przeczytaj więcej",
"Recurring": "Recurring",
"READY": "GOTOWE",
"Recurring": "Cykliczne",
"RESOLVED": "RESOLVED",
"SCHEDULED": "ZAPLANOWANE",
"Scheduled Events (%count)": "Zaplanowane wydarzenia (%count)",
"Script": "Scenariusz",
"Select Language": "Wybierz Język",
"Select latency metric to display": "Select latency metric to display",
"Select latency metric to display": "Wybierz metrykę opóźnienia do wyświetlenia",
"Select Range": "Wybierz Zakres",
"Sending...": "Przesyłka...",
"Standard": "Standard",
@@ -97,13 +116,16 @@
"Subscribe": "Subskrybować",
"Subscribe to Updates": "Subskrybuj aktualizacje",
"There are no incidents or maintenances scheduled for this month.": "Na ten miesiąc nie zaplanowano żadnych incydentów ani konserwacji.",
"There are no ongoing incidents or maintenance events.": "There are no ongoing incidents or maintenance events.",
"There are no ongoing incidents or maintenance events.": "Brak trwających incydentów ani prac konserwacyjnych.",
"Total Incidents": "Całkowita liczba incydentów",
"Total Maintenances": "Całkowita konserwacja",
"Under Maintenance": "W ramach konserwacji",
"Unknown impact": "Nieznany wpływ",
"UP": "DZIAŁA",
"Upcoming": "Nadchodzące",
"Updates": "Updates",
"Update Incident": "Aktualizuj incydent",
"Update Maintenance": "Aktualizuj konserwację",
"Updates": "Aktualizacje",
"Updates (%count)": "Aktualizacje (%count)",
"Uptime": "Czas pracy",
"Uptime Badge": "Odznaka dostępności",
+30 -8
View File
@@ -8,14 +8,21 @@
"Avg Latency": "Latência Média",
"Back": "Voltar",
"Badges": "Emblemas",
"CANCELLED": "CANCELADO",
"COMPLETED": "CONCLUÍDO",
"Continue": "Continuar",
"Copied": "Copiado",
"Current": "Atual",
"Dark": "Escuro",
"Day": "Dia",
"Day Uptime": "Tempo de atividade diurno",
"Days": "Dias",
"Degraded": "Degradado",
"DEGRADED": "DEGRADADO",
"Degraded Performance": "Desempenho degradado",
"Didn't receive the code? Resend": "Não recebeu o código? ",
"Down": "Fora do ar",
"DOWN": "FORA DO AR",
"Duration": "Duração",
"Email address": "Endereço de email",
"Embed Monitor": "Incorporar monitor",
@@ -32,11 +39,14 @@
"Get notified about incidents and scheduled maintenance.": "Seja notificado sobre incidentes e manutenções programadas.",
"Get notified about incidents updates": "Seja notificado sobre atualizações de incidentes",
"Get notified about scheduled maintenance": "Seja notificado sobre manutenções programadas",
"IDENTIFIED": "IDENTIFIED",
"iFrame": "iFrame",
"Impact": "Impacto",
"incident": "Incidente",
"Incident Updates": "Atualizações de incidentes",
"Incidents": "Incidentes",
"Included Monitors": "Included Monitors",
"INVESTIGATING": "INVESTIGATING",
"Last Updated": "Última atualização",
"Latency": "Latência",
"Latency Embed": "Incorporação de latência",
@@ -47,32 +57,37 @@
"Light": "Luz",
"Live Status": "Status ao vivo",
"Loading your preferences...": "Carregando suas preferências...",
"maintenance": "Manutenção",
"MAINTENANCE": "MANUTENÇÃO",
"Maintenance Updates": "Atualizações de manutenção",
"Maintenances": "Manutenção",
"Major System Outage": "Indisponibilidade grave do sistema",
"Manage Site": "Gerenciar site",
"Manage your notification preferences.": "Gerencie suas preferências de notificação.",
"Max Latency": "Latência máxima",
"Maximum Latency": "Latência máxima",
"Min Latency": "Latência mínima",
"Minimum Latency": "Latência mínima",
"Minute-by-minute status data for this day": "Dados de status minuto a minuto para este dia",
"MONITORING": "MONITORING",
"Network error. Please try again.": "Erro de rede. ",
"No Events in %currentMonth": "Nenhum evento em %currentMonth",
"No events to show": "No events to show",
"No events to show": "Não eventos para exibir",
"No incidents for this day": "Nenhum incidente neste dia",
"No latency data available for this day": "Não há dados de latência disponíveis para este dia",
"No maintenances for this day": "Sem manutenções para este dia",
"No monitors affected": "Nenhum monitor afetado",
"No monitors available.": "No monitors available.",
"No monitors available.": "Não monitores disponíveis.",
"No ongoing maintenances": "Sem manutenções contínuas",
"No past maintenances": "Sem manutenções anteriores",
"No Status Available": "Nenhum status disponível",
"No upcoming maintenances": "Sem manutenções futuras",
"No Updates": "No Updates",
"No Updates": "Sem atualizações",
"No updates yet": "Ainda não há atualizações",
"Notifications": "Notifications",
"Notifications": "Notificações",
"One-time": "Único",
"Ongoing": "Em andamento",
"Operational": "Operacional",
"Partial Degraded Performance": "Desempenho parcialmente degradado",
"Partial System Outage": "Indisponibilidade parcial do sistema",
"Past": "Passado",
@@ -82,10 +97,14 @@
"Please enter the 6-digit verification code": "Insira o código de verificação de 6 dígitos",
"Read less": "Leia menos",
"Read more": "Leia mais",
"Recurring": "Recurring",
"READY": "PRONTO",
"Recurring": "Recorrente",
"RESOLVED": "RESOLVED",
"SCHEDULED": "AGENDADO",
"Scheduled Events (%count)": "Eventos agendados (%count)",
"Script": "Roteiro",
"Select Language": "Selecione o idioma",
"Select latency metric to display": "Select latency metric to display",
"Select latency metric to display": "Selecione a métrica de latência para exibir",
"Select Range": "Selecione o intervalo",
"Sending...": "Enviando...",
"Standard": "Padrão",
@@ -97,13 +116,16 @@
"Subscribe": "Inscrever-se",
"Subscribe to Updates": "Assine atualizações",
"There are no incidents or maintenances scheduled for this month.": "Não há incidentes ou manutenções programadas para este mês.",
"There are no ongoing incidents or maintenance events.": "There are no ongoing incidents or maintenance events.",
"There are no ongoing incidents or maintenance events.": "Não há incidentes ou manutenções em andamento.",
"Total Incidents": "Total de incidentes",
"Total Maintenances": "Manutenção total",
"Under Maintenance": "Em manutenção",
"Unknown impact": "Impacto desconhecido",
"UP": "ATIVO",
"Upcoming": "Por vir",
"Updates": "Updates",
"Update Incident": "Atualizar incidente",
"Update Maintenance": "Atualizar manutenção",
"Updates": "Atualizações",
"Updates (%count)": "Atualizações (%count)",
"Uptime": "Tempo de atividade",
"Uptime Badge": "Selo de tempo de atividade",
+30 -8
View File
@@ -9,14 +9,21 @@
"Avg Latency": "Средняя задержка",
"Back": "Назад",
"Badges": "Значки",
"CANCELLED": "ОТМЕНЕНО",
"COMPLETED": "ЗАВЕРШЕНО",
"Continue": "Продолжить",
"Copied": "Скопировано",
"Current": "Текущие",
"Dark": "Темная",
"Day": "Day",
"Day Uptime": "Время работы за день",
"Days": "Days",
"Degraded": "Деградация",
"DEGRADED": "УХУДШЕНИЕ",
"Degraded Performance": "Снижение производительности",
"Didn't receive the code? Resend": "Не получили код? Отправить повторно",
"Down": "Недоступен",
"DOWN": "НЕ РАБОТАЕТ",
"Duration": "Продолжительность",
"Email address": "Адрес электронной почты",
"Embed Monitor": "Встроить монитор",
@@ -33,11 +40,14 @@
"Get notified about incidents and scheduled maintenance.": "Получайте уведомления об инцидентах и плановом обслуживании.",
"Get notified about incidents updates": "Получайте уведомления об обновлениях инцидентов",
"Get notified about scheduled maintenance": "Получайте уведомления о плановом обслуживании",
"IDENTIFIED": "IDENTIFIED",
"iFrame": "iFrame",
"Impact": "Влияние",
"incident": "Инцидент",
"Incident Updates": "Обновления инцидентов",
"Incidents": "Инциденты",
"Included Monitors": "Included Monitors",
"INVESTIGATING": "INVESTIGATING",
"Last Updated": "Последнее обновление",
"Latency": "Задержка",
"Latency Embed": "Встроенная задержка",
@@ -48,32 +58,37 @@
"Light": "Светлая",
"Live Status": "Статус в реальном времени",
"Loading your preferences...": "Загрузка настроек...",
"maintenance": "Обслуживание",
"MAINTENANCE": "ОБСЛУЖИВАНИЕ",
"Maintenance Updates": "Обновления обслуживания",
"Maintenances": "Обслуживание",
"Major System Outage": "Крупный сбой системы",
"Manage Site": "Управление сайтом",
"Manage your notification preferences.": "Управляйте настройками уведомлений.",
"Max Latency": "Max Latency",
"Maximum Latency": "Max Latency",
"Min Latency": "Min Latency",
"Minimum Latency": "Min Latency",
"Minute-by-minute status data for this day": "Поминутные данные о статусе за этот день",
"MONITORING": "MONITORING",
"Network error. Please try again.": "Ошибка сети. Пожалуйста, попробуйте снова.",
"No Events in %currentMonth": "Нет событий в %currentMonth",
"No events to show": "No events to show",
"No events to show": "Нет событий для отображения",
"No incidents for this day": "Нет инцидентов за этот день",
"No latency data available for this day": "Нет данных о задержке за этот день",
"No maintenances for this day": "Нет обслуживания за этот день",
"No monitors affected": "Мониторы не затронуты",
"No monitors available.": "No monitors available.",
"No monitors available.": "Нет доступных мониторов.",
"No ongoing maintenances": "Нет текущих обслуживаний",
"No past maintenances": "Нет прошедших обслуживаний",
"No Status Available": "Статус недоступен",
"No upcoming maintenances": "Нет предстоящих обслуживаний",
"No Updates": "No Updates",
"No Updates": "Нет обновлений",
"No updates yet": "Обновлений пока нет",
"Notifications": "Notifications",
"Notifications": "Уведомления",
"One-time": "Одноразовое",
"Ongoing": "Текущие",
"Operational": "Работает",
"Partial Degraded Performance": "Частичное снижение производительности",
"Partial System Outage": "Частичный сбой системы",
"Past": "Прошедшие",
@@ -83,10 +98,14 @@
"Please enter the 6-digit verification code": "Пожалуйста, введите 6-значный код подтверждения",
"Read less": "Читать меньше",
"Read more": "Читать далее",
"Recurring": "Recurring",
"READY": "ГОТОВО",
"Recurring": "Повторяющийся",
"RESOLVED": "RESOLVED",
"SCHEDULED": "ЗАПЛАНИРОВАНО",
"Scheduled Events (%count)": "Запланированные события (%count)",
"Script": "Скрипт",
"Select Language": "Выберите язык",
"Select latency metric to display": "Select latency metric to display",
"Select latency metric to display": "Выберите метрику задержки для отображения",
"Select Range": "Выберите диапазон",
"Sending...": "Отправка...",
"Standard": "Стандартный",
@@ -98,13 +117,16 @@
"Subscribe": "Подписаться",
"Subscribe to Updates": "Подписаться на обновления",
"There are no incidents or maintenances scheduled for this month.": "На этот месяц не запланировано инцидентов или обслуживания.",
"There are no ongoing incidents or maintenance events.": "There are no ongoing incidents or maintenance events.",
"There are no ongoing incidents or maintenance events.": "Нет текущих инцидентов или работ по обслуживанию.",
"Total Incidents": "Total Incidents",
"Total Maintenances": "Total Maintenances",
"Under Maintenance": "На обслуживании",
"Unknown impact": "Неизвестное влияние",
"UP": "РАБОТАЕТ",
"Upcoming": "Предстоящие",
"Updates": "Updates",
"Update Incident": "Обновить инцидент",
"Update Maintenance": "Обновить обслуживание",
"Updates": "Обновления",
"Updates (%count)": "Обновления (%count)",
"Uptime": "Время работы",
"Uptime Badge": "Значок времени работы",
+115 -74
View File
@@ -2,114 +2,155 @@
"name": "Slovenčina",
"mappings": {
"%latency %metric latency": "%latency %metric latencia",
"Affected Monitors (%count)": "Ovplyvnené monitory (%count)",
"All Systems Operational": "Všetky systémy sú funkčné",
"24 Hours": "24 hodín",
"30 Days": "30 dní",
"7 Days": "7 dní",
"90 Days": "90 dní",
"Affected Monitors (%count)": "Ovládacie monitory (%count)",
"All Systems Operational": "Všetky systémy sú v prevádzke",
"average": "priemerná",
"Average Latency": "Priemerná latencia",
"Avg Latency": "Priem. latencia",
"Back": "Späť",
"Badges": "Odznaky",
"CANCELLED": "ZRUŠENÉ",
"COMPLETED": "DOKONČENÉ",
"Continue": "Pokračovať",
"Copied": "Skopírované",
"Current": "Aktuálne",
"Dark": "Tmavý",
"Day": "deň",
"Day Uptime": "Day Uptime",
"Days": "dní",
"Degraded Performance": "Znížený výkon",
"Didn't receive the code? Resend": "Nedostali ste kód? ",
"Day": "Deň",
"Day Uptime": "Denná dostupnosť",
"Days": "Dní",
"Degraded": "Zhoršený",
"DEGRADED": "ZHORŠENÝ",
"Degraded Performance": "Zhoršený výkon",
"Didn't receive the code? Resend": "Nedostali ste kód? Odoslať znova",
"Down": "Nedostupný",
"DOWN": "VÝPADOK",
"Duration": "Trvanie",
"Edit Monitor": "Upraviť monitor",
"Email address": "E-mailová adresa",
"Embed Monitor": "Vložiť monitor",
"Embed this monitor in your website or app": "Vložte tento monitor na svoj web alebo do aplikácie",
"End Time": "Čas ukončenia",
"Enter the verification code sent to your email.": "Zadajte overovací kód odoslaný na váš e-mail.",
"End Time": "Koniec",
"Enter the verification code sent to your email.": "Zadajte overovací kód zaslaný na váš e-mail.",
"Events": "Udalosti",
"Failed to load data": "Nepodarilo sa načítať údaje",
"Failed to load latency data": "Nepodarilo sa načítať údaje o latencii",
"Failed to load status data for this day": "Nepodarilo sa načítať údaje o stave pre tento deň",
"Failed to send verification code": "Odoslanie overovacieho kódu zlyhalo",
"Failed to update preference": "Predvoľbu sa nepodarilo aktualizovať",
"Get badges for this monitor": "Získajte odznaky pre tento monitor",
"Get notified about incidents and scheduled maintenance.": "Získajte upozornenia na incidenty a plánovanú údržbu.",
"Get notified about incidents updates": "Získajte upozornenia na aktualizácie incidentov",
"Get notified about scheduled maintenance": "Získajte upozornenia na plánovanú údržbu",
"Failed to load data": "Nepodarilo sa načítať dáta",
"Failed to load latency data": "Nepodarilo sa načítať dáta latencie",
"Failed to load status data for this day": "Nepodarilo sa načítať dáta stavu pre tento deň",
"Failed to send verification code": "Nepodarilo sa odoslať overovací kód",
"Failed to update preference": "Nepodarilo sa aktualizovať nastavenia",
"Format": "Formát",
"Get badges for this monitor": "Získať odznaky pre tento monitor",
"Get notified about incidents and scheduled maintenance.": "Dostávajte upozornenia na incidenty a plánovanú údržbu.",
"Get notified about incidents updates": "Dostávajte upozornenia na aktualizácie incidentov",
"Get notified about scheduled maintenance": "Dostávajte upozornenia na plánovanú údržbu",
"Home": "Domov",
"IDENTIFIED": "IDENTIFIKOVANÉ",
"iFrame": "iFrame",
"Impact": "Vplyv",
"Impact": "Dopad",
"incident": "Incident",
"Incident": "Incident",
"Incident Updates": "Aktualizácie incidentov",
"Incidents": "Incidenty",
"Included Monitors": "Included Monitors",
"Last Updated": "Posledná aktualizácia",
"Included Monitors": "Monitorov zahrnuté",
"INVESTIGATING": "VYŠETROVANIE",
"Last Updated": "Naposledy aktualizované",
"Latency": "Latencia",
"Latency Embed": "Latency Embed",
"Latency Over Time": "Latencia v priebehu času",
"Latency Embed": "Vložená latencia",
"Latency Over Time": "Latencia v čase",
"Latency Trend": "Trend latencie",
"Latest Latency": "Najnovšia latencia",
"Latest Status": "Najnovší stav",
"Light": "Svetlo",
"Live Status": "Živý stav",
"Loading your preferences...": "Načítavajú sa vaše preferencie...",
"Latest Latency": "Posledná latencia",
"Latest Status": "Posledný stav",
"Light": "Svetlý",
"Live Status": "Aktuálny stav",
"Loading your preferences...": "Načítavam vaše nastavenia...",
"maintenance": "Údržba",
"Maintenance": "Údržba",
"MAINTENANCE": "ÚDRŽBA",
"Maintenance Updates": "Aktualizácie údržby",
"Maintenances": "Údržba",
"Major System Outage": "Veľký výpadok systému",
"Manage your notification preferences.": "Spravujte svoje predvoľby upozornení.",
"Max Latency": "Maximálna latencia",
"Maintenances": "Údržby",
"Major System Outage": "Závažný výpadok systému",
"Manage Site": "Spravovať stránku",
"Manage your notification preferences.": "Spravujte svoje nastavenia oznámení.",
"Max Latency": "Max. latencia",
"maximum": "maximálna",
"Maximum Latency": "Maximálna latencia",
"Min Latency": "Minimálna latencia",
"Min Latency": "Min. latencia",
"minimum": "minimálna",
"Minimum Latency": "Minimálna latencia",
"Minute-by-minute status data for this day": "Údaje o stave z minúty na minútu pre tento deň",
"Network error. Please try again.": "Chyba siete. ",
"No Events in %currentMonth": "Žiadne udalosti v %currentMonth",
"No events to show": "No events to show",
"No incidents for this day": "Žiadne incidenty pre tento deň",
"No latency data available for this day": "Pre tento deň nie sú k dispozícii žiadne údaje o latencii",
"No maintenances for this day": "Na tento deň nie sú žiadne údržby",
"No monitors affected": "Nie sú ovplyvnené žiadne monitory",
"No monitors available.": "No monitors available.",
"No ongoing maintenances": "Žiadna priebežná údržba",
"No past maintenances": "Žiadna minulá údržba",
"Minute-by-minute status data for this day": "Minútové dáta stavu pre tento deň",
"MONITORING": "MONITOROVANIE",
"Network error. Please try again.": "Chyba siete. Skúste to znova.",
"No Events in %currentMonth": "V mesiaci %currentMonth nie sú plánované žiadne udalosti",
"No events to show": "Žiadne udalosti na zobrazenie",
"No incidents for this day": "Pre tento deň nie sú evidované žiadne incidenty",
"No latency data available for this day": "Pre tento deň nie sú k dispozícii dáta latencie",
"No maintenances for this day": "Pre tento deň nie je naplánovaná žiadna údržba",
"No monitors affected": "Žiadne zasiahnuté monitory",
"No monitors available.": "Žiadne dostupné monitory.",
"No ongoing maintenances": "Žiadna prebiehajúca údržba",
"No past maintenances": "Žiadna minula údržba",
"No Status Available": "Stav nie je k dispozícii",
"No upcoming maintenances": "Žiadna nadchádzajúca údržba",
"No Updates": "No Updates",
"No updates yet": "Zatiaľ žiadne aktualizácie",
"Notifications": "Notifications",
"No Updates": "Žiadne aktualizácie",
"No updates yet": "Zatiaľ bez aktualizácií",
"NO_DATA": "Žiadne dáta",
"Notifications": "Oznámenia",
"One-time": "Jednorazovo",
"Ongoing": "Prebieha",
"Partial Degraded Performance": "Čiastočne znížený výkon",
"Ongoing": "Prebiehajúce",
"Ongoing Maintenances": "Prebiehajúce údržby",
"Operational": "V prevádzke",
"Partial Degraded Performance": "Čiastočne zhoršený výkon",
"Partial System Outage": "Čiastočný výpadok systému",
"Past": "Minulosť",
"Per-Minute Status": "Stav za minútu",
"Pinging": "Ping",
"Past": "Minulé",
"Per-Minute Status": "Stav po minútach",
"Pinging": "Pingovanie",
"Please enter a valid email address": "Zadajte platnú e-mailovú adresu",
"Please enter the 6-digit verification code": "Zadajte 6-miestny overovací kód",
"Read less": "Čítajte menej",
"Read more": "Prečítajte si viac",
"Recurring": "Recurring",
"Read less": "Zobraziť menej",
"Read more": "Zobraziť viac",
"READY": "PRIPRAVENÉ",
"Recent Incidents": "Nedávne incidenty",
"Recurring": "Opakujúce sa",
"RESOLVED": "VYRIEŠENÉ",
"SCHEDULED": "NAPLÁNOVANÉ",
"Scheduled Events (%count)": "Plánované udalosti (%count)",
"Scheduled Windows": "Naplánované úlohy",
"Script": "Skript",
"Select Language": "Vyberte Jazyk",
"Select latency metric to display": "Select latency metric to display",
"Select Range": "Vyberte položku Rozsah",
"Sending...": "Odosiela sa...",
"Standard": "Štandardné",
"Start Time": "Čas začiatku",
"Select Language": "Vyberte jazyk",
"Select latency metric to display": "Vyberte metriku latencie na zobrazenie",
"Select Range": "Vyberte rozsah",
"Sending...": "Odosielanie...",
"Standard": "Štandardný",
"Start Time": "Začiatok",
"Status": "Stav",
"Status Badge": "Stavový odznak",
"Status Embed": "Stav Vložiť",
"Status Badge": "Odznak stavu",
"Status Embed": "Status na vloženie",
"Status history and latency trend": "História stavu a trend latencie",
"Subscribe": "Prihlásiť sa na odber",
"Subscribe to Updates": "Prihláste sa na odber aktualizácií",
"There are no incidents or maintenances scheduled for this month.": "Na tento mesiac nie sú naplánované žiadne incidenty ani údržba.",
"There are no ongoing incidents or maintenance events.": "There are no ongoing incidents or maintenance events.",
"Total Incidents": "Celkový počet incidentov",
"Total Maintenances": "Celková údržba",
"Under Maintenance": "V časti Údržba",
"Unknown impact": "Neznámy vplyv",
"Subscribe": "Odober",
"Subscribe to Updates": "Odober aktualizácie",
"Theme": "Motív",
"There are no incidents or maintenances scheduled for this month.": "Na tento mesiac nie sú naplánované incidenty ani údržba.",
"There are no ongoing incidents or maintenance events.": "V tejto chvíli neprebiehajú žiadne incidenty ani údržba.",
"Timeline": "Časová os",
"Total Incidents": "Celkom incidentov",
"Total Maintenances": "Celkom údržieb",
"Under Maintenance": "Prebieha údržba",
"Unknown impact": "Neznámy dopad",
"UP": "V PREVÁDZKE",
"Upcoming": "Nadchádzajúce",
"Updates": "Updates",
"Upcoming Maintenances": "Nadchádzajúce údržby",
"Update Incident": "Aktualizovať incident",
"Update Maintenance": "Aktualizovať údržbu",
"Updates": "Aktualizácie",
"Updates (%count)": "Aktualizácie (%count)",
"Uptime": "Uptime",
"Uptime": "Dostupnosť",
"Uptime Badge": "Odznak dostupnosti",
"Verification failed": "Overenie zlyhalo",
"Verify": "Overiť",
"Verifying": "Overuje sa",
"Verifying": "Overovanie",
"We sent a 6-digit code to": "Poslali sme 6-miestny kód na"
}
}
+30 -8
View File
@@ -8,14 +8,21 @@
"Avg Latency": "Ortalama Gecikme",
"Back": "Geri",
"Badges": "Rozetler",
"CANCELLED": "İPTAL EDİLMİŞ",
"COMPLETED": "TAMAMLANMIŞ",
"Continue": "Devam etmek",
"Copied": "Kopyalandı",
"Current": "Güncel",
"Dark": "Karanlık",
"Day": "Gün",
"Day Uptime": "Gün Çalışma Süresi",
"Days": "Günler",
"Degraded": "Bozulmuş",
"DEGRADED": "DÜŞÜK PERFORMANS",
"Degraded Performance": "Düşük performans",
"Didn't receive the code? Resend": "Kodu almadınız mı? ",
"Down": "Çevrimdışı",
"DOWN": "ÇÖKME",
"Duration": "Süre",
"Email address": "E-posta adresi",
"Embed Monitor": "Gömülü Monitör",
@@ -32,11 +39,14 @@
"Get notified about incidents and scheduled maintenance.": "Olaylar ve planlı bakım hakkında bildirim alın.",
"Get notified about incidents updates": "Olay güncellemeleri hakkında bildirim alın",
"Get notified about scheduled maintenance": "Planlanmış bakım hakkında bildirim alın",
"IDENTIFIED": "IDENTIFIED",
"iFrame": "iFrame",
"Impact": "Darbe",
"incident": "Olay",
"Incident Updates": "Olay Güncellemeleri",
"Incidents": "Olaylar",
"Included Monitors": "Included Monitors",
"INVESTIGATING": "INVESTIGATING",
"Last Updated": "Son Güncelleme",
"Latency": "Gecikme",
"Latency Embed": "Gecikme Yerleştirme",
@@ -47,32 +57,37 @@
"Light": "Işık",
"Live Status": "Canlı Durum",
"Loading your preferences...": "Tercihleriniz yükleniyor...",
"maintenance": "Bakım",
"MAINTENANCE": "BAKIM",
"Maintenance Updates": "Bakım Güncellemeleri",
"Maintenances": "Bakımlar",
"Major System Outage": "Büyük sistem kesintisi",
"Manage Site": "Siteyi yönet",
"Manage your notification preferences.": "Bildirim tercihlerinizi yönetin.",
"Max Latency": "Maksimum Gecikme",
"Maximum Latency": "Maksimum Gecikme",
"Min Latency": "Minimum Gecikme",
"Minimum Latency": "Minimum Gecikme",
"Minute-by-minute status data for this day": "Bu günün dakika dakika durum verileri",
"MONITORING": "MONITORING",
"Network error. Please try again.": "Ağ hatası. ",
"No Events in %currentMonth": "%currentMonth Bölgesinde Etkinlik Yok",
"No events to show": "No events to show",
"No events to show": "Gösterilecek olay yok",
"No incidents for this day": "Bu gün için herhangi bir olay yok",
"No latency data available for this day": "Bu güne ait gecikme verisi yok",
"No maintenances for this day": "Bu gün için bakım yok",
"No monitors affected": "Hiçbir monitör etkilenmedi",
"No monitors available.": "No monitors available.",
"No monitors available.": "Kullanılabilir monitör yok.",
"No ongoing maintenances": "Devam eden bakım yok",
"No past maintenances": "Geçmiş bakım yok",
"No Status Available": "Durum bilgisi yok",
"No upcoming maintenances": "Yaklaşan bakım yok",
"No Updates": "No Updates",
"No Updates": "Güncelleme yok",
"No updates yet": "Henüz güncelleme yok",
"Notifications": "Notifications",
"Notifications": "Bildirimler",
"One-time": "Bir kerelik",
"Ongoing": "devam ediyor",
"Operational": "Çalışıyor",
"Partial Degraded Performance": "Kısmi düşük performans",
"Partial System Outage": "Kısmi sistem kesintisi",
"Past": "Geçmiş",
@@ -82,10 +97,14 @@
"Please enter the 6-digit verification code": "Lütfen 6 haneli doğrulama kodunu girin",
"Read less": "Daha az oku",
"Read more": "Devamını oku",
"Recurring": "Recurring",
"READY": "HAZIR",
"Recurring": "Tekrarlayan",
"RESOLVED": "RESOLVED",
"SCHEDULED": "PLANLANMIŞ",
"Scheduled Events (%count)": "Planlanan etkinlikler (%count)",
"Script": "Senaryo",
"Select Language": "Dil Seçiniz",
"Select latency metric to display": "Select latency metric to display",
"Select latency metric to display": "Gösterilecek gecikme metriğini seçin",
"Select Range": "Aralığı Seçin",
"Sending...": "Gönderiliyor...",
"Standard": "Standart",
@@ -97,13 +116,16 @@
"Subscribe": "Abone",
"Subscribe to Updates": "Güncellemelere abone olun",
"There are no incidents or maintenances scheduled for this month.": "Bu ay için planlanmış herhangi bir olay veya bakım bulunmamaktadır.",
"There are no ongoing incidents or maintenance events.": "There are no ongoing incidents or maintenance events.",
"There are no ongoing incidents or maintenance events.": "Devam eden olay veya bakım etkinliği yok.",
"Total Incidents": "Toplam Olaylar",
"Total Maintenances": "Toplam Bakımlar",
"Under Maintenance": "Bakımda",
"Unknown impact": "Bilinmeyen etki",
"UP": "ÇALIŞIYOR",
"Upcoming": "Yaklaşan",
"Updates": "Updates",
"Update Incident": "Olayı güncelle",
"Update Maintenance": "Bakımı güncelle",
"Updates": "Güncellemeler",
"Updates (%count)": "Güncellemeler (%count)",
"Uptime": "Çalışma süresi",
"Uptime Badge": "Çalışma Süresi Rozeti",
+30 -8
View File
@@ -8,14 +8,21 @@
"Avg Latency": "Độ trễ trung bình",
"Back": "Mặt sau",
"Badges": "Huy hiệu",
"CANCELLED": "ĐÃ HỦY",
"COMPLETED": "HOÀN THÀNH",
"Continue": "Tiếp tục",
"Copied": "Đã sao chép",
"Current": "Hiện tại",
"Dark": "Tối tăm",
"Day": "Ngày",
"Day Uptime": "Thời gian hoạt động trong ngày",
"Days": "Ngày",
"Degraded": "Suy giảm",
"DEGRADED": "SUY GIẢM",
"Degraded Performance": "Hiệu suất suy giảm",
"Didn't receive the code? Resend": "Không nhận được mã? ",
"Down": "Ngừng hoạt động",
"DOWN": "NGỪNG HOẠT ĐỘNG",
"Duration": "Khoảng thời gian",
"Email address": "Địa chỉ email",
"Embed Monitor": "Màn hình nhúng",
@@ -32,11 +39,14 @@
"Get notified about incidents and scheduled maintenance.": "Nhận thông báo về sự cố và bảo trì theo lịch trình.",
"Get notified about incidents updates": "Nhận thông báo về cập nhật sự cố",
"Get notified about scheduled maintenance": "Nhận thông báo về bảo trì theo lịch trình",
"IDENTIFIED": "IDENTIFIED",
"iFrame": "iFrame",
"Impact": "Sự va chạm",
"incident": "Sự cố",
"Incident Updates": "Cập nhật sự cố",
"Incidents": "Sự cố",
"Included Monitors": "Included Monitors",
"INVESTIGATING": "INVESTIGATING",
"Last Updated": "Cập nhật lần cuối",
"Latency": "Độ trễ",
"Latency Embed": "Nhúng độ trễ",
@@ -47,32 +57,37 @@
"Light": "Ánh sáng",
"Live Status": "Trạng thái trực tiếp",
"Loading your preferences...": "Đang tải tùy chọn của bạn...",
"maintenance": "Bảo trì",
"MAINTENANCE": "BẢO TRÌ",
"Maintenance Updates": "Cập nhật bảo trì",
"Maintenances": "Bảo trì",
"Major System Outage": "Sự cố hệ thống nghiêm trọng",
"Manage Site": "Quản lý trang",
"Manage your notification preferences.": "Quản lý tùy chọn thông báo của bạn.",
"Max Latency": "Độ trễ tối đa",
"Maximum Latency": "Độ trễ tối đa",
"Min Latency": "Độ trễ tối thiểu",
"Minimum Latency": "Độ trễ tối thiểu",
"Minute-by-minute status data for this day": "Dữ liệu trạng thái từng phút cho ngày này",
"MONITORING": "MONITORING",
"Network error. Please try again.": "Lỗi mạng. ",
"No Events in %currentMonth": "Không có sự kiện nào trong %currentMonth",
"No events to show": "No events to show",
"No events to show": "Không có sự kiện nào để hiển thị",
"No incidents for this day": "Không có sự cố nào trong ngày này",
"No latency data available for this day": "Không có dữ liệu về độ trễ cho ngày này",
"No maintenances for this day": "Không bảo trì trong ngày này",
"No monitors affected": "Không có màn hình nào bị ảnh hưởng",
"No monitors available.": "No monitors available.",
"No monitors available.": "Không có trình giám sát nào khả dụng.",
"No ongoing maintenances": "Không có bảo trì liên tục",
"No past maintenances": "Không có bảo trì trước đây",
"No Status Available": "Không có trạng thái khả dụng",
"No upcoming maintenances": "Không có bảo trì sắp tới",
"No Updates": "No Updates",
"No Updates": "Không có cập nhật",
"No updates yet": "Chưa có cập nhật nào",
"Notifications": "Notifications",
"Notifications": "Thông báo",
"One-time": "Một lần",
"Ongoing": "Đang thực hiện",
"Operational": "Hoạt động",
"Partial Degraded Performance": "Hiệu suất suy giảm một phần",
"Partial System Outage": "Sự cố hệ thống một phần",
"Past": "Quá khứ",
@@ -82,10 +97,14 @@
"Please enter the 6-digit verification code": "Vui lòng nhập mã xác minh gồm 6 chữ số",
"Read less": "Đọc ít hơn",
"Read more": "Đọc thêm",
"Recurring": "Recurring",
"READY": "SẴN SÀNG",
"Recurring": "Định kỳ",
"RESOLVED": "RESOLVED",
"SCHEDULED": "ĐÃ LÊN LỊCH",
"Scheduled Events (%count)": "Sự kiện đã lên lịch (%count)",
"Script": "Kịch bản",
"Select Language": "Chọn ngôn ngữ",
"Select latency metric to display": "Select latency metric to display",
"Select latency metric to display": "Chọn chỉ số độ trễ để hiển thị",
"Select Range": "Chọn phạm vi",
"Sending...": "Đang gửi...",
"Standard": "Tiêu chuẩn",
@@ -97,13 +116,16 @@
"Subscribe": "Đặt mua",
"Subscribe to Updates": "Đăng ký cập nhật",
"There are no incidents or maintenances scheduled for this month.": "Không có sự cố hoặc bảo trì nào được lên kế hoạch trong tháng này.",
"There are no ongoing incidents or maintenance events.": "There are no ongoing incidents or maintenance events.",
"There are no ongoing incidents or maintenance events.": "Không có sự cố hoặc sự kiện bảo trì nào đang diễn ra.",
"Total Incidents": "Tổng số sự cố",
"Total Maintenances": "Tổng số lần bảo trì",
"Under Maintenance": "Đang bảo trì",
"Unknown impact": "Tác động không xác định",
"UP": "HOẠT ĐỘNG",
"Upcoming": "Sắp tới",
"Updates": "Updates",
"Update Incident": "Cập nhật sự cố",
"Update Maintenance": "Cập nhật bảo trì",
"Updates": "Cập nhật",
"Updates (%count)": "Cập nhật (%count)",
"Uptime": "Thời gian hoạt động",
"Uptime Badge": "Huy hiệu thời gian hoạt động",
+30 -8
View File
@@ -8,14 +8,21 @@
"Avg Latency": "平均延迟",
"Back": "后退",
"Badges": "徽章",
"CANCELLED": "已取消",
"COMPLETED": "已完成",
"Continue": "继续",
"Copied": "已复制",
"Current": "当前",
"Dark": "黑暗的",
"Day": "天",
"Day Uptime": "日间正常运行时间",
"Days": "天",
"Degraded": "降级",
"DEGRADED": "降级",
"Degraded Performance": "性能下降",
"Didn't receive the code? Resend": "没有收到代码?",
"Down": "宕机",
"DOWN": "故障",
"Duration": "期间",
"Email address": "电子邮件",
"Embed Monitor": "嵌入监视器",
@@ -32,11 +39,14 @@
"Get notified about incidents and scheduled maintenance.": "获取有关事件和定期维护的通知。",
"Get notified about incidents updates": "获取有关事件更新的通知",
"Get notified about scheduled maintenance": "获取有关定期维护的通知",
"IDENTIFIED": "IDENTIFIED",
"iFrame": "框架",
"Impact": "影响",
"incident": "事件",
"Incident Updates": "事件更新",
"Incidents": "事件",
"Included Monitors": "Included Monitors",
"INVESTIGATING": "INVESTIGATING",
"Last Updated": "最后更新",
"Latency": "延迟",
"Latency Embed": "延迟嵌入",
@@ -47,32 +57,37 @@
"Light": "光",
"Live Status": "实时状态",
"Loading your preferences...": "正在加载您的偏好设置...",
"maintenance": "维护",
"MAINTENANCE": "维护中",
"Maintenance Updates": "维护更新",
"Maintenances": "维护保养",
"Major System Outage": "重大系统故障",
"Manage Site": "管理站点",
"Manage your notification preferences.": "管理您的通知首选项。",
"Max Latency": "最大延迟",
"Maximum Latency": "最大延迟",
"Min Latency": "最短延迟",
"Minimum Latency": "最短延迟",
"Minute-by-minute status data for this day": "当日每分钟的状态数据",
"MONITORING": "MONITORING",
"Network error. Please try again.": "网络错误。",
"No Events in %currentMonth": "%currentMonth 没有活动",
"No events to show": "No events to show",
"No events to show": "没有可显示的事件",
"No incidents for this day": "这一天没有发生任何事件",
"No latency data available for this day": "当天没有可用的延迟数据",
"No maintenances for this day": "今日无维护",
"No monitors affected": "没有显示器受到影响",
"No monitors available.": "No monitors available.",
"No monitors available.": "没有可用的监控项。",
"No ongoing maintenances": "无需持续维护",
"No past maintenances": "过去没有维护过",
"No Status Available": "无可用状态",
"No upcoming maintenances": "没有即将进行的维护",
"No Updates": "No Updates",
"No Updates": "没有更新",
"No updates yet": "还没有更新",
"Notifications": "Notifications",
"Notifications": "通知",
"One-time": "一度",
"Ongoing": "进行中",
"Operational": "正常运行",
"Partial Degraded Performance": "部分性能下降",
"Partial System Outage": "部分系统故障.",
"Past": "过去的",
@@ -82,10 +97,14 @@
"Please enter the 6-digit verification code": "请输入6位验证码",
"Read less": "少读书",
"Read more": "阅读更多",
"Recurring": "Recurring",
"READY": "就绪",
"Recurring": "周期性",
"RESOLVED": "RESOLVED",
"SCHEDULED": "已安排",
"Scheduled Events (%count)": "计划事件 (%count)",
"Script": "脚本",
"Select Language": "选择语言",
"Select latency metric to display": "Select latency metric to display",
"Select latency metric to display": "选择要显示的延迟指标",
"Select Range": "选择范围",
"Sending...": "正在发送...",
"Standard": "标准",
@@ -97,13 +116,16 @@
"Subscribe": "订阅",
"Subscribe to Updates": "订阅更新",
"There are no incidents or maintenances scheduled for this month.": "本月没有安排任何事故或维护。",
"There are no ongoing incidents or maintenance events.": "There are no ongoing incidents or maintenance events.",
"There are no ongoing incidents or maintenance events.": "当前没有正在进行的事件或维护活动。",
"Total Incidents": "事故总数",
"Total Maintenances": "全面维护",
"Under Maintenance": "维护中",
"Unknown impact": "未知影响",
"UP": "正常",
"Upcoming": "即将推出",
"Updates": "Updates",
"Update Incident": "更新事件",
"Update Maintenance": "更新维护",
"Updates": "更新",
"Updates (%count)": "更新 (%count)",
"Uptime": "正常运行时间",
"Uptime Badge": "正常运行时间徽章",
+133
View File
@@ -0,0 +1,133 @@
{
"name": "繁體中文(台灣)",
"mappings": {
"%latency %metric latency": "%latency %metric 延遲",
"Affected Monitors (%count)": "受影響的監控項目 (%count)",
"All Systems Operational": "所有系統正常運作",
"Average Latency": "平均延遲",
"Avg Latency": "平均延遲",
"Back": "返回",
"Badges": "徽章",
"CANCELLED": "已取消",
"COMPLETED": "已完成",
"Continue": "繼續",
"Copied": "已複製",
"Current": "目前",
"Dark": "深色",
"Day": "天",
"Day Uptime": "當日正常運作時間",
"Days": "天",
"DEGRADED": "降級",
"Degraded Performance": "效能降低",
"Didn't receive the code? Resend": "沒有收到驗證碼?重新傳送",
"DOWN": "故障",
"Duration": "持續時間",
"Edit Monitor": "編輯監控",
"Email address": "電子郵件地址",
"Embed Monitor": "嵌入監控",
"Embed this monitor in your website or app": "將此監控嵌入您的網站或應用程式",
"End Time": "結束時間",
"Enter the verification code sent to your email.": "請輸入傳送至您電子郵件的驗證碼。",
"Events": "事件",
"Failed to load data": "載入資料失敗",
"Failed to load latency data": "載入延遲資料失敗",
"Failed to load status data for this day": "無法載入當日的狀態資料",
"Failed to send verification code": "傳送驗證碼失敗",
"Failed to update preference": "更新偏好設定失敗",
"Get badges for this monitor": "取得此監控的徽章",
"Get notified about incidents and scheduled maintenance.": "接收事件與排程維護的通知。",
"Get notified about incidents updates": "接收事件更新通知",
"Get notified about scheduled maintenance": "接收排程維護通知",
"IDENTIFIED": "已確認",
"iFrame": "iFrame",
"Impact": "影響",
"Incident Updates": "事件更新",
"Incidents": "事件",
"Included Monitors": "包含的監控項目",
"INVESTIGATING": "調查中",
"Last Updated": "最後更新",
"Latency": "延遲",
"Latency Embed": "延遲嵌入",
"Latency Over Time": "延遲趨勢圖",
"Latency Trend": "延遲趨勢",
"Latest Latency": "最新延遲",
"Latest Status": "最新狀態",
"Light": "淺色",
"Live Status": "即時狀態",
"Loading your preferences...": "正在載入您的偏好設定……",
"MAINTENANCE": "維護中",
"Maintenance Updates": "維護更新",
"Maintenances": "維護作業",
"Major System Outage": "重大系統中斷",
"Manage Site": "管理網站",
"Manage your notification preferences.": "管理您的通知偏好設定。",
"Max Latency": "最大延遲",
"Maximum Latency": "最大延遲",
"Min Latency": "最小延遲",
"Minimum Latency": "最小延遲",
"Minute-by-minute status data for this day": "當日逐分鐘狀態資料",
"MONITORING": "監控中",
"Network error. Please try again.": "網路錯誤,請重試。",
"No Events in %currentMonth": "%currentMonth 沒有事件",
"No events to show": "沒有可顯示的事件",
"No incidents for this day": "當日無事件",
"No latency data available for this day": "當日無延遲資料",
"No maintenances for this day": "當日無維護作業",
"No monitors affected": "沒有受影響的監控項目",
"No monitors available.": "沒有可用的監控項目。",
"No ongoing maintenances": "沒有進行中的維護作業",
"No past maintenances": "沒有過去的維護作業",
"No Status Available": "無可用狀態",
"No upcoming maintenances": "沒有即將進行的維護作業",
"No Updates": "沒有更新",
"No updates yet": "尚無更新",
"Notifications": "通知",
"One-time": "單次",
"Ongoing": "進行中",
"Partial Degraded Performance": "部分效能降低",
"Partial System Outage": "部分系統中斷",
"Past": "過去",
"Per-Minute Status": "逐分鐘狀態",
"Pinging": "偵測中",
"Please enter a valid email address": "請輸入有效的電子郵件地址",
"Please enter the 6-digit verification code": "請輸入 6 位數驗證碼",
"Read less": "收合",
"Read more": "展開",
"READY": "就緒",
"Recurring": "週期性",
"RESOLVED": "已解決",
"SCHEDULED": "已排程",
"Scheduled Events (%count)": "排程事件 (%count)",
"Script": "程式碼",
"Select Language": "選擇語言",
"Select latency metric to display": "選擇要顯示的延遲指標",
"Select Range": "選擇範圍",
"Sending...": "傳送中……",
"Standard": "標準",
"Start Time": "開始時間",
"Status": "狀態",
"Status Badge": "狀態徽章",
"Status Embed": "狀態嵌入",
"Status history and latency trend": "狀態歷程與延遲趨勢",
"Subscribe": "訂閱",
"Subscribe to Updates": "訂閱更新",
"There are no incidents or maintenances scheduled for this month.": "本月沒有排程的事件或維護作業。",
"There are no ongoing incidents or maintenance events.": "目前沒有進行中的事件或維護作業。",
"Total Incidents": "事件總數",
"Total Maintenances": "維護總數",
"Under Maintenance": "維護中",
"Unknown impact": "影響不明",
"UP": "正常",
"Upcoming": "即將進行",
"Update Incident": "更新事件",
"Update Maintenance": "更新維護",
"Updates": "更新",
"Updates (%count)": "更新 (%count)",
"Uptime": "正常運作時間",
"Uptime Badge": "正常運作時間徽章",
"Verification failed": "驗證失敗",
"Verify": "驗證",
"Verifying": "驗證中",
"We sent a 6-digit code to": "我們已傳送 6 位數驗證碼至"
}
}
@@ -2,6 +2,7 @@ import { json, error } from "@sveltejs/kit";
import type { APIServerRequest } from "$lib/server/types/api-server";
import type { IncidentForMonitorListWithComments, MaintenanceEventsMonitorList } from "$lib/server/types/db";
import db from "$lib/server/db/db";
import { GetAllSiteData } from "$lib/server/controllers/siteDataController";
interface EventsByMonthRequest {
start_ts: number;
@@ -15,6 +16,13 @@ export interface EventsByMonthResponse {
export default async function post(req: APIServerRequest): Promise<Response> {
const body = req.body as EventsByMonthRequest;
const queryParams = req.query;
const rawPagePath = queryParams.get("page_path");
let pagePath = rawPagePath?.trim() || null;
if (pagePath && ["undefined", "null"].includes(pagePath.toLowerCase())) {
pagePath = null;
}
if (!body.start_ts || typeof body.start_ts !== "number") {
return error(400, { message: "start_ts is required and must be a number" });
@@ -24,9 +32,28 @@ export default async function post(req: APIServerRequest): Promise<Response> {
return error(400, { message: "end_ts is required and must be a number" });
}
// get site data
const siteData = await GetAllSiteData();
const { globalPageVisibilitySettings } = siteData;
const isExclusivePageEnabled = !!globalPageVisibilitySettings?.forceExclusivity;
if (isExclusivePageEnabled && !pagePath) {
pagePath = "";
}
let pathMonitors: string[] | undefined = undefined;
if (pagePath !== null) {
const pageData = await db.getPageByPath(pagePath);
if (!pageData) {
return error(404, { message: "Page not found" });
}
const monitorsForPage = await db.getPageMonitorsExcludeHidden(pageData.id);
pathMonitors = monitorsForPage.map((m) => m.monitor_tag);
}
const [incidents, maintenances] = await Promise.all([
db.getIncidentsForEventsByDateRange(body.start_ts, body.end_ts),
db.getMaintenanceEventsForEventsByDateRange(body.start_ts, body.end_ts),
db.getIncidentsForEventsByDateRange(body.start_ts, body.end_ts, pathMonitors),
db.getMaintenanceEventsForEventsByDateRange(body.start_ts, body.end_ts, pathMonitors),
]);
const response: EventsByMonthResponse = {
+24 -1
View File
@@ -1,17 +1,40 @@
import { json } from "@sveltejs/kit";
import type { APIServerRequest } from "$lib/server/types/api-server";
import { GetAllPages } from "$lib/server/controllers/pagesController";
import { GetSiteDataByKey } from "$lib/server/controllers/siteDataController";
import type { PageNavItem } from "$lib/server/controllers/dashboardController";
import type { PageOrderingSettings } from "$lib/types/site";
/**
* GET /dashboard-apis/pages
* Returns all pages as PageNavItem[] (page_title, page_path)
* Respects pageOrderingSettings if enabled
*/
export default async function get(_req: APIServerRequest): Promise<Response> {
const allPagesData = await GetAllPages();
const pages: PageNavItem[] = allPagesData.map((p) => ({
const pageOrderingSettings = (await GetSiteDataByKey("pageOrderingSettings")) as PageOrderingSettings | null;
let orderedPages = allPagesData;
if (pageOrderingSettings?.enabled && pageOrderingSettings.order?.length > 0) {
const orderMap = new Map(pageOrderingSettings.order.map((id, idx) => [id, idx]));
orderedPages = [...allPagesData].sort((a, b) => {
const aIdx = orderMap.get(a.id);
const bIdx = orderMap.get(b.id);
// Pages in the order list come first, sorted by their position
if (aIdx !== undefined && bIdx !== undefined) return aIdx - bIdx;
if (aIdx !== undefined) return -1;
if (bIdx !== undefined) return 1;
// Pages not in the order list keep their default order (by id)
return a.id - b.id;
});
}
const pages: PageNavItem[] = orderedPages.map((p) => ({
page_title: p.page_title,
page_path: p.page_path,
page_header: p.page_header,
page_logo: p.page_logo,
}));
return json(pages);
}
+5
View File
@@ -13,6 +13,11 @@ export async function setCache<T>(key: string, value: T | null | undefined, ttlS
await redis.set(getCacheKey(key), payload, "EX", ttl);
}
export async function deleteCache(key: string): Promise<void> {
const redis = redisIOConnection();
await redis.del(getCacheKey(key));
}
export async function getCache<T>(
key: string,
fetcher?: () => Promise<T | null | undefined> | T | null | undefined,
+6 -1
View File
@@ -1,5 +1,5 @@
import type { MonitoringData } from "../types/db.js";
import { setCache, getCache } from "./cache.js";
import { setCache, getCache, deleteCache } from "./cache.js";
export async function SetLastMonitoringValue(tag: string, value: MonitoringData): Promise<void> {
await setCache<MonitoringData>(tag + ":last_status", value, 86400); //set ttl to 1 day
}
@@ -20,3 +20,8 @@ export async function SetLastHeartbeat(tag: string, timestamp: number): Promise<
export async function GetLastHeartbeat(tag: string): Promise<{ timestamp: number } | null> {
return await getCache<{ timestamp: number }>("last_heartbeat:" + tag, undefined, 45 * 86400);
}
export async function DeleteMonitorCaches(tag: string): Promise<void> {
await deleteCache(tag + ":last_status");
await deleteCache("last_heartbeat:" + tag);
}
@@ -55,12 +55,14 @@ export const VerifyToken = async (token: string): Promise<TokenPayload | undefin
export const GetSMTPFromENV = (): SMTPConfiguration | null => {
//if variables are not return null
const smtpPassword = process.env.SMTP_PASS || process.env.SMTP_PASSWORD;
const fromEmail = process.env.SMTP_FROM_EMAIL || process.env.SMTP_SENDER;
if (
!!!process.env.SMTP_HOST ||
!!!process.env.SMTP_PORT ||
!!!process.env.SMTP_USER ||
!!!process.env.SMTP_FROM_EMAIL ||
!!!process.env.SMTP_PASS
!!!fromEmail ||
!!!smtpPassword
) {
return null;
}
@@ -69,8 +71,8 @@ export const GetSMTPFromENV = (): SMTPConfiguration | null => {
smtp_host: process.env.SMTP_HOST,
smtp_port: Number(process.env.SMTP_PORT),
smtp_user: process.env.SMTP_USER,
smtp_sender: process.env.SMTP_FROM_EMAIL,
smtp_pass: process.env.SMTP_PASS,
smtp_sender: fromEmail,
smtp_pass: smtpPassword,
smtp_secure: !!Number(process.env.SMTP_SECURE),
};
};
@@ -3,7 +3,7 @@ import { GetMinuteStartNowTimestampUTC, BeginningOfMinute, BeginningOfDay } from
import { GetPageByPathWithMonitors, GetLatestMonitoringDataAllActive } from "./controller.js";
import { GetMonitorsParsed } from "./monitorsController.js";
import { GetStatusSummary, GetStatusBgColor } from "../../clientTools";
import { formatDistanceStrict } from "date-fns";
import type {
IncidentRecord,
IncidentCommentRecord,
@@ -32,7 +32,8 @@ export interface NotificationEvent {
eventTitle: string;
eventDate: string;
eventType: string;
eventDuration: string;
eventStartDateTime: number;
eventEndDateTime: number | null;
eventStatus: string;
}
@@ -150,6 +151,8 @@ export const GetAffectedMonitorsByIncidentId = async (
export interface PageNavItem {
page_title: string;
page_path: string;
page_header: string;
page_logo: string | null;
}
export interface PageDashboardData {
@@ -218,11 +221,6 @@ export const BuildNotificationPayload = (
): NotificationPayload => {
const notifications: Array<NotificationEvent & { sortTs: number }> = [];
const durationFromRange = (startTs: number, endTs: number | null): string => {
const effectiveEnd = Math.max(startTs, endTs ?? nowTs);
return formatDistanceStrict(new Date(startTs * 1000), new Date(effectiveEnd * 1000));
};
for (const incident of ongoingIncidents) {
const ts = incident.start_date_time;
notifications.push({
@@ -231,7 +229,8 @@ export const BuildNotificationPayload = (
eventTitle: incident.title,
eventDate: new Date(ts * 1000).toISOString(),
eventType: "incident",
eventDuration: durationFromRange(incident.start_date_time, incident.end_date_time),
eventStartDateTime: incident.start_date_time,
eventEndDateTime: incident.end_date_time,
eventStatus: incident.state,
});
}
@@ -244,7 +243,8 @@ export const BuildNotificationPayload = (
eventTitle: incident.title,
eventDate: new Date(ts * 1000).toISOString(),
eventType: "incident",
eventDuration: durationFromRange(incident.start_date_time, incident.end_date_time),
eventStartDateTime: incident.start_date_time,
eventEndDateTime: incident.end_date_time,
eventStatus: incident.state,
});
}
@@ -257,7 +257,8 @@ export const BuildNotificationPayload = (
eventTitle: maintenance.title,
eventDate: new Date(ts * 1000).toISOString(),
eventType: "maintenance",
eventDuration: durationFromRange(maintenance.start_date_time, maintenance.end_date_time),
eventStartDateTime: maintenance.start_date_time,
eventEndDateTime: maintenance.end_date_time,
eventStatus: GC.ONGOING,
});
}
@@ -270,7 +271,8 @@ export const BuildNotificationPayload = (
eventTitle: maintenance.title,
eventDate: new Date(ts * 1000).toISOString(),
eventType: "maintenance",
eventDuration: durationFromRange(maintenance.start_date_time, maintenance.end_date_time),
eventStartDateTime: maintenance.start_date_time,
eventEndDateTime: maintenance.end_date_time,
eventStatus: GC.SCHEDULED,
});
}
@@ -283,7 +285,8 @@ export const BuildNotificationPayload = (
eventTitle: maintenance.title,
eventDate: new Date(ts * 1000).toISOString(),
eventType: "maintenance",
eventDuration: durationFromRange(maintenance.start_date_time, maintenance.end_date_time),
eventStartDateTime: maintenance.start_date_time,
eventEndDateTime: maintenance.end_date_time,
eventStatus: GC.COMPLETED,
});
}
@@ -526,3 +526,30 @@ export const ParseIncidentToAPIResp = async (
return resp;
};
export const DeleteIncident = async (incident_id: number): Promise<{ success: boolean }> => {
const incident = await db.getIncidentById(incident_id);
if (!incident) {
throw new Error(`Incident with id ${incident_id} does not exist`);
}
// Set incident_id to null in monitor_alerts_v2
const alerts = await db.getAlertsByIncidentId(incident_id);
for (const alert of alerts) {
await db.updateMonitorAlertV2(alert.id, { incident_id: null });
}
// Delete incident monitors
const monitors = await db.getIncidentMonitorsByIncidentID(incident_id);
for (const monitor of monitors) {
await db.removeIncidentMonitor(incident_id, monitor.monitor_tag);
}
// Delete incident comments permanently
await db.deleteIncidentCommentsByIncidentID(incident_id);
// Delete the incident
await db.deleteIncident(incident_id);
return { success: true };
};
+14 -3
View File
@@ -2,8 +2,15 @@ import MobileDetect from "mobile-detect";
import type { Cookies } from "@sveltejs/kit";
import type { UserRecordPublic } from "$lib/server/types/db";
import seedSiteData from "$lib/server/db/seedSiteData";
import { GetAllSiteData, GetLoggedInSession, GetLocaleFromCookie, GetUsersCount, IsEmailSetup } from "./controller.js";
import type { EventDisplaySettings } from "$lib/types/site.js";
import {
GetAllSiteData,
GetLoggedInSession,
GetLocaleFromCookie,
GetUsersCount,
IsEmailSetup,
IsSetupComplete,
} from "./controller.js";
import type { EventDisplaySettings, GlobalPageVisibilitySettings, SiteDateTimeFormat } from "$lib/types/site.js";
export interface LayoutServerData {
isMobile: boolean;
@@ -62,6 +69,8 @@ export interface LayoutServerData {
eventDisplaySettings: EventDisplaySettings;
socialPreviewImage?: string;
customCSS?: string;
globalPageVisibilitySettings: GlobalPageVisibilitySettings;
dateAndTimeFormat: SiteDateTimeFormat;
}
export async function GetLayoutServerData(cookies: Cookies, request: Request): Promise<LayoutServerData> {
@@ -75,7 +84,7 @@ export async function GetLayoutServerData(cookies: Cookies, request: Request): P
GetUsersCount(),
]);
const isSetupComplete = process.env.KENER_SECRET_KEY !== undefined && Object.keys(siteData).length > 0;
const isSetupComplete = await IsSetupComplete();
const selectedLang = GetLocaleFromCookie(siteData, cookies);
const siteStatusColors = siteData.colors;
@@ -128,5 +137,7 @@ export async function GetLayoutServerData(cookies: Cookies, request: Request): P
eventDisplaySettings: siteData.eventDisplaySettings || seedSiteData.eventDisplaySettings,
socialPreviewImage: siteData.socialPreviewImage,
customCSS: siteData.customCSS,
globalPageVisibilitySettings: siteData.globalPageVisibilitySettings || seedSiteData.globalPageVisibilitySettings,
dateAndTimeFormat: siteData.dateAndTimeFormat || seedSiteData.dateAndTimeFormat,
};
}
@@ -12,7 +12,8 @@ import type {
MaintenanceEventFilter,
} from "../types/db.js";
import { GetMinuteStartNowTimestampUTC } from "../tool.js";
import { maintenanceToVariables } from "../notification/notification_utils.js";
import { maintenanceToVariables, siteDataToVariables } from "../notification/notification_utils.js";
import { GetAllSiteData } from "./controller.js";
import subscriberQueue from "../queues/subscriberQueue.js";
import GC from "../../global-constants";
@@ -540,6 +541,9 @@ export const formatDurationSeconds = (seconds: number): string => {
export const UpdateMaintenanceEventStatuses = async (): Promise<void> => {
const currentTimestamp = GetMinuteStartNowTimestampUTC();
const sixtyMinutesInSeconds = 60 * 60;
const siteData = await GetAllSiteData();
const siteVars = siteDataToVariables(siteData);
const siteUrl = siteVars.site_url;
try {
// 1. Mark SCHEDULED events starting within 60 minutes as READY
@@ -559,6 +563,7 @@ export const UpdateMaintenanceEventStatuses = async (): Promise<void> => {
`**is starting in ${timeUntilStart}**`,
"starting_soon",
"Maintenance Starting Soon",
siteUrl,
);
await subscriberQueue.push(update);
}
@@ -576,6 +581,7 @@ export const UpdateMaintenanceEventStatuses = async (): Promise<void> => {
"**is now in progress**",
"ongoing",
"Maintenance In Progress",
siteUrl,
);
await subscriberQueue.push(update);
}
@@ -593,6 +599,7 @@ export const UpdateMaintenanceEventStatuses = async (): Promise<void> => {
"**is now in progress**",
"ongoing",
"Maintenance In Progress",
siteUrl,
);
await subscriberQueue.push(update);
}
@@ -610,6 +617,7 @@ export const UpdateMaintenanceEventStatuses = async (): Promise<void> => {
"**has been completed**",
"completed",
"Maintenance Completed",
siteUrl,
);
await subscriberQueue.push(update);
}
@@ -151,7 +151,7 @@ export async function CreateMonitorAlertConfig(
};
// Insert alert config
const [id] = await db.insertMonitorAlertConfig(insertData);
const id = await db.insertMonitorAlertConfig(insertData);
// Add triggers if provided
if (data.trigger_ids && data.trigger_ids.length > 0) {
@@ -25,8 +25,9 @@ import type { DayWiseStatus, NumberWithChange } from "../../types/monitor.js";
import GC, { getBadgeStyle, type BadgeStyle } from "../../global-constants.js";
import { makeBadge } from "badge-maker";
import { ErrorSvg } from "../../anywhere.js";
import { GetLastMonitoringValue, SetLastHeartbeat } from "../cache/setGet.js";
import type { HeartbeatMonitor } from "../types/monitor.js";
import { GetLastMonitoringValue, SetLastHeartbeat, DeleteMonitorCaches } from "../cache/setGet.js";
import { translate, isLocaleAvailable } from "../i18n.js";
import type { HeartbeatMonitor, GroupMonitorTypeData } from "../types/monitor.js";
interface GroupUpdateData {
monitor_tag: string;
@@ -39,6 +40,31 @@ interface MonitorInput extends MonitorRecordInsert {
id?: number;
}
/**
* Validates that a monitor tag is URL-friendly: lowercase alphanumeric, hyphens, and underscores only.
* Must start and end with an alphanumeric character.
*/
const VALID_TAG_REGEX = /^[a-z0-9][a-z0-9_-]*[a-z0-9]$/;
const VALID_TAG_SINGLE_CHAR_REGEX = /^[a-z0-9]$/;
function isValidMonitorTag(tag: string): boolean {
if (!tag || tag.length === 0) return false;
if (tag.length === 1) return VALID_TAG_SINGLE_CHAR_REGEX.test(tag);
return VALID_TAG_REGEX.test(tag);
}
function validateMonitorTag(tag: string): void {
const trimmed = tag?.trim();
if (!trimmed) {
throw new Error("Monitor tag is required");
}
if (!isValidMonitorTag(trimmed)) {
throw new Error(
"Monitor tag must be URL-friendly: only lowercase letters, numbers, hyphens, and underscores. Must start and end with a letter or number.",
);
}
}
interface DayGroupData {
timestamp: number;
total: number;
@@ -169,6 +195,7 @@ export const CreateUpdateMonitor = async (monitor: MonitorInput): Promise<number
if (monitorData.id) {
return await db.updateMonitor(monitorData as MonitorRecord);
} else {
validateMonitorTag(monitorData.tag);
return await db.insertMonitor(monitorData);
}
};
@@ -178,6 +205,7 @@ export const CreateMonitor = async (monitor: MonitorInput): Promise<number[]> =>
if (monitorData.id) {
throw new Error("monitor id must be empty or 0");
}
validateMonitorTag(monitorData.tag);
return await db.insertMonitor(monitorData);
};
@@ -198,6 +226,7 @@ export const CloneMonitor = async ({ sourceTag, newTag, newName }: CloneMonitorI
if (!newTagTrimmed) {
throw new Error("Tag is required");
}
validateMonitorTag(newTagTrimmed);
if (!newNameTrimmed) {
throw new Error("Name is required");
}
@@ -348,12 +377,71 @@ export const RegisterHeartbeat = async (tag: string, secret: string): Promise<st
throw new Error("Invalid heartbeat secret");
};
/**
* Removes a monitor tag from all GROUP monitors that reference it,
* rebalances weights equally, and deactivates groups left with < 2 members.
*/
async function removeTagFromGroupMonitors(tag: string): Promise<void> {
const groupMonitors = await GetMonitorsParsed({ monitor_type: "GROUP" });
for (const group of groupMonitors) {
const typeData = group.type_data as GroupMonitorTypeData;
if (!typeData.monitors || !Array.isArray(typeData.monitors)) continue;
const hasMember = typeData.monitors.some((m) => m.tag === tag);
if (!hasMember) continue;
// Remove the deleted tag
const remaining = typeData.monitors.filter((m) => m.tag !== tag);
// Rebalance weights equally across remaining monitors
if (remaining.length > 0) {
const weight = Math.round((1 / remaining.length) * 1000) / 1000;
for (let i = 0; i < remaining.length; i++) {
remaining[i].weight =
i === remaining.length - 1
? Math.round((1 - weight * (remaining.length - 1)) * 1000) / 1000
: weight;
}
}
typeData.monitors = remaining;
const updateData: Record<string, unknown> = {
id: group.id,
tag: group.tag,
name: group.name,
description: group.description,
image: group.image,
cron: group.cron,
default_status: group.default_status,
status: remaining.length < 2 ? "INACTIVE" : group.status,
category_name: group.category_name,
monitor_type: group.monitor_type,
type_data: JSON.stringify(typeData),
day_degraded_minimum_count: group.day_degraded_minimum_count,
day_down_minimum_count: group.day_down_minimum_count,
include_degraded_in_downtime: group.include_degraded_in_downtime,
is_hidden: group.is_hidden,
monitor_settings_json:
typeof group.monitor_settings_json === "string"
? group.monitor_settings_json
: JSON.stringify(group.monitor_settings_json),
external_url: group.external_url,
};
await db.updateMonitor(updateData as unknown as MonitorRecord);
}
}
export const DeleteMonitorCompletelyUsingTag = async (tag: string): Promise<number> => {
await db.deleteMonitorDataByTag(tag);
await db.deleteIncidentMonitorsByTag(tag);
await db.deleteMonitorAlertsByTag(tag);
await db.deletePageMonitorsByTag(tag);
await db.deleteMaintenanceMonitorsByTag(tag);
await removeTagFromGroupMonitors(tag);
await DeleteMonitorCaches(tag);
return await db.deleteMonitorsByTag(tag);
};
@@ -419,6 +507,7 @@ export interface BadgeParams {
color?: string | null;
style?: string | null;
metric?: string | null;
locale?: string | null;
}
function formatDuration(rangeInSeconds: number): string {
@@ -472,7 +561,34 @@ export const GetBadge = async (badgeType: BadgeType, params: BadgeParams): Promi
}
const status = (lastObj?.status as string) || GC.NO_DATA;
message = status;
// Resolve locale: validate against activated locales, fall back to configured default
const i18nData = await db.getSiteDataByKey("i18n");
let i18nConfig: { defaultLocale: string; locales: Array<{ code: string; selected: boolean }> } | null = null;
if (i18nData?.value) {
try {
i18nConfig = typeof i18nData.value === "string" ? JSON.parse(i18nData.value) : i18nData.value;
} catch {
i18nConfig = null;
}
}
const defaultLocale = i18nConfig?.defaultLocale || "en";
const activatedCodes = new Set(
i18nConfig?.locales?.filter((l) => l.selected).map((l) => l.code) ?? ["en"],
);
const requestedLocale = params.locale || defaultLocale;
const locale = activatedCodes.has(requestedLocale) && isLocaleAvailable(requestedLocale)
? requestedLocale
: defaultLocale;
const statusLocaleKey: Record<string, string> = {
[GC.UP]: "Operational",
[GC.DEGRADED]: "Degraded",
[GC.DOWN]: "Down",
[GC.MAINTENANCE]: "Under Maintenance",
[GC.NO_DATA]: "No Status Available",
};
message = translate(locale, statusLocaleKey[status] || status, defaultLocale);
// Use status-specific color if no custom color provided
if (!params.color) {
@@ -107,6 +107,7 @@ export async function AddMonitorToPage(
page_id: number,
monitor_tag: string,
monitor_settings_json?: string | null,
position?: number,
): Promise<void> {
// Check if page exists
const page = await db.getPageById(page_id);
@@ -120,13 +121,38 @@ export async function AddMonitorToPage(
throw new Error(`Monitor "${monitor_tag}" already exists on this page`);
}
// If no position specified, append at end
let finalPosition = position;
if (finalPosition === undefined) {
const existing = await db.getPageMonitors(page_id);
finalPosition = existing.length > 0 ? Math.max(...existing.map((m) => m.position)) + 1 : 0;
}
await db.addMonitorToPage({
page_id,
monitor_tag,
monitor_settings_json: monitor_settings_json || null,
position: finalPosition,
});
}
/**
* Reorder monitors on a page
*/
export async function ReorderPageMonitors(page_id: number, monitor_tags: string[]): Promise<void> {
const page = await db.getPageById(page_id);
if (!page) {
throw new Error(`Page with id ${page_id} not found`);
}
const monitorPositions = monitor_tags.map((tag, index) => ({
monitor_tag: tag,
position: index,
}));
await db.updatePageMonitorPositions(page_id, monitorPositions);
}
/**
* Remove monitor from a page
*/
@@ -4,6 +4,8 @@ import type { Cookies } from "@sveltejs/kit";
import type {
DataRetentionPolicy,
EventDisplaySettings,
GlobalPageVisibilitySettings,
PageOrderingSettings,
SiteAnalyticsItem,
SiteAnnouncement,
SiteCategory,
@@ -15,6 +17,7 @@ import type {
SiteNavItem,
SiteStatusColors,
SiteSubMenuOptions,
SiteDateTimeFormat,
SiteSubscriptionsSettings,
} from "../../types/site.js";
@@ -56,6 +59,9 @@ export interface SiteDataTransformed {
eventDisplaySettings?: EventDisplaySettings;
socialPreviewImage?: string;
customCSS?: string;
globalPageVisibilitySettings?: GlobalPageVisibilitySettings;
pageOrderingSettings?: PageOrderingSettings;
dateAndTimeFormat?: SiteDateTimeFormat;
}
export function InsertKeyValue(key: string, value: string): Promise<number[]> {
@@ -130,6 +136,12 @@ export const IsSetupComplete = async (): Promise<boolean> => {
if (process.env.KENER_SECRET_KEY === undefined) {
return false;
}
if (process.env.ORIGIN === undefined) {
return false;
}
if (process.env.REDIS_URL === undefined) {
return false;
}
let data = await db.getAllSiteData();
if (!data) {
@@ -261,4 +261,19 @@ export const siteDataKeys: SiteDataKey[] = [
isValid: (value) => typeof value === "string",
data_type: "string",
},
{
key: "globalPageVisibilitySettings",
isValid: IsValidJSONString,
data_type: "object",
},
{
key: "pageOrderingSettings",
isValid: IsValidJSONString,
data_type: "object",
},
{
key: "dateAndTimeFormat",
isValid: IsValidJSONString,
data_type: "object",
},
];
+70 -44
View File
@@ -35,6 +35,36 @@ interface NewUserInput {
role: string;
}
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const normalizeEmail = (email: string): string => email.trim().toLowerCase();
const normalizeName = (name: string): string => name.trim().replace(/\s+/g, " ");
const validateEmailOrThrow = (email: string): string => {
const normalizedEmail = normalizeEmail(email);
if (!normalizedEmail) {
throw new Error("Email cannot be empty");
}
if (!EMAIL_REGEX.test(normalizedEmail)) {
throw new Error("Please enter a valid email address");
}
return normalizedEmail;
};
const validateNameOrThrow = (name: string): string => {
const normalizedName = normalizeName(name);
if (!normalizedName) {
throw new Error("Name cannot be empty");
}
if (normalizedName.length < 2) {
throw new Error("Name must be at least 2 characters");
}
if (normalizedName.length > 100) {
throw new Error("Name must be less than 100 characters");
}
return normalizedName;
};
export const GetAllUsersPaginated = async (data: PaginationInput): Promise<UserRecordPublic[]> => {
return await db.getUsersPaginated(data.page, data.limit);
};
@@ -125,15 +155,8 @@ export const CreateNewUser = async (currentUser: { role: string }, data: NewUser
throw new Error("Only admins and editors can create new users");
}
//if data.email empty, throw error
if (!!!data.email) {
throw new Error("Email cannot be empty");
}
//if data.name empty, throw error
if (!!!data.name) {
throw new Error("Name cannot be empty");
}
const normalizedEmail = validateEmailOrThrow(data.email);
const normalizedName = validateNameOrThrow(data.name);
//if data.password empty, throw error
if (!!!data.password) {
@@ -156,21 +179,17 @@ export const CreateNewUser = async (currentUser: { role: string }, data: NewUser
);
}
let user = {
email: data.email,
email: normalizedEmail,
password_hash: await HashPassword(data.password),
name: data.name,
name: normalizedName,
role: data.role,
};
return await db.insertUser(user);
};
export const CreateFirstUser = async (data: { email: string; name: string; password: string }): Promise<number[]> => {
if (!data.email) {
throw new Error("Email cannot be empty");
}
if (!data.name) {
throw new Error("Name cannot be empty");
}
const normalizedEmail = validateEmailOrThrow(data.email);
const normalizedName = validateNameOrThrow(data.name);
if (!data.password) {
throw new Error("Password cannot be empty");
}
@@ -180,10 +199,11 @@ export const CreateFirstUser = async (data: { email: string; name: string; passw
);
}
const user = {
email: data.email,
email: normalizedEmail,
password_hash: await HashPassword(data.password),
name: data.name,
name: normalizedName,
role: "admin",
is_owner: "YES",
};
return await db.insertUser(user);
};
@@ -209,8 +229,10 @@ export const UpdatePassword = async (data: PasswordUpdateInput): Promise<number>
});
};
const VALID_ROLES = ["admin", "editor", "member"] as const;
export const ManualUpdateUserData = async (
byUser: { role: string },
byUser: { id: number; role: string; is_owner: string },
forUserId: number,
data: ManualUserUpdateInput,
): Promise<number | undefined> => {
@@ -222,8 +244,15 @@ export const ManualUpdateUserData = async (
if (byUser.role !== "admin") {
throw new Error("You do not have permission to update user");
}
// non-owner admins cannot modify other admins (self-updates are allowed)
if (forUser.role === "admin" && byUser.is_owner !== "YES" && forUser.id !== byUser.id) {
throw new Error("Only the owner can modify other admins");
}
if (data.updateType == "role") {
if (!data.role) throw new Error("Role is required");
if (!VALID_ROLES.includes(data.role as (typeof VALID_ROLES)[number])) {
throw new Error(`Invalid role. Must be one of: ${VALID_ROLES.join(", ")}`);
}
return await db.updateUserRole(forUser.id, data.role);
} else if (data.updateType == "is_active") {
if (data.is_active === undefined) throw new Error("is_active is required");
@@ -235,6 +264,8 @@ export const ManualUpdateUserData = async (
newPassword: data.password,
newPlainPassword: data.passwordPlain,
});
} else {
throw new Error(`Unsupported update type: ${data.updateType}`);
}
};
@@ -251,6 +282,9 @@ export const GetLoggedInSession = async (cookies: Cookies): Promise<UserRecordPu
if (!userDB) {
return null;
}
if (!userDB.is_active) {
return null;
}
return userDB;
};
@@ -264,37 +298,31 @@ export const GetTotalUserPages = async (limit: number): Promise<number> => {
//send invitation email to user for account creation
export const SendInvitationEmail = async (email: string, role: string, name: string, currentUserRole: string) => {
let acceptedRoles = ["member", "editor"];
if (!acceptedRoles.includes(role)) {
throw new Error("Invalid role");
}
if (currentUserRole === "member") {
throw new Error("Only admins and editors can create new users");
}
//if data.email empty, throw error
if (!!!email) {
throw new Error("Email cannot be empty");
// Admins can add admin, editor, member; Editors can only add editor, member
const acceptedRoles = currentUserRole === "admin" ? ["admin", "editor", "member"] : ["editor", "member"];
if (!acceptedRoles.includes(role)) {
throw new Error("Invalid role");
}
//if data.name empty, throw error
if (!!!name) {
throw new Error("Name cannot be empty");
}
const normalizedEmail = validateEmailOrThrow(email);
const normalizedName = validateNameOrThrow(name);
// Check if user with this email already exists
const existingUser = await db.getUserByEmail(email);
const existingUser = await db.getUserByEmail(normalizedEmail);
if (existingUser) {
throw new Error(`A user with email ${email} already exists`);
throw new Error(`A user with email ${normalizedEmail} already exists`);
}
//create user with empty password and is_active = 0
try {
await db.insertUser({
email,
email: normalizedEmail,
password_hash: "",
name,
name: normalizedName,
role,
is_active: 0,
});
@@ -302,13 +330,13 @@ export const SendInvitationEmail = async (email: string, role: string, name: str
// Handle database constraint errors
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage.includes("UNIQUE constraint failed") || errorMessage.includes("duplicate")) {
throw new Error(`A user with email ${email} already exists`);
throw new Error(`A user with email ${normalizedEmail} already exists`);
}
throw error;
}
const token = await GenerateToken({
email,
email: normalizedEmail,
validTill: Date.now() + 7 * 24 * 60 * 60 * 1000, //7 days
});
@@ -328,7 +356,7 @@ export const SendInvitationEmail = async (email: string, role: string, name: str
template.template_html_body || "",
template.template_subject || "Your Invitation to Join",
emailVars,
[email],
[normalizedEmail],
undefined,
template.template_text_body || "",
);
@@ -341,11 +369,9 @@ export const ResendInvitationEmail = async (email: string, currentUserRole: stri
throw new Error("Only admins and editors can resend invitations");
}
if (!email) {
throw new Error("Email cannot be empty");
}
const normalizedEmail = validateEmailOrThrow(email);
const user = await db.getUserByEmail(email);
const user = await db.getUserByEmail(normalizedEmail);
if (!user) {
throw new Error("User not found");
}
@@ -356,7 +382,7 @@ export const ResendInvitationEmail = async (email: string, currentUserRole: stri
}
const token = await GenerateToken({
email,
email: normalizedEmail,
validTill: Date.now() + 7 * 24 * 60 * 60 * 1000, //7 days
});
@@ -1,180 +0,0 @@
import crypto from "crypto";
import db from "$lib/server/db/db";
import type { VaultRecord } from "$lib/server/db/repositories/vault";
const DUMMY_SECRET = "DUMMY_SECRET_KEY_32_BYTES_LONG!";
const ALGORITHM = "aes-256-cbc";
const IV_LENGTH = 16;
/**
* Get the encryption key from environment variable
* Pads or truncates to 32 bytes for AES-256
*/
function getEncryptionKey(): Buffer {
const key = process.env.KENER_SECRET_KEY || DUMMY_SECRET;
// AES-256 requires a 32-byte key
const keyBuffer = Buffer.alloc(32);
Buffer.from(key).copy(keyBuffer);
return keyBuffer;
}
/**
* Encrypt a value using AES-256-CBC
*/
export function encryptValue(plainText: string): string {
const key = getEncryptionKey();
const iv = crypto.randomBytes(IV_LENGTH);
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
let encrypted = cipher.update(plainText, "utf8", "hex");
encrypted += cipher.final("hex");
// Prepend IV to encrypted data (IV:encrypted)
return iv.toString("hex") + ":" + encrypted;
}
/**
* Decrypt a value using AES-256-CBC
*/
export function decryptValue(encryptedText: string): string {
const key = getEncryptionKey();
const parts = encryptedText.split(":");
if (parts.length !== 2) {
throw new Error("Invalid encrypted value format");
}
const iv = Buffer.from(parts[0], "hex");
const encrypted = parts[1];
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
let decrypted = decipher.update(encrypted, "hex", "utf8");
decrypted += decipher.final("utf8");
return decrypted;
}
/**
* Interface for decrypted vault secret
*/
export interface VaultSecretDecrypted {
id: number;
secret_name: string;
secret_value: string; // Decrypted value
created_at: Date;
updated_at: Date;
}
/**
* Get all secrets with decrypted values
*/
export async function GetAllSecrets(): Promise<VaultSecretDecrypted[]> {
const secrets = await db.getAllSecrets();
return secrets.map((secret) => ({
...secret,
secret_value: decryptValue(secret.secret_value),
}));
}
/**
* Get a secret by ID with decrypted value
*/
export async function GetSecretById(id: number): Promise<VaultSecretDecrypted | undefined> {
const secret = await db.getSecretById(id);
if (!secret) return undefined;
return {
...secret,
secret_value: decryptValue(secret.secret_value),
};
}
/**
* Get a secret by name with decrypted value
*/
export async function GetSecretByName(secretName: string): Promise<VaultSecretDecrypted | undefined> {
const secret = await db.getSecretByName(secretName);
if (!secret) return undefined;
return {
...secret,
secret_value: decryptValue(secret.secret_value),
};
}
/**
* Create a new secret (encrypts the value before storing)
*/
export async function CreateSecret(
secretName: string,
secretValue: string,
): Promise<{ success: boolean; error?: string; id?: number }> {
// Validate input
if (!secretName || !secretName.trim()) {
return { success: false, error: "Secret name is required" };
}
if (!secretValue) {
return { success: false, error: "Secret value is required" };
}
// Check if name already exists
const exists = await db.secretNameExists(secretName.trim());
if (exists) {
return { success: false, error: `Secret with name "${secretName}" already exists` };
}
// Encrypt and store
const encryptedValue = encryptValue(secretValue);
const ids = await db.insertSecret({
secret_name: secretName.trim(),
secret_value: encryptedValue,
});
return { success: true, id: ids[0] };
}
/**
* Update a secret by ID (encrypts the new value before storing)
*/
export async function UpdateSecret(
id: number,
data: { secret_name?: string; secret_value?: string },
): Promise<{ success: boolean; error?: string }> {
// Check if secret exists
const existing = await db.getSecretById(id);
if (!existing) {
return { success: false, error: `Secret with ID ${id} not found` };
}
// If updating name, check for duplicates
if (data.secret_name && data.secret_name !== existing.secret_name) {
const nameExists = await db.secretNameExists(data.secret_name.trim(), id);
if (nameExists) {
return { success: false, error: `Secret with name "${data.secret_name}" already exists` };
}
}
// Build update data
const updateData: { secret_name?: string; secret_value?: string } = {};
if (data.secret_name !== undefined) {
updateData.secret_name = data.secret_name.trim();
}
if (data.secret_value !== undefined) {
updateData.secret_value = encryptValue(data.secret_value);
}
await db.updateSecretById(id, updateData);
return { success: true };
}
/**
* Delete a secret by ID
*/
export async function DeleteSecret(id: number): Promise<{ success: boolean; error?: string }> {
const existing = await db.getSecretById(id);
if (!existing) {
return { success: false, error: `Secret with ID ${id} not found` };
}
await db.deleteSecretById(id);
return { success: true };
}
/**
* Get secrets count
*/
export async function GetSecretsCount(): Promise<number> {
return await db.getSecretsCount();
}
+4 -29
View File
@@ -14,7 +14,6 @@ import { MaintenancesRepository } from "./repositories/maintenances.js";
import { MonitorAlertConfigRepository } from "./repositories/monitorAlertConfig.js";
import { SubscriptionSystemRepository } from "./repositories/subscriptionSystem.js";
import { EmailTemplateConfigRepository } from "./repositories/emailTemplateConfig.js";
import { VaultRepository } from "./repositories/vault.js";
// Re-export types from base
export type { MonitorFilter, TriggerFilter, IncidentFilter, CountResult } from "./repositories/base.js";
@@ -44,7 +43,6 @@ class DbImpl {
private monitorAlertConfig!: MonitorAlertConfigRepository;
private subscriptionSystem!: SubscriptionSystemRepository;
private emailTemplateConfig!: EmailTemplateConfigRepository;
private vault!: VaultRepository;
// Method bindings - declared with definite assignment assertion
// ============ Monitoring Data ============
@@ -189,6 +187,7 @@ class DbImpl {
updateIncidentCommentByID!: IncidentsRepository["updateIncidentCommentByID"];
updateIncidentCommentStatusByID!: IncidentsRepository["updateIncidentCommentStatusByID"];
getIncidentCommentByID!: IncidentsRepository["getIncidentCommentByID"];
deleteIncidentCommentsByIncidentID!: IncidentsRepository["deleteIncidentCommentsByIncidentID"];
// ============ Images ============
insertImage!: ImagesRepository["insertImage"];
@@ -214,6 +213,7 @@ class DbImpl {
monitorExistsOnPage!: PagesRepository["monitorExistsOnPage"];
deletePageMonitorsByTag!: PagesRepository["deletePageMonitorsByTag"];
deletePageMonitorsByPageId!: PagesRepository["deletePageMonitorsByPageId"];
updatePageMonitorPositions!: PagesRepository["updatePageMonitorPositions"];
// ============ Maintenances ============
createMaintenance!: MaintenancesRepository["createMaintenance"];
@@ -347,18 +347,6 @@ class DbImpl {
deleteEmailTemplate!: EmailTemplateConfigRepository["deleteEmailTemplate"];
upsertEmailTemplate!: EmailTemplateConfigRepository["upsertEmailTemplate"];
// ============ Vault ============
getAllSecrets!: VaultRepository["getAllSecrets"];
getSecretById!: VaultRepository["getSecretById"];
getSecretByName!: VaultRepository["getSecretByName"];
insertSecret!: VaultRepository["insertSecret"];
updateSecretById!: VaultRepository["updateSecretById"];
updateSecretByName!: VaultRepository["updateSecretByName"];
deleteSecretById!: VaultRepository["deleteSecretById"];
deleteSecretByName!: VaultRepository["deleteSecretByName"];
secretNameExists!: VaultRepository["secretNameExists"];
getSecretsCount!: VaultRepository["getSecretsCount"];
constructor(opts: KnexType.Config) {
this.knex = Knex(opts);
@@ -375,7 +363,6 @@ class DbImpl {
this.monitorAlertConfig = new MonitorAlertConfigRepository(this.knex);
this.subscriptionSystem = new SubscriptionSystemRepository(this.knex);
this.emailTemplateConfig = new EmailTemplateConfigRepository(this.knex);
this.vault = new VaultRepository(this.knex);
// Bind methods after repositories are initialized
this.bindMonitoringMethods();
@@ -390,7 +377,6 @@ class DbImpl {
this.bindMonitorAlertConfigMethods();
this.bindSubscriptionSystemMethods();
this.bindEmailTemplateConfigMethods();
this.bindVaultMethods();
this.init();
}
@@ -545,6 +531,7 @@ class DbImpl {
this.updateIncidentCommentByID = this.incidents.updateIncidentCommentByID.bind(this.incidents);
this.updateIncidentCommentStatusByID = this.incidents.updateIncidentCommentStatusByID.bind(this.incidents);
this.getIncidentCommentByID = this.incidents.getIncidentCommentByID.bind(this.incidents);
this.deleteIncidentCommentsByIncidentID = this.incidents.deleteIncidentCommentsByIncidentID.bind(this.incidents);
}
private bindImagesMethods(): void {
@@ -570,6 +557,7 @@ class DbImpl {
this.monitorExistsOnPage = this.pages.monitorExistsOnPage.bind(this.pages);
this.deletePageMonitorsByTag = this.pages.deletePageMonitorsByTag.bind(this.pages);
this.deletePageMonitorsByPageId = this.pages.deletePageMonitorsByPageId.bind(this.pages);
this.updatePageMonitorPositions = this.pages.updatePageMonitorPositions.bind(this.pages);
}
private bindMaintenancesMethods(): void {
@@ -794,19 +782,6 @@ class DbImpl {
this.upsertEmailTemplate = this.emailTemplateConfig.upsertEmailTemplate.bind(this.emailTemplateConfig);
}
private bindVaultMethods(): void {
this.getAllSecrets = this.vault.getAllSecrets.bind(this.vault);
this.getSecretById = this.vault.getSecretById.bind(this.vault);
this.getSecretByName = this.vault.getSecretByName.bind(this.vault);
this.insertSecret = this.vault.insertSecret.bind(this.vault);
this.updateSecretById = this.vault.updateSecretById.bind(this.vault);
this.updateSecretByName = this.vault.updateSecretByName.bind(this.vault);
this.deleteSecretById = this.vault.deleteSecretById.bind(this.vault);
this.deleteSecretByName = this.vault.deleteSecretByName.bind(this.vault);
this.secretNameExists = this.vault.secretNameExists.bind(this.vault);
this.getSecretsCount = this.vault.getSecretsCount.bind(this.vault);
}
async init(): Promise<void> {}
async close(): Promise<void> {
+1
View File
@@ -9,6 +9,7 @@ export interface MonitorFilter {
tag?: string;
is_hidden?: string;
tags?: string[];
search?: string;
}
export interface TriggerFilter {
+15 -3
View File
@@ -818,6 +818,10 @@ export class IncidentsRepository extends BaseRepository {
return await this.knex("incident_comments").where({ id }).first();
}
async deleteIncidentCommentsByIncidentID(incident_id: number): Promise<number> {
return await this.knex("incident_comments").where({ incident_id }).del();
}
/**
* Get incidents within a date range for the events page
* Returns incidents that started within the given date range
@@ -826,8 +830,9 @@ export class IncidentsRepository extends BaseRepository {
async getIncidentsForEventsByDateRange(
startTs: number,
endTs: number,
monitorTags?: string[],
): Promise<IncidentForMonitorListWithComments[]> {
const rows = await this.knex("incidents")
const query = this.knex("incidents")
.select(
"incidents.id",
"incidents.title",
@@ -848,8 +853,15 @@ export class IncidentsRepository extends BaseRepository {
.where("incidents.incident_type", GC.INCIDENT)
.andWhere("incidents.status", "OPEN")
.andWhere("incidents.start_date_time", ">=", startTs)
.andWhere("incidents.start_date_time", "<=", endTs)
.orderBy("incidents.start_date_time", "desc");
.andWhere("incidents.start_date_time", "<=", endTs);
if (monitorTags) {
query.andWhere(function () {
this.whereIn("incident_monitors.monitor_tag", monitorTags).orWhere("incidents.is_global", "YES");
});
}
const rows = await query.orderBy("incidents.start_date_time", "desc");
const incidents = this.groupIncidentsByIdForMonitorListFilterHidden(rows);
+11 -3
View File
@@ -631,8 +631,9 @@ export class MaintenancesRepository extends BaseRepository {
async getMaintenanceEventsForEventsByDateRange(
startTs: number,
endTs: number,
monitorTags?: string[],
): Promise<MaintenanceEventsMonitorList[]> {
const rows = await this.knex("maintenances_events")
const query = this.knex("maintenances_events")
.select(
"maintenances_events.id",
"maintenances.title",
@@ -652,8 +653,15 @@ export class MaintenancesRepository extends BaseRepository {
.leftJoin("maintenance_monitors", "maintenances_events.maintenance_id", "maintenance_monitors.maintenance_id")
.leftJoin("monitors", "maintenance_monitors.monitor_tag", "monitors.tag")
.andWhere("maintenances_events.start_date_time", ">=", startTs)
.andWhere("maintenances_events.start_date_time", "<=", endTs)
.orderBy("maintenances_events.start_date_time", "desc");
.andWhere("maintenances_events.start_date_time", "<=", endTs);
if (monitorTags) {
query.andWhere(function () {
this.whereIn("maintenance_monitors.monitor_tag", monitorTags).orWhere("maintenances.is_global", "YES");
});
}
const rows = await query.orderBy("maintenances_events.start_date_time", "desc");
return this.groupMaintenancesByIdForMonitorList(rows);
}
@@ -26,8 +26,9 @@ export class MonitorAlertConfigRepository extends BaseRepository {
/**
* Insert a new monitor alert config
*/
async insertMonitorAlertConfig(data: MonitorAlertConfigInsert): Promise<number[]> {
return await this.knex("monitor_alerts_config").insert({
async insertMonitorAlertConfig(data: MonitorAlertConfigInsert): Promise<number> {
const dbType = GetDbType();
const insertData = {
monitor_tag: data.monitor_tag,
alert_for: data.alert_for,
alert_value: data.alert_value,
@@ -39,7 +40,21 @@ export class MonitorAlertConfigRepository extends BaseRepository {
severity: data.severity || "WARNING",
created_at: this.knex.fn.now(),
updated_at: this.knex.fn.now(),
});
};
if (dbType === "postgresql") {
const result = await this.knex("monitor_alerts_config").insert(insertData).returning("id");
const inserted = Array.isArray(result) ? result[0] : result;
return typeof inserted === "object" && inserted !== null
? Number((inserted as { id: number }).id)
: Number(inserted);
}
const result = await this.knex("monitor_alerts_config").insert(insertData);
const inserted = Array.isArray(result) ? result[0] : result;
return typeof inserted === "object" && inserted !== null
? Number((inserted as { id: number }).id)
: Number(inserted);
}
/**

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