mirror of
https://github.com/rajnandan1/kener.git
synced 2026-08-07 23:32:02 +00:00
Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| db6cb6cf7d | |||
| babeeb75b2 | |||
| e0187605e7 | |||
| e2861f1e59 | |||
| 01aa4d9984 | |||
| 555372f175 | |||
| bd5fd409d1 | |||
| f61d82e13f | |||
| f39588fff7 | |||
| 0716f271df | |||
| 1085c3e561 | |||
| 93907e6d96 | |||
| d03fd63c64 | |||
| 3ff43af787 | |||
| 7f2fef8e8e | |||
| 7658170865 | |||
| b1f8a505a5 | |||
| 104da58646 | |||
| ed6e97e8c9 | |||
| 0b25274874 | |||
| bb8ab41bfe | |||
| b43a5fb343 | |||
| 554caa5018 | |||
| ffe7403043 | |||
| a26d0ece59 | |||
| a4277f7ed0 | |||
| f75aaf9cef | |||
| 4e1ecf41ee | |||
| f23fb5313c | |||
| 18489c5339 | |||
| 4044bae26d | |||
| 02686caa78 | |||
| 797aef80d6 | |||
| a8841ad8a3 | |||
| 0b2cd5fc8a | |||
| 9d349716e5 | |||
| 92d068ef49 | |||
| c6e3620151 | |||
| d92165d0f8 | |||
| a56bbead8d | |||
| db3fb923f0 | |||
| bb9d88a095 | |||
| 912da6b8f4 | |||
| 3b07623346 | |||
| 702ceca9b0 | |||
| 21f2433919 | |||
| 4e7791b104 | |||
| f79c24c80d | |||
| 087c2f25fb | |||
| 36f2ae1f69 | |||
| dcd830eb82 | |||
| 80637fd4aa | |||
| a7f0072f32 | |||
| a39e676a23 | |||
| fbc926036e | |||
| 5b508d19bc | |||
| 589281ce13 | |||
| 36aec8c519 | |||
| 978bfb05f2 | |||
| 3ffd0538a1 | |||
| 932a05a9ee | |||
| 15c62fa40f |
@@ -0,0 +1,123 @@
|
||||
---
|
||||
name: ss-shadcn-svelte
|
||||
description: >
|
||||
Use shadcn-svelte components in SvelteKit projects. Detects whether the current project is a SvelteKit
|
||||
app with shadcn-svelte installed, lists available components, and provides access to full component
|
||||
documentation via the official llms.txt. Helps choose the right UI components for the job — buttons,
|
||||
forms, dialogs, tables, charts, and more — following shadcn-svelte best practices.
|
||||
Use this skill whenever the user is working in a SvelteKit project and wants to: add UI components,
|
||||
build forms, create dialogs or modals, add a data table, use a date picker, build a sidebar or
|
||||
navigation, add charts, use a combobox or select, create an alert or toast notification, or generally
|
||||
build UI with pre-built accessible components. Also trigger when the user mentions "shadcn", "shadcn-svelte",
|
||||
"bits-ui", or asks about available components in their Svelte project.
|
||||
---
|
||||
|
||||
# shadcn-svelte — Component-Aware Svelte UI Assistant
|
||||
|
||||
Use the right shadcn-svelte components when building UI in SvelteKit projects. This skill detects your project setup, shows what's available, and gives you access to full component documentation.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
The project must be a SvelteKit app with shadcn-svelte initialized:
|
||||
|
||||
```bash
|
||||
# Initialize shadcn-svelte in an existing SvelteKit project
|
||||
npx shadcn-svelte@latest init
|
||||
```
|
||||
|
||||
## How to use
|
||||
|
||||
### Step 1: Detect project setup
|
||||
|
||||
Run the detection script to verify this is a SvelteKit project with shadcn-svelte and see which components are already installed:
|
||||
|
||||
```bash
|
||||
bash <skill-path>/scripts/detect.sh .
|
||||
```
|
||||
|
||||
This will:
|
||||
- Confirm it's a SvelteKit project (checks for `svelte.config.js/ts` and `@sveltejs/kit` in package.json)
|
||||
- Confirm shadcn-svelte is installed (checks for `components.json`, `bits-ui`, or `shadcn-svelte` in package.json)
|
||||
- List all currently installed components in the project's UI directory
|
||||
- Provide the documentation URL
|
||||
|
||||
If the script exits with code 1, the project either isn't SvelteKit or doesn't have shadcn-svelte — do not proceed with shadcn-svelte components in that case.
|
||||
|
||||
### Step 2: Read the component documentation
|
||||
|
||||
The full component documentation for LLMs is available at:
|
||||
|
||||
```
|
||||
https://www.shadcn-svelte.com/llms.txt
|
||||
```
|
||||
|
||||
Fetch this URL to get a structured index of all available components organized by category, with links to individual component documentation pages (in `.md` format).
|
||||
|
||||
When you need to use a specific component, read its individual documentation page from the links provided in `llms.txt`. Each component doc includes:
|
||||
- Import statements and usage examples
|
||||
- Available props, events, and slots
|
||||
- Variants and configuration options
|
||||
- Accessibility information
|
||||
|
||||
### Step 3: Use the right component for the job
|
||||
|
||||
When building UI, follow this decision process:
|
||||
|
||||
1. **Run detection** to confirm shadcn-svelte is available and see installed components
|
||||
2. **Fetch llms.txt** to see all available components
|
||||
3. **Read the specific component docs** for the components you plan to use
|
||||
4. **Check if the component is installed** — if not, add it:
|
||||
```bash
|
||||
npx shadcn-svelte@latest add <component-name>
|
||||
```
|
||||
5. **Import and use the component** following the documentation patterns
|
||||
|
||||
### Component categories
|
||||
|
||||
shadcn-svelte components are organized into these categories:
|
||||
|
||||
| Category | Components |
|
||||
|----------|-----------|
|
||||
| **Layout** | Aspect Ratio, Collapsible, Resizable, Scroll Area, Separator, Sidebar |
|
||||
| **Form & Input** | Button, Calendar, Checkbox, Combobox, Date Picker, Input, Input OTP, Label, Radio Group, Range Calendar, Select, Slider, Switch, Textarea, Toggle, Toggle Group |
|
||||
| **Data Display** | Accordion, Avatar, Badge, Card, Carousel, Chart, Table, Data Table |
|
||||
| **Feedback** | Alert, Alert Dialog, Progress, Skeleton, Sonner (Toast) |
|
||||
| **Overlay** | Context Menu, Dialog, Drawer, Dropdown Menu, Hover Card, Menubar, Popover, Sheet, Tooltip |
|
||||
| **Navigation** | Breadcrumb, Command, Pagination, Tabs |
|
||||
| **Typography** | Typography |
|
||||
|
||||
### Adding new components
|
||||
|
||||
```bash
|
||||
# Add a single component
|
||||
npx shadcn-svelte@latest add button
|
||||
|
||||
# Add multiple components
|
||||
npx shadcn-svelte@latest add button card dialog
|
||||
|
||||
# List all available components
|
||||
npx shadcn-svelte@latest add
|
||||
```
|
||||
|
||||
### Import patterns
|
||||
|
||||
Components are typically imported from the project's `$lib/components/ui` directory:
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import * as Card from "$lib/components/ui/card";
|
||||
import * as Dialog from "$lib/components/ui/dialog";
|
||||
</script>
|
||||
```
|
||||
|
||||
Some components use namespace imports (with `* as`) when they have multiple sub-components (Card, Dialog, Sheet, Table, etc.), while simpler components use named imports (Button, Input, Badge, etc.).
|
||||
|
||||
## Important guidelines
|
||||
|
||||
- **Always run detection first** before suggesting shadcn-svelte components
|
||||
- **Always read component docs** before using a component — don't guess at props or patterns
|
||||
- **Check installed components** and add missing ones before importing
|
||||
- **Use the project's configured path** — the components directory may vary based on `components.json` configuration
|
||||
- **Follow Svelte 5 patterns** — shadcn-svelte uses runes (`$state`, `$derived`, `$effect`) and snippet-based composition
|
||||
- **Prefer composition** — shadcn-svelte components are designed to be composed together, not used as monolithic blocks
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# detect.sh — Check if the current project is a SvelteKit project with shadcn-svelte installed.
|
||||
# Exits 0 and prints component info if detected, exits 1 otherwise.
|
||||
|
||||
PROJECT_DIR="${1:-.}"
|
||||
|
||||
# Resolve to absolute path
|
||||
PROJECT_DIR="$(cd "$PROJECT_DIR" && pwd)"
|
||||
|
||||
# --- Step 1: Check for SvelteKit ---
|
||||
|
||||
SVELTEKIT=false
|
||||
|
||||
# Check for svelte.config.js or svelte.config.ts
|
||||
if [[ -f "$PROJECT_DIR/svelte.config.js" ]] || [[ -f "$PROJECT_DIR/svelte.config.ts" ]]; then
|
||||
SVELTEKIT=true
|
||||
fi
|
||||
|
||||
# Also verify package.json has @sveltejs/kit
|
||||
if [[ -f "$PROJECT_DIR/package.json" ]]; then
|
||||
if grep -q '"@sveltejs/kit"' "$PROJECT_DIR/package.json" 2>/dev/null; then
|
||||
SVELTEKIT=true
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$SVELTEKIT" != "true" ]]; then
|
||||
echo "NOT_SVELTEKIT"
|
||||
echo "This is not a SvelteKit project. No svelte.config.js/ts found and @sveltejs/kit is not in package.json."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Step 2: Check for shadcn-svelte ---
|
||||
|
||||
SHADCN=false
|
||||
|
||||
# Check for components.json (shadcn-svelte config file)
|
||||
if [[ -f "$PROJECT_DIR/components.json" ]]; then
|
||||
# Verify it's actually a shadcn config (has $schema or style field)
|
||||
if grep -qE '"(\$schema|style)"' "$PROJECT_DIR/components.json" 2>/dev/null; then
|
||||
SHADCN=true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check for bits-ui in package.json (core dependency of shadcn-svelte)
|
||||
if [[ -f "$PROJECT_DIR/package.json" ]]; then
|
||||
if grep -q '"bits-ui"' "$PROJECT_DIR/package.json" 2>/dev/null; then
|
||||
SHADCN=true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check for shadcn-svelte in package.json
|
||||
if [[ -f "$PROJECT_DIR/package.json" ]]; then
|
||||
if grep -q '"shadcn-svelte"' "$PROJECT_DIR/package.json" 2>/dev/null; then
|
||||
SHADCN=true
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$SHADCN" != "true" ]]; then
|
||||
echo "NO_SHADCN_SVELTE"
|
||||
echo "SvelteKit project detected, but shadcn-svelte is not installed."
|
||||
echo "Install it with: npx shadcn-svelte@latest init"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Step 3: Gather installed components ---
|
||||
|
||||
echo "DETECTED"
|
||||
echo "SvelteKit project with shadcn-svelte detected."
|
||||
echo ""
|
||||
|
||||
# Check which components are already installed by scanning the components directory
|
||||
COMPONENTS_DIR=""
|
||||
|
||||
# Try to read the components alias from components.json
|
||||
if [[ -f "$PROJECT_DIR/components.json" ]]; then
|
||||
# Extract the aliases.components path
|
||||
ALIAS_PATH=$(grep -o '"components"[[:space:]]*:[[:space:]]*"[^"]*"' "$PROJECT_DIR/components.json" | head -1 | sed 's/.*"components"[[:space:]]*:[[:space:]]*"//' | sed 's/"//')
|
||||
|
||||
if [[ -n "$ALIAS_PATH" ]]; then
|
||||
# Resolve $lib to src/lib
|
||||
RESOLVED_PATH="${ALIAS_PATH//\$lib/src/lib}"
|
||||
if [[ -d "$PROJECT_DIR/$RESOLVED_PATH/ui" ]]; then
|
||||
COMPONENTS_DIR="$PROJECT_DIR/$RESOLVED_PATH/ui"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Fallback: check common locations
|
||||
if [[ -z "$COMPONENTS_DIR" ]]; then
|
||||
for dir in "src/lib/components/ui" "src/lib/ui" "src/components/ui"; do
|
||||
if [[ -d "$PROJECT_DIR/$dir" ]]; then
|
||||
COMPONENTS_DIR="$PROJECT_DIR/$dir"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ -n "$COMPONENTS_DIR" ]] && [[ -d "$COMPONENTS_DIR" ]]; then
|
||||
echo "Installed components (in $COMPONENTS_DIR):"
|
||||
for comp_dir in "$COMPONENTS_DIR"/*/; do
|
||||
if [[ -d "$comp_dir" ]]; then
|
||||
comp_name=$(basename "$comp_dir")
|
||||
echo " - $comp_name"
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo "Documentation: https://www.shadcn-svelte.com/llms.txt"
|
||||
@@ -1,250 +0,0 @@
|
||||
---
|
||||
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)
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../.agents/skills/ss-shadcn-svelte
|
||||
@@ -1,46 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,39 +0,0 @@
|
||||
# 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
|
||||
@@ -24,18 +24,29 @@ src/routes/(api)/api/
|
||||
- **Repository**: `src/lib/server/db/repositories/*.ts` - Database operations
|
||||
- **DbImpl**: `src/lib/server/db/dbimpl.ts` - Bindings for repository methods
|
||||
|
||||
### Current Locals (set by middleware in `hooks.server.ts`)
|
||||
```typescript
|
||||
interface Locals {
|
||||
user?: SessionUser; // Auth session
|
||||
monitor?: MonitorRecordTyped; // /api/monitors/:monitor_tag/*
|
||||
incident?: IncidentRecord; // /api/incidents/:incident_id/*
|
||||
maintenance?: MaintenanceRecord; // /api/maintenances/:maintenance_id/*
|
||||
page?: PageRecord; // /api/pages/:page_path/*
|
||||
}
|
||||
```
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
### Use snake_case for API payloads
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
// Correct
|
||||
interface CreateMonitorRequest {
|
||||
monitor_tag: string;
|
||||
start_date_time: number;
|
||||
duration_seconds: number;
|
||||
}
|
||||
|
||||
// ❌ Wrong
|
||||
// Wrong
|
||||
interface CreateMonitorRequest {
|
||||
monitorTag: string;
|
||||
startDateTime: number;
|
||||
@@ -251,7 +262,7 @@ export const DELETE: RequestHandler = async ({ locals }) => {
|
||||
|
||||
// Delete related records first (cascade)
|
||||
await db.deleteResourceRelatedRecords(resource.id);
|
||||
|
||||
|
||||
// Delete the resource itself
|
||||
await db.deleteResource(resource.id);
|
||||
|
||||
@@ -275,8 +286,8 @@ const normalizedTs = GetMinuteStartTimestampUTC(body.start_date_time);
|
||||
const now = GetNowTimestampUTC();
|
||||
|
||||
// For optional timestamp with fallback
|
||||
const timestamp = body.timestamp !== undefined
|
||||
? GetMinuteStartTimestampUTC(body.timestamp)
|
||||
const timestamp = body.timestamp !== undefined
|
||||
? GetMinuteStartTimestampUTC(body.timestamp)
|
||||
: GetMinuteStartNowTimestampUTC();
|
||||
```
|
||||
|
||||
@@ -300,8 +311,8 @@ if (typeof body.count !== "number" || isNaN(body.count) || body.count <= 0) {
|
||||
```typescript
|
||||
const VALID_STATUSES = ["ACTIVE", "INACTIVE"];
|
||||
if (body.status && !VALID_STATUSES.includes(body.status)) {
|
||||
return json({
|
||||
error: { code: "BAD_REQUEST", message: `status must be one of: ${VALID_STATUSES.join(", ")}` }
|
||||
return json({
|
||||
error: { code: "BAD_REQUEST", message: `status must be one of: ${VALID_STATUSES.join(", ")}` }
|
||||
}, { status: 400 });
|
||||
}
|
||||
```
|
||||
@@ -311,8 +322,8 @@ if (body.status && !VALID_STATUSES.includes(body.status)) {
|
||||
if (body.monitor_tag) {
|
||||
const monitor = await db.getMonitorByTag(body.monitor_tag);
|
||||
if (!monitor) {
|
||||
return json({
|
||||
error: { code: "BAD_REQUEST", message: `Monitor with tag '${body.monitor_tag}' not found` }
|
||||
return json({
|
||||
error: { code: "BAD_REQUEST", message: `Monitor with tag '${body.monitor_tag}' not found` }
|
||||
}, { status: 400 });
|
||||
}
|
||||
}
|
||||
@@ -324,7 +335,7 @@ if (body.items !== undefined) {
|
||||
if (!Array.isArray(body.items)) {
|
||||
return json({ error: { code: "BAD_REQUEST", message: "items must be an array" } }, { status: 400 });
|
||||
}
|
||||
|
||||
|
||||
for (const item of body.items) {
|
||||
if (!item.tag || typeof item.tag !== "string") {
|
||||
return json({ error: { code: "BAD_REQUEST", message: "Each item must have a valid tag" } }, { status: 400 });
|
||||
|
||||
@@ -2,25 +2,37 @@
|
||||
|
||||
## Project Overview
|
||||
|
||||
Kener is an open-source status page application built with **SvelteKit 2.x** (**Svelte 5**) and **Node.js**, and is migrating to a **TypeScript-first** codebase. It provides real-time monitoring, uptime tracking, incident management, and customizable dashboards.
|
||||
Kener is an open-source status page application built with **SvelteKit 2.x** (**Svelte 5**) and **Node.js/Express**. It is a **TypeScript-first** codebase providing real-time monitoring, uptime tracking, incident management, and customizable dashboards.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Entry Points
|
||||
- **`main.js`** - Production server entry: Express + SvelteKit handler + cron scheduler
|
||||
- **`src/lib/server/startup.js`** - Cron job scheduler for monitors (runs every minute)
|
||||
### Dual Process Model
|
||||
|
||||
In development, `npm run dev` runs two parallel processes:
|
||||
1. **SvelteKit dev server** (`vite dev`) - serves the frontend with HMR
|
||||
2. **Cron scheduler** (`vite-node src/lib/server/startup.ts`) - runs monitor checks, maintenance scheduling, daily cleanup
|
||||
|
||||
In production, **`scripts/main.ts`** is the single entry point: Express server + SvelteKit handler + migrations + seeds + scheduler startup. Built output runs via `node build/main.js`.
|
||||
|
||||
### Route Groups (SvelteKit)
|
||||
- **`(kener)/`** - Public status page routes
|
||||
- **`(manage)/`** - Admin dashboard (requires authentication)
|
||||
- **`(embed)/`** - Embeddable widgets
|
||||
- **`(docs)/`** - Documentation pages
|
||||
- **`(api)/`** - SvelteKit API routes
|
||||
- **`(account)/`** - Account/auth pages
|
||||
- **`(ext)/`** - External integrations
|
||||
- **`(assets)/`** - Asset serving
|
||||
|
||||
### Core Server Components
|
||||
- **`src/lib/server/controllers/controller.js`** - Main business logic (~1700 lines), handles monitors, incidents, auth, email
|
||||
- **`src/lib/server/db/dbimpl.js`** - Database abstraction layer using Knex.js
|
||||
- **`src/lib/server/services/`** - Monitor type implementations: API, Ping, TCP, DNS, SSL, SQL, Heartbeat, GameDig, Group
|
||||
- **`src/lib/server/cron-minute.js`** - Per-monitor cron execution logic
|
||||
- **`src/lib/server/controllers/`** - Domain-split controllers (18 TypeScript files): `apiController.ts`, `incidentController.ts`, `monitorsController.ts`, `maintenanceController.ts`, `pagesController.ts`, `userController.ts`, `dashboardController.ts`, `emailController.ts`, `siteDataController.ts`, `validators.ts`, etc.
|
||||
- **`src/lib/server/db/dbimpl.ts`** - Database abstraction layer using Knex.js with repository composition pattern
|
||||
- **`src/lib/server/db/repositories/`** - Domain-driven repositories: `monitors.ts`, `incidents.ts`, `maintenances.ts`, `pages.ts`, `users.ts`, `alerts.ts`, `monitoring.ts`, `images.ts`, `subscriptionSystem.ts`, `emailTemplateConfig.ts`, `monitorAlertConfig.ts`, `site-data.ts`
|
||||
- **`src/lib/server/services/`** - Monitor type implementations (all TypeScript): `apiCall.ts`, `pingCall.ts`, `tcpCall.ts`, `dnsCall.ts`, `sslCall.ts`, `sqlCall.ts`, `heartbeatCall.ts`, `gamedigCall.ts`, `groupCall.ts`, `grpcCall.ts`, `noneCall.ts`
|
||||
- **`src/lib/server/schedulers/`** - Scheduling via `croner`: `appScheduler.ts`, `monitorSchedulers.ts`, `maintenanceScheduler.ts`, `dailyCleanup.ts`, `shutdown.ts`
|
||||
- **`src/lib/server/queues/`** - Job queues via **BullMQ** + **Redis**: `monitorExecuteQueue.ts`, `monitorResponseQueue.ts`, `alertingQueue.ts`, `emailQueue.ts`, `subscriberQueue.ts`
|
||||
- **`src/lib/server/api-server/`** - Express-side API handlers with file-based routing (directory/method pattern: e.g., `monitor-bar/get.ts`)
|
||||
- **`src/lib/server/cron-minute.ts`** - Per-monitor cron execution logic
|
||||
|
||||
### Database
|
||||
- Supports SQLite (default), PostgreSQL, MySQL via **Knex.js**
|
||||
@@ -28,83 +40,97 @@ Kener is an open-source status page application built with **SvelteKit 2.x** (**
|
||||
- Migrations in `/migrations/`, seeds in `/seeds/`
|
||||
- Run migrations: `npm run migrate` or auto-runs on `npm start`
|
||||
|
||||
### 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`
|
||||
|
||||
## Development Commands
|
||||
|
||||
```bash
|
||||
npm run dev # Start dev server with hot reload + cron scheduler
|
||||
npm run build # Production build
|
||||
npm run preview # Preview production build
|
||||
npm run check # Typecheck + Svelte checks (uses tsconfig)
|
||||
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
|
||||
```
|
||||
|
||||
## Key Patterns
|
||||
|
||||
### Svelte 5 + TypeScript conventions
|
||||
- Prefer **TypeScript** for new/modified code (`.ts`, and `.svelte` with `lang="ts"`).
|
||||
- Prefer **Svelte 5 runes** for component state/effects in new code (e.g. `$state`, `$derived`, `$effect`).
|
||||
- Prefer Svelte 5 props via `$props()` in new components. Keep existing `export let` props where already used to avoid churn.
|
||||
- For SvelteKit route typing, prefer generated `$types` (e.g. `import type { PageServerLoad } from './$types'`).
|
||||
- Avoid packages that hard-require Svelte 4 (they can break or force `--legacy-peer-deps`).
|
||||
- Use **TypeScript** for all code (`.ts`, and `.svelte` with `lang="ts"`).
|
||||
- Use **Svelte 5 runes** (`$state`, `$derived`, `$effect`, `$props()`) in components.
|
||||
- For SvelteKit route typing, use generated `$types` (e.g. `import type { PageServerLoad } from './$types'`).
|
||||
- Avoid packages that hard-require Svelte 4.
|
||||
|
||||
### Monitor Types
|
||||
Defined in `src/lib/server/services/service.js`. Each type has its own implementation file:
|
||||
```javascript
|
||||
// Supported: API, PING, TCP, DNS, GROUP, SSL, SQL, HEARTBEAT, GAMEDIG
|
||||
Defined in `src/lib/server/services/service.ts`. Each type has its own implementation file:
|
||||
```typescript
|
||||
// Supported: API, PING, TCP, DNS, GROUP, SSL, SQL, HEARTBEAT, GAMEDIG, GRPC, NONE
|
||||
```
|
||||
|
||||
### Status Constants
|
||||
Use constants from `src/lib/server/constants`:
|
||||
```javascript
|
||||
import { UP, DOWN, DEGRADED, MAINTENANCE, NO_DATA } from "./constants";
|
||||
Use constants from `src/lib/global-constants.ts`:
|
||||
```typescript
|
||||
// In Svelte/client code:
|
||||
import { UP, DOWN, DEGRADED, MAINTENANCE, NO_DATA } from "$lib/global-constants";
|
||||
|
||||
// In server code (use relative path):
|
||||
import { UP, DOWN, DEGRADED, MAINTENANCE, NO_DATA } from "./global-constants";
|
||||
```
|
||||
|
||||
### API Authentication
|
||||
APIs use Bearer token auth verified via `VerifyAPIKey()`:
|
||||
```javascript
|
||||
import { VerifyAPIKey } from "$lib/server/controllers/controller.js";
|
||||
```typescript
|
||||
import { VerifyAPIKey } from "$lib/server/controllers/apiController";
|
||||
```
|
||||
|
||||
### Database Queries
|
||||
Always use the db singleton, never instantiate Knex directly:
|
||||
```javascript
|
||||
```typescript
|
||||
import db from "$lib/server/db/db";
|
||||
const monitor = await db.getMonitorByTag(tag);
|
||||
```
|
||||
|
||||
### Timestamps
|
||||
All timestamps are **UTC seconds** (not milliseconds). Use helpers from `src/lib/server/tool.js`:
|
||||
```javascript
|
||||
import { GetMinuteStartNowTimestampUTC, GetNowTimestampUTC } from "./tool";
|
||||
All timestamps are **UTC seconds** (not milliseconds). Use helpers from `src/lib/server/tool.ts`:
|
||||
```typescript
|
||||
import { GetMinuteStartTimestampUTC, GetNowTimestampUTC } from "$lib/server/tool";
|
||||
```
|
||||
|
||||
### i18n
|
||||
Locales are in `src/lib/locales/`. Add new translations by creating `{code}.json` and updating `locales.json`.
|
||||
21 locale files in `src/lib/locales/` (en, de, fr, es, hi, ja, ko, zh-CN, zh-TW, pt-BR, ru, etc.). Add new translations by creating `{code}.json` and updating `locales.json`.
|
||||
|
||||
## UI Components
|
||||
|
||||
Uses **shadcn-svelte** components in `src/lib/components/ui/`. Import pattern:
|
||||
```javascript
|
||||
Uses **shadcn-svelte** components in `src/lib/components/ui/` (40+ components). Import pattern:
|
||||
```typescript
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
```
|
||||
|
||||
Styling: **TailwindCSS** with HSL CSS variables for theming (see `tailwind.config.js`).
|
||||
Styling: **Tailwind CSS v4** with CSS-based configuration (no `tailwind.config.js`). Theme uses HSL CSS variables defined in `src/routes/layout.css`.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Required in `.env`:
|
||||
- `KENER_SECRET_KEY` - JWT secret for auth
|
||||
Required:
|
||||
- `KENER_SECRET_KEY` - Secret key for auth
|
||||
- `ORIGIN` - Site URL (e.g., `http://localhost:3000`)
|
||||
- `DATABASE_URL` - Database connection string
|
||||
- `REDIS_URL` - Redis connection string (required for BullMQ job queues)
|
||||
|
||||
Optional:
|
||||
- `DATABASE_URL` - Database connection string (defaults to SQLite)
|
||||
- `KENER_BASE_PATH` - Base path for reverse proxy
|
||||
- `PORT` - Server port (default 3000)
|
||||
- `RESEND_API_KEY` / `RESEND_SENDER_EMAIL` - Email notifications
|
||||
|
||||
## File Conventions
|
||||
|
||||
- Server-only code: `src/lib/server/`
|
||||
- Shared utilities: `src/lib/` (except `server/`)
|
||||
- Route data loading: `+page.server.ts` / `+layout.server.ts` (and client-side `+page.ts` / `+layout.ts` when needed)
|
||||
- Client utilities: `src/lib/client/`
|
||||
- Route data loading: `+page.server.ts` / `+layout.server.ts`
|
||||
- API endpoints: `+server.ts` files returning `json()`
|
||||
|
||||
## Types & Interfaces
|
||||
@@ -112,29 +138,7 @@ Optional:
|
||||
Place types and interfaces in the appropriate folder based on where they are used:
|
||||
|
||||
- **`src/lib/types/`** - Shared types (safe to import from both server and client code). Use for domain models, DTOs, API response types, and anything needed on both sides.
|
||||
- **`src/lib/server/types/`** - Server-only types. Use for DB models, internal service types, auth/session types, and anything that uses `$env/static/private` or Node-only APIs.
|
||||
- **`src/lib/client/types/`** - Client-only types. Use for UI-specific types, component prop types, and anything that relies on browser/DOM APIs.
|
||||
- **`src/lib/server/types/`** - Server-only types (`db.ts`, `auth.ts`, `monitor.ts`, `api-server.ts`). Use for DB models, internal service types, auth/session types.
|
||||
- **`src/lib/client/types/`** - Client-only types (`ui.ts`). Use for UI-specific types, component prop types.
|
||||
|
||||
Always use `import type { ... }` when importing types to avoid accidental runtime imports.
|
||||
|
||||
# Other skills
|
||||
|
||||
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.
|
||||
+2
-1
@@ -19,4 +19,5 @@ config/static/*
|
||||
!config/static/.kener
|
||||
**/*.yaml
|
||||
**/*.yml
|
||||
.github/
|
||||
.github/
|
||||
src/lib/components/ui
|
||||
@@ -38,23 +38,3 @@ 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.
|
||||
|
||||
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
## 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**.
|
||||
Kener is an open-source status page application built with **SvelteKit 2.x (Svelte 5)** and **Node.js/Express**. It is a **TypeScript-first** codebase providing real-time monitoring, uptime tracking, incident management, and customizable dashboards.
|
||||
|
||||
## Development Commands
|
||||
|
||||
@@ -51,7 +51,7 @@ In production, `scripts/main.ts` is the single entry point: Express server + Sve
|
||||
|
||||
Each monitor type has a dedicated implementation in `src/lib/server/services/`:
|
||||
|
||||
- Types: API, Ping, TCP, DNS, SSL, SQL, Heartbeat, GameDig, Group
|
||||
- Types: API, Ping, TCP, DNS, SSL, SQL, Heartbeat, GameDig, Group, gRPC, None
|
||||
- Scheduled via `src/lib/server/schedulers/` using `croner`
|
||||
- Job queues managed with **BullMQ** + **Redis** (`src/lib/server/queues/`)
|
||||
|
||||
@@ -83,21 +83,21 @@ All timestamps are **UTC seconds** (not milliseconds). Use helpers from `src/lib
|
||||
|
||||
### Status Constants
|
||||
|
||||
#### When svelte code
|
||||
Constants are exported as a **default export** from `src/lib/global-constants.ts`:
|
||||
|
||||
```typescript
|
||||
import { UP, DOWN, DEGRADED, MAINTENANCE, NO_DATA } from "$lib/server/global-constants.ts"
|
||||
```
|
||||
// In Svelte/client code or SvelteKit routes:
|
||||
import GC from "$lib/global-constants"
|
||||
// Usage: GC.UP, GC.DOWN, GC.DEGRADED, GC.MAINTENANCE, GC.NO_DATA
|
||||
|
||||
#### When server code use directory traversal
|
||||
|
||||
```typescript
|
||||
import { UP, DOWN, DEGRADED, MAINTENANCE, NO_DATA } from "./global-constants.ts"
|
||||
// In server code (use relative path):
|
||||
import GC from "../../global-constants.js"
|
||||
// Usage: GC.UP, GC.DOWN, etc.
|
||||
```
|
||||
|
||||
### API Authentication
|
||||
|
||||
APIs use Bearer token auth: `import { VerifyAPIKey } from "$lib/server/controllers/controller.js"`
|
||||
APIs use Bearer token auth: `import { VerifyAPIKey } from "$lib/server/controllers/apiController"`
|
||||
|
||||
### Types Location
|
||||
|
||||
@@ -111,8 +111,8 @@ Locale files in `src/lib/locales/`. Add translations by creating `{code}.json` a
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Required: `KENER_SECRET_KEY`, `ORIGIN`, `DATABASE_URL`
|
||||
Optional: `KENER_BASE_PATH`, `PORT` (default 3000), `RESEND_API_KEY`, `RESEND_SENDER_EMAIL`
|
||||
Required: `KENER_SECRET_KEY`, `ORIGIN`, `REDIS_URL`
|
||||
Optional: `DATABASE_URL` (defaults to SQLite), `KENER_BASE_PATH`, `PORT` (default 3000), `RESEND_API_KEY`, `RESEND_SENDER_EMAIL`
|
||||
|
||||
## Skills
|
||||
|
||||
@@ -121,4 +121,3 @@ 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/`
|
||||
|
||||
@@ -135,6 +135,7 @@ ARG KENER_BASE_PATH=
|
||||
ENV NODE_ENV=production \
|
||||
PORT=${PORT} \
|
||||
KENER_BASE_PATH=${KENER_BASE_PATH} \
|
||||
BODY_SIZE_LIMIT=3M \
|
||||
TZ=UTC \
|
||||
# Required so Node can import .ts migration/seed files at runtime
|
||||
NODE_OPTIONS="--experimental-strip-types"
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Default body size limit for SvelteKit adapter-node (512K default is too small for image uploads)
|
||||
export BODY_SIZE_LIMIT="${BODY_SIZE_LIMIT:-3M}"
|
||||
|
||||
# Index documentation into Redis when docs are bundled in the image
|
||||
if [ -f /app/scripts/index-docs.ts ]; then
|
||||
echo "[kener] Indexing documentation into Redis..."
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/knip@5/schema.json",
|
||||
"entry": ["src/app.html", "build/main.js", "scripts/**/*.{js,ts}", "migrations/**/*.{js,ts}", "seeds/**/*.{js,ts}"],
|
||||
"project": ["src/**/*.{js,ts,svelte}", "scripts/**/*.{js,ts}", "migrations/**/*.{js,ts}", "seeds/**/*.{js,ts}"],
|
||||
"ignore": ["src/lib/components/ui/**"],
|
||||
"ignoreDependencies": [
|
||||
"@babel/runtime",
|
||||
"js-yaml",
|
||||
"mysql2",
|
||||
"node-cache",
|
||||
"pg",
|
||||
"pg-pool",
|
||||
"randomstring",
|
||||
"style-to-object",
|
||||
"lucide-svelte",
|
||||
"marked-gfm-heading-id"
|
||||
],
|
||||
"ignoreExportsUsedInFile": {
|
||||
"interface": true,
|
||||
"type": true
|
||||
},
|
||||
"ignoreBinaries": [],
|
||||
"compilers": {
|
||||
"css": ["postcss"],
|
||||
"svelte": ["svelte"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* Migration: Multi-Monitor Alerts
|
||||
*
|
||||
* This migration refactors the monitor alerting system to support many-to-many
|
||||
* relationships between alert configurations and monitors. Previously, each
|
||||
* `monitor_alerts_config` row was tied to exactly one monitor via a `monitor_tag`
|
||||
* foreign key column. This migration:
|
||||
*
|
||||
* 1. Creates a new `monitor_alerts_config_monitors` junction table that links
|
||||
* `monitor_alerts_config` rows to one or more `monitors` rows, enabling a
|
||||
* single alert configuration to fire across multiple monitors.
|
||||
*
|
||||
* 2. Adds a `monitor_tag` column to `monitor_alerts_v2` so that each firing
|
||||
* alert record knows which specific monitor triggered it (important when one
|
||||
* config covers many monitors).
|
||||
*
|
||||
* 3. Migrates existing data: copies every `monitor_alerts_config.monitor_tag`
|
||||
* value into the new junction table and backfills `monitor_alerts_v2.monitor_tag`
|
||||
* from the same source, preserving all historical alert records.
|
||||
*
|
||||
* 4. Removes the one-to-one constraint on `monitor_alerts_config.monitor_tag` by
|
||||
* dropping its foreign key and setting the column nullable (SQLite workaround:
|
||||
* nulls the column directly since SQLite cannot drop foreign key constraints
|
||||
* inline).
|
||||
*
|
||||
* 5. Adds a composite index on `monitor_alerts_v2 (config_id, monitor_tag,
|
||||
* alert_status)` for fast per-monitor alert status lookups.
|
||||
*
|
||||
* The `down` migration reverses these steps: restores the first junction-table
|
||||
* entry back onto `monitor_alerts_config.monitor_tag`, re-adds the foreign key
|
||||
* (non-SQLite), removes the `monitor_tag` column from `monitor_alerts_v2`
|
||||
* (non-SQLite), and drops the junction table.
|
||||
*/
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
// Step 1: Create monitor_alerts_config_monitors junction table
|
||||
if (!(await knex.schema.hasTable("monitor_alerts_config_monitors"))) {
|
||||
await knex.schema.createTable("monitor_alerts_config_monitors", (table) => {
|
||||
table.integer("monitor_alerts_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());
|
||||
|
||||
// Composite primary key
|
||||
table.primary(["monitor_alerts_id", "monitor_tag"]);
|
||||
|
||||
// Foreign keys
|
||||
table.foreign("monitor_alerts_id").references("id").inTable("monitor_alerts_config").onDelete("CASCADE");
|
||||
table.foreign("monitor_tag").references("tag").inTable("monitors").onDelete("CASCADE");
|
||||
});
|
||||
}
|
||||
|
||||
// Step 2: Add monitor_tag column to monitor_alerts_v2 for per-monitor alert tracking
|
||||
const hasV2Column = await knex.schema.hasColumn("monitor_alerts_v2", "monitor_tag");
|
||||
if (!hasV2Column) {
|
||||
await knex.schema.alterTable("monitor_alerts_v2", (table) => {
|
||||
table.string("monitor_tag", 255).nullable();
|
||||
table.foreign("monitor_tag").references("tag").inTable("monitors").onDelete("CASCADE");
|
||||
});
|
||||
}
|
||||
|
||||
// Step 3: Migrate existing data from monitor_alerts_config.monitor_tag to junction table
|
||||
// and backfill monitor_alerts_v2.monitor_tag from the same source
|
||||
const existingConfigs = await knex("monitor_alerts_config").whereNotNull("monitor_tag").select("id", "monitor_tag");
|
||||
|
||||
if (existingConfigs.length > 0) {
|
||||
// Build a config_id -> monitor_tag map for backfilling alerts
|
||||
const configTagMap = new Map<number, string>();
|
||||
const inserts = existingConfigs.map((config: { id: number; monitor_tag: string }) => {
|
||||
configTagMap.set(config.id, config.monitor_tag);
|
||||
return {
|
||||
monitor_alerts_id: config.id,
|
||||
monitor_tag: config.monitor_tag,
|
||||
created_at: knex.fn.now(),
|
||||
updated_at: knex.fn.now(),
|
||||
};
|
||||
});
|
||||
|
||||
// Batch insert into junction table
|
||||
const chunkSize = 100;
|
||||
for (let i = 0; i < inserts.length; i += chunkSize) {
|
||||
await knex("monitor_alerts_config_monitors").insert(inserts.slice(i, i + chunkSize));
|
||||
}
|
||||
|
||||
// Backfill monitor_tag on existing monitor_alerts_v2 rows
|
||||
const existingAlerts = await knex("monitor_alerts_v2").whereNull("monitor_tag").select("id", "config_id");
|
||||
for (const alert of existingAlerts) {
|
||||
const tag = configTagMap.get(alert.config_id);
|
||||
if (tag) {
|
||||
await knex("monitor_alerts_v2").where({ id: alert.id }).update({ monitor_tag: tag });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Drop foreign key and make monitor_tag nullable on monitor_alerts_config
|
||||
const dbClient = knex.client.config.client;
|
||||
|
||||
if (dbClient === "sqlite3" || dbClient === "better-sqlite3") {
|
||||
// SQLite cannot ALTER COLUMN, so we rebuild the table with monitor_tag nullable
|
||||
await knex.transaction(async (trx) => {
|
||||
await trx.raw("PRAGMA foreign_keys = OFF");
|
||||
await trx.raw("DROP TABLE IF EXISTS monitor_alerts_config_new");
|
||||
await trx.raw(`
|
||||
CREATE TABLE monitor_alerts_config_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
monitor_tag VARCHAR(255),
|
||||
alert_for VARCHAR(50) NOT NULL,
|
||||
alert_value VARCHAR(255) NOT NULL,
|
||||
failure_threshold INTEGER NOT NULL DEFAULT 1,
|
||||
success_threshold INTEGER NOT NULL DEFAULT 1,
|
||||
alert_description TEXT,
|
||||
create_incident VARCHAR(10) NOT NULL DEFAULT 'NO',
|
||||
is_active VARCHAR(10) NOT NULL DEFAULT 'YES',
|
||||
severity VARCHAR(50) NOT NULL DEFAULT 'WARNING',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
await trx.raw(`
|
||||
INSERT INTO monitor_alerts_config_new
|
||||
(id, monitor_tag, alert_for, alert_value, failure_threshold,
|
||||
success_threshold, alert_description, create_incident,
|
||||
is_active, severity, created_at, updated_at)
|
||||
SELECT
|
||||
id, NULL, alert_for, alert_value, failure_threshold,
|
||||
success_threshold, alert_description, create_incident,
|
||||
is_active, severity, created_at, updated_at
|
||||
FROM monitor_alerts_config
|
||||
`);
|
||||
await trx.raw("DROP TABLE monitor_alerts_config");
|
||||
await trx.raw("ALTER TABLE monitor_alerts_config_new RENAME TO monitor_alerts_config");
|
||||
try {
|
||||
await trx.raw("CREATE INDEX idx_monitor_alerts_config_monitor_tag ON monitor_alerts_config (monitor_tag)");
|
||||
} catch (_e) {
|
||||
/* index may already exist */
|
||||
}
|
||||
try {
|
||||
await trx.raw("CREATE INDEX idx_monitor_alerts_config_is_active ON monitor_alerts_config (is_active)");
|
||||
} catch (_e) {
|
||||
/* index may already exist */
|
||||
}
|
||||
await trx.raw("PRAGMA foreign_keys = ON");
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
await knex.schema.alterTable("monitor_alerts_config", (table) => {
|
||||
table.dropForeign(["monitor_tag"]);
|
||||
});
|
||||
} catch (_e) {
|
||||
// Foreign key may not exist or already dropped
|
||||
}
|
||||
|
||||
await knex.schema.alterTable("monitor_alerts_config", (table) => {
|
||||
table.string("monitor_tag", 255).nullable().alter();
|
||||
});
|
||||
|
||||
await knex("monitor_alerts_config").update({ monitor_tag: null });
|
||||
}
|
||||
|
||||
// Step 5: Add composite index on monitor_alerts_v2 for fast lookups
|
||||
try {
|
||||
await knex.raw(
|
||||
"CREATE INDEX idx_monitor_alerts_v2_config_tag_status ON monitor_alerts_v2 (config_id, monitor_tag, alert_status)",
|
||||
);
|
||||
} catch (_e) {
|
||||
/* index already exists */
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
// Step 1: Drop the composite index on monitor_alerts_v2
|
||||
try {
|
||||
await knex.raw("DROP INDEX IF EXISTS idx_monitor_alerts_v2_config_tag_status");
|
||||
} catch (_e) {
|
||||
/* index may not exist */
|
||||
}
|
||||
|
||||
// Step 2: Copy first monitor_tag from junction table back to monitor_alerts_config
|
||||
const configs = await knex("monitor_alerts_config").select("id");
|
||||
|
||||
for (const config of configs) {
|
||||
const firstMonitor = await knex("monitor_alerts_config_monitors").where({ monitor_alerts_id: config.id }).first();
|
||||
|
||||
if (firstMonitor) {
|
||||
await knex("monitor_alerts_config").where({ id: config.id }).update({ monitor_tag: firstMonitor.monitor_tag });
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Delete configs that have no monitors (can't satisfy NOT NULL)
|
||||
await knex("monitor_alerts_config").whereNull("monitor_tag").del();
|
||||
|
||||
// Step 4: Re-add foreign key constraint on monitor_alerts_config (non-SQLite only)
|
||||
const dbClient = knex.client.config.client;
|
||||
if (dbClient !== "sqlite3" && dbClient !== "better-sqlite3") {
|
||||
await knex.schema.alterTable("monitor_alerts_config", (table) => {
|
||||
table.string("monitor_tag", 255).notNullable().alter();
|
||||
table.foreign("monitor_tag").references("tag").inTable("monitors").onDelete("CASCADE");
|
||||
});
|
||||
}
|
||||
|
||||
// Step 5: Drop monitor_tag column from monitor_alerts_v2 (non-SQLite only)
|
||||
const hasV2Column = await knex.schema.hasColumn("monitor_alerts_v2", "monitor_tag");
|
||||
if (hasV2Column) {
|
||||
if (dbClient !== "sqlite3" && dbClient !== "better-sqlite3") {
|
||||
await knex.schema.alterTable("monitor_alerts_v2", (table) => {
|
||||
table.dropForeign(["monitor_tag"]);
|
||||
table.dropColumn("monitor_tag");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Step 6: Drop junction table
|
||||
await knex.schema.dropTableIfExists("monitor_alerts_config_monitors");
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* Migration: Fix SQLite monitor_alerts_config.monitor_tag NOT NULL constraint
|
||||
*
|
||||
* The earlier migration 20260325120000_multi_monitor_alerts nulled out data in
|
||||
* monitor_alerts_config.monitor_tag for SQLite but could not alter the column
|
||||
* constraint (SQLite doesn't support ALTER COLUMN). This migration recreates
|
||||
* the table with monitor_tag as nullable, preserving all data and indexes.
|
||||
*
|
||||
* Only runs on SQLite/better-sqlite3; other databases already had the column
|
||||
* altered in the previous migration.
|
||||
*/
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const dbClient = knex.client.config.client;
|
||||
if (dbClient !== "sqlite3" && dbClient !== "better-sqlite3") {
|
||||
return; // Already handled by 20260325120000_multi_monitor_alerts
|
||||
}
|
||||
|
||||
// Check if monitor_tag is already nullable (migration 1 already rebuilt the table)
|
||||
const tableInfo: Array<{ name: string; notnull: number }> = await knex.raw(
|
||||
"PRAGMA table_info(monitor_alerts_config)",
|
||||
);
|
||||
const monitorTagCol = tableInfo.find((col) => col.name === "monitor_tag");
|
||||
if (monitorTagCol && monitorTagCol.notnull === 0) {
|
||||
return; // Column is already nullable, nothing to do
|
||||
}
|
||||
|
||||
// Column is still NOT NULL — rebuild the table to make it nullable
|
||||
try {
|
||||
await knex.transaction(async (trx) => {
|
||||
await trx.raw("PRAGMA foreign_keys = OFF");
|
||||
await trx.raw("DROP TABLE IF EXISTS monitor_alerts_config_new");
|
||||
await trx.raw(`
|
||||
CREATE TABLE monitor_alerts_config_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
monitor_tag VARCHAR(255),
|
||||
alert_for VARCHAR(50) NOT NULL,
|
||||
alert_value VARCHAR(255) NOT NULL,
|
||||
failure_threshold INTEGER NOT NULL DEFAULT 1,
|
||||
success_threshold INTEGER NOT NULL DEFAULT 1,
|
||||
alert_description TEXT,
|
||||
create_incident VARCHAR(10) NOT NULL DEFAULT 'NO',
|
||||
is_active VARCHAR(10) NOT NULL DEFAULT 'YES',
|
||||
severity VARCHAR(50) NOT NULL DEFAULT 'WARNING',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
await trx.raw(`
|
||||
INSERT INTO monitor_alerts_config_new
|
||||
(id, monitor_tag, alert_for, alert_value, failure_threshold,
|
||||
success_threshold, alert_description, create_incident,
|
||||
is_active, severity, created_at, updated_at)
|
||||
SELECT
|
||||
id, NULL, alert_for, alert_value, failure_threshold,
|
||||
success_threshold, alert_description, create_incident,
|
||||
is_active, severity, created_at, updated_at
|
||||
FROM monitor_alerts_config
|
||||
`);
|
||||
|
||||
await trx.raw("DROP TABLE monitor_alerts_config");
|
||||
await trx.raw("ALTER TABLE monitor_alerts_config_new RENAME TO monitor_alerts_config");
|
||||
|
||||
try {
|
||||
await trx.raw("CREATE INDEX idx_monitor_alerts_config_monitor_tag ON monitor_alerts_config (monitor_tag)");
|
||||
} catch (_e) {
|
||||
/* index may already exist */
|
||||
}
|
||||
try {
|
||||
await trx.raw("CREATE INDEX idx_monitor_alerts_config_is_active ON monitor_alerts_config (is_active)");
|
||||
} catch (_e) {
|
||||
/* index may already exist */
|
||||
}
|
||||
await trx.raw("PRAGMA foreign_keys = ON");
|
||||
});
|
||||
} catch (e) {
|
||||
await knex.raw("PRAGMA foreign_keys = ON");
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const dbClient = knex.client.config.client;
|
||||
if (dbClient !== "sqlite3" && dbClient !== "better-sqlite3") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Revert: make monitor_tag NOT NULL again via table rebuild
|
||||
try {
|
||||
await knex.transaction(async (trx) => {
|
||||
await trx.raw("PRAGMA foreign_keys = OFF");
|
||||
await trx.raw(`
|
||||
CREATE TABLE monitor_alerts_config_old (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
monitor_tag VARCHAR(255) NOT NULL,
|
||||
alert_for VARCHAR(50) NOT NULL,
|
||||
alert_value VARCHAR(255) NOT NULL,
|
||||
failure_threshold INTEGER NOT NULL DEFAULT 1,
|
||||
success_threshold INTEGER NOT NULL DEFAULT 1,
|
||||
alert_description TEXT,
|
||||
create_incident VARCHAR(10) NOT NULL DEFAULT 'NO',
|
||||
is_active VARCHAR(10) NOT NULL DEFAULT 'YES',
|
||||
severity VARCHAR(50) NOT NULL DEFAULT 'WARNING',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// Only copy rows that have a non-null monitor_tag
|
||||
await trx.raw(`
|
||||
INSERT INTO monitor_alerts_config_old
|
||||
(id, monitor_tag, alert_for, alert_value, failure_threshold,
|
||||
success_threshold, alert_description, create_incident,
|
||||
is_active, severity, created_at, updated_at)
|
||||
SELECT
|
||||
id, monitor_tag, alert_for, alert_value, failure_threshold,
|
||||
success_threshold, alert_description, create_incident,
|
||||
is_active, severity, created_at, updated_at
|
||||
FROM monitor_alerts_config
|
||||
WHERE monitor_tag IS NOT NULL
|
||||
`);
|
||||
|
||||
await trx.raw("DROP TABLE monitor_alerts_config");
|
||||
await trx.raw("ALTER TABLE monitor_alerts_config_old RENAME TO monitor_alerts_config");
|
||||
|
||||
try {
|
||||
await trx.raw("CREATE INDEX idx_monitor_alerts_config_monitor_tag ON monitor_alerts_config (monitor_tag)");
|
||||
} catch (_e) {
|
||||
/* index may already exist */
|
||||
}
|
||||
try {
|
||||
await trx.raw("CREATE INDEX idx_monitor_alerts_config_is_active ON monitor_alerts_config (is_active)");
|
||||
} catch (_e) {
|
||||
/* index may already exist */
|
||||
}
|
||||
await trx.raw("PRAGMA foreign_keys = ON");
|
||||
});
|
||||
} catch (e) {
|
||||
await knex.raw("PRAGMA foreign_keys = ON");
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
Generated
+270
-221
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "kener",
|
||||
"version": "4.0.17",
|
||||
"version": "4.0.21",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "kener",
|
||||
"version": "4.0.17",
|
||||
"version": "4.0.21",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4",
|
||||
@@ -51,6 +51,7 @@
|
||||
"front-matter": "^4.0.2",
|
||||
"gamedig": "^5.3.2",
|
||||
"glob": "^13.0.6",
|
||||
"heic-convert": "^2.1.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"ioredis": "^5.8.2",
|
||||
"js-yaml": "^4.1.1",
|
||||
@@ -88,7 +89,7 @@
|
||||
"@lucide/svelte": "^0.561.0",
|
||||
"@sveltejs/adapter-auto": "^7.0.0",
|
||||
"@sveltejs/adapter-node": "^5.4.0",
|
||||
"@sveltejs/kit": "^2.48.5",
|
||||
"@sveltejs/kit": "^2.53.3",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tailwindcss/vite": "^4.1.17",
|
||||
@@ -97,6 +98,7 @@
|
||||
"@types/d3-shape": "^3.1.8",
|
||||
"@types/dns2": "^2.0.10",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/heic-convert": "^2.1.0",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/mustache": "^4.2.6",
|
||||
"@types/node": "^25.0.3",
|
||||
@@ -112,7 +114,7 @@
|
||||
"prettier": "^3.7.4",
|
||||
"prettier-plugin-svelte": "^3.4.0",
|
||||
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||
"svelte": "^5.43.8",
|
||||
"svelte": "^5.53.5",
|
||||
"svelte-awesome-color-picker": "^4.1.0",
|
||||
"svelte-check": "^4.3.4",
|
||||
"svelte-sonner": "^1.0.7",
|
||||
@@ -1790,9 +1792,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz",
|
||||
"integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==",
|
||||
"version": "4.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz",
|
||||
"integrity": "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1803,9 +1805,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz",
|
||||
"integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==",
|
||||
"version": "4.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.0.tgz",
|
||||
"integrity": "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1816,9 +1818,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz",
|
||||
"integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==",
|
||||
"version": "4.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.0.tgz",
|
||||
"integrity": "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1829,9 +1831,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz",
|
||||
"integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==",
|
||||
"version": "4.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.0.tgz",
|
||||
"integrity": "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1842,9 +1844,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz",
|
||||
"integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==",
|
||||
"version": "4.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.0.tgz",
|
||||
"integrity": "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1855,9 +1857,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz",
|
||||
"integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==",
|
||||
"version": "4.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.0.tgz",
|
||||
"integrity": "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1868,9 +1870,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz",
|
||||
"integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==",
|
||||
"version": "4.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.0.tgz",
|
||||
"integrity": "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1881,9 +1883,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz",
|
||||
"integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==",
|
||||
"version": "4.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.0.tgz",
|
||||
"integrity": "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1894,9 +1896,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz",
|
||||
"integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==",
|
||||
"version": "4.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.0.tgz",
|
||||
"integrity": "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1907,9 +1909,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz",
|
||||
"integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==",
|
||||
"version": "4.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.0.tgz",
|
||||
"integrity": "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1920,9 +1922,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz",
|
||||
"integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==",
|
||||
"version": "4.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.0.tgz",
|
||||
"integrity": "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -1933,9 +1935,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-musl": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz",
|
||||
"integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==",
|
||||
"version": "4.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.0.tgz",
|
||||
"integrity": "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -1946,9 +1948,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz",
|
||||
"integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==",
|
||||
"version": "4.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.0.tgz",
|
||||
"integrity": "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -1959,9 +1961,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-musl": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz",
|
||||
"integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==",
|
||||
"version": "4.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.0.tgz",
|
||||
"integrity": "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -1972,9 +1974,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz",
|
||||
"integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==",
|
||||
"version": "4.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.0.tgz",
|
||||
"integrity": "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -1985,9 +1987,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz",
|
||||
"integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==",
|
||||
"version": "4.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.0.tgz",
|
||||
"integrity": "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -1998,9 +2000,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz",
|
||||
"integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==",
|
||||
"version": "4.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.0.tgz",
|
||||
"integrity": "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -2011,9 +2013,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz",
|
||||
"integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==",
|
||||
"version": "4.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.0.tgz",
|
||||
"integrity": "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2024,9 +2026,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz",
|
||||
"integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==",
|
||||
"version": "4.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.0.tgz",
|
||||
"integrity": "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2037,9 +2039,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openbsd-x64": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz",
|
||||
"integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==",
|
||||
"version": "4.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.0.tgz",
|
||||
"integrity": "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2050,9 +2052,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz",
|
||||
"integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==",
|
||||
"version": "4.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.0.tgz",
|
||||
"integrity": "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2063,9 +2065,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz",
|
||||
"integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==",
|
||||
"version": "4.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.0.tgz",
|
||||
"integrity": "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2076,9 +2078,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz",
|
||||
"integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==",
|
||||
"version": "4.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.0.tgz",
|
||||
"integrity": "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -2089,9 +2091,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz",
|
||||
"integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==",
|
||||
"version": "4.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.0.tgz",
|
||||
"integrity": "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2102,9 +2104,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz",
|
||||
"integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==",
|
||||
"version": "4.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.0.tgz",
|
||||
"integrity": "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2260,9 +2262,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/kit": {
|
||||
"version": "2.52.0",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.52.0.tgz",
|
||||
"integrity": "sha512-zG+HmJuSF7eC0e7xt2htlOcEMAdEtlVdb7+gAr+ef08EhtwUsjLxcAwBgUCJY3/5p08OVOxVZti91WfXeuLvsg==",
|
||||
"version": "2.55.0",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.55.0.tgz",
|
||||
"integrity": "sha512-MdFRjevVxmAknf2NbaUkDF16jSIzXMWd4Nfah0Qp8TtQVoSp3bV4jKt8mX7z7qTUTWvgSaxtR0EG5WJf53gcuA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
@@ -2270,12 +2272,11 @@
|
||||
"@types/cookie": "^0.6.0",
|
||||
"acorn": "^8.14.1",
|
||||
"cookie": "^0.6.0",
|
||||
"devalue": "^5.6.2",
|
||||
"devalue": "^5.6.4",
|
||||
"esm-env": "^1.2.2",
|
||||
"kleur": "^4.1.5",
|
||||
"magic-string": "^0.30.5",
|
||||
"mrmime": "^2.0.0",
|
||||
"sade": "^1.8.1",
|
||||
"set-cookie-parser": "^3.0.0",
|
||||
"sirv": "^3.0.0"
|
||||
},
|
||||
@@ -2287,10 +2288,10 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "^1.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0",
|
||||
"svelte": "^4.0.0 || ^5.0.0-next.0",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0"
|
||||
"vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@opentelemetry/api": {
|
||||
@@ -2756,6 +2757,13 @@
|
||||
"@types/send": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/heic-convert": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/heic-convert/-/heic-convert-2.1.0.tgz",
|
||||
"integrity": "sha512-Cf5Sdc2Gm2pfZ0uN1zjj35wcf3mF1lJCMIzws5OdJynrdMJRTIRUGa5LegbVg0hatzOPkH2uAf2JRjPYgl9apg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"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",
|
||||
@@ -2859,8 +2867,7 @@
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@uiw/codemirror-theme-github": {
|
||||
"version": "4.25.4",
|
||||
@@ -2975,9 +2982,9 @@
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/aria-query": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
|
||||
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz",
|
||||
"integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -3133,10 +3140,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"license": "MIT"
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/barse": {
|
||||
"version": "0.4.3",
|
||||
@@ -3325,13 +3335,15 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
|
||||
"integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
@@ -3746,12 +3758,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/concurrently": {
|
||||
"version": "9.2.1",
|
||||
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz",
|
||||
@@ -3800,9 +3806,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
||||
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
@@ -4529,9 +4535,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/devalue": {
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.2.tgz",
|
||||
"integrity": "sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==",
|
||||
"version": "5.6.4",
|
||||
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.4.tgz",
|
||||
"integrity": "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dns2": {
|
||||
@@ -4582,9 +4588,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz",
|
||||
"integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==",
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.3.tgz",
|
||||
"integrity": "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
@@ -5055,25 +5061,16 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/cookie": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-sha256": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz",
|
||||
"integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==",
|
||||
"license": "Unlicense"
|
||||
},
|
||||
"node_modules/fast-xml-parser": {
|
||||
"version": "5.2.5",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz",
|
||||
"integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==",
|
||||
"node_modules/fast-xml-builder": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz",
|
||||
"integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -5082,7 +5079,24 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"strnum": "^2.1.0"
|
||||
"path-expression-matcher": "^1.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-xml-parser": {
|
||||
"version": "5.5.8",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.8.tgz",
|
||||
"integrity": "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-xml-builder": "^1.1.4",
|
||||
"path-expression-matcher": "^1.2.0",
|
||||
"strnum": "^2.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"fxparser": "src/cli/cli.js"
|
||||
@@ -5568,42 +5582,6 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/glob/node_modules/balanced-match": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.3.tgz",
|
||||
"integrity": "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/glob/node_modules/brace-expansion": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz",
|
||||
"integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/glob/node_modules/minimatch": {
|
||||
"version": "10.2.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz",
|
||||
"integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^5.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/globalthis": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
|
||||
@@ -5760,6 +5738,32 @@
|
||||
"he": "bin/he"
|
||||
}
|
||||
},
|
||||
"node_modules/heic-convert": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/heic-convert/-/heic-convert-2.1.0.tgz",
|
||||
"integrity": "sha512-1qDuRvEHifTVAj3pFIgkqGgJIr0M3X7cxEPjEp0oG4mo8GFjq99DpCo8Eg3kg17Cy0MTjxpFdoBHOatj7ZVKtg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"heic-decode": "^2.0.0",
|
||||
"jpeg-js": "^0.4.4",
|
||||
"pngjs": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/heic-decode": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/heic-decode/-/heic-decode-2.1.0.tgz",
|
||||
"integrity": "sha512-0fB3O3WMk38+PScbHLVp66jcNhsZ/ErtQ6u2lMYu/YxXgbBtl+oKOhGQHa4RpvE68k8IzbWkABzHnyAIjR758A==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"libheif-js": "^1.19.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/highlight.js": {
|
||||
"version": "11.11.1",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
|
||||
@@ -6399,6 +6403,12 @@
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/jpeg-js": {
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz",
|
||||
"integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
@@ -6638,6 +6648,15 @@
|
||||
"integrity": "sha512-GgOXd0Eo6phYgh0DJtjQ2tO8dc0IVINtZJeARPeiIJqge+HdsWSuaDTe8ztQ7j/cONByDZ3zeB325AHiv5O0dg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/libheif-js": {
|
||||
"version": "1.19.8",
|
||||
"resolved": "https://registry.npmjs.org/libheif-js/-/libheif-js-1.19.8.tgz",
|
||||
"integrity": "sha512-vQJWusIxO7wavpON1dusciL8Go9jsIQ+EUrckauFYAiSTjcmLAsuJh3SszLpvkwPci3JcL41ek2n+LUZGFpPIQ==",
|
||||
"license": "LGPL-3.0",
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/libmime": {
|
||||
"version": "5.3.7",
|
||||
"resolved": "https://registry.npmjs.org/libmime/-/libmime-5.3.7.tgz",
|
||||
@@ -7101,27 +7120,27 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mailparser": {
|
||||
"version": "3.9.1",
|
||||
"resolved": "https://registry.npmjs.org/mailparser/-/mailparser-3.9.1.tgz",
|
||||
"integrity": "sha512-6vHZcco3fWsDMkf4Vz9iAfxvwrKNGbHx0dV1RKVphQ/zaNY34Buc7D37LSa09jeSeybWzYcTPjhiZFxzVRJedA==",
|
||||
"version": "3.9.5",
|
||||
"resolved": "https://registry.npmjs.org/mailparser/-/mailparser-3.9.5.tgz",
|
||||
"integrity": "sha512-i5mXdEqDrh7I095uiQiebOrc00AOIJRLlpze1nBb82oZpI4GaCJIn9ypXR2bqb/Ayr7q+nmT6k11yYU5Age/Gg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@zone-eu/mailsplit": "5.4.8",
|
||||
"encoding-japanese": "2.2.0",
|
||||
"he": "1.2.0",
|
||||
"html-to-text": "9.0.5",
|
||||
"iconv-lite": "0.7.0",
|
||||
"iconv-lite": "0.7.2",
|
||||
"libmime": "5.3.7",
|
||||
"linkify-it": "5.0.0",
|
||||
"nodemailer": "7.0.11",
|
||||
"nodemailer": "8.0.3",
|
||||
"punycode.js": "2.3.1",
|
||||
"tlds": "1.261.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mailparser/node_modules/iconv-lite": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
|
||||
"integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
|
||||
"integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
@@ -7135,9 +7154,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mailparser/node_modules/nodemailer": {
|
||||
"version": "7.0.11",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.11.tgz",
|
||||
"integrity": "sha512-gnXhNRE0FNhD7wPSCGhdNh46Hs6nm+uTyg+Kq0cZukNQiYdnCsoQjodNP9BQVG9XrcK/v6/MgpAPBUFyzh9pvw==",
|
||||
"version": "8.0.3",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.3.tgz",
|
||||
"integrity": "sha512-JQNBqvK+bj3NMhUFR3wmCl3SYcOeMotDiwDBvIoCuQdF0PvlIY0BH+FJ2CG7u4cXKPChplE78oowlH/Otsc4ZQ==",
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
@@ -7308,15 +7327,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"license": "ISC",
|
||||
"version": "10.2.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
|
||||
"integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
"brace-expansion": "^5.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
@@ -7416,6 +7438,7 @@
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
||||
"integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
@@ -8040,6 +8063,21 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-expression-matcher": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.2.0.tgz",
|
||||
"integrity": "sha512-DwmPWeFn+tq7TiyJ2CxezCAirXjFxvaiD03npak3cRjlP9+OjTmSy1EpIrEbh+l6JgUundniloMLDQ/6VTdhLQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
@@ -8253,6 +8291,15 @@
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pngjs": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz",
|
||||
"integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/possible-typed-array-names": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
||||
@@ -8920,9 +8967,9 @@
|
||||
"license": "Unlicense"
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz",
|
||||
"integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==",
|
||||
"version": "4.60.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz",
|
||||
"integrity": "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
@@ -8935,31 +8982,31 @@
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.57.1",
|
||||
"@rollup/rollup-android-arm64": "4.57.1",
|
||||
"@rollup/rollup-darwin-arm64": "4.57.1",
|
||||
"@rollup/rollup-darwin-x64": "4.57.1",
|
||||
"@rollup/rollup-freebsd-arm64": "4.57.1",
|
||||
"@rollup/rollup-freebsd-x64": "4.57.1",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.57.1",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.57.1",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.57.1",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.57.1",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.57.1",
|
||||
"@rollup/rollup-linux-loong64-musl": "4.57.1",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.57.1",
|
||||
"@rollup/rollup-linux-ppc64-musl": "4.57.1",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.57.1",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.57.1",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.57.1",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.57.1",
|
||||
"@rollup/rollup-linux-x64-musl": "4.57.1",
|
||||
"@rollup/rollup-openbsd-x64": "4.57.1",
|
||||
"@rollup/rollup-openharmony-arm64": "4.57.1",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.57.1",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.57.1",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.57.1",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.57.1",
|
||||
"@rollup/rollup-android-arm-eabi": "4.60.0",
|
||||
"@rollup/rollup-android-arm64": "4.60.0",
|
||||
"@rollup/rollup-darwin-arm64": "4.60.0",
|
||||
"@rollup/rollup-darwin-x64": "4.60.0",
|
||||
"@rollup/rollup-freebsd-arm64": "4.60.0",
|
||||
"@rollup/rollup-freebsd-x64": "4.60.0",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.60.0",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.60.0",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.60.0",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.60.0",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.60.0",
|
||||
"@rollup/rollup-linux-loong64-musl": "4.60.0",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.60.0",
|
||||
"@rollup/rollup-linux-ppc64-musl": "4.60.0",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.60.0",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.60.0",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.60.0",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.60.0",
|
||||
"@rollup/rollup-linux-x64-musl": "4.60.0",
|
||||
"@rollup/rollup-openbsd-x64": "4.60.0",
|
||||
"@rollup/rollup-openharmony-arm64": "4.60.0",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.60.0",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.60.0",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.60.0",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.60.0",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
@@ -9033,6 +9080,7 @@
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
|
||||
"integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mri": "^1.1.0"
|
||||
@@ -9790,9 +9838,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/strnum": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz",
|
||||
"integrity": "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==",
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.2.tgz",
|
||||
"integrity": "sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -9845,20 +9893,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/svelte": {
|
||||
"version": "5.50.1",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.50.1.tgz",
|
||||
"integrity": "sha512-/Jlom4ddkISyVHXpM2O5dXP9pYnaiFrVQzPbIL1/pEoOa77ZunCb6nDgUCTNCQ/X3t64z9ukrK6R+BbB3kPR3A==",
|
||||
"version": "5.54.1",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.54.1.tgz",
|
||||
"integrity": "sha512-ow8tncN097Ty8U1H+C3bM1xNlsCbnO2UZeN0lWBnv8f3jKho7QTTQ2LWbMXrPQDodLjH91n4kpNnLolyRhVE6A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/remapping": "^2.3.4",
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
"@sveltejs/acorn-typescript": "^1.0.5",
|
||||
"@types/estree": "^1.0.5",
|
||||
"@types/trusted-types": "^2.0.7",
|
||||
"acorn": "^8.12.1",
|
||||
"aria-query": "^5.3.1",
|
||||
"aria-query": "5.3.1",
|
||||
"axobject-query": "^4.1.0",
|
||||
"clsx": "^2.1.1",
|
||||
"devalue": "^5.6.2",
|
||||
"devalue": "^5.6.4",
|
||||
"esm-env": "^1.2.1",
|
||||
"esrap": "^2.2.2",
|
||||
"is-reference": "^3.0.3",
|
||||
@@ -10389,9 +10438,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "7.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.21.0.tgz",
|
||||
"integrity": "sha512-Hn2tCQpoDt1wv23a68Ctc8Cr/BHpUSfaPYrkajTXOS9IKpxVRx/X5m1K2YkbK2ipgZgxXSgsUinl3x+2YdSSfg==",
|
||||
"version": "7.24.5",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.24.5.tgz",
|
||||
"integrity": "sha512-3IWdCpjgxp15CbJnsi/Y9TCDE7HWVN19j1hmzVhoAkY/+CJx449tVxT5wZc1Gwg8J+P0LWvzlBzxYRnHJ+1i7Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.18.1"
|
||||
|
||||
+15
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "kener",
|
||||
"version": "4.0.17",
|
||||
"version": "4.0.21",
|
||||
"type": "module",
|
||||
"private": false,
|
||||
"license": "MIT",
|
||||
@@ -55,7 +55,7 @@
|
||||
"@lucide/svelte": "^0.561.0",
|
||||
"@sveltejs/adapter-auto": "^7.0.0",
|
||||
"@sveltejs/adapter-node": "^5.4.0",
|
||||
"@sveltejs/kit": "^2.48.5",
|
||||
"@sveltejs/kit": "^2.53.3",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tailwindcss/vite": "^4.1.17",
|
||||
@@ -64,6 +64,7 @@
|
||||
"@types/d3-shape": "^3.1.8",
|
||||
"@types/dns2": "^2.0.10",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/heic-convert": "^2.1.0",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/mustache": "^4.2.6",
|
||||
"@types/node": "^25.0.3",
|
||||
@@ -79,7 +80,7 @@
|
||||
"prettier": "^3.7.4",
|
||||
"prettier-plugin-svelte": "^3.4.0",
|
||||
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||
"svelte": "^5.43.8",
|
||||
"svelte": "^5.53.5",
|
||||
"svelte-awesome-color-picker": "^4.1.0",
|
||||
"svelte-check": "^4.3.4",
|
||||
"svelte-sonner": "^1.0.7",
|
||||
@@ -139,6 +140,7 @@
|
||||
"front-matter": "^4.0.2",
|
||||
"gamedig": "^5.3.2",
|
||||
"glob": "^13.0.6",
|
||||
"heic-convert": "^2.1.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"ioredis": "^5.8.2",
|
||||
"js-yaml": "^4.1.1",
|
||||
@@ -170,5 +172,15 @@
|
||||
"style-to-object": "^1.0.14",
|
||||
"svelte-codemirror-editor": "^2.1.0",
|
||||
"vite-plugin-package-version": "^1.1.0"
|
||||
},
|
||||
"overrides": {
|
||||
"fast-xml-parser": "^5.5.6",
|
||||
"rollup": "^4.59.0",
|
||||
"undici": "^7.24.0",
|
||||
"minimatch": "^10.2.3",
|
||||
"devalue": "^5.6.4",
|
||||
"dompurify": "^3.3.2",
|
||||
"cookie": "^0.7.0",
|
||||
"mailparser": "^3.9.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
+82
-74
@@ -1,6 +1,6 @@
|
||||
import { handler } from "../build/handler.js";
|
||||
import dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
|
||||
import express from "express";
|
||||
import Startup from "../src/lib/server/startup.ts";
|
||||
import shutdownSchedulers from "../src/lib/server/schedulers/shutdown.ts";
|
||||
@@ -12,82 +12,90 @@ import knexOb from "../knexfile.js";
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const base = process.env.KENER_BASE_PATH || "";
|
||||
|
||||
const app: any = express();
|
||||
const db = knex(knexOb);
|
||||
async function start() {
|
||||
// Dynamic import so BODY_SIZE_LIMIT from .env is available
|
||||
// before the handler reads it at module top-level
|
||||
const { handler } = await import("../build/handler.js");
|
||||
|
||||
app.get(base + "/healthcheck", (req: any, res: any) => {
|
||||
res.end("ok");
|
||||
});
|
||||
const app: any = express();
|
||||
const db = knex(knexOb);
|
||||
|
||||
app.use(handler);
|
||||
app.get(base + "/healthcheck", (req: any, res: any) => {
|
||||
res.end("ok");
|
||||
});
|
||||
|
||||
//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}`);
|
||||
}
|
||||
}
|
||||
app.use(handler);
|
||||
|
||||
console.log("Running migrations...");
|
||||
await db.migrate.latest(); // Runs migrations to the latest state
|
||||
console.log("Migrations completed successfully!");
|
||||
} catch (err) {
|
||||
console.error("Error running migrations:", err);
|
||||
}
|
||||
//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!");
|
||||
} catch (err) {
|
||||
console.error("Error running migrations:", err);
|
||||
}
|
||||
}
|
||||
|
||||
//seed
|
||||
async function runSeed() {
|
||||
try {
|
||||
console.log("Running seed...");
|
||||
await db.seed.run(); // Runs seed to the latest state
|
||||
console.log("Seed completed successfully!");
|
||||
} catch (err) {
|
||||
console.error("Error running seed:", err);
|
||||
}
|
||||
}
|
||||
|
||||
app.listen(PORT, async () => {
|
||||
await runMigrations();
|
||||
await runSeed();
|
||||
await db.destroy();
|
||||
Startup();
|
||||
console.log("Kener is running on port " + PORT + "!");
|
||||
});
|
||||
|
||||
// Graceful shutdown handler
|
||||
async function gracefulShutdown(signal: string) {
|
||||
console.log(`\nReceived ${signal}. Starting graceful shutdown...`);
|
||||
|
||||
try {
|
||||
console.log("Shutting down schedulers...");
|
||||
await shutdownSchedulers();
|
||||
console.log("Schedulers shut down successfully.");
|
||||
|
||||
console.log("Shutting down queues...");
|
||||
await shutdownQueues();
|
||||
console.log("Queues shut down successfully.");
|
||||
|
||||
console.log("Closing database connection...");
|
||||
await dbInstance.close();
|
||||
console.log("Database connection closed successfully.");
|
||||
|
||||
console.log("Graceful shutdown completed.");
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
console.error("Error during graceful shutdown:", err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle termination signals
|
||||
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
|
||||
process.on("SIGINT", () => gracefulShutdown("SIGINT"));
|
||||
}
|
||||
|
||||
//seed
|
||||
async function runSeed() {
|
||||
try {
|
||||
console.log("Running seed...");
|
||||
await db.seed.run(); // Runs seed to the latest state
|
||||
console.log("Seed completed successfully!");
|
||||
} catch (err) {
|
||||
console.error("Error running seed:", err);
|
||||
}
|
||||
}
|
||||
|
||||
app.listen(PORT, async () => {
|
||||
await runMigrations();
|
||||
await runSeed();
|
||||
await db.destroy();
|
||||
Startup();
|
||||
console.log("Kener is running on port " + PORT + "!");
|
||||
});
|
||||
|
||||
// Graceful shutdown handler
|
||||
async function gracefulShutdown(signal: string) {
|
||||
console.log(`\nReceived ${signal}. Starting graceful shutdown...`);
|
||||
|
||||
try {
|
||||
console.log("Shutting down schedulers...");
|
||||
await shutdownSchedulers();
|
||||
console.log("Schedulers shut down successfully.");
|
||||
|
||||
console.log("Shutting down queues...");
|
||||
await shutdownQueues();
|
||||
console.log("Queues shut down successfully.");
|
||||
|
||||
console.log("Closing database connection...");
|
||||
await dbInstance.close();
|
||||
console.log("Database connection closed successfully.");
|
||||
|
||||
console.log("Graceful shutdown completed.");
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
console.error("Error during graceful shutdown:", err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle termination signals
|
||||
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
|
||||
process.on("SIGINT", () => gracefulShutdown("SIGINT"));
|
||||
start();
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"version": 1,
|
||||
"skills": {
|
||||
"ss-shadcn-svelte": {
|
||||
"source": "rajnandan1/such-skills",
|
||||
"sourceType": "github",
|
||||
"computedHash": "0678d0cad0bce1d56731c9613e75f2d274810ad2a17ecea2d21434b6416c90b7"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta property="og:locale" content="en_US" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
</Drawer.Trigger>
|
||||
<Drawer.Content class="max-h-[80vh]">
|
||||
<Drawer.Header>
|
||||
<Drawer.Title>{$t("Included Monitors")}</Drawer.Title>
|
||||
<Drawer.Title>{$t("Included Monitors (%count)", { count: String(tags.length) })}</Drawer.Title>
|
||||
</Drawer.Header>
|
||||
<div class="scrollbar-hidden flex flex-col overflow-y-auto px-4 pb-4">
|
||||
{#if tags.length === 0}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -152,8 +152,7 @@
|
||||
days={days as number}
|
||||
endOfDayTodayAtTz={endOfDayTodayAtTz as number}
|
||||
>
|
||||
{groupChildTags.length}
|
||||
{$t("Included Monitors")}
|
||||
{$t("Included Monitors (%count)", { count: String(groupChildTags.length) })}
|
||||
</GroupMonitorPopover>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -219,7 +219,7 @@
|
||||
{#if groupTags.length > 0}
|
||||
<div class="flex justify-center">
|
||||
<GroupMonitorPopover tags={groupTags} days={selectedDays} {endOfDayTodayAtTz}>
|
||||
{$t("Included Monitors")} ({groupTags.length})
|
||||
{$t("Included Monitors (%count)", { count: String(groupTags.length) })}
|
||||
<ArrowUp class="size-3" />
|
||||
</GroupMonitorPopover>
|
||||
</div>
|
||||
|
||||
+30
-29
@@ -22,39 +22,39 @@
|
||||
"Day": "Den",
|
||||
"Day Uptime": "Denní dostupnost",
|
||||
"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",
|
||||
"Degraded": "Omezený",
|
||||
"DEGRADED": "OMEZENÝ",
|
||||
"Degraded Performance": "Snížený výkon",
|
||||
"Didn't receive the code? Resend": "Nepřišel kód? Odeslat znovu",
|
||||
"Down": "Nedostupné",
|
||||
"DOWN": "NEDOSTUPNÉ",
|
||||
"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",
|
||||
"Embed this monitor in your website or app": "Vložte tento monitor na web nebo do aplikace",
|
||||
"End Time": "Konec",
|
||||
"Enter the verification code sent to your email.": "Zadejte ověřovací kód zaslaný na váš e-mail.",
|
||||
"Enter the verification code sent to your email.": "Zadejte ověřovací kód zaslaný na e-mail",
|
||||
"Events": "Události",
|
||||
"Failed to load data": "Nepodařilo se načíst data",
|
||||
"Failed to load latency data": "Nepodařilo se načíst data latence",
|
||||
"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í",
|
||||
"Failed to update preference": "Nepodařilo se uložit 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ánovanou údržbu.",
|
||||
"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": "Monitorů zahrnuto",
|
||||
"incident": "Incident",
|
||||
"Included Monitors (%count)": "Zahrnuté monitory (%count)",
|
||||
"INVESTIGATING": "VYŠETŘOVÁNÍ",
|
||||
"Last Updated": "Naposledy aktualizováno",
|
||||
"Latency": "Latence",
|
||||
@@ -62,18 +62,18 @@
|
||||
"Latency Over Time": "Latence v čase",
|
||||
"Latency Trend": "Trend latence",
|
||||
"Latest Latency": "Poslední latence",
|
||||
"Latest Status": "Poslední stav",
|
||||
"Latest Status": "Naposledy zjištěný stav",
|
||||
"Light": "Světlý",
|
||||
"Live Status": "Aktuální stav",
|
||||
"Loading your preferences...": "Načítám vaše nastavení...",
|
||||
"Loading your preferences...": "Načítání nastavení...",
|
||||
"maintenance": "údržba",
|
||||
"Maintenance": "Údržba",
|
||||
"MAINTENANCE": "ÚDRŽBA",
|
||||
"maintenance": "Údržba",
|
||||
"Maintenance Updates": "Aktualizace údržby",
|
||||
"Maintenances": "Údrždy",
|
||||
"Maintenances": "Údržby",
|
||||
"Major System Outage": "Závažný výpadek systému",
|
||||
"Manage Site": "Spravovat stránku",
|
||||
"Manage your notification preferences.": "Spravujte svá nastavení oznámení.",
|
||||
"Manage your notification preferences.": "Spravujte nastavení oznámení",
|
||||
"Max Latency": "Max. latence",
|
||||
"maximum": "maximální",
|
||||
"Maximum Latency": "Maximální latence",
|
||||
@@ -82,33 +82,34 @@
|
||||
"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.",
|
||||
"Network error. Please try again.": "Chyba sítě. Zkuste to znovu",
|
||||
"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 latency data available for this day": "Pro tento den nejsou dostupná data latence",
|
||||
"No maintenances for this day": "Pro tento den není naplánovaná žádná údržba",
|
||||
"No monitors affected": "Žádné zasažené monitory",
|
||||
"No monitors available.": "Žádné dostupné monitory.",
|
||||
"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žba",
|
||||
"No Updates": "Žádné aktualizace",
|
||||
"No updates yet": "Zatím bez aktualizací",
|
||||
"No updates yet": "Zatím žádné aktualizace",
|
||||
"NO_DATA": "Žádná data",
|
||||
"Notifications": "Oznámení",
|
||||
"One-time": "Jednorázově",
|
||||
"Ongoing": "Probíhající",
|
||||
"ONGOING": "PROBÍHAJÍCÍ",
|
||||
"Ongoing Maintenances": "Probíhající údržby",
|
||||
"Operational": "V provozu",
|
||||
"Partial Degraded Performance": "Částečně zhoršený výkon",
|
||||
"Partial Degraded Performance": "Částečně omezený provoz",
|
||||
"Partial System Outage": "Částečný výpadek systému",
|
||||
"Past": "Minulé",
|
||||
"Per-Minute Status": "Stav po minutách",
|
||||
"Pinging": "Pingování",
|
||||
"Please enter a valid email address": "Zadejte platnou e-mailovou adresu",
|
||||
"Please enter the 6-digit verification code": "Zadejte prosím 6místný ověřovací kód",
|
||||
"Please enter the 6-digit verification code": "Zadejte 6místný ověřovací kód",
|
||||
"Read less": "Zobrazit méně",
|
||||
"Read more": "Zobrazit více",
|
||||
"READY": "PŘIPRAVENO",
|
||||
@@ -120,20 +121,20 @@
|
||||
"Scheduled Windows": "Naplánované úlohy",
|
||||
"Script": "Skript",
|
||||
"Select Language": "Vyberte jazyk",
|
||||
"Select latency metric to display": "Vyberte metriku latence k zobrazení",
|
||||
"Select latency metric to display": "Vyberte metriku latence",
|
||||
"Select Range": "Vyberte rozsah",
|
||||
"Sending...": "Odesílání...",
|
||||
"Standard": "Standardní",
|
||||
"Start Time": "Začátek",
|
||||
"Status": "Stav",
|
||||
"Status Badge": "Odznak stavu",
|
||||
"Status Embed": "Status pro vložení",
|
||||
"Status Embed": "Stav pro vložení",
|
||||
"Status history and latency trend": "Historie stavu a trend latence",
|
||||
"Subscribe": "Odebírat",
|
||||
"Subscribe": "Přihlásit se k odběru",
|
||||
"Subscribe to Updates": "Odebírat aktualizace",
|
||||
"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.",
|
||||
"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.": "V tuto chvíli neprobíhají žádné incidenty ani údržba",
|
||||
"Timeline": "Časová osa",
|
||||
"Total Incidents": "Celkem incidentů",
|
||||
"Total Maintenances": "Celkem údržeb",
|
||||
@@ -151,6 +152,6 @@
|
||||
"Verification failed": "Ověření se nezdařilo",
|
||||
"Verify": "Ověřit",
|
||||
"Verifying": "Ověřování",
|
||||
"We sent a 6-digit code to": "Poslali jsme 6místný kód na"
|
||||
"We sent a 6-digit code to": "Odeslali jsme 6místný kód na"
|
||||
}
|
||||
}
|
||||
|
||||
+13
-11
@@ -18,9 +18,11 @@
|
||||
"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",
|
||||
@@ -37,13 +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": "IDENTIFIED",
|
||||
"IDENTIFIED": "IDENTIFIZIERT",
|
||||
"iFrame": "iFrame",
|
||||
"Impact": "Auswirkungen",
|
||||
"incident": "Vorfall",
|
||||
"Incident Updates": "Vorfallaktualisierungen",
|
||||
"Incidents": "Vorfälle",
|
||||
"Included Monitors": "Enthaltene Monitore",
|
||||
"incident": "Vorfall",
|
||||
"Included Monitors (%count)": "Enthaltene Monitore (%count)",
|
||||
"INVESTIGATING": "WIRD UNTERSUCHT",
|
||||
"Last Updated": "Zuletzt aktualisiert",
|
||||
"Latency": "Latenz",
|
||||
"Latency Embed": "Latenz-Einbettung",
|
||||
@@ -54,9 +57,10 @@
|
||||
"Light": "Hell",
|
||||
"Live Status": "Live-Status",
|
||||
"Loading your preferences...": "Deine Einstellungen werden geladen...",
|
||||
"maintenance": "Wartung",
|
||||
"MAINTENANCE": "WARTUNG",
|
||||
"Maintenance Updates": "Wartungsaktualisierungen",
|
||||
"Maintenances": "Wartungen",
|
||||
"maintenance": "Wartung",
|
||||
"Major System Outage": "Schwerwiegender Systemausfall",
|
||||
"Manage Site": "Seite verwalten",
|
||||
"Manage your notification preferences.": "Verwalten Sie Ihre Benachrichtigungseinstellungen.",
|
||||
@@ -65,7 +69,7 @@
|
||||
"Min Latency": "Min. Latenz",
|
||||
"Minimum Latency": "Min. Latenz",
|
||||
"Minute-by-minute status data for this day": "Minutenweise Statusdaten für diesen Tag",
|
||||
"MONITORING": "MONITORING",
|
||||
"MONITORING": "WIRD ÜBERWACHT",
|
||||
"Network error. Please try again.": "Netzwerkfehler. ",
|
||||
"No Events in %currentMonth": "Keine Ereignisse in %currentMonth",
|
||||
"No events to show": "Keine Ereignisse zum Anzeigen",
|
||||
@@ -95,7 +99,7 @@
|
||||
"Read more": "Mehr lesen",
|
||||
"READY": "BEREIT",
|
||||
"Recurring": "Wiederkehrend",
|
||||
"RESOLVED": "RESOLVED",
|
||||
"RESOLVED": "BEHOBEN",
|
||||
"SCHEDULED": "GEPLANT",
|
||||
"Scheduled Events (%count)": "Geplante Ereignisse (%count)",
|
||||
"Script": "Skript",
|
||||
@@ -117,7 +121,9 @@
|
||||
"Total Maintenances": "Gesamtwartungen",
|
||||
"Under Maintenance": "Unter Wartung",
|
||||
"Unknown impact": "Unbekannte Auswirkung",
|
||||
"UP": "AKTIV",
|
||||
"Upcoming": "Demnächst",
|
||||
"Update Incident": "Vorfall aktualisieren",
|
||||
"Update Maintenance": "Wartung aktualisieren",
|
||||
"Updates": "Aktualisierungen",
|
||||
"Updates (%count)": "Aktualisierungen (%count)",
|
||||
@@ -126,10 +132,6 @@
|
||||
"Verification failed": "Die Überprüfung ist fehlgeschlagen",
|
||||
"Verify": "Verifizieren",
|
||||
"Verifying": "Verifizieren",
|
||||
"We sent a 6-digit code to": "Wir haben einen 6-stelligen Code an gesendet",
|
||||
"INVESTIGATING": "WIRD UNTERSUCHT",
|
||||
"IDENTIFIED": "IDENTIFIZIERT",
|
||||
"MONITORING": "WIRD ÜBERWACHT",
|
||||
"RESOLVED": "BEHOBEN"
|
||||
"We sent a 6-digit code to": "Wir haben einen 6-stelligen Code an gesendet"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,9 +18,11 @@
|
||||
"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",
|
||||
@@ -40,10 +42,10 @@
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"iFrame": "iFrame",
|
||||
"Impact": "Indvirkning",
|
||||
"incident": "Hændelse",
|
||||
"Incident Updates": "Hændelsesopdateringer",
|
||||
"Incidents": "Hændelser",
|
||||
"Included Monitors": "Included Monitors",
|
||||
"incident": "Hændelse",
|
||||
"Included Monitors (%count)": "Included Monitors (%count)",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"Last Updated": "Sidst opdateret",
|
||||
"Latency": "Latency",
|
||||
@@ -55,9 +57,10 @@
|
||||
"Light": "Lys",
|
||||
"Live Status": "Live status",
|
||||
"Loading your preferences...": "Indlæser dine præferencer...",
|
||||
"maintenance": "Vedligeholdelse",
|
||||
"MAINTENANCE": "VEDLIGEHOLDELSE",
|
||||
"Maintenance Updates": "Vedligeholdelsesopdateringer",
|
||||
"Maintenances": "Vedligeholdelse",
|
||||
"maintenance": "Vedligeholdelse",
|
||||
"Major System Outage": "Større systemnedbrud",
|
||||
"Manage Site": "Administrer side",
|
||||
"Manage your notification preferences.": "Administrer dine meddelelsespræferencer.",
|
||||
@@ -118,7 +121,9 @@
|
||||
"Total Maintenances": "Samlet vedligeholdelse",
|
||||
"Under Maintenance": "Under Vedligeholdelse",
|
||||
"Unknown impact": "Ukendt påvirkning",
|
||||
"UP": "OPPE",
|
||||
"Upcoming": "Kommende",
|
||||
"Update Incident": "Opdater hændelse",
|
||||
"Update Maintenance": "Opdater vedligeholdelse",
|
||||
"Updates": "Opdateringer",
|
||||
"Updates (%count)": "Opdateringer (%count)",
|
||||
|
||||
@@ -18,9 +18,11 @@
|
||||
"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",
|
||||
@@ -40,10 +42,10 @@
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"iFrame": "iFrame",
|
||||
"Impact": "Impact",
|
||||
"incident": "Incident",
|
||||
"Incident Updates": "Incident Updates",
|
||||
"Incidents": "Incidents",
|
||||
"Included Monitors": "Included Monitors",
|
||||
"incident": "Incident",
|
||||
"Included Monitors (%count)": "Included Monitors (%count)",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"Last Updated": "Last Updated",
|
||||
"Latency": "Latency",
|
||||
@@ -55,9 +57,10 @@
|
||||
"Light": "Light",
|
||||
"Live Status": "Live Status",
|
||||
"Loading your preferences...": "Loading your preferences...",
|
||||
"maintenance": "Maintenance",
|
||||
"MAINTENANCE": "MAINTENANCE",
|
||||
"Maintenance Updates": "Maintenance Updates",
|
||||
"Maintenances": "Maintenances",
|
||||
"maintenance": "Maintenance",
|
||||
"Major System Outage": "Major System Outage",
|
||||
"Manage Site": "Manage Site",
|
||||
"Manage your notification preferences.": "Manage your notification preferences.",
|
||||
@@ -118,7 +121,9 @@
|
||||
"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)",
|
||||
|
||||
@@ -18,9 +18,11 @@
|
||||
"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",
|
||||
@@ -40,10 +42,10 @@
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"iFrame": "marco flotante",
|
||||
"Impact": "Impacto",
|
||||
"incident": "Incidente",
|
||||
"Incident Updates": "Actualizaciones de incidentes",
|
||||
"Incidents": "Incidentes",
|
||||
"Included Monitors": "Included Monitors",
|
||||
"incident": "Incidente",
|
||||
"Included Monitors (%count)": "Included Monitors (%count)",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"Last Updated": "Última actualización",
|
||||
"Latency": "Estado latente",
|
||||
@@ -55,9 +57,10 @@
|
||||
"Light": "Luz",
|
||||
"Live Status": "Estado en vivo",
|
||||
"Loading your preferences...": "Cargando tus preferencias...",
|
||||
"maintenance": "Mantenimiento",
|
||||
"MAINTENANCE": "MANTENIMIENTO",
|
||||
"Maintenance Updates": "Actualizaciones de mantenimiento",
|
||||
"Maintenances": "Mantenimientos",
|
||||
"maintenance": "Mantenimiento",
|
||||
"Major System Outage": "Interrupción importante del sistema",
|
||||
"Manage Site": "Gestionar sitio",
|
||||
"Manage your notification preferences.": "Administre sus preferencias de notificación.",
|
||||
@@ -118,7 +121,9 @@
|
||||
"Total Maintenances": "Mantenimientos totales",
|
||||
"Under Maintenance": "En mantenimiento",
|
||||
"Unknown impact": "Impacto desconocido",
|
||||
"UP": "ACTIVO",
|
||||
"Upcoming": "Próximo",
|
||||
"Update Incident": "Actualizar incidente",
|
||||
"Update Maintenance": "Actualizar mantenimiento",
|
||||
"Updates": "Actualizaciones",
|
||||
"Updates (%count)": "Actualizaciones (%count)",
|
||||
|
||||
@@ -18,9 +18,11 @@
|
||||
"Day Uptime": "تایم روز",
|
||||
"Days": "روزها",
|
||||
"Degraded": "کاهش یافته",
|
||||
"DEGRADED": "کاهش یافته",
|
||||
"Degraded Performance": "عملکرد کاهشیافته",
|
||||
"Didn't receive the code? Resend": "کد را دریافت نکردید؟ ",
|
||||
"Down": "از کار افتاده",
|
||||
"DOWN": "قطع",
|
||||
"Duration": "مدت",
|
||||
"Email address": "آدرس ایمیل",
|
||||
"Embed Monitor": "تعبیه مانیتور",
|
||||
@@ -40,10 +42,10 @@
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"iFrame": "آی فریم",
|
||||
"Impact": "تاثیر",
|
||||
"incident": "رخداد",
|
||||
"Incident Updates": "به روز رسانی حادثه",
|
||||
"Incidents": "حوادث",
|
||||
"Included Monitors": "Included Monitors",
|
||||
"incident": "رخداد",
|
||||
"Included Monitors (%count)": "Included Monitors (%count)",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"Last Updated": "آخرین به روز رسانی",
|
||||
"Latency": "تأخیر",
|
||||
@@ -55,9 +57,10 @@
|
||||
"Light": "نور",
|
||||
"Live Status": "وضعیت زنده",
|
||||
"Loading your preferences...": "در حال بارگیری تنظیمات برگزیده شما...",
|
||||
"maintenance": "نگهداری",
|
||||
"MAINTENANCE": "تعمیر و نگهداری",
|
||||
"Maintenance Updates": "به روز رسانی های تعمیر و نگهداری",
|
||||
"Maintenances": "تعمیر و نگهداری",
|
||||
"maintenance": "نگهداری",
|
||||
"Major System Outage": "قطعی عمده سیستم",
|
||||
"Manage Site": "مدیریت سایت",
|
||||
"Manage your notification preferences.": "تنظیمات برگزیده اعلان خود را مدیریت کنید.",
|
||||
@@ -118,7 +121,9 @@
|
||||
"Total Maintenances": "کل تعمیر و نگهداری",
|
||||
"Under Maintenance": "تحت تعمیر و نگهداری",
|
||||
"Unknown impact": "تاثیر نامعلوم",
|
||||
"UP": "فعال",
|
||||
"Upcoming": "آینده",
|
||||
"Update Incident": "بهروزرسانی رویداد",
|
||||
"Update Maintenance": "بهروزرسانی نگهداری",
|
||||
"Updates": "بهروزرسانیها",
|
||||
"Updates (%count)": "بهروزرسانیها (%count)",
|
||||
|
||||
@@ -18,9 +18,11 @@
|
||||
"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",
|
||||
@@ -40,10 +42,10 @@
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"iFrame": "iFrame",
|
||||
"Impact": "Impact",
|
||||
"incident": "Incident",
|
||||
"Incident Updates": "Mises à jour des incidents",
|
||||
"Incidents": "Incidents",
|
||||
"Included Monitors": "Included Monitors",
|
||||
"incident": "Incident",
|
||||
"Included Monitors (%count)": "Included Monitors (%count)",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"Last Updated": "Dernière mise à jour",
|
||||
"Latency": "Latence",
|
||||
@@ -55,9 +57,10 @@
|
||||
"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",
|
||||
"maintenance": "Maintenance",
|
||||
"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.",
|
||||
@@ -118,7 +121,9 @@
|
||||
"Total Maintenances": "Entretiens totaux",
|
||||
"Under Maintenance": "En maintenance",
|
||||
"Unknown impact": "Impact inconnu",
|
||||
"UP": "OPÉRATIONNEL",
|
||||
"Upcoming": "Prochain",
|
||||
"Update Incident": "Mettre à jour l'incident",
|
||||
"Update Maintenance": "Mettre à jour la maintenance",
|
||||
"Updates": "Mises à jour",
|
||||
"Updates (%count)": "Mises à jour (%count)",
|
||||
|
||||
@@ -18,9 +18,11 @@
|
||||
"Day Uptime": "दिन का अपटाइम",
|
||||
"Days": "दिन",
|
||||
"Degraded": "अवनत",
|
||||
"DEGRADED": "अवक्रमित",
|
||||
"Degraded Performance": "प्रदर्शन में गिरावट",
|
||||
"Didn't receive the code? Resend": "कोड नहीं मिला? फिर से भेजें",
|
||||
"Down": "बंद",
|
||||
"DOWN": "बंद",
|
||||
"Duration": "अवधि",
|
||||
"Email address": "ईमेल पता",
|
||||
"Embed Monitor": "मॉनिटर एम्बेड करें",
|
||||
@@ -40,10 +42,10 @@
|
||||
"IDENTIFIED": "पहचाना गया",
|
||||
"iFrame": "iFrame",
|
||||
"Impact": "प्रभाव",
|
||||
"incident": "घटना",
|
||||
"Incident Updates": "घटना अपडेट",
|
||||
"Incidents": "घटनाएँ",
|
||||
"Included Monitors": "शामिल मॉनिटर",
|
||||
"incident": "घटना",
|
||||
"Included Monitors (%count)": "शामिल मॉनिटर (%count)",
|
||||
"INVESTIGATING": "जाँच कर रहे हैं",
|
||||
"Last Updated": "अंतिम अपडेट",
|
||||
"Latency": "लेटेंसी",
|
||||
@@ -55,9 +57,10 @@
|
||||
"Light": "लाइट",
|
||||
"Live Status": "लाइव स्थिति",
|
||||
"Loading your preferences...": "आपकी प्राथमिकताएँ लोड हो रही हैं...",
|
||||
"maintenance": "रखरखाव",
|
||||
"MAINTENANCE": "रखरखाव",
|
||||
"Maintenance Updates": "रखरखाव अपडेट",
|
||||
"Maintenances": "रखरखाव",
|
||||
"maintenance": "रखरखाव",
|
||||
"Major System Outage": "सिस्टम का बड़ा आउटेज",
|
||||
"Manage Site": "साइट प्रबंधित करें",
|
||||
"Manage your notification preferences.": "अपनी सूचना प्राथमिकताएँ प्रबंधित करें।",
|
||||
@@ -118,7 +121,9 @@
|
||||
"Total Maintenances": "कुल रखरखाव",
|
||||
"Under Maintenance": "रखरखाव जारी है",
|
||||
"Unknown impact": "अज्ञात प्रभाव",
|
||||
"UP": "चालू",
|
||||
"Upcoming": "आने वाला",
|
||||
"Update Incident": "घटना अपडेट करें",
|
||||
"Update Maintenance": "रखरखाव अपडेट करें",
|
||||
"Updates": "अपडेट्स",
|
||||
"Updates (%count)": "अपडेट (%count)",
|
||||
|
||||
@@ -18,9 +18,11 @@
|
||||
"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",
|
||||
@@ -40,10 +42,10 @@
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"iFrame": "iFrame",
|
||||
"Impact": "Impatto",
|
||||
"incident": "Incidente",
|
||||
"Incident Updates": "Aggiornamenti sugli incidenti",
|
||||
"Incidents": "Incidenti",
|
||||
"Included Monitors": "Included Monitors",
|
||||
"incident": "Incidente",
|
||||
"Included Monitors (%count)": "Included Monitors (%count)",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"Last Updated": "Ultimo aggiornamento",
|
||||
"Latency": "Latenza",
|
||||
@@ -55,9 +57,10 @@
|
||||
"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",
|
||||
"maintenance": "Manutenzione",
|
||||
"Major System Outage": "Interruzione grave del sistema",
|
||||
"Manage Site": "Gestisci sito",
|
||||
"Manage your notification preferences.": "Gestisci le tue preferenze di notifica.",
|
||||
@@ -118,7 +121,9 @@
|
||||
"Total Maintenances": "Manutenzioni totali",
|
||||
"Under Maintenance": "In manutenzione",
|
||||
"Unknown impact": "Impatto sconosciuto",
|
||||
"UP": "ATTIVO",
|
||||
"Upcoming": "Prossimamente",
|
||||
"Update Incident": "Aggiorna incidente",
|
||||
"Update Maintenance": "Aggiorna manutenzione",
|
||||
"Updates": "Aggiornamenti",
|
||||
"Updates (%count)": "Aggiornamenti (%count)",
|
||||
|
||||
@@ -18,9 +18,11 @@
|
||||
"Day Uptime": "一日の稼働時間",
|
||||
"Days": "日数",
|
||||
"Degraded": "低下",
|
||||
"DEGRADED": "劣化",
|
||||
"Degraded Performance": "パフォーマンスの低下",
|
||||
"Didn't receive the code? Resend": "コードを受け取っていませんか?",
|
||||
"Down": "停止",
|
||||
"DOWN": "障害",
|
||||
"Duration": "間隔",
|
||||
"Email address": "電子メールアドレス",
|
||||
"Embed Monitor": "埋め込みモニター",
|
||||
@@ -40,10 +42,10 @@
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"iFrame": "iFrame",
|
||||
"Impact": "インパクト",
|
||||
"incident": "インシデント",
|
||||
"Incident Updates": "インシデントの最新情報",
|
||||
"Incidents": "事件",
|
||||
"Included Monitors": "Included Monitors",
|
||||
"incident": "インシデント",
|
||||
"Included Monitors (%count)": "Included Monitors (%count)",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"Last Updated": "最終更新日",
|
||||
"Latency": "レイテンシー",
|
||||
@@ -55,9 +57,10 @@
|
||||
"Light": "ライト",
|
||||
"Live Status": "ライブステータス",
|
||||
"Loading your preferences...": "設定を読み込んでいます...",
|
||||
"maintenance": "メンテナンス",
|
||||
"MAINTENANCE": "メンテナンス",
|
||||
"Maintenance Updates": "メンテナンスアップデート",
|
||||
"Maintenances": "メンテナンス",
|
||||
"maintenance": "メンテナンス",
|
||||
"Major System Outage": "大規模なシステム障害",
|
||||
"Manage Site": "サイト管理",
|
||||
"Manage your notification preferences.": "通知設定を管理します。",
|
||||
@@ -118,7 +121,9 @@
|
||||
"Total Maintenances": "トータルメンテナンス",
|
||||
"Under Maintenance": "メンテナンス中",
|
||||
"Unknown impact": "未知の影響",
|
||||
"UP": "正常",
|
||||
"Upcoming": "今後の予定",
|
||||
"Update Incident": "インシデントを更新",
|
||||
"Update Maintenance": "メンテナンスを更新",
|
||||
"Updates": "更新",
|
||||
"Updates (%count)": "アップデート (%count)",
|
||||
|
||||
@@ -18,9 +18,11 @@
|
||||
"Day Uptime": "일일 가동 시간",
|
||||
"Days": "날",
|
||||
"Degraded": "저하",
|
||||
"DEGRADED": "성능 저하",
|
||||
"Degraded Performance": "성능 저하",
|
||||
"Didn't receive the code? Resend": "코드를 받지 못하셨나요? 재전송",
|
||||
"Down": "중단",
|
||||
"DOWN": "장애",
|
||||
"Duration": "지속",
|
||||
"Email address": "이메일 주소",
|
||||
"Embed Monitor": "모니터 내장",
|
||||
@@ -40,10 +42,10 @@
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"iFrame": "아이프레임",
|
||||
"Impact": "영향",
|
||||
"incident": "인시던트",
|
||||
"Incident Updates": "사고 업데이트",
|
||||
"Incidents": "사건",
|
||||
"Included Monitors": "Included Monitors",
|
||||
"incident": "인시던트",
|
||||
"Included Monitors (%count)": "Included Monitors (%count)",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"Last Updated": "마지막 업데이트",
|
||||
"Latency": "숨어 있음",
|
||||
@@ -55,9 +57,10 @@
|
||||
"Light": "빛",
|
||||
"Live Status": "실시간 현황",
|
||||
"Loading your preferences...": "환경설정 로드 중...",
|
||||
"maintenance": "유지보수",
|
||||
"MAINTENANCE": "점검 중",
|
||||
"Maintenance Updates": "유지보수 업데이트",
|
||||
"Maintenances": "유지보수",
|
||||
"maintenance": "유지보수",
|
||||
"Major System Outage": "대규모 시스템 장애",
|
||||
"Manage Site": "사이트 관리",
|
||||
"Manage your notification preferences.": "알림 기본 설정을 관리하세요.",
|
||||
@@ -118,7 +121,9 @@
|
||||
"Total Maintenances": "총 유지보수",
|
||||
"Under Maintenance": "유지보수 중",
|
||||
"Unknown impact": "알 수 없는 영향",
|
||||
"UP": "정상",
|
||||
"Upcoming": "예정",
|
||||
"Update Incident": "인시던트 업데이트",
|
||||
"Update Maintenance": "유지보수 업데이트",
|
||||
"Updates": "업데이트",
|
||||
"Updates (%count)": "업데이트(%count)",
|
||||
|
||||
@@ -18,9 +18,11 @@
|
||||
"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",
|
||||
@@ -40,10 +42,10 @@
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"iFrame": "iFrame",
|
||||
"Impact": "Påvirkning",
|
||||
"incident": "Hendelse",
|
||||
"Incident Updates": "Hendelsesoppdateringer",
|
||||
"Incidents": "Hendelser",
|
||||
"Included Monitors": "Included Monitors",
|
||||
"incident": "Hendelse",
|
||||
"Included Monitors (%count)": "Included Monitors (%count)",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"Last Updated": "Sist oppdatert",
|
||||
"Latency": "Latens",
|
||||
@@ -55,9 +57,10 @@
|
||||
"Light": "Lys",
|
||||
"Live Status": "Live-status",
|
||||
"Loading your preferences...": "Laster inn innstillingene dine ...",
|
||||
"maintenance": "Vedlikehold",
|
||||
"MAINTENANCE": "VEDLIKEHOLD",
|
||||
"Maintenance Updates": "Vedlikeholdsoppdateringer",
|
||||
"Maintenances": "Vedlikehold",
|
||||
"maintenance": "Vedlikehold",
|
||||
"Major System Outage": "Større systemutfall",
|
||||
"Manage Site": "Administrer nettsted",
|
||||
"Manage your notification preferences.": "Administrer varslingspreferansene dine.",
|
||||
@@ -118,7 +121,9 @@
|
||||
"Total Maintenances": "Totalt vedlikehold",
|
||||
"Under Maintenance": "Under Vedlikehold",
|
||||
"Unknown impact": "Ukjent påvirkning",
|
||||
"UP": "OPPE",
|
||||
"Upcoming": "Kommende",
|
||||
"Update Incident": "Oppdater hendelse",
|
||||
"Update Maintenance": "Oppdater vedlikehold",
|
||||
"Updates": "Oppdateringer",
|
||||
"Updates (%count)": "Oppdateringer (%count)",
|
||||
|
||||
@@ -18,9 +18,11 @@
|
||||
"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",
|
||||
@@ -40,10 +42,10 @@
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"iFrame": "iFrame",
|
||||
"Impact": "Invloed",
|
||||
"incident": "Incident",
|
||||
"Incident Updates": "Incidentupdates",
|
||||
"Incidents": "Incidenten",
|
||||
"Included Monitors": "Included Monitors",
|
||||
"incident": "Incident",
|
||||
"Included Monitors (%count)": "Included Monitors (%count)",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"Last Updated": "Laatst bijgewerkt",
|
||||
"Latency": "Latentie",
|
||||
@@ -55,9 +57,10 @@
|
||||
"Light": "Licht",
|
||||
"Live Status": "Live-status",
|
||||
"Loading your preferences...": "Uw voorkeuren laden...",
|
||||
"maintenance": "Onderhoud",
|
||||
"MAINTENANCE": "ONDERHOUD",
|
||||
"Maintenance Updates": "Onderhoudsupdates",
|
||||
"Maintenances": "Onderhouden",
|
||||
"maintenance": "Onderhoud",
|
||||
"Major System Outage": "Grote systeemstoring",
|
||||
"Manage Site": "Site beheren",
|
||||
"Manage your notification preferences.": "Beheer uw meldingsvoorkeuren.",
|
||||
@@ -118,7 +121,9 @@
|
||||
"Total Maintenances": "Totaal onderhoud",
|
||||
"Under Maintenance": "Onder Onderhoud",
|
||||
"Unknown impact": "Onbekende impact",
|
||||
"UP": "ACTIEF",
|
||||
"Upcoming": "Aankomend",
|
||||
"Update Incident": "Incident bijwerken",
|
||||
"Update Maintenance": "Onderhoud bijwerken",
|
||||
"Updates": "Actualisaties",
|
||||
"Updates (%count)": "Updates (%count)",
|
||||
|
||||
@@ -18,9 +18,11 @@
|
||||
"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",
|
||||
@@ -40,10 +42,10 @@
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"iFrame": "iFramka",
|
||||
"Impact": "Uderzenie",
|
||||
"incident": "Incydent",
|
||||
"Incident Updates": "Aktualizacje incydentów",
|
||||
"Incidents": "Incydenty",
|
||||
"Included Monitors": "Included Monitors",
|
||||
"incident": "Incydent",
|
||||
"Included Monitors (%count)": "Included Monitors (%count)",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"Last Updated": "Ostatnia aktualizacja",
|
||||
"Latency": "Utajenie",
|
||||
@@ -55,9 +57,10 @@
|
||||
"Light": "Światło",
|
||||
"Live Status": "Stan na żywo",
|
||||
"Loading your preferences...": "Ładowanie Twoich preferencji...",
|
||||
"maintenance": "Konserwacja",
|
||||
"MAINTENANCE": "KONSERWACJA",
|
||||
"Maintenance Updates": "Aktualizacje konserwacyjne",
|
||||
"Maintenances": "Konserwacje",
|
||||
"maintenance": "Konserwacja",
|
||||
"Major System Outage": "Poważna awaria systemu",
|
||||
"Manage Site": "Zarządzaj stroną",
|
||||
"Manage your notification preferences.": "Zarządzaj preferencjami powiadomień.",
|
||||
@@ -118,7 +121,9 @@
|
||||
"Total Maintenances": "Całkowita konserwacja",
|
||||
"Under Maintenance": "W ramach konserwacji",
|
||||
"Unknown impact": "Nieznany wpływ",
|
||||
"UP": "DZIAŁA",
|
||||
"Upcoming": "Nadchodzące",
|
||||
"Update Incident": "Aktualizuj incydent",
|
||||
"Update Maintenance": "Aktualizuj konserwację",
|
||||
"Updates": "Aktualizacje",
|
||||
"Updates (%count)": "Aktualizacje (%count)",
|
||||
|
||||
@@ -18,9 +18,11 @@
|
||||
"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",
|
||||
@@ -40,10 +42,10 @@
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"iFrame": "iFrame",
|
||||
"Impact": "Impacto",
|
||||
"incident": "Incidente",
|
||||
"Incident Updates": "Atualizações de incidentes",
|
||||
"Incidents": "Incidentes",
|
||||
"Included Monitors": "Included Monitors",
|
||||
"incident": "Incidente",
|
||||
"Included Monitors (%count)": "Included Monitors (%count)",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"Last Updated": "Última atualização",
|
||||
"Latency": "Latência",
|
||||
@@ -55,9 +57,10 @@
|
||||
"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",
|
||||
"maintenance": "Manutenção",
|
||||
"Major System Outage": "Indisponibilidade grave do sistema",
|
||||
"Manage Site": "Gerenciar site",
|
||||
"Manage your notification preferences.": "Gerencie suas preferências de notificação.",
|
||||
@@ -118,7 +121,9 @@
|
||||
"Total Maintenances": "Manutenção total",
|
||||
"Under Maintenance": "Em manutenção",
|
||||
"Unknown impact": "Impacto desconhecido",
|
||||
"UP": "ATIVO",
|
||||
"Upcoming": "Por vir",
|
||||
"Update Incident": "Atualizar incidente",
|
||||
"Update Maintenance": "Atualizar manutenção",
|
||||
"Updates": "Atualizações",
|
||||
"Updates (%count)": "Atualizações (%count)",
|
||||
|
||||
@@ -19,9 +19,11 @@
|
||||
"Day Uptime": "Время работы за день",
|
||||
"Days": "Days",
|
||||
"Degraded": "Деградация",
|
||||
"DEGRADED": "УХУДШЕНИЕ",
|
||||
"Degraded Performance": "Снижение производительности",
|
||||
"Didn't receive the code? Resend": "Не получили код? Отправить повторно",
|
||||
"Down": "Недоступен",
|
||||
"DOWN": "НЕ РАБОТАЕТ",
|
||||
"Duration": "Продолжительность",
|
||||
"Email address": "Адрес электронной почты",
|
||||
"Embed Monitor": "Встроить монитор",
|
||||
@@ -41,10 +43,10 @@
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"iFrame": "iFrame",
|
||||
"Impact": "Влияние",
|
||||
"incident": "Инцидент",
|
||||
"Incident Updates": "Обновления инцидентов",
|
||||
"Incidents": "Инциденты",
|
||||
"Included Monitors": "Included Monitors",
|
||||
"incident": "Инцидент",
|
||||
"Included Monitors (%count)": "Included Monitors (%count)",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"Last Updated": "Последнее обновление",
|
||||
"Latency": "Задержка",
|
||||
@@ -56,9 +58,10 @@
|
||||
"Light": "Светлая",
|
||||
"Live Status": "Статус в реальном времени",
|
||||
"Loading your preferences...": "Загрузка настроек...",
|
||||
"maintenance": "Обслуживание",
|
||||
"MAINTENANCE": "ОБСЛУЖИВАНИЕ",
|
||||
"Maintenance Updates": "Обновления обслуживания",
|
||||
"Maintenances": "Обслуживание",
|
||||
"maintenance": "Обслуживание",
|
||||
"Major System Outage": "Крупный сбой системы",
|
||||
"Manage Site": "Управление сайтом",
|
||||
"Manage your notification preferences.": "Управляйте настройками уведомлений.",
|
||||
@@ -119,7 +122,9 @@
|
||||
"Total Maintenances": "Total Maintenances",
|
||||
"Under Maintenance": "На обслуживании",
|
||||
"Unknown impact": "Неизвестное влияние",
|
||||
"UP": "РАБОТАЕТ",
|
||||
"Upcoming": "Предстоящие",
|
||||
"Update Incident": "Обновить инцидент",
|
||||
"Update Maintenance": "Обновить обслуживание",
|
||||
"Updates": "Обновления",
|
||||
"Updates (%count)": "Обновления (%count)",
|
||||
|
||||
+35
-34
@@ -6,7 +6,7 @@
|
||||
"30 Days": "30 dní",
|
||||
"7 Days": "7 dní",
|
||||
"90 Days": "90 dní",
|
||||
"Affected Monitors (%count)": "Ovládacie monitory (%count)",
|
||||
"Affected Monitors (%count)": "Ovplyvnené monitory (%count)",
|
||||
"All Systems Operational": "Všetky systémy sú v prevádzke",
|
||||
"average": "priemerná",
|
||||
"Average Latency": "Priemerná latencia",
|
||||
@@ -22,58 +22,58 @@
|
||||
"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",
|
||||
"Degraded": "Obmedzený",
|
||||
"DEGRADED": "OBMEDZENÝ",
|
||||
"Degraded Performance": "Znížený výkon",
|
||||
"Didn't receive the code? Resend": "Neprišiel kód? Odoslať znova",
|
||||
"Down": "Nedostupné",
|
||||
"DOWN": "NEDOSTUPNÉ",
|
||||
"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",
|
||||
"Embed this monitor in your website or app": "Vložte tento monitor na web alebo do aplikácie",
|
||||
"End Time": "Koniec",
|
||||
"Enter the verification code sent to your email.": "Zadajte overovací kód zaslaný na váš e-mail.",
|
||||
"Enter the verification code sent to your email.": "Zadajte overovací kód zaslaný na e-mail",
|
||||
"Events": "Udalosti",
|
||||
"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 load status data for this day": "Nepodarilo sa načítať stavové dáta pre tento deň",
|
||||
"Failed to send verification code": "Nepodarilo sa odoslať overovací kód",
|
||||
"Failed to update preference": "Nepodarilo sa aktualizovať nastavenia",
|
||||
"Failed to update preference": "Nepodarilo sa uložiť nastavenie",
|
||||
"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 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": "Dopad",
|
||||
"incident": "incident",
|
||||
"Incident": "Incident",
|
||||
"Incident Updates": "Aktualizácie incidentov",
|
||||
"Incidents": "Incidenty",
|
||||
"Included Monitors": "Monitorov zahrnuté",
|
||||
"incident": "Incident",
|
||||
"INVESTIGATING": "VYŠETROVANIE",
|
||||
"Included Monitors (%count)": "Zahrnuté monitory (%count)",
|
||||
"INVESTIGATING": "PREBIEHA VYŠETROVANIE",
|
||||
"Last Updated": "Naposledy aktualizované",
|
||||
"Latency": "Latencia",
|
||||
"Latency Embed": "Vložená latencia",
|
||||
"Latency Over Time": "Latencia v čase",
|
||||
"Latency Trend": "Trend latencie",
|
||||
"Latest Latency": "Posledná latencia",
|
||||
"Latest Status": "Posledný stav",
|
||||
"Latest Status": "Naposledy zistený stav",
|
||||
"Light": "Svetlý",
|
||||
"Live Status": "Aktuálny stav",
|
||||
"Loading your preferences...": "Načítavam vaše nastavenia...",
|
||||
"Loading your preferences...": "Načítavanie nastavení...",
|
||||
"maintenance": "údržba",
|
||||
"Maintenance": "Údržba",
|
||||
"MAINTENANCE": "ÚDRŽBA",
|
||||
"maintenance": "Údržba",
|
||||
"Maintenance Updates": "Aktualizácie údržby",
|
||||
"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í.",
|
||||
"Manage your notification preferences.": "Spravujte nastavenia upozornení",
|
||||
"Max Latency": "Max. latencia",
|
||||
"maximum": "maximálna",
|
||||
"Maximum Latency": "Maximálna latencia",
|
||||
@@ -82,27 +82,28 @@
|
||||
"Minimum Latency": "Minimálna latencia",
|
||||
"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",
|
||||
"Network error. Please try again.": "Chyba siete. Skúste to znova",
|
||||
"No Events in %currentMonth": "V mesiaci %currentMonth nie sú ž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 latency data available for this day": "Pre tento deň nie sú dostupné 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 monitors affected": "Žiadne ovplyvnené monitory",
|
||||
"No monitors available.": "Žiadne dostupné monitory",
|
||||
"No ongoing maintenances": "Žiadna prebiehajúca údržba",
|
||||
"No past maintenances": "Žiadna minula údržba",
|
||||
"No past maintenances": "Žiadna minulá údržba",
|
||||
"No Status Available": "Stav nie je k dispozícii",
|
||||
"No upcoming maintenances": "Žiadna nadchádzajúca údržba",
|
||||
"No Updates": "Žiadne aktualizácie",
|
||||
"No updates yet": "Zatiaľ bez aktualizácií",
|
||||
"No updates yet": "Zatiaľ žiadne aktualizácie",
|
||||
"NO_DATA": "Žiadne dáta",
|
||||
"Notifications": "Oznámenia",
|
||||
"Notifications": "Upozornenia",
|
||||
"One-time": "Jednorazovo",
|
||||
"Ongoing": "Prebiehajúce",
|
||||
"ONGOING": "PREBIEHAJÚCE",
|
||||
"Ongoing Maintenances": "Prebiehajúce údržby",
|
||||
"Operational": "V prevádzke",
|
||||
"Partial Degraded Performance": "Čiastočne zhoršený výkon",
|
||||
"Partial Degraded Performance": "Čiastočne obmedzený výkon",
|
||||
"Partial System Outage": "Čiastočný výpadok systému",
|
||||
"Past": "Minulé",
|
||||
"Per-Minute Status": "Stav po minútach",
|
||||
@@ -120,20 +121,20 @@
|
||||
"Scheduled Windows": "Naplánované úlohy",
|
||||
"Script": "Skript",
|
||||
"Select Language": "Vyberte jazyk",
|
||||
"Select latency metric to display": "Vyberte metriku latencie na zobrazenie",
|
||||
"Select latency metric to display": "Vyberte metriku latencie",
|
||||
"Select Range": "Vyberte rozsah",
|
||||
"Sending...": "Odosielanie...",
|
||||
"Standard": "Štandardný",
|
||||
"Start Time": "Začiatok",
|
||||
"Status": "Stav",
|
||||
"Status Badge": "Odznak stavu",
|
||||
"Status Embed": "Status na vloženie",
|
||||
"Status Embed": "Stav na vloženie",
|
||||
"Status history and latency trend": "História stavu a trend latencie",
|
||||
"Subscribe": "Odoberať",
|
||||
"Subscribe": "Prihlásiť sa na odber",
|
||||
"Subscribe to Updates": "Odoberať 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.",
|
||||
"There are no incidents or maintenances scheduled for this month.": "Na tento mesiac nie sú naplánované žiadne incidenty ani údržby",
|
||||
"There are no ongoing incidents or maintenance events.": "Momentálne neprebiehajú žiadne incidenty ani údržba",
|
||||
"Timeline": "Časová os",
|
||||
"Total Incidents": "Celkom incidentov",
|
||||
"Total Maintenances": "Celkom údržieb",
|
||||
@@ -151,6 +152,6 @@
|
||||
"Verification failed": "Overenie zlyhalo",
|
||||
"Verify": "Overiť",
|
||||
"Verifying": "Overovanie",
|
||||
"We sent a 6-digit code to": "Poslali sme 6-miestny kód na"
|
||||
"We sent a 6-digit code to": "Odoslali sme 6-miestny kód na"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,9 +18,11 @@
|
||||
"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",
|
||||
@@ -40,10 +42,10 @@
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"iFrame": "iFrame",
|
||||
"Impact": "Darbe",
|
||||
"incident": "Olay",
|
||||
"Incident Updates": "Olay Güncellemeleri",
|
||||
"Incidents": "Olaylar",
|
||||
"Included Monitors": "Included Monitors",
|
||||
"incident": "Olay",
|
||||
"Included Monitors (%count)": "Included Monitors (%count)",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"Last Updated": "Son Güncelleme",
|
||||
"Latency": "Gecikme",
|
||||
@@ -55,9 +57,10 @@
|
||||
"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",
|
||||
"maintenance": "Bakım",
|
||||
"Major System Outage": "Büyük sistem kesintisi",
|
||||
"Manage Site": "Siteyi yönet",
|
||||
"Manage your notification preferences.": "Bildirim tercihlerinizi yönetin.",
|
||||
@@ -118,7 +121,9 @@
|
||||
"Total Maintenances": "Toplam Bakımlar",
|
||||
"Under Maintenance": "Bakımda",
|
||||
"Unknown impact": "Bilinmeyen etki",
|
||||
"UP": "ÇALIŞIYOR",
|
||||
"Upcoming": "Yaklaşan",
|
||||
"Update Incident": "Olayı güncelle",
|
||||
"Update Maintenance": "Bakımı güncelle",
|
||||
"Updates": "Güncellemeler",
|
||||
"Updates (%count)": "Güncellemeler (%count)",
|
||||
|
||||
@@ -18,9 +18,11 @@
|
||||
"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",
|
||||
@@ -40,10 +42,10 @@
|
||||
"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",
|
||||
"incident": "Sự cố",
|
||||
"Included Monitors (%count)": "Included Monitors (%count)",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"Last Updated": "Cập nhật lần cuối",
|
||||
"Latency": "Độ trễ",
|
||||
@@ -55,9 +57,10 @@
|
||||
"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ì",
|
||||
"maintenance": "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.",
|
||||
@@ -118,7 +121,9 @@
|
||||
"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",
|
||||
"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)",
|
||||
|
||||
@@ -18,9 +18,11 @@
|
||||
"Day Uptime": "日间正常运行时间",
|
||||
"Days": "天",
|
||||
"Degraded": "降级",
|
||||
"DEGRADED": "降级",
|
||||
"Degraded Performance": "性能下降",
|
||||
"Didn't receive the code? Resend": "没有收到代码?",
|
||||
"Down": "宕机",
|
||||
"DOWN": "故障",
|
||||
"Duration": "期间",
|
||||
"Email address": "电子邮件",
|
||||
"Embed Monitor": "嵌入监视器",
|
||||
@@ -40,10 +42,10 @@
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"iFrame": "框架",
|
||||
"Impact": "影响",
|
||||
"incident": "事件",
|
||||
"Incident Updates": "事件更新",
|
||||
"Incidents": "事件",
|
||||
"Included Monitors": "Included Monitors",
|
||||
"incident": "事件",
|
||||
"Included Monitors (%count)": "Included Monitors (%count)",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"Last Updated": "最后更新",
|
||||
"Latency": "延迟",
|
||||
@@ -55,9 +57,10 @@
|
||||
"Light": "光",
|
||||
"Live Status": "实时状态",
|
||||
"Loading your preferences...": "正在加载您的偏好设置...",
|
||||
"maintenance": "维护",
|
||||
"MAINTENANCE": "维护中",
|
||||
"Maintenance Updates": "维护更新",
|
||||
"Maintenances": "维护保养",
|
||||
"maintenance": "维护",
|
||||
"Major System Outage": "重大系统故障",
|
||||
"Manage Site": "管理站点",
|
||||
"Manage your notification preferences.": "管理您的通知首选项。",
|
||||
@@ -118,7 +121,9 @@
|
||||
"Total Maintenances": "全面维护",
|
||||
"Under Maintenance": "维护中",
|
||||
"Unknown impact": "未知影响",
|
||||
"UP": "正常",
|
||||
"Upcoming": "即将推出",
|
||||
"Update Incident": "更新事件",
|
||||
"Update Maintenance": "更新维护",
|
||||
"Updates": "更新",
|
||||
"Updates (%count)": "更新 (%count)",
|
||||
|
||||
@@ -17,8 +17,10 @@
|
||||
"Day": "天",
|
||||
"Day Uptime": "當日正常運作時間",
|
||||
"Days": "天",
|
||||
"DEGRADED": "降級",
|
||||
"Degraded Performance": "效能降低",
|
||||
"Didn't receive the code? Resend": "沒有收到驗證碼?重新傳送",
|
||||
"DOWN": "故障",
|
||||
"Duration": "持續時間",
|
||||
"Edit Monitor": "編輯監控",
|
||||
"Email address": "電子郵件地址",
|
||||
@@ -41,7 +43,7 @@
|
||||
"Impact": "影響",
|
||||
"Incident Updates": "事件更新",
|
||||
"Incidents": "事件",
|
||||
"Included Monitors": "包含的監控項目",
|
||||
"Included Monitors (%count)": "包含的監控項目 (%count)",
|
||||
"INVESTIGATING": "調查中",
|
||||
"Last Updated": "最後更新",
|
||||
"Latency": "延遲",
|
||||
@@ -53,6 +55,7 @@
|
||||
"Light": "淺色",
|
||||
"Live Status": "即時狀態",
|
||||
"Loading your preferences...": "正在載入您的偏好設定……",
|
||||
"MAINTENANCE": "維護中",
|
||||
"Maintenance Updates": "維護更新",
|
||||
"Maintenances": "維護作業",
|
||||
"Major System Outage": "重大系統中斷",
|
||||
@@ -114,6 +117,7 @@
|
||||
"Total Maintenances": "維護總數",
|
||||
"Under Maintenance": "維護中",
|
||||
"Unknown impact": "影響不明",
|
||||
"UP": "正常",
|
||||
"Upcoming": "即將進行",
|
||||
"Update Incident": "更新事件",
|
||||
"Update Maintenance": "更新維護",
|
||||
|
||||
@@ -159,9 +159,13 @@ export interface PageDashboardData {
|
||||
pageStatus: { statusSummary: string; statusClass: string };
|
||||
ongoingIncidents: IncidentForMonitorListWithComments[];
|
||||
ongoingMaintenances: MaintenanceEventsMonitorList[];
|
||||
upcomingMaintenances: MaintenanceEventsMonitorList[];
|
||||
monitorTags: string[];
|
||||
monitorGroupMembersByTag: Record<string, string[]>;
|
||||
pageDetails: PageRecordTyped;
|
||||
socialPagePreviewImage?: string;
|
||||
metaPageTitle?: string;
|
||||
metaPageDescription?: string;
|
||||
}
|
||||
|
||||
const BuildPageStatus = (latestData: Array<{ status?: string | null; latency?: number | null }>, nowTs: number) => {
|
||||
@@ -342,19 +346,39 @@ export const GetPageDashboardData = async (
|
||||
updated_at: pageDetails.updated_at,
|
||||
};
|
||||
|
||||
let socialPagePreviewImage: string | undefined = layoutData.socialPreviewImage;
|
||||
let metaPageTitle: string | undefined = layoutData.metaSiteTitle;
|
||||
let metaPageDescription: string | undefined = layoutData.metaSiteDescription;
|
||||
if (!!pageDetails.page_settings_json) {
|
||||
try {
|
||||
const pageSettings = JSON.parse(pageDetails.page_settings_json);
|
||||
if (pageSettings) {
|
||||
socialPagePreviewImage = pageSettings.socialPagePreviewImage || layoutData.socialPreviewImage;
|
||||
metaPageTitle = pageSettings.metaPageTitle || layoutData.metaSiteTitle;
|
||||
metaPageDescription = pageSettings.metaPageDescription || layoutData.metaSiteDescription;
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore JSON parsing errors and fallback to layout data or defaults
|
||||
}
|
||||
}
|
||||
|
||||
if (monitorTags.length === 0) {
|
||||
return {
|
||||
pageStatus: BuildPageStatus([], nowTs),
|
||||
ongoingIncidents: [],
|
||||
ongoingMaintenances: [],
|
||||
upcomingMaintenances: [],
|
||||
monitorTags,
|
||||
monitorGroupMembersByTag: {},
|
||||
pageDetails: pageDetailsTyped,
|
||||
socialPagePreviewImage,
|
||||
metaPageTitle,
|
||||
metaPageDescription,
|
||||
};
|
||||
}
|
||||
const eventSettings = layoutData.eventDisplaySettings;
|
||||
// Fetch all dashboard data in parallel (respecting feature toggles)
|
||||
const [latestData, parsedMonitors, ongoingIncidents, ongoingMaintenances] = await Promise.all([
|
||||
const [latestData, parsedMonitors, ongoingIncidents, ongoingMaintenances, upcomingMaintenances] = await Promise.all([
|
||||
GetLatestMonitoringDataAllActive(monitorTags),
|
||||
GetMonitorsParsed({ tags: monitorTags, status: "ACTIVE", is_hidden: "NO" }),
|
||||
eventSettings.incidents.enabled && eventSettings.incidents.ongoing.show
|
||||
@@ -363,6 +387,13 @@ export const GetPageDashboardData = async (
|
||||
eventSettings.maintenances.enabled && eventSettings.maintenances.ongoing.show
|
||||
? GetOngoingMaintenances(monitorTags, nowTs)
|
||||
: Promise.resolve([] as MaintenanceEventsMonitorList[]),
|
||||
eventSettings.maintenances.enabled && eventSettings.maintenances.upcoming.show
|
||||
? GetUpcomingMaintenanceEventsForMonitorList(
|
||||
monitorTags,
|
||||
eventSettings.maintenances.upcoming.maxCount,
|
||||
eventSettings.maintenances.upcoming.daysInFuture,
|
||||
)
|
||||
: Promise.resolve([] as MaintenanceEventsMonitorList[]),
|
||||
]);
|
||||
|
||||
const pageStatus = BuildPageStatus(latestData, nowTs);
|
||||
@@ -381,8 +412,12 @@ export const GetPageDashboardData = async (
|
||||
pageStatus,
|
||||
ongoingIncidents,
|
||||
ongoingMaintenances,
|
||||
upcomingMaintenances,
|
||||
monitorTags,
|
||||
monitorGroupMembersByTag,
|
||||
pageDetails: pageDetailsTyped,
|
||||
socialPagePreviewImage,
|
||||
metaPageTitle,
|
||||
metaPageDescription,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
@@ -71,6 +71,8 @@ export interface LayoutServerData {
|
||||
customCSS?: string;
|
||||
globalPageVisibilitySettings: GlobalPageVisibilitySettings;
|
||||
dateAndTimeFormat: SiteDateTimeFormat;
|
||||
metaSiteTitle?: string;
|
||||
metaSiteDescription?: string;
|
||||
}
|
||||
|
||||
export async function GetLayoutServerData(cookies: Cookies, request: Request): Promise<LayoutServerData> {
|
||||
@@ -139,5 +141,7 @@ export async function GetLayoutServerData(cookies: Cookies, request: Request): P
|
||||
customCSS: siteData.customCSS,
|
||||
globalPageVisibilitySettings: siteData.globalPageVisibilitySettings || seedSiteData.globalPageVisibilitySettings,
|
||||
dateAndTimeFormat: siteData.dateAndTimeFormat || seedSiteData.dateAndTimeFormat,
|
||||
metaSiteTitle: siteData.metaSiteTitle,
|
||||
metaSiteDescription: siteData.metaSiteDescription,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import { maintenanceToVariables, siteDataToVariables } from "../notification/not
|
||||
import { GetAllSiteData } from "./controller.js";
|
||||
import subscriberQueue from "../queues/subscriberQueue.js";
|
||||
import GC from "../../global-constants";
|
||||
import seedSiteData from "../db/seedSiteData.js";
|
||||
|
||||
// ============ Input Interfaces ============
|
||||
|
||||
@@ -78,6 +79,7 @@ export interface MaintenanceWithEvents extends MaintenanceWithMonitors {
|
||||
export function determineEventStatus(
|
||||
eventStartTimestamp: number,
|
||||
eventEndTimestamp: number,
|
||||
reminderBufferSeconds: number = 3600,
|
||||
): "SCHEDULED" | "READY" | "ONGOING" | "COMPLETED" | "CANCELLED" {
|
||||
const nowTimestamp = Math.floor(Date.now() / 1000);
|
||||
|
||||
@@ -87,38 +89,90 @@ export function determineEventStatus(
|
||||
if (nowTimestamp >= eventStartTimestamp) {
|
||||
return "ONGOING";
|
||||
}
|
||||
// 60 minutes = 3600 seconds
|
||||
if (eventStartTimestamp - nowTimestamp <= 3600) {
|
||||
if (eventStartTimestamp - nowTimestamp <= reminderBufferSeconds) {
|
||||
return "READY";
|
||||
}
|
||||
return "SCHEDULED";
|
||||
}
|
||||
|
||||
// ============ Helper to create a maintenance event with notification ============
|
||||
|
||||
export const CreateMaintenanceEventWithNotification = async (
|
||||
maintenance_id: number,
|
||||
start_date_time: number,
|
||||
end_date_time: number,
|
||||
title: string,
|
||||
description: string | null,
|
||||
): Promise<MaintenanceEventRecord> => {
|
||||
const siteData = await GetAllSiteData();
|
||||
const notificationSettings =
|
||||
siteData.globalMaintenanceNotificationSettings || seedSiteData.globalMaintenanceNotificationSettings;
|
||||
const reminderBufferSeconds = notificationSettings.reminder_buffer_hours * 3600;
|
||||
|
||||
const event = await db.createMaintenanceEvent({
|
||||
maintenance_id,
|
||||
start_date_time,
|
||||
end_date_time,
|
||||
status: determineEventStatus(start_date_time, end_date_time, reminderBufferSeconds),
|
||||
});
|
||||
|
||||
try {
|
||||
if (notificationSettings.event_types.created) {
|
||||
const siteVars = siteDataToVariables(siteData);
|
||||
const siteUrl = siteVars.site_url;
|
||||
const monitors = await db.getMonitorsByMaintenanceId(maintenance_id);
|
||||
const monitorNames = monitors.map((m) => `${m.monitor_name}(${m.monitor_impact})`).join(", ");
|
||||
const eventDetailed: MaintenanceEventRecordDetailed = {
|
||||
id: event.id,
|
||||
maintenance_id,
|
||||
start_date_time,
|
||||
end_date_time,
|
||||
status: event.status as MaintenanceEventRecordDetailed["status"],
|
||||
created_at: new Date(),
|
||||
updated_at: new Date(),
|
||||
title,
|
||||
description,
|
||||
};
|
||||
const update = maintenanceToVariables(
|
||||
eventDetailed,
|
||||
monitorNames,
|
||||
"**has been created**",
|
||||
"created",
|
||||
"Maintenance Created",
|
||||
siteUrl,
|
||||
);
|
||||
await subscriberQueue.push(update);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error sending created notification for maintenance event ${event.id}:`, err);
|
||||
}
|
||||
|
||||
return event;
|
||||
};
|
||||
|
||||
// ============ Helper to generate upcoming events from RRULE ============
|
||||
|
||||
/**
|
||||
* Generate maintenance events for the next N days based on the RRULE
|
||||
* Generate maintenance events based on the RRULE
|
||||
* @param maintenance_id - The maintenance record ID
|
||||
* @param start_date_time - Unix timestamp for the DTSTART
|
||||
* @param rrule - The RRULE string (e.g., FREQ=WEEKLY;BYDAY=SU)
|
||||
* @param duration_seconds - Duration of each maintenance window
|
||||
* @param daysAhead - Number of days to look ahead (default 7)
|
||||
* @param count - Maximum number of events to create (default 1)
|
||||
*/
|
||||
export const GenerateMaintenanceEvents = async (
|
||||
maintenance_id: number,
|
||||
start_date_time: number,
|
||||
rrule: string,
|
||||
duration_seconds: number,
|
||||
daysAhead: number = 7,
|
||||
count: number = 1,
|
||||
): Promise<MaintenanceEventRecord[]> => {
|
||||
const createdEvents: MaintenanceEventRecord[] = [];
|
||||
|
||||
// Convert start timestamp to Date (UTC)
|
||||
const dtstart = new Date(start_date_time * 1000);
|
||||
|
||||
// Define the window to generate events
|
||||
const now = new Date();
|
||||
const windowEnd = addDays(now, daysAhead);
|
||||
|
||||
try {
|
||||
// Build the full RRULE string with DTSTART
|
||||
@@ -127,22 +181,30 @@ export const GenerateMaintenanceEvents = async (
|
||||
// Parse the RRULE
|
||||
const rule = rrulestr(fullRrule);
|
||||
|
||||
// Get occurrences between now and window end
|
||||
// For one-time (COUNT=1), we use dtstart as the reference
|
||||
let occurrences: Date[];
|
||||
// Get occurrences based on count
|
||||
let occurrences: Date[] = [];
|
||||
|
||||
if (rrule.includes("COUNT=1")) {
|
||||
// One-time maintenance: only create event if start_date_time is in the future or within window
|
||||
if (dtstart >= now || (dtstart <= windowEnd && dtstart >= addDays(now, -1))) {
|
||||
// One-time maintenance: only create event if start_date_time is recent or in the future
|
||||
if (dtstart >= now || dtstart >= addDays(now, -1)) {
|
||||
occurrences = [dtstart];
|
||||
} else {
|
||||
occurrences = [];
|
||||
}
|
||||
} else {
|
||||
// Recurring: get all occurrences in the window
|
||||
occurrences = rule.between(now, windowEnd, true);
|
||||
// Recurring: get the next `count` occurrences from now
|
||||
let searchFrom = now;
|
||||
for (let i = 0; i < count; i++) {
|
||||
const next = rule.after(searchFrom, i === 0);
|
||||
if (!next) break;
|
||||
occurrences.push(next);
|
||||
searchFrom = new Date(next.getTime() + 1000);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch maintenance info for notifications
|
||||
const maintenance = await db.getMaintenanceById(maintenance_id);
|
||||
const maintenanceTitle = maintenance?.title || "";
|
||||
const maintenanceDescription = maintenance?.description || null;
|
||||
|
||||
// Create events for each occurrence
|
||||
for (const occurrence of occurrences) {
|
||||
const eventStart = Math.floor(occurrence.getTime() / 1000);
|
||||
@@ -153,12 +215,13 @@ export const GenerateMaintenanceEvents = async (
|
||||
const alreadyExists = existing.some((e) => e.start_date_time === eventStart);
|
||||
|
||||
if (!alreadyExists) {
|
||||
const event = await db.createMaintenanceEvent({
|
||||
const event = await CreateMaintenanceEventWithNotification(
|
||||
maintenance_id,
|
||||
start_date_time: eventStart,
|
||||
end_date_time: eventEnd,
|
||||
status: determineEventStatus(eventStart, eventEnd),
|
||||
});
|
||||
eventStart,
|
||||
eventEnd,
|
||||
maintenanceTitle,
|
||||
maintenanceDescription,
|
||||
);
|
||||
createdEvents.push(event);
|
||||
}
|
||||
}
|
||||
@@ -202,8 +265,8 @@ export const CreateMaintenance = async (data: CreateMaintenanceInput): Promise<{
|
||||
await db.addMonitorsToMaintenanceWithStatus(maintenance.id, data.monitors);
|
||||
}
|
||||
|
||||
// Generate initial events for the next 7 days
|
||||
await GenerateMaintenanceEvents(maintenance.id, data.start_date_time, data.rrule, data.duration_seconds, 7);
|
||||
// Generate initial events
|
||||
await GenerateMaintenanceEvents(maintenance.id, data.start_date_time, data.rrule, data.duration_seconds, 1);
|
||||
|
||||
return {
|
||||
maintenance_id: maintenance.id,
|
||||
@@ -336,7 +399,7 @@ export const UpdateMaintenance = async (id: number, data: UpdateMaintenanceInput
|
||||
}
|
||||
}
|
||||
// Regenerate the event
|
||||
await GenerateMaintenanceEvents(id, updated.start_date_time, updated.rrule, updated.duration_seconds, 7);
|
||||
await GenerateMaintenanceEvents(id, updated.start_date_time, updated.rrule, updated.duration_seconds, 1);
|
||||
} else {
|
||||
// For recurring maintenances: delete future SCHEDULED events and regenerate
|
||||
for (const event of events) {
|
||||
@@ -345,8 +408,8 @@ export const UpdateMaintenance = async (id: number, data: UpdateMaintenanceInput
|
||||
await db.deleteMaintenanceEvent(event.id);
|
||||
}
|
||||
}
|
||||
// Regenerate events for the next 7 days
|
||||
await GenerateMaintenanceEvents(id, updated.start_date_time, updated.rrule, updated.duration_seconds, 7);
|
||||
// Regenerate events
|
||||
await GenerateMaintenanceEvents(id, updated.start_date_time, updated.rrule, updated.duration_seconds, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -377,11 +440,16 @@ export const CreateMaintenanceEvent = async (data: CreateMaintenanceEventInput):
|
||||
throw new Error("End date/time must be after start date/time");
|
||||
}
|
||||
|
||||
const siteData = await GetAllSiteData();
|
||||
const notificationSettings =
|
||||
siteData.globalMaintenanceNotificationSettings || seedSiteData.globalMaintenanceNotificationSettings;
|
||||
const reminderBufferSeconds = notificationSettings.reminder_buffer_hours * 3600;
|
||||
|
||||
const event = await db.createMaintenanceEvent({
|
||||
maintenance_id: data.maintenance_id,
|
||||
start_date_time: data.start_date_time,
|
||||
end_date_time: data.end_date_time,
|
||||
status: determineEventStatus(data.start_date_time, data.end_date_time),
|
||||
status: determineEventStatus(data.start_date_time, data.end_date_time, reminderBufferSeconds),
|
||||
});
|
||||
|
||||
return event;
|
||||
@@ -534,20 +602,23 @@ export const formatDurationSeconds = (seconds: number): string => {
|
||||
|
||||
/**
|
||||
* Update maintenance event statuses based on current time:
|
||||
* 1. SCHEDULED events starting within 60 minutes → READY
|
||||
* 1. SCHEDULED events starting within the reminder buffer → READY
|
||||
* 2. READY events where current time is within start/end → ONGOING
|
||||
* 3. ONGOING events where end_date_time has passed → COMPLETED
|
||||
*/
|
||||
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;
|
||||
//get global maintenance notification settings
|
||||
const notificationSettings =
|
||||
siteData.globalMaintenanceNotificationSettings || seedSiteData.globalMaintenanceNotificationSettings;
|
||||
|
||||
const reminderBufferSeconds = notificationSettings.reminder_buffer_hours * 3600;
|
||||
try {
|
||||
// 1. Mark SCHEDULED events starting within 60 minutes as READY
|
||||
const scheduledEvents = await db.getScheduledEventsStartingSoon(currentTimestamp, sixtyMinutesInSeconds);
|
||||
// 1. Mark SCHEDULED events starting within the reminder buffer as READY
|
||||
const scheduledEvents = await db.getScheduledEventsStartingSoon(currentTimestamp, reminderBufferSeconds);
|
||||
for (const event of scheduledEvents) {
|
||||
await db.updateMaintenanceEventStatus(event.id, GC.READY);
|
||||
console.log(`Maintenance event ${event.id} marked as READY (starts at ${event.start_date_time})`);
|
||||
@@ -557,15 +628,17 @@ export const UpdateMaintenanceEventStatuses = async (): Promise<void> => {
|
||||
new Date(event.start_date_time * 1000),
|
||||
new Date(currentTimestamp * 1000),
|
||||
);
|
||||
const update = maintenanceToVariables(
|
||||
event,
|
||||
monitorNames,
|
||||
`**is starting in ${timeUntilStart}**`,
|
||||
"starting_soon",
|
||||
"Maintenance Starting Soon",
|
||||
siteUrl,
|
||||
);
|
||||
await subscriberQueue.push(update);
|
||||
if (notificationSettings.event_types.reminder) {
|
||||
const update = maintenanceToVariables(
|
||||
event,
|
||||
monitorNames,
|
||||
`**is starting in ${timeUntilStart}**`,
|
||||
"starting_soon",
|
||||
"Maintenance Starting Soon",
|
||||
siteUrl,
|
||||
);
|
||||
await subscriberQueue.push(update);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Catch-up: SCHEDULED events that missed the READY window and already started → ONGOING
|
||||
@@ -575,15 +648,18 @@ export const UpdateMaintenanceEventStatuses = async (): Promise<void> => {
|
||||
console.log(`Maintenance event ${event.id} marked as ONGOING (catch-up from SCHEDULED)`);
|
||||
const monitors = await db.getMonitorsByMaintenanceId(event.maintenance_id);
|
||||
const monitorNames = monitors.map((m) => `${m.monitor_name}(${m.monitor_impact})`).join(", ");
|
||||
const update = maintenanceToVariables(
|
||||
event,
|
||||
monitorNames,
|
||||
"**is now in progress**",
|
||||
"ongoing",
|
||||
"Maintenance In Progress",
|
||||
siteUrl,
|
||||
);
|
||||
await subscriberQueue.push(update);
|
||||
|
||||
if (notificationSettings.event_types.started) {
|
||||
const update = maintenanceToVariables(
|
||||
event,
|
||||
monitorNames,
|
||||
"**is now in progress**",
|
||||
"ongoing",
|
||||
"Maintenance In Progress",
|
||||
siteUrl,
|
||||
);
|
||||
await subscriberQueue.push(update);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Mark READY events that are now in progress as ONGOING
|
||||
@@ -593,15 +669,18 @@ export const UpdateMaintenanceEventStatuses = async (): Promise<void> => {
|
||||
console.log(`Maintenance event ${event.id} marked as ONGOING`);
|
||||
const monitors = await db.getMonitorsByMaintenanceId(event.maintenance_id);
|
||||
const monitorNames = monitors.map((m) => `${m.monitor_name}(${m.monitor_impact})`).join(", ");
|
||||
const update = maintenanceToVariables(
|
||||
event,
|
||||
monitorNames,
|
||||
"**is now in progress**",
|
||||
"ongoing",
|
||||
"Maintenance In Progress",
|
||||
siteUrl,
|
||||
);
|
||||
await subscriberQueue.push(update);
|
||||
|
||||
if (notificationSettings.event_types.started) {
|
||||
const update = maintenanceToVariables(
|
||||
event,
|
||||
monitorNames,
|
||||
"**is now in progress**",
|
||||
"ongoing",
|
||||
"Maintenance In Progress",
|
||||
siteUrl,
|
||||
);
|
||||
await subscriberQueue.push(update);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Mark ONGOING events that have ended as COMPLETED
|
||||
@@ -611,15 +690,18 @@ export const UpdateMaintenanceEventStatuses = async (): Promise<void> => {
|
||||
console.log(`Maintenance event ${event.id} marked as COMPLETED`);
|
||||
const monitors = await db.getMonitorsByMaintenanceId(event.maintenance_id);
|
||||
const monitorNames = monitors.map((m) => `${m.monitor_name}(${m.monitor_impact})`).join(", ");
|
||||
const update = maintenanceToVariables(
|
||||
event,
|
||||
monitorNames,
|
||||
"**has been completed**",
|
||||
"completed",
|
||||
"Maintenance Completed",
|
||||
siteUrl,
|
||||
);
|
||||
await subscriberQueue.push(update);
|
||||
|
||||
if (notificationSettings.event_types.ended) {
|
||||
const update = maintenanceToVariables(
|
||||
event,
|
||||
monitorNames,
|
||||
"**has been completed**",
|
||||
"completed",
|
||||
"Maintenance Completed",
|
||||
siteUrl,
|
||||
);
|
||||
await subscriberQueue.push(update);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating maintenance event statuses:", error);
|
||||
|
||||
@@ -127,19 +127,20 @@ export async function CreateMonitorAlertConfig(
|
||||
// Validate input
|
||||
validateMonitorAlertConfigInput(data);
|
||||
|
||||
if (!data.monitor_tag) {
|
||||
throw new Error("monitor_tag is required");
|
||||
if (!data.monitor_tags || data.monitor_tags.length === 0) {
|
||||
throw new Error("At least one monitor is required");
|
||||
}
|
||||
|
||||
// Check if monitor exists
|
||||
const monitor = await db.getMonitorByTag(data.monitor_tag);
|
||||
if (!monitor) {
|
||||
throw new Error(`Monitor with tag '${data.monitor_tag}' not found`);
|
||||
// Check if all monitors exist
|
||||
for (const tag of data.monitor_tags) {
|
||||
const monitor = await db.getMonitorByTag(tag);
|
||||
if (!monitor) {
|
||||
throw new Error(`Monitor with tag '${tag}' not found`);
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare insert data
|
||||
const insertData: MonitorAlertConfigInsert = {
|
||||
monitor_tag: data.monitor_tag,
|
||||
alert_for: data.alert_for,
|
||||
alert_value: data.alert_value,
|
||||
failure_threshold: data.failure_threshold,
|
||||
@@ -153,6 +154,9 @@ export async function CreateMonitorAlertConfig(
|
||||
// Insert alert config
|
||||
const id = await db.insertMonitorAlertConfig(insertData);
|
||||
|
||||
// Add monitors to junction table
|
||||
await db.addMonitorsToAlertConfig(id, data.monitor_tags);
|
||||
|
||||
// Add triggers if provided
|
||||
if (data.trigger_ids && data.trigger_ids.length > 0) {
|
||||
await db.addTriggersToMonitorAlertConfig(id, data.trigger_ids);
|
||||
@@ -194,6 +198,19 @@ export async function UpdateMonitorAlertConfig(
|
||||
validateAlertValue(alertFor, data.alert_value);
|
||||
}
|
||||
|
||||
// Validate monitor_tags if provided
|
||||
if (data.monitor_tags !== undefined) {
|
||||
if (data.monitor_tags.length === 0) {
|
||||
throw new Error("At least one monitor is required");
|
||||
}
|
||||
for (const tag of data.monitor_tags) {
|
||||
const monitor = await db.getMonitorByTag(tag);
|
||||
if (!monitor) {
|
||||
throw new Error(`Monitor with tag '${tag}' not found`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare update data
|
||||
const updateData: MonitorAlertConfigUpdate = {};
|
||||
if (data.alert_for !== undefined) updateData.alert_for = data.alert_for;
|
||||
@@ -210,6 +227,11 @@ export async function UpdateMonitorAlertConfig(
|
||||
await db.updateMonitorAlertConfig(data.id, updateData);
|
||||
}
|
||||
|
||||
// Update monitors if provided
|
||||
if (data.monitor_tags !== undefined) {
|
||||
await db.replaceAlertConfigMonitors(data.id, data.monitor_tags);
|
||||
}
|
||||
|
||||
// Update triggers if provided
|
||||
if (data.trigger_ids !== undefined) {
|
||||
await db.replaceMonitorAlertConfigTriggers(data.id, data.trigger_ids);
|
||||
@@ -420,6 +442,7 @@ function validateAlertStatus(value: string): asserts value is MonitorAlertStatus
|
||||
*/
|
||||
export async function CreateMonitorAlertV2(
|
||||
configId: number,
|
||||
monitorTag?: string | null,
|
||||
incidentId?: number | null,
|
||||
): Promise<MonitorAlertV2Record> {
|
||||
// Check if config exists
|
||||
@@ -438,6 +461,7 @@ export async function CreateMonitorAlertV2(
|
||||
|
||||
const insertData: MonitorAlertV2Insert = {
|
||||
config_id: configId,
|
||||
monitor_tag: monitorTag || null,
|
||||
incident_id: incidentId || null,
|
||||
alert_status: "TRIGGERED",
|
||||
};
|
||||
|
||||
@@ -399,9 +399,7 @@ async function removeTagFromGroupMonitors(tag: string): Promise<void> {
|
||||
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;
|
||||
i === remaining.length - 1 ? Math.round((1 - weight * (remaining.length - 1)) * 1000) / 1000 : weight;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -573,13 +571,10 @@ export const GetBadge = async (badgeType: BadgeType, params: BadgeParams): Promi
|
||||
}
|
||||
}
|
||||
const defaultLocale = i18nConfig?.defaultLocale || "en";
|
||||
const activatedCodes = new Set(
|
||||
i18nConfig?.locales?.filter((l) => l.selected).map((l) => l.code) ?? ["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 locale =
|
||||
activatedCodes.has(requestedLocale) && isLocaleAvailable(requestedLocale) ? requestedLocale : defaultLocale;
|
||||
|
||||
const statusLocaleKey: Record<string, string> = {
|
||||
[GC.UP]: "Operational",
|
||||
|
||||
@@ -19,6 +19,8 @@ import type {
|
||||
SiteSubMenuOptions,
|
||||
SiteDateTimeFormat,
|
||||
SiteSubscriptionsSettings,
|
||||
SitemapXMLConfig,
|
||||
GlobalMaintenanceNotificationSettings,
|
||||
} from "../../types/site.js";
|
||||
|
||||
export interface SiteDataTransformed {
|
||||
@@ -62,6 +64,10 @@ export interface SiteDataTransformed {
|
||||
globalPageVisibilitySettings?: GlobalPageVisibilitySettings;
|
||||
pageOrderingSettings?: PageOrderingSettings;
|
||||
dateAndTimeFormat?: SiteDateTimeFormat;
|
||||
metaSiteTitle?: string;
|
||||
metaSiteDescription?: string;
|
||||
sitemap?: SitemapXMLConfig;
|
||||
globalMaintenanceNotificationSettings?: GlobalMaintenanceNotificationSettings;
|
||||
}
|
||||
|
||||
export function InsertKeyValue(key: string, value: string): Promise<number[]> {
|
||||
|
||||
@@ -43,12 +43,12 @@ export const siteDataKeys: SiteDataKey[] = [
|
||||
},
|
||||
{
|
||||
key: "favicon",
|
||||
isValid: (value) => typeof value === "string" && value.trim().length > 0,
|
||||
isValid: (value) => typeof value === "string",
|
||||
data_type: "string",
|
||||
},
|
||||
{
|
||||
key: "logo",
|
||||
isValid: (value) => typeof value === "string" && value.trim().length > 0,
|
||||
isValid: (value) => typeof value === "string",
|
||||
data_type: "string",
|
||||
},
|
||||
{
|
||||
@@ -276,4 +276,24 @@ export const siteDataKeys: SiteDataKey[] = [
|
||||
isValid: IsValidJSONString,
|
||||
data_type: "object",
|
||||
},
|
||||
{
|
||||
key: "metaSiteTitle",
|
||||
isValid: (value) => typeof value === "string",
|
||||
data_type: "string",
|
||||
},
|
||||
{
|
||||
key: "metaSiteDescription",
|
||||
isValid: (value) => typeof value === "string",
|
||||
data_type: "string",
|
||||
},
|
||||
{
|
||||
key: "sitemap",
|
||||
isValid: IsValidJSONString,
|
||||
data_type: "object",
|
||||
},
|
||||
{
|
||||
key: "globalMaintenanceNotificationSettings",
|
||||
isValid: IsValidJSONString,
|
||||
data_type: "object",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -187,6 +187,7 @@ class DbImpl {
|
||||
updateIncidentCommentByID!: IncidentsRepository["updateIncidentCommentByID"];
|
||||
updateIncidentCommentStatusByID!: IncidentsRepository["updateIncidentCommentStatusByID"];
|
||||
getIncidentCommentByID!: IncidentsRepository["getIncidentCommentByID"];
|
||||
deleteIncidentCommentsByIncidentID!: IncidentsRepository["deleteIncidentCommentsByIncidentID"];
|
||||
|
||||
// ============ Images ============
|
||||
insertImage!: ImagesRepository["insertImage"];
|
||||
@@ -291,6 +292,12 @@ class DbImpl {
|
||||
isTriggerUsedInMonitorAlertConfig!: MonitorAlertConfigRepository["isTriggerUsedInMonitorAlertConfig"];
|
||||
getMonitorAlertConfigsByTriggerId!: MonitorAlertConfigRepository["getMonitorAlertConfigsByTriggerId"];
|
||||
|
||||
// ============ Monitor Alert Config Monitors ============
|
||||
addMonitorsToAlertConfig!: MonitorAlertConfigRepository["addMonitorsToAlertConfig"];
|
||||
removeAllMonitorsFromAlertConfig!: MonitorAlertConfigRepository["removeAllMonitorsFromAlertConfig"];
|
||||
replaceAlertConfigMonitors!: MonitorAlertConfigRepository["replaceAlertConfigMonitors"];
|
||||
getAlertConfigMonitorTags!: MonitorAlertConfigRepository["getAlertConfigMonitorTags"];
|
||||
|
||||
// ============ Monitor Alerts V2 ============
|
||||
insertMonitorAlertV2!: MonitorAlertConfigRepository["insertMonitorAlertV2"];
|
||||
updateMonitorAlertV2!: MonitorAlertConfigRepository["updateMonitorAlertV2"];
|
||||
@@ -530,6 +537,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 {
|
||||
@@ -690,6 +698,14 @@ class DbImpl {
|
||||
this.monitorAlertConfig,
|
||||
);
|
||||
|
||||
// Monitor Alert Config Monitors
|
||||
this.addMonitorsToAlertConfig = this.monitorAlertConfig.addMonitorsToAlertConfig.bind(this.monitorAlertConfig);
|
||||
this.removeAllMonitorsFromAlertConfig = this.monitorAlertConfig.removeAllMonitorsFromAlertConfig.bind(
|
||||
this.monitorAlertConfig,
|
||||
);
|
||||
this.replaceAlertConfigMonitors = this.monitorAlertConfig.replaceAlertConfigMonitors.bind(this.monitorAlertConfig);
|
||||
this.getAlertConfigMonitorTags = this.monitorAlertConfig.getAlertConfigMonitorTags.bind(this.monitorAlertConfig);
|
||||
|
||||
// Monitor Alerts V2
|
||||
this.insertMonitorAlertV2 = this.monitorAlertConfig.insertMonitorAlertV2.bind(this.monitorAlertConfig);
|
||||
this.updateMonitorAlertV2 = this.monitorAlertConfig.updateMonitorAlertV2.bind(this.monitorAlertConfig);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,6 +7,8 @@ import type {
|
||||
MonitorAlertConfigTriggerRecord,
|
||||
MonitorAlertConfigTriggerInsert,
|
||||
MonitorAlertConfigWithTriggers,
|
||||
MonitorAlertConfigMonitorRecord,
|
||||
MonitorAlertConfigMonitorInsert,
|
||||
TriggerRecord,
|
||||
MonitorAlertV2Record,
|
||||
MonitorAlertV2Insert,
|
||||
@@ -29,7 +31,7 @@ export class MonitorAlertConfigRepository extends BaseRepository {
|
||||
async insertMonitorAlertConfig(data: MonitorAlertConfigInsert): Promise<number> {
|
||||
const dbType = GetDbType();
|
||||
const insertData = {
|
||||
monitor_tag: data.monitor_tag,
|
||||
monitor_tag: data.monitor_tag || null,
|
||||
alert_for: data.alert_for,
|
||||
alert_value: data.alert_value,
|
||||
failure_threshold: data.failure_threshold,
|
||||
@@ -88,29 +90,35 @@ export class MonitorAlertConfigRepository extends BaseRepository {
|
||||
* Get monitor alert configs with optional filtering
|
||||
*/
|
||||
async getMonitorAlertConfigs(filter: MonitorAlertConfigFilter): Promise<MonitorAlertConfigRecord[]> {
|
||||
let query = this.knex("monitor_alerts_config").whereRaw("1=1");
|
||||
let query = this.knex("monitor_alerts_config as mac").select("mac.*").whereRaw("1=1");
|
||||
|
||||
if (filter.id !== undefined) {
|
||||
query = query.andWhere("id", filter.id);
|
||||
query = query.andWhere("mac.id", filter.id);
|
||||
}
|
||||
if (filter.monitor_tag !== undefined) {
|
||||
query = query.andWhere("monitor_tag", filter.monitor_tag);
|
||||
query = query
|
||||
.join("monitor_alerts_config_monitors as macm", "mac.id", "macm.monitor_alerts_id")
|
||||
.andWhere("macm.monitor_tag", filter.monitor_tag);
|
||||
}
|
||||
if (filter.alert_for !== undefined) {
|
||||
query = query.andWhere("alert_for", filter.alert_for);
|
||||
query = query.andWhere("mac.alert_for", filter.alert_for);
|
||||
}
|
||||
if (filter.is_active !== undefined) {
|
||||
query = query.andWhere("is_active", filter.is_active);
|
||||
query = query.andWhere("mac.is_active", filter.is_active);
|
||||
}
|
||||
|
||||
return await query.orderBy("id", "desc");
|
||||
return await query.orderBy("mac.id", "desc");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all monitor alert configs for a specific monitor tag
|
||||
*/
|
||||
async getMonitorAlertConfigsByMonitorTag(monitorTag: string): Promise<MonitorAlertConfigRecord[]> {
|
||||
return await this.knex("monitor_alerts_config").where({ monitor_tag: monitorTag }).orderBy("id", "desc");
|
||||
return await this.knex("monitor_alerts_config as mac")
|
||||
.select("mac.*")
|
||||
.join("monitor_alerts_config_monitors as macm", "mac.id", "macm.monitor_alerts_id")
|
||||
.where("macm.monitor_tag", monitorTag)
|
||||
.orderBy("mac.id", "desc");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,9 +132,12 @@ export class MonitorAlertConfigRepository extends BaseRepository {
|
||||
* Get all active monitor alert configs for a specific monitor
|
||||
*/
|
||||
async getActiveMonitorAlertConfigsByMonitorTag(monitorTag: string): Promise<MonitorAlertConfigRecord[]> {
|
||||
return await this.knex("monitor_alerts_config")
|
||||
.where({ monitor_tag: monitorTag, is_active: "YES" })
|
||||
.orderBy("id", "desc");
|
||||
return await this.knex("monitor_alerts_config as mac")
|
||||
.select("mac.*")
|
||||
.join("monitor_alerts_config_monitors as macm", "mac.id", "macm.monitor_alerts_id")
|
||||
.where("macm.monitor_tag", monitorTag)
|
||||
.andWhere("mac.is_active", "YES")
|
||||
.orderBy("mac.id", "desc");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -140,26 +151,51 @@ export class MonitorAlertConfigRepository extends BaseRepository {
|
||||
* Delete all monitor alert configs for a specific monitor tag
|
||||
*/
|
||||
async deleteMonitorAlertConfigsByMonitorTag(monitorTag: string): Promise<number> {
|
||||
return await this.knex("monitor_alerts_config").where({ monitor_tag: monitorTag }).del();
|
||||
// Find all config IDs that have this monitor tag in the junction table
|
||||
const configIds = await this.knex("monitor_alerts_config_monitors")
|
||||
.select("monitor_alerts_id")
|
||||
.where({ monitor_tag: monitorTag });
|
||||
|
||||
if (configIds.length === 0) return 0;
|
||||
|
||||
// Remove the monitor from the junction table
|
||||
await this.knex("monitor_alerts_config_monitors").where({ monitor_tag: monitorTag }).del();
|
||||
|
||||
// Delete any configs that now have zero monitors
|
||||
const ids = configIds.map((r: { monitor_alerts_id: number }) => r.monitor_alerts_id);
|
||||
let deletedCount = 0;
|
||||
for (const id of ids) {
|
||||
const remainingMonitors = await this.knex("monitor_alerts_config_monitors")
|
||||
.count("* as count")
|
||||
.where({ monitor_alerts_id: id })
|
||||
.first<CountResult>();
|
||||
if (Number(remainingMonitors?.count) === 0) {
|
||||
await this.knex("monitor_alerts_config").where({ id }).del();
|
||||
deletedCount++;
|
||||
}
|
||||
}
|
||||
return deletedCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count monitor alert configs with optional filtering
|
||||
*/
|
||||
async getMonitorAlertConfigsCount(filter: MonitorAlertConfigFilter): Promise<CountResult | undefined> {
|
||||
let query = this.knex("monitor_alerts_config").count("* as count");
|
||||
let query = this.knex("monitor_alerts_config as mac").count("* as count");
|
||||
|
||||
if (filter.id !== undefined) {
|
||||
query = query.andWhere("id", filter.id);
|
||||
query = query.andWhere("mac.id", filter.id);
|
||||
}
|
||||
if (filter.monitor_tag !== undefined) {
|
||||
query = query.andWhere("monitor_tag", filter.monitor_tag);
|
||||
query = query
|
||||
.join("monitor_alerts_config_monitors as macm", "mac.id", "macm.monitor_alerts_id")
|
||||
.andWhere("macm.monitor_tag", filter.monitor_tag);
|
||||
}
|
||||
if (filter.alert_for !== undefined) {
|
||||
query = query.andWhere("alert_for", filter.alert_for);
|
||||
query = query.andWhere("mac.alert_for", filter.alert_for);
|
||||
}
|
||||
if (filter.is_active !== undefined) {
|
||||
query = query.andWhere("is_active", filter.is_active);
|
||||
query = query.andWhere("mac.is_active", filter.is_active);
|
||||
}
|
||||
|
||||
return await query.first<CountResult>();
|
||||
@@ -174,29 +210,33 @@ export class MonitorAlertConfigRepository extends BaseRepository {
|
||||
filter?: MonitorAlertConfigFilter,
|
||||
): Promise<{ configs: MonitorAlertConfigRecord[]; total: number }> {
|
||||
// Build count query
|
||||
let countQuery = this.knex("monitor_alerts_config").count("* as count");
|
||||
let countQuery = this.knex("monitor_alerts_config as mac").count("* as count");
|
||||
if (filter?.monitor_tag) {
|
||||
countQuery = countQuery.where("monitor_tag", filter.monitor_tag);
|
||||
countQuery = countQuery
|
||||
.join("monitor_alerts_config_monitors as macm", "mac.id", "macm.monitor_alerts_id")
|
||||
.where("macm.monitor_tag", filter.monitor_tag);
|
||||
}
|
||||
if (filter?.is_active) {
|
||||
countQuery = countQuery.andWhere("is_active", filter.is_active);
|
||||
countQuery = countQuery.andWhere("mac.is_active", filter.is_active);
|
||||
}
|
||||
if (filter?.alert_for) {
|
||||
countQuery = countQuery.andWhere("alert_for", filter.alert_for);
|
||||
countQuery = countQuery.andWhere("mac.alert_for", filter.alert_for);
|
||||
}
|
||||
const totalResult = await countQuery.first<CountResult>();
|
||||
const total = totalResult ? Number(totalResult.count) : 0;
|
||||
|
||||
// Build paginated query
|
||||
let query = this.knex("monitor_alerts_config").orderBy("id", "desc");
|
||||
let query = this.knex("monitor_alerts_config as mac").select("mac.*").orderBy("mac.id", "desc");
|
||||
if (filter?.monitor_tag) {
|
||||
query = query.where("monitor_tag", filter.monitor_tag);
|
||||
query = query
|
||||
.join("monitor_alerts_config_monitors as macm", "mac.id", "macm.monitor_alerts_id")
|
||||
.where("macm.monitor_tag", filter.monitor_tag);
|
||||
}
|
||||
if (filter?.is_active) {
|
||||
query = query.andWhere("is_active", filter.is_active);
|
||||
query = query.andWhere("mac.is_active", filter.is_active);
|
||||
}
|
||||
if (filter?.alert_for) {
|
||||
query = query.andWhere("alert_for", filter.alert_for);
|
||||
query = query.andWhere("mac.alert_for", filter.alert_for);
|
||||
}
|
||||
const configs = await query.limit(limit).offset((page - 1) * limit);
|
||||
|
||||
@@ -278,6 +318,51 @@ export class MonitorAlertConfigRepository extends BaseRepository {
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Monitor Alert Config Monitors CRUD ============
|
||||
|
||||
/**
|
||||
* Add multiple monitors to an alert config
|
||||
*/
|
||||
async addMonitorsToAlertConfig(alertConfigId: number, monitorTags: string[]): Promise<void> {
|
||||
if (monitorTags.length === 0) return;
|
||||
|
||||
const inserts = monitorTags.map((monitorTag) => ({
|
||||
monitor_alerts_id: alertConfigId,
|
||||
monitor_tag: monitorTag,
|
||||
created_at: this.knex.fn.now(),
|
||||
updated_at: this.knex.fn.now(),
|
||||
}));
|
||||
|
||||
await this.knex("monitor_alerts_config_monitors").insert(inserts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all monitors from an alert config
|
||||
*/
|
||||
async removeAllMonitorsFromAlertConfig(alertConfigId: number): Promise<number> {
|
||||
return await this.knex("monitor_alerts_config_monitors").where({ monitor_alerts_id: alertConfigId }).del();
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace all monitors for an alert config (remove old, add new)
|
||||
*/
|
||||
async replaceAlertConfigMonitors(alertConfigId: number, monitorTags: string[]): Promise<void> {
|
||||
await this.removeAllMonitorsFromAlertConfig(alertConfigId);
|
||||
if (monitorTags.length > 0) {
|
||||
await this.addMonitorsToAlertConfig(alertConfigId, monitorTags);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get monitor tags for an alert config
|
||||
*/
|
||||
async getAlertConfigMonitorTags(alertConfigId: number): Promise<string[]> {
|
||||
const records = await this.knex("monitor_alerts_config_monitors")
|
||||
.select("monitor_tag")
|
||||
.where({ monitor_alerts_id: alertConfigId });
|
||||
return records.map((r: { monitor_tag: string }) => r.monitor_tag);
|
||||
}
|
||||
|
||||
// ============ Composite / Join Operations ============
|
||||
|
||||
/**
|
||||
@@ -292,9 +377,12 @@ export class MonitorAlertConfigRepository extends BaseRepository {
|
||||
.select("t.*")
|
||||
.where("mact.monitor_alerts_id", id);
|
||||
|
||||
const monitorTags = await this.getAlertConfigMonitorTags(id);
|
||||
|
||||
return {
|
||||
...config,
|
||||
triggers: triggerRecords as TriggerRecord[],
|
||||
monitor_tags: monitorTags,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -311,9 +399,12 @@ export class MonitorAlertConfigRepository extends BaseRepository {
|
||||
.select("t.*")
|
||||
.where("mact.monitor_alerts_id", config.id);
|
||||
|
||||
const monitorTags = await this.getAlertConfigMonitorTags(config.id);
|
||||
|
||||
result.push({
|
||||
...config,
|
||||
triggers: triggerRecords as TriggerRecord[],
|
||||
monitor_tags: monitorTags,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -333,9 +424,12 @@ export class MonitorAlertConfigRepository extends BaseRepository {
|
||||
.select("t.*")
|
||||
.where("mact.monitor_alerts_id", config.id);
|
||||
|
||||
const monitorTags = await this.getAlertConfigMonitorTags(config.id);
|
||||
|
||||
result.push({
|
||||
...config,
|
||||
triggers: triggerRecords as TriggerRecord[],
|
||||
monitor_tags: monitorTags,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -373,6 +467,7 @@ export class MonitorAlertConfigRepository extends BaseRepository {
|
||||
const dbType = GetDbType();
|
||||
const insertData: Record<string, unknown> = {
|
||||
config_id: data.config_id,
|
||||
monitor_tag: data.monitor_tag || null,
|
||||
incident_id: data.incident_id || null,
|
||||
alert_status: data.alert_status,
|
||||
created_at: this.knex.fn.now(),
|
||||
@@ -432,6 +527,9 @@ export class MonitorAlertConfigRepository extends BaseRepository {
|
||||
if (filter.config_id !== undefined) {
|
||||
query = query.andWhere("config_id", filter.config_id);
|
||||
}
|
||||
if (filter.monitor_tag !== undefined) {
|
||||
query = query.andWhere("monitor_tag", filter.monitor_tag);
|
||||
}
|
||||
if (filter.incident_id !== undefined) {
|
||||
query = query.andWhere("incident_id", filter.incident_id);
|
||||
}
|
||||
|
||||
@@ -372,8 +372,18 @@ export class MonitoringRepository extends BaseRepository {
|
||||
});
|
||||
}
|
||||
|
||||
async deleteMonitorDataByTag(tag: string): Promise<number> {
|
||||
return await this.knex("monitoring_data").where("monitor_tag", tag).del();
|
||||
async deleteMonitorDataByTag(tag?: string, start?: number, end?: number): Promise<number> {
|
||||
const query = this.knex("monitoring_data");
|
||||
if (tag) {
|
||||
query.where("monitor_tag", tag);
|
||||
}
|
||||
if (start !== undefined) {
|
||||
query.where("timestamp", ">=", start);
|
||||
}
|
||||
if (end !== undefined) {
|
||||
query.where("timestamp", "<=", end);
|
||||
}
|
||||
return await query.del();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -164,6 +164,19 @@ const seedSiteData = {
|
||||
dateOnly: "PP",
|
||||
timeOnly: "p",
|
||||
},
|
||||
sitemap: {
|
||||
mode: "off",
|
||||
urls: [],
|
||||
},
|
||||
globalMaintenanceNotificationSettings: {
|
||||
event_types: {
|
||||
created: false,
|
||||
reminder: true,
|
||||
started: true,
|
||||
ended: true,
|
||||
},
|
||||
reminder_buffer_hours: 1,
|
||||
},
|
||||
};
|
||||
|
||||
export default seedSiteData;
|
||||
|
||||
@@ -10,9 +10,11 @@ export function alertToVariables(
|
||||
config: MonitorAlertConfigRecord,
|
||||
alert: MonitorAlertV2Record,
|
||||
siteVars: SiteDataForNotification,
|
||||
monitorTag?: string,
|
||||
): AlertVariableMap {
|
||||
const createdAtDate = alert.created_at instanceof Date ? alert.created_at : new Date(alert.created_at);
|
||||
const alert_name = `Alert ${config.monitor_tag} for ${config.alert_for} ${config.alert_value} ${alert.alert_status} at ${createdAtDate.toISOString()}`;
|
||||
const effectiveMonitorTag = monitorTag || config.monitor_tag || "unknown";
|
||||
const alert_name = `Alert ${effectiveMonitorTag} for ${config.alert_for} ${config.alert_value} ${alert.alert_status} at ${createdAtDate.toISOString()}`;
|
||||
|
||||
return {
|
||||
alert_id: alert.id,
|
||||
@@ -24,7 +26,7 @@ export function alertToVariables(
|
||||
alert_message: config.alert_description || "",
|
||||
alert_source: GC.ALERT,
|
||||
alert_timestamp: createdAtDate.toISOString(),
|
||||
alert_cta_url: siteVars.site_url + "monitors/" + config.monitor_tag,
|
||||
alert_cta_url: siteVars.site_url + "monitors/" + effectiveMonitorTag,
|
||||
alert_cta_text: "Open Alert Details",
|
||||
alert_incident_id: alert.incident_id ? alert.incident_id : undefined,
|
||||
alert_incident_url: alert.incident_id ? siteVars.site_url + "incidents/" + alert.incident_id : undefined,
|
||||
|
||||
@@ -141,8 +141,9 @@ async function sendAlertNotifications(
|
||||
activeAlert: MonitorAlertV2Record,
|
||||
monitor_alerts_configured: MonitorAlertConfigRecord,
|
||||
templateSiteVars: SiteDataForNotification,
|
||||
monitorTag?: string,
|
||||
): Promise<void> {
|
||||
const templateAlertVars = alertToVariables(monitor_alerts_configured, activeAlert, templateSiteVars);
|
||||
const templateAlertVars = alertToVariables(monitor_alerts_configured, activeAlert, templateSiteVars, monitorTag);
|
||||
const triggers = await GetTriggersByMonitorAlertConfigId(monitor_alerts_configured.id);
|
||||
|
||||
for (let i = 0; i < triggers.length; i++) {
|
||||
@@ -233,9 +234,10 @@ const addWorker = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get existing alerts
|
||||
// Get existing alerts for this specific monitor + config combination
|
||||
let alertsExisting = await GetMonitorAlertsV2({
|
||||
config_id: monitor_alerts_configured.id,
|
||||
monitor_tag: monitor_tag,
|
||||
alert_status: GC.TRIGGERED,
|
||||
});
|
||||
let activeAlert = null;
|
||||
@@ -244,9 +246,9 @@ const addWorker = () => {
|
||||
}
|
||||
|
||||
if (isAffected) {
|
||||
// Trigger alert if not already active
|
||||
// Trigger alert if not already active for this monitor
|
||||
if (!activeAlert) {
|
||||
activeAlert = await CreateMonitorAlertV2(monitor_alerts_configured.id);
|
||||
activeAlert = await CreateMonitorAlertV2(monitor_alerts_configured.id, monitor_tag);
|
||||
if (monitor_alerts_configured.create_incident === GC.YES) {
|
||||
let newIncidentNumber = await createNewIncident(
|
||||
activeAlert,
|
||||
@@ -261,7 +263,7 @@ const addWorker = () => {
|
||||
}
|
||||
}
|
||||
// Send triggered alert notifications
|
||||
await sendAlertNotifications(activeAlert, monitor_alerts_configured, templateSiteVars);
|
||||
await sendAlertNotifications(activeAlert, monitor_alerts_configured, templateSiteVars, monitor_tag);
|
||||
}
|
||||
} else {
|
||||
// Resolve any existing alert
|
||||
@@ -302,7 +304,7 @@ const addWorker = () => {
|
||||
}
|
||||
|
||||
// Send resolution notifications
|
||||
await sendAlertNotifications(activeAlert, monitor_alerts_configured, templateSiteVars);
|
||||
await sendAlertNotifications(activeAlert, monitor_alerts_configured, templateSiteVars, monitor_tag);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -348,6 +350,7 @@ export const push = async (monitor_tag: string, ts: number, status: string, opti
|
||||
monitor_tag: monitor.tag,
|
||||
is_active: GC.YES,
|
||||
});
|
||||
|
||||
if (monitorAlertsConfigurations.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ const addWorker = () => {
|
||||
fromEmail,
|
||||
templateTextBody,
|
||||
);
|
||||
console.log(`📧 Email sent to ${toEmails}`);
|
||||
// console.log(`📧 Email sent to ${toEmails}`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to send email to ${toEmails}:`, error);
|
||||
throw error; // Re-throw to trigger retry
|
||||
|
||||
@@ -42,6 +42,7 @@ const addWorker = () => {
|
||||
});
|
||||
|
||||
if (!dbRes) {
|
||||
console.error("Failed to insert monitoring data for monitorTag:", monitorTag, "timestamp:", ts);
|
||||
throw new Error("Failed to insert monitoring data");
|
||||
}
|
||||
|
||||
@@ -52,7 +53,6 @@ const addWorker = () => {
|
||||
latency: latency,
|
||||
type: type,
|
||||
});
|
||||
|
||||
alertingQueue.push(monitorTag, ts, status);
|
||||
|
||||
return dbRes;
|
||||
|
||||
@@ -4,7 +4,7 @@ import db from "../db/db.js";
|
||||
import { rrulestr } from "rrule";
|
||||
import { addDays } from "date-fns";
|
||||
import type { MaintenanceRecord, MaintenanceEventRecord } from "../types/db.js";
|
||||
import { determineEventStatus } from "../controllers/maintenanceController.js";
|
||||
import { determineEventStatus, CreateMaintenanceEventWithNotification } from "../controllers/maintenanceController.js";
|
||||
|
||||
let maintenanceSchedulerQueue: Queue | null = null;
|
||||
let worker: Worker | null = null;
|
||||
@@ -58,12 +58,13 @@ const generateEventsForMaintenance = async (maintenance: MaintenanceRecord): Pro
|
||||
|
||||
// Check if event already exists for this start time
|
||||
if (!existingStartTimes.has(eventStart)) {
|
||||
await db.createMaintenanceEvent({
|
||||
maintenance_id: maintenance.id,
|
||||
start_date_time: eventStart,
|
||||
end_date_time: eventEnd,
|
||||
status: determineEventStatus(eventStart, eventEnd),
|
||||
});
|
||||
await CreateMaintenanceEventWithNotification(
|
||||
maintenance.id,
|
||||
eventStart,
|
||||
eventEnd,
|
||||
maintenance.title,
|
||||
maintenance.description || null,
|
||||
);
|
||||
eventsCreated++;
|
||||
console.log(
|
||||
`Created maintenance event for "${maintenance.title}" at ${new Date(eventStart * 1000).toISOString()}`,
|
||||
|
||||
@@ -154,25 +154,32 @@ class ApiCall {
|
||||
}
|
||||
}
|
||||
|
||||
if (!!!evalResp) {
|
||||
if (!evalResp || typeof evalResp !== "object") {
|
||||
evalResp = {
|
||||
status: GC.DOWN,
|
||||
latency: latency,
|
||||
type: GC.ERROR,
|
||||
};
|
||||
} else if (
|
||||
!!!evalResp.status ||
|
||||
([GC.UP, GC.DOWN, GC.DEGRADED, GC.MAINTENANCE] as string[]).indexOf(evalResp.status) === -1
|
||||
) {
|
||||
errorMessage += " | Eval must return an object with 'status' and 'latency' fields, got no response";
|
||||
} else if (evalResp.status === undefined) {
|
||||
evalResp = {
|
||||
status: GC.DOWN,
|
||||
latency: latency,
|
||||
type: GC.ERROR,
|
||||
};
|
||||
errorMessage += ` | Eval must return an object with a 'status' field (one of: ${GC.UP}, ${GC.DOWN}, ${GC.DEGRADED}, ${GC.MAINTENANCE}), but 'status' was missing`;
|
||||
} else if (([GC.UP, GC.DOWN, GC.DEGRADED, GC.MAINTENANCE] as string[]).indexOf(evalResp.status) === -1) {
|
||||
evalResp = {
|
||||
status: GC.DOWN,
|
||||
latency: latency,
|
||||
type: GC.ERROR,
|
||||
};
|
||||
errorMessage += ` | Eval returned invalid 'status' value "${evalResp.status}". Must be one of: ${GC.UP}, ${GC.DOWN}, ${GC.DEGRADED}, ${GC.MAINTENANCE}`;
|
||||
} else {
|
||||
evalResp.type = GC.REALTIME;
|
||||
// Ensure latency is a valid number
|
||||
// Ensure latency is a valid number; fall back to measured latency
|
||||
if (typeof evalResp.latency !== "number" || isNaN(evalResp.latency)) {
|
||||
errorMessage += ` | Eval 'latency' must be a number, got ${JSON.stringify(evalResp.latency)}. Using measured latency instead`;
|
||||
evalResp.latency = latency;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ class TcpCall {
|
||||
error_message: `Error in tcpEval: ${message}`,
|
||||
};
|
||||
}
|
||||
if (!!!evalResp) {
|
||||
if (!evalResp) {
|
||||
const message = "tcpEval did not return a valid response.";
|
||||
console.log(`Error in tcpEval for ${tag}:`, message);
|
||||
return {
|
||||
@@ -65,6 +65,19 @@ class TcpCall {
|
||||
error_message: `Error in tcpEval: ${message}`,
|
||||
};
|
||||
}
|
||||
|
||||
//evalResp to be an object with status and latency
|
||||
if (!("status" in evalResp) || !("latency" in evalResp)) {
|
||||
const message = "tcpEval did not return status or latency.";
|
||||
console.log(`Error in tcpEval for ${tag}:`, message);
|
||||
return {
|
||||
status: GC.DOWN,
|
||||
latency: 0,
|
||||
type: GC.ERROR,
|
||||
error_message: `Error in tcpEval: ${message}`,
|
||||
};
|
||||
}
|
||||
|
||||
//reduce to get the status
|
||||
return {
|
||||
status: evalResp?.status || GC.DOWN,
|
||||
|
||||
@@ -444,6 +444,9 @@ export interface PageSettingsType {
|
||||
mobile: number;
|
||||
};
|
||||
monitor_layout_style: "default-list" | "default-grid" | "compact-list" | "compact-grid";
|
||||
metaPageTitle?: string;
|
||||
metaPageDescription?: string;
|
||||
socialPagePreviewImage?: string;
|
||||
}
|
||||
|
||||
export interface PageRecordTyped {
|
||||
@@ -625,7 +628,7 @@ export type YesNoType = "YES" | "NO";
|
||||
|
||||
export interface MonitorAlertConfigRecord {
|
||||
id: number;
|
||||
monitor_tag: string;
|
||||
monitor_tag: string | null;
|
||||
alert_for: AlertForType;
|
||||
alert_value: string;
|
||||
failure_threshold: number;
|
||||
@@ -639,7 +642,7 @@ export interface MonitorAlertConfigRecord {
|
||||
}
|
||||
|
||||
export interface MonitorAlertConfigInsert {
|
||||
monitor_tag: string;
|
||||
monitor_tag?: string | null;
|
||||
alert_for: AlertForType;
|
||||
alert_value: string;
|
||||
failure_threshold: number;
|
||||
@@ -681,13 +684,27 @@ export interface MonitorAlertConfigTriggerInsert {
|
||||
trigger_id: number;
|
||||
}
|
||||
|
||||
// ============ monitor_alerts_config_monitors table ============
|
||||
export interface MonitorAlertConfigMonitorRecord {
|
||||
monitor_alerts_id: number;
|
||||
monitor_tag: string;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
}
|
||||
|
||||
export interface MonitorAlertConfigMonitorInsert {
|
||||
monitor_alerts_id: number;
|
||||
monitor_tag: string;
|
||||
}
|
||||
|
||||
// ============ Composite types for monitor_alerts_config ============
|
||||
export interface MonitorAlertConfigWithTriggers extends MonitorAlertConfigRecord {
|
||||
triggers: TriggerRecord[];
|
||||
monitor_tags: string[];
|
||||
}
|
||||
|
||||
export interface MonitorAlertConfigCreateInput {
|
||||
monitor_tag: string;
|
||||
monitor_tags: string[];
|
||||
alert_for: AlertForType;
|
||||
alert_value: string;
|
||||
failure_threshold: number;
|
||||
@@ -701,6 +718,7 @@ export interface MonitorAlertConfigCreateInput {
|
||||
|
||||
export interface MonitorAlertConfigUpdateInput {
|
||||
id: number;
|
||||
monitor_tags?: string[];
|
||||
alert_for?: AlertForType;
|
||||
alert_value?: string;
|
||||
failure_threshold?: number;
|
||||
@@ -718,6 +736,7 @@ export type MonitorAlertStatusType = "TRIGGERED" | "RESOLVED";
|
||||
export interface MonitorAlertV2Record {
|
||||
id: number;
|
||||
config_id: number;
|
||||
monitor_tag: string | null;
|
||||
incident_id: number | null;
|
||||
alert_status: MonitorAlertStatusType;
|
||||
created_at: Date;
|
||||
@@ -726,11 +745,13 @@ export interface MonitorAlertV2Record {
|
||||
|
||||
export interface MonitorAlertV2Insert {
|
||||
config_id: number;
|
||||
monitor_tag?: string | null;
|
||||
incident_id?: number | null;
|
||||
alert_status: MonitorAlertStatusType;
|
||||
}
|
||||
|
||||
export interface MonitorAlertV2Update {
|
||||
monitor_tag?: string | null;
|
||||
incident_id?: number | null;
|
||||
alert_status?: MonitorAlertStatusType;
|
||||
}
|
||||
@@ -738,6 +759,7 @@ export interface MonitorAlertV2Update {
|
||||
export interface MonitorAlertV2Filter {
|
||||
id?: number;
|
||||
config_id?: number;
|
||||
monitor_tag?: string;
|
||||
incident_id?: number;
|
||||
alert_status?: MonitorAlertStatusType;
|
||||
}
|
||||
|
||||
@@ -28,7 +28,12 @@
|
||||
}
|
||||
gtag("js", new Date());
|
||||
|
||||
gtag("config", "{{id}}");
|
||||
var gtagConfig = {};
|
||||
var transportUrl = "{{transport_url}}";
|
||||
if (!!transportUrl) {
|
||||
gtagConfig.transport_url = transportUrl;
|
||||
}
|
||||
gtag("config", "{{id}}", gtagConfig);
|
||||
|
||||
window.addEventListener("analyticsEvent", function (e) {
|
||||
// Extract event name and data from the custom event
|
||||
@@ -43,6 +48,7 @@
|
||||
|
||||
//on dom ready
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
loadJS("https://www.googletagmanager.com/gtag/js?id={{id}}", initJS, document.getElementsByTagName("head")[0]);
|
||||
var scriptHost = "{{script_host}}" || "https://www.googletagmanager.com/gtag/js?id={{id}}";
|
||||
loadJS(scriptHost, initJS, document.getElementsByTagName("head")[0]);
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -37,7 +37,7 @@ function createTimezoneStore() {
|
||||
// Check localStorage for saved preference (only in browser)
|
||||
if (typeof window !== "undefined") {
|
||||
const savedTimezone = localStorage.getItem(TIMEZONE_STORAGE_KEY);
|
||||
if (savedTimezone && allTimezones.includes(savedTimezone)) {
|
||||
if (savedTimezone && (savedTimezone === "UTC" || allTimezones.includes(savedTimezone))) {
|
||||
preferredTimezone = savedTimezone;
|
||||
} else {
|
||||
// Save the default timezone to localStorage
|
||||
@@ -47,7 +47,7 @@ function createTimezoneStore() {
|
||||
|
||||
set({
|
||||
selectedTimezone: preferredTimezone,
|
||||
availableTimezones: allTimezones,
|
||||
availableTimezones: ["UTC", ...allTimezones],
|
||||
});
|
||||
},
|
||||
|
||||
@@ -56,7 +56,7 @@ function createTimezoneStore() {
|
||||
*/
|
||||
setTimezone(timezone: string): void {
|
||||
update((state) => {
|
||||
if (state.availableTimezones.includes(timezone)) {
|
||||
if (timezone === "UTC" || state.availableTimezones.includes(timezone)) {
|
||||
// Save to localStorage
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem(TIMEZONE_STORAGE_KEY, timezone);
|
||||
|
||||
@@ -141,3 +141,20 @@ export interface SiteDateTimeFormat {
|
||||
dateOnly: string;
|
||||
timeOnly: string;
|
||||
}
|
||||
|
||||
export interface SitemapXMLConfig {
|
||||
mode: "auto" | "manual" | "off";
|
||||
urls: {
|
||||
loc: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface GlobalMaintenanceNotificationSettings {
|
||||
event_types: {
|
||||
created: boolean;
|
||||
reminder: boolean;
|
||||
started: boolean;
|
||||
ended: boolean;
|
||||
};
|
||||
reminder_buffer_hours: number;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
<Toaster />
|
||||
|
||||
<svelte:head>
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
{@html `<style>:root{--up:${colorUp};--degraded:${colorDegraded};--down:${colorDown};--maintenance:${colorMaintenance};}</style>`}
|
||||
</svelte:head>
|
||||
<main>
|
||||
|
||||
@@ -193,6 +193,12 @@ export const DELETE: RequestHandler = async ({ locals }) => {
|
||||
// Incident is validated by middleware and available in locals
|
||||
const incident = locals.incident!;
|
||||
|
||||
// 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) {
|
||||
|
||||
@@ -264,7 +264,7 @@ export const PATCH: RequestHandler = async ({ locals, request }) => {
|
||||
updatedMaintenance.start_date_time,
|
||||
updatedMaintenance.rrule,
|
||||
updatedMaintenance.duration_seconds,
|
||||
7,
|
||||
1,
|
||||
);
|
||||
}
|
||||
} else if (!isOneTime && scheduleChanged) {
|
||||
@@ -287,7 +287,7 @@ export const PATCH: RequestHandler = async ({ locals, request }) => {
|
||||
updatedMaintenance.start_date_time,
|
||||
updatedMaintenance.rrule,
|
||||
updatedMaintenance.duration_seconds,
|
||||
7,
|
||||
1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
<ModeWatcher />
|
||||
<Toaster />
|
||||
|
||||
<svelte:head></svelte:head>
|
||||
<main>
|
||||
{@render children()}
|
||||
</main>
|
||||
|
||||
@@ -221,6 +221,10 @@
|
||||
{
|
||||
"title": "Internationalization",
|
||||
"content": "v4/internationalization"
|
||||
},
|
||||
{
|
||||
"title": "Analytics",
|
||||
"content": "v4/analytics"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -264,6 +268,10 @@
|
||||
{
|
||||
"title": "Custom JS & CSS",
|
||||
"content": "v4/guides/custom-js-css-guide"
|
||||
},
|
||||
{
|
||||
"title": "Kener vs Other Status Pages",
|
||||
"content": "v4/guides/comparison"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -295,6 +303,22 @@
|
||||
"group": "v4.x",
|
||||
"collapsible": false,
|
||||
"pages": [
|
||||
{
|
||||
"title": "v4.0.22",
|
||||
"content": "v4/changelogs/v4.0.22"
|
||||
},
|
||||
{
|
||||
"title": "v4.0.20",
|
||||
"content": "v4/changelogs/v4.0.20"
|
||||
},
|
||||
{
|
||||
"title": "v4.0.19",
|
||||
"content": "v4/changelogs/v4.0.19"
|
||||
},
|
||||
{
|
||||
"title": "v4.0.18",
|
||||
"content": "v4/changelogs/v4.0.18"
|
||||
},
|
||||
{
|
||||
"title": "v4.0.17",
|
||||
"content": "v4/changelogs/v4.0.17"
|
||||
|
||||
@@ -23,8 +23,6 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{data.config.name}</title>
|
||||
<meta name="description" content="Documentation for {data.config.name}" />
|
||||
<!-- favicon -->
|
||||
<link rel="icon" href={data.config.favicon} />
|
||||
</svelte:head>
|
||||
|
||||
@@ -149,9 +149,45 @@
|
||||
<svelte:head>
|
||||
<title>{data.title} - Documentation</title>
|
||||
<meta name="description" content={data.description || `Documentation for ${data.title}`} />
|
||||
<meta property="article:author" content="https://github.com/rajnandan1" />
|
||||
<link rel="canonical" href={`https://kener.ing/docs/${data.slug}`} />
|
||||
<meta property="og:title" content="{data.title} - Documentation" />
|
||||
<meta property="og:description" content={data.description || `Documentation for ${data.title}`} />
|
||||
<meta property="og:type" content="article" />
|
||||
<meta property="og:url" content={`https://kener.ing/docs/${data.slug}`} />
|
||||
<meta property="og:logo" content="https://kener.ing/logo96.png" />
|
||||
<meta property="og:image" content="https://kener.ing/og.jpg" />
|
||||
<meta name="twitter:title" content="{data.title} - Documentation" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:description" content={data.description || `Documentation for ${data.title}`} />
|
||||
<meta name="twitter:image" content="https://kener.ing/og.jpg" />
|
||||
{@html `<script type="application/ld+json">${JSON.stringify({
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BreadcrumbList",
|
||||
itemListElement: [
|
||||
{
|
||||
"@type": "ListItem",
|
||||
position: 1,
|
||||
name: "Documentation",
|
||||
item: "https://kener.ing/docs"
|
||||
},
|
||||
...(data.group
|
||||
? [
|
||||
{
|
||||
"@type": "ListItem",
|
||||
position: 2,
|
||||
name: data.group
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
"@type": "ListItem",
|
||||
position: data.group ? 3 : 2,
|
||||
name: data.title,
|
||||
item: `https://kener.ing/docs/${data.slug}`
|
||||
}
|
||||
]
|
||||
})}</script>`}
|
||||
</svelte:head>
|
||||
|
||||
<div class="mx-auto flex justify-between gap-8">
|
||||
|
||||
@@ -9,23 +9,8 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Kener Documentation</title>
|
||||
<!-- social preview og.jpg -->
|
||||
<meta property="og:image" content="/og.jpg" />
|
||||
<meta property="og:title" content="Kener Documentation" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Comprehensive documentation for Kener, the open-source status page generator. Learn how to set up, customize, and manage your own status page with Kener."
|
||||
/>
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content="Kener Documentation" />
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="Comprehensive documentation for Kener, the open-source status page generator. Learn how to set up, customize, and manage your own status page with Kener."
|
||||
/>
|
||||
<meta name="twitter:image" content="/og.jpg" />
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Q3MLRXCBFT"></script>
|
||||
<script async src="https://saki-production.up.railway.app/googletagmanager/gtag/js?id=G-Q3MLRXCBFT"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag() {
|
||||
@@ -33,7 +18,7 @@
|
||||
}
|
||||
gtag("js", new Date());
|
||||
|
||||
gtag("config", "G-Q3MLRXCBFT");
|
||||
gtag("config", "G-Q3MLRXCBFT", { transport_url: "https://saki-production.up.railway.app/google-analytics" });
|
||||
</script>
|
||||
</svelte:head>
|
||||
|
||||
|
||||
+770
-283
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,142 @@
|
||||
---
|
||||
title: Analytics
|
||||
description: Connect analytics providers to track traffic on your public status page
|
||||
---
|
||||
|
||||
Kener supports injecting analytics scripts into the public status page. Go to **Manage → Analytics Providers** to configure any of the supported providers.
|
||||
|
||||
## How it works {#how-it-works}
|
||||
|
||||
When a provider is enabled, Kener injects its tracking script into every public status page response via `/capture.js`. Multiple providers can be active simultaneously.
|
||||
|
||||
## Supported providers {#supported-providers}
|
||||
|
||||
| Provider | Key |
|
||||
| ----------------- | ---------------------------- |
|
||||
| Google Analytics | `analytics.googleTagManager` |
|
||||
| Plausible | `analytics.plausible` |
|
||||
| Mixpanel | `analytics.mixpanel` |
|
||||
| Amplitude | `analytics.amplitude` |
|
||||
| Microsoft Clarity | `analytics.clarity` |
|
||||
| Umami | `analytics.umami` |
|
||||
| PostHog | `analytics.posthog` |
|
||||
|
||||
---
|
||||
|
||||
## Google Analytics {#google-analytics}
|
||||
|
||||
Uses Google Tag Manager / gtag.js.
|
||||
|
||||
| Field | Required | Example |
|
||||
| -------------- | -------- | ----------------------------------------------------------- |
|
||||
| Measurement ID | Yes | `G-S05E5E5E5E5` |
|
||||
| Transport URL | No | `https://www.google-analytics.com` |
|
||||
| Script Host | No | `https://www.googletagmanager.com/gtag/js?id=G-S05E5E5E5E5` |
|
||||
|
||||
**Measurement ID** is found in your Google Analytics property under **Admin → Data Streams → Web → Measurement ID**.
|
||||
|
||||
**Script Host** is the full URL of the gtag.js script to load, including the `?id=` query parameter. Leave it empty to use Google's default CDN (`https://www.googletagmanager.com/gtag/js?id=<your-id>`). Set a custom URL if you proxy the script through your own domain, e.g. `https://your-proxy.example.com/gtag/js?id=G-XXXXXXXXXX`.
|
||||
|
||||
Leave **Transport URL** empty to send hits directly to Google Analytics.
|
||||
|
||||
> [!TIP]
|
||||
> Ad blockers commonly block requests to `www.googletagmanager.com` and `www.google-analytics.com`. Use [Saki](https://saki.rajnandan.com/) — a self-hostable Nginx proxy — to route through your own domain:
|
||||
>
|
||||
> - **Script Host**: `https://saki.rajnandan.com/tg/script.js?id=G-XXXXXXXXXX`
|
||||
> - **Transport URL**: `https://saki.rajnandan.com/an/`
|
||||
|
||||
---
|
||||
|
||||
## Plausible {#plausible}
|
||||
|
||||
Privacy-friendly analytics with no cookies.
|
||||
|
||||
| Field | Required | Example |
|
||||
| ------------- | -------- | ---------------------------------------------------------------- |
|
||||
| Domain | Yes | `kener.ing` |
|
||||
| API | Yes | `https://plausible.io/api/event` |
|
||||
| Script Source | Yes | `https://plausible.io/js/script.pageview-props.tagged-events.js` |
|
||||
|
||||
**Domain** must match exactly what you registered at plausible.io. For self-hosted Plausible, replace the **API** and **Script Source** URLs with your instance's host.
|
||||
|
||||
---
|
||||
|
||||
## Mixpanel {#mixpanel}
|
||||
|
||||
| Field | Required | Example |
|
||||
| ------------- | -------- | -------------------------- |
|
||||
| Project Token | Yes | `abc123def456` |
|
||||
| API Host | No | `https://api.mixpanel.com` |
|
||||
|
||||
Find your **Project Token** in Mixpanel under **Settings → Project Settings**. Leave **API Host** empty to use the default Mixpanel endpoint.
|
||||
|
||||
> [!TIP]
|
||||
> To bypass ad blockers, use [Saki](https://saki.rajnandan.com/). Set **API Host** to `https://saki.rajnandan.com/mxa/`.
|
||||
|
||||
---
|
||||
|
||||
## Amplitude {#amplitude}
|
||||
|
||||
| Field | Required | Example |
|
||||
| ----------------- | -------- | -------------------------------------- |
|
||||
| Amplitude API Key | Yes | `a1b2c3d4e5f6...` |
|
||||
| Server URL | No | `https://api2.amplitude.com/2/httpapi` |
|
||||
|
||||
Find your **Amplitude API Key** under **Settings → Projects → [Your Project] → General**. Leave **Server URL** empty to use Amplitude's default. Use `https://api.eu.amplitude.com/2/httpapi` for EU data residency.
|
||||
|
||||
> [!TIP]
|
||||
> To bypass ad blockers, use [Saki](https://saki.rajnandan.com/). Set **Server URL** to `https://saki.rajnandan.com/aapi/2/httpapi`.
|
||||
|
||||
---
|
||||
|
||||
## Microsoft Clarity {#microsoft-clarity}
|
||||
|
||||
| Field | Required | Example |
|
||||
| ---------- | -------- | ------------ |
|
||||
| Project ID | Yes | `abc1234xyz` |
|
||||
|
||||
Find your **Project ID** in Microsoft Clarity under **Settings → Overview → Tracking Code** (the value after `clarity.ms/tag/`).
|
||||
|
||||
---
|
||||
|
||||
## Umami {#umami}
|
||||
|
||||
Self-hostable, cookie-free analytics.
|
||||
|
||||
| Field | Required | Example |
|
||||
| ---------- | -------- | -------------------------------------- |
|
||||
| Website ID | Yes | `5e1c3b29-3f7d-4f2a-8e6b-d3f1a2b3c4d5` |
|
||||
| Script URL | Yes | `https://cloud.umami.is/script.js` |
|
||||
|
||||
Find your **Website ID** in Umami under **Settings → Websites**. For self-hosted Umami, replace the **Script URL** with your instance URL, e.g. `https://umami.example.com/script.js`.
|
||||
|
||||
---
|
||||
|
||||
## PostHog {#posthog}
|
||||
|
||||
Product analytics with session recordings.
|
||||
|
||||
| Field | Required | Example |
|
||||
| -------- | -------- | -------------------------- |
|
||||
| API Key | Yes | `phc_xxxxxxxxxxxxxxxxxxxx` |
|
||||
| API Host | Yes | `https://us.i.posthog.com` |
|
||||
|
||||
Find your **API Key** in PostHog under **Settings → Project → Project API key**. Set **API Host** to `https://eu.i.posthog.com` for the EU cloud, or your self-hosted PostHog URL.
|
||||
|
||||
> [!TIP]
|
||||
> To bypass ad blockers, use [Saki](https://saki.rajnandan.com/). Set **API Host** to `https://saki.rajnandan.com/pha/`.
|
||||
|
||||
---
|
||||
|
||||
## Enable a provider {#enable-a-provider}
|
||||
|
||||
1. Go to **Manage → Analytics Providers**.
|
||||
2. Select the provider from the left panel.
|
||||
3. Fill in the required fields.
|
||||
4. Set **Status** to **Enable**.
|
||||
5. Click **Save Changes**.
|
||||
|
||||
A green dot next to the provider name indicates it is active on the public site.
|
||||
|
||||
> [!NOTE]
|
||||
> Changes take effect immediately — no restart required.
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
title: v4.0.18 Changelog
|
||||
description: See what's new in Kener v4.0.18, including new features, improvements, and bug fixes
|
||||
---
|
||||
|
||||
## New features {#new-features}
|
||||
|
||||
### Incident deletion {#incident-deletion}
|
||||
|
||||
Admins and editors can now permanently delete incidents from the manage dashboard. Deleting an incident removes all associated data including comments, monitor links, and alert associations. A confirmation dialog requires typing `delete incident {id}` to prevent accidental deletions.
|
||||
|
||||
The deletion is also available via the API using `DELETE /api/v4/incidents/{incident_id}`, which now correctly unlinks associated alerts before removing the incident.
|
||||
|
||||
### Monitoring data deletion {#monitoring-data-deletion}
|
||||
|
||||
Added the ability to delete monitoring data with flexible filtering options:
|
||||
|
||||
- **By monitor**: delete data for a specific monitor tag, or all monitors at once
|
||||
- **By date range**: specify start and end timestamps to delete data within a window
|
||||
- **From the monitor page**: each monitor's Danger Zone card now includes a "Delete Monitoring Data" section with date range inputs and confirmation
|
||||
|
||||
This is available both from **Manage → Monitoring Data** (bulk deletion with filters) and from individual monitor settings pages.
|
||||
|
||||
## Bug fixes {#bug-fixes}
|
||||
|
||||
### Timezone handling with UTC {#timezone-utc-fix}
|
||||
|
||||
Fixed an issue where selecting UTC as the timezone was not properly recognized. UTC is now explicitly included in the available timezones list, and the timezone store correctly validates and persists the UTC selection. The minute grid component also now respects the selected timezone when grouping data by hour.
|
||||
|
||||
## Improvements {#improvements}
|
||||
|
||||
### Missing translations {#missing-translations}
|
||||
|
||||
Added missing translation keys across all supported locales, including uppercase status labels (`UP`, `DOWN`, `DEGRADED`, `MAINTENANCE`) and the `Update Incident` string.
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
title: v4.0.19 Changelog
|
||||
description: See what's new in Kener v4.0.19, including new features, improvements, and bug fixes
|
||||
---
|
||||
|
||||
## New features {#new-features}
|
||||
|
||||
### Site-level SEO meta tags {#site-level-seo}
|
||||
|
||||
Admins can now set a custom **meta title** and **meta description** for the entire status page from **Manage → Site Configurations → Social Preview & SEO**. These values are used as defaults for all pages and control how the site appears in search engine results and social link previews.
|
||||
|
||||
Both fields are optional — leave them empty to use the existing page title and header as defaults.
|
||||
|
||||
| Field | Effect |
|
||||
| :----------------- | :---------------------------------------------------- |
|
||||
| `Meta Title` | Sets the `<title>` tag shown in search results |
|
||||
| `Meta Description` | Sets the `<meta name="description">` tag for snippets |
|
||||
|
||||
The **social preview image** upload has been merged into this same card. A single Save button persists the image, title, and description together.
|
||||
|
||||
### Page-level SEO meta tags {#page-level-seo}
|
||||
|
||||
Each page now supports its own **meta title**, **meta description**, and **social preview image** via **Manage → Pages → [page] → Social Preview & SEO**. These override the site-level defaults for that specific page.
|
||||
|
||||
The override hierarchy is:
|
||||
|
||||
1. **Page-level** setting from `page_settings_json` (if set)
|
||||
2. **Site-level** setting from Site Configurations (if set)
|
||||
3. **Auto-generated** defaults (page title + site name, page header)
|
||||
|
||||
| Meta tag | Page-level key | Site-level fallback | Auto fallback |
|
||||
| :--------------------- | :----------------------- | :-------------------- | :-------------------------- |
|
||||
| `<title>` | `metaPageTitle` | `metaSiteTitle` | `{page_title} - {siteName}` |
|
||||
| `<meta description>` | `metaPageDescription` | `metaSiteDescription` | Page header text |
|
||||
| `<meta og:image>` | `socialPagePreviewImage` | `socialPreviewImage` | None |
|
||||
| `<meta twitter:image>` | `socialPagePreviewImage` | `socialPreviewImage` | None |
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
title: v4.0.20 Changelog
|
||||
description: See what's new in Kener v4.0.20, including new features, improvements, and bug fixes
|
||||
---
|
||||
|
||||
## New features {#new-features}
|
||||
|
||||
### Sitemap configuration {#sitemap-configuration}
|
||||
|
||||
Admins can now configure how `sitemap.xml` is generated from **Manage → Site Configurations → Sitemap**. Three modes are available:
|
||||
|
||||
| Mode | Behaviour |
|
||||
| :--------- | :--------------------------------------------------------------------------------------------------------- |
|
||||
| **Auto** | Automatically includes all pages, visible active monitors, and current/previous month event pages |
|
||||
| **Manual** | Only the URLs you explicitly add are included |
|
||||
| **Off** | Returns a 404 for `/sitemap.xml` |
|
||||
|
||||
In **Auto** mode you can also add extra manual URLs alongside the auto-generated ones. The sitemap URL is shown in the admin UI for easy copying.
|
||||
|
||||
### HEIC/HEIF image support {#heic-heif-support}
|
||||
|
||||
All image upload fields — logo, favicon, social preview, page logo, monitor image — now accept **HEIC** and **HEIF** formats (common on iPhones). Uploaded HEIC/HEIF images are automatically converted to JPEG server-side using `heic-convert` before processing with sharp.
|
||||
|
||||
The default `BODY_SIZE_LIMIT` has been increased to **3 MB** (from the SvelteKit default of 512 KB) to accommodate larger image uploads. You can override this via the `BODY_SIZE_LIMIT` environment variable.
|
||||
|
||||
## Improvements {#improvements}
|
||||
|
||||
### Open Graph & Twitter Card meta tags {#og-twitter-meta}
|
||||
|
||||
All public-facing pages now emit complete Open Graph and Twitter Card meta tags:
|
||||
|
||||
- **Status pages** (`/`, `/[page_path]`) — `og:title`, `og:description`, `og:type`, `og:image`, `twitter:card`, `twitter:image`
|
||||
- **Incident pages** — article-type OG tags with the first comment as description
|
||||
- **Maintenance pages** — article-type OG tags with the maintenance description
|
||||
- **Event pages** — OG tags with month-specific title and description
|
||||
- **Monitor pages** — OG tags with monitor name and status
|
||||
|
||||
### Improved SEO semantics {#seo-semantics}
|
||||
|
||||
- Page headers and incident/maintenance titles are now wrapped in proper `<h1>`/`<h2>` HTML heading tags for better SEO structure.
|
||||
- `robots` meta tag (`noindex, nofollow`) added to admin, account, and embed layouts to prevent search engines from indexing private pages.
|
||||
- `robots.txt` expanded with explicit `Allow`/`Disallow` rules and entries for AI crawlers (GPTBot, ClaudeBot, PerplexityBot, Google-Extended, etc.).
|
||||
|
||||
### Page-level SEO field key fix {#page-seo-key-fix}
|
||||
|
||||
Page-level SEO settings now use the correct keys (`metaPageTitle`, `metaPageDescription`, `socialPagePreviewImage`) consistently. Previously, the dashboard controller read `metaTitle`/`metaDescription`/`socialPreviewImage` which didn't match what the page settings form saved. Pages with zero monitors also now correctly return SEO metadata.
|
||||
|
||||
### Separate save buttons for page settings {#separate-save-buttons}
|
||||
|
||||
The page settings UI now has separate **Save** buttons and loading states for Display settings and SEO settings, so saving one section doesn't affect the other.
|
||||
|
||||
## Bug fixes {#bug-fixes}
|
||||
|
||||
- Fixed image upload error handling on pages — non-OK responses are now caught before attempting to parse JSON.
|
||||
- Removed unused `MonitorAlerting.svelte` component and stale `llms.txt`/`sitemap.xml` server handlers.
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
title: v4.0.22 Changelog
|
||||
description: See what's new in Kener v4.0.22, including new features, improvements, and bug fixes
|
||||
---
|
||||
|
||||
## New features {#new-features}
|
||||
|
||||
> [!NOTE]
|
||||
> The migration for sqlite3 was broken in the release 4.0.21. So it has been fixed in this release.
|
||||
|
||||
### Multi-monitor alert configurations {#multi-monitor-alerts}
|
||||
|
||||
Alert configurations can now be applied to **multiple monitors** at once. Previously each alert config was tied to a single monitor — creating the same alert for several monitors required duplicating it.
|
||||
|
||||
- A searchable multi-select UI replaces the single-monitor dropdown when creating or editing alerts.
|
||||
- Alert logs show which specific monitor triggered each alert.
|
||||
- Existing single-monitor configs are automatically migrated to the new schema.
|
||||
|
||||
New database migration adds a `monitor_alerts_config_monitors` junction table and a `monitor_tag` column on `monitor_alerts_v2` to track per-monitor trigger history.
|
||||
|
||||
### Upcoming maintenances on status pages {#upcoming-maintenances}
|
||||
|
||||
Upcoming scheduled maintenance events are now displayed on the home page, custom page routes, and individual monitor detail pages. This gives visitors advance notice of planned work.
|
||||
|
||||
Visibility is controlled by the existing **Event Display Settings → Maintenances → Upcoming** toggle in **Manage → Site Configurations**.
|
||||
|
||||
### Maintenance subscriber notifications {#maintenance-notifications}
|
||||
|
||||
Maintenance events can now trigger subscriber notifications at each lifecycle stage: **created**, **reminder**, **started**, and **ended**. Configure which events send notifications in **Manage → Site Configurations → Maintenance Notification Settings**.
|
||||
|
||||
| Event type | When sent | Default |
|
||||
| :--------- | :--------------------------- | :------ |
|
||||
| Created | New event is generated | Off |
|
||||
| Reminder | Event enters READY state | On |
|
||||
| Started | Event enters ONGOING state | On |
|
||||
| Ended | Event enters COMPLETED state | On |
|
||||
|
||||
The **reminder buffer** (default 1 hour) controls how far before the start time the READY transition and reminder notification happen.
|
||||
|
||||
### Analytics documentation {#analytics-docs}
|
||||
|
||||
A new [Analytics](/docs/v4/analytics) documentation page covers all seven supported providers — Google Analytics, Plausible, Mixpanel, Amplitude, Microsoft Clarity, Umami, and PostHog — with configuration tables, setup instructions, and ad-blocker proxy tips.
|
||||
|
||||
## Improvements {#improvements}
|
||||
|
||||
### Google Tag Manager transport URL and script host {#gtm-transport-url}
|
||||
|
||||
The Google Tag Manager integration now supports two new optional fields:
|
||||
|
||||
- **Transport URL** — custom endpoint for analytics hits (useful for bypassing ad blockers with a proxy like [Saki](https://github.com/nicholasgasior/saki)).
|
||||
- **Script Host** — custom URL for the gtag.js script instead of the default `googletagmanager.com`.
|
||||
|
||||
Configure both in **Manage → Analytics Providers → Google Analytics**.
|
||||
|
||||
### Improved TCP and API monitor error handling {#tcp-api-error-handling}
|
||||
|
||||
- **TCP monitors** now collect all host connection failure messages and report them as a combined error.
|
||||
- **API monitors** now create an HTTPS agent with keep-alive and socket pooling, distinguish timeout errors more accurately (`ECONNABORTED` vs message-based), and truncate eval errors to 200 characters.
|
||||
|
||||
### "Included Monitors" shows count {#included-monitors-count}
|
||||
|
||||
The group monitor popover and related components now display the number of included monitors — e.g. "Included Monitors (5)" — making it easier to see group size at a glance. All locale files updated with the `%count` placeholder.
|
||||
|
||||
### Czech and Slovak translation updates {#czech-slovak-translations}
|
||||
|
||||
Updated Czech (`cs.json`) and Slovak (`sk.json`) translations with new and corrected strings.
|
||||
|
||||
## Bug fixes {#bug-fixes}
|
||||
|
||||
- Fixed custom CSS and custom fonts documentation guide links pointing to incorrect URLs.
|
||||
- Removed unused `<svelte:head>` elements and stale metadata from documentation layouts.
|
||||
- Removed `knip.json` — the project now uses default knip configuration.
|
||||
|
||||
## Security {#security}
|
||||
|
||||
Patched critical and high-severity vulnerabilities by adding `overrides` for `fast-xml-parser`, `rollup`, `undici`, `minimatch`, `devalue`, `dompurify`, `cookie`, and `mailparser`. Updated `@sveltejs/kit` to ^2.53.3 and `svelte` to ^5.53.5.
|
||||
@@ -31,7 +31,7 @@ Response: Array of todo objects with `completed` boolean field.
|
||||
### Eval function:
|
||||
|
||||
```javascript
|
||||
(async function (statusCode, responseTime, responseRaw) {
|
||||
async function (statusCode, responseTime, responseRaw) {
|
||||
if (statusCode !== 200) {
|
||||
return { status: "DOWN", latency: responseTime }
|
||||
}
|
||||
@@ -58,7 +58,7 @@ Response: Array of todo objects with `completed` boolean field.
|
||||
}
|
||||
|
||||
return { status: "DOWN", latency: responseTime }
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## POST request body with $SECRET_PARAM (no eval) {#post-secret-param-body}
|
||||
@@ -81,7 +81,7 @@ SECRET_PARAM=your_real_secret_value
|
||||
## Cheerio HTML content check {#cheerio-html-check-eval}
|
||||
|
||||
```javascript
|
||||
(async function (statusCode, responseTime, responseRaw, modules) {
|
||||
async function (statusCode, responseTime, responseRaw, modules) {
|
||||
if (statusCode !== 200) {
|
||||
return { status: "DOWN", latency: responseTime }
|
||||
}
|
||||
@@ -99,5 +99,5 @@ SECRET_PARAM=your_real_secret_value
|
||||
status: status ? "UP" : "DOWN",
|
||||
latency: responseTime
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
---
|
||||
title: Kener vs Other Status Pages
|
||||
description: Compare Kener with Upptime, Gatus, cState, Cachet, Uptime Kuma, Atlassian Statuspage, Better Stack, Instatus, Hyperping, and PagerDuty
|
||||
---
|
||||
|
||||
Choosing the right status page tool depends on your monitoring needs, budget, and how much control you want. This guide compares Kener with popular open-source alternatives and SaaS products.
|
||||
|
||||
## Quick Comparison Table {#quick-comparison}
|
||||
|
||||
| Feature | Kener | Upptime | Gatus | cState | Cachet | Uptime Kuma |
|
||||
|---|---|---|---|---|---|---|
|
||||
| **Type** | Open source | Open source | Open source | Open source | Open source | Open source |
|
||||
| **Built with** | SvelteKit + Node.js | GitHub Actions | Go | Hugo (static) | PHP / Laravel | Node.js |
|
||||
| **Self-hosted** | Yes | GitHub-only | Yes | Static hosting | Yes | Yes |
|
||||
| **Admin dashboard** | Yes | No (GitHub UI) | No (YAML config) | No (CMS/CLI) | Yes | Yes |
|
||||
| **Active monitoring** | Yes (11 types) | HTTP only | Yes (8+ types) | No | No | Yes (10+ types) |
|
||||
| **Incident management** | Yes (full lifecycle) | GitHub Issues | No | Markdown files | Yes | No |
|
||||
| **Maintenance windows** | Yes (with RRULE) | Yes (basic) | No | Markdown files | Yes (basic) | Yes (basic) |
|
||||
| **Alerting** | Yes (4 channels) | Yes (9+ channels) | Yes (10+ channels) | No | No | Yes (78+ channels) |
|
||||
| **Subscriber notifications** | Yes (email) | No | No | No | Yes (email) | No |
|
||||
| **Multi-database** | SQLite, PostgreSQL, MySQL | None (file-based) | SQLite, PostgreSQL | None (static) | MySQL, PostgreSQL, SQLite | SQLite |
|
||||
| **REST API** | Yes (17+ endpoints) | Read-only | No | Read-only | Yes | Yes |
|
||||
| **RBAC** | Yes (3 roles) | No | No | No | Yes (basic) | No |
|
||||
| **i18n** | Yes | Yes | No | Yes | Yes (10+ languages) | Yes (30+ languages) |
|
||||
| **Custom theming** | Yes (CSS, colors, fonts) | Basic | No | Hugo themes | Yes (Bootstrap) | Basic |
|
||||
| **Embeddable widgets** | Yes (badges, bars, charts) | No | No | Badges only | Yes (badges) | Yes (badges) |
|
||||
| **License** | MIT | MIT | Apache 2.0 | MIT | BSD-3 | MIT |
|
||||
|
||||
## Open Source Alternatives {#open-source}
|
||||
|
||||
### Upptime {#upptime}
|
||||
|
||||
[Upptime](https://github.com/upptime/upptime) runs entirely on GitHub infrastructure — Actions for monitoring, Issues for incidents, and Pages for the status page. It requires zero server setup.
|
||||
|
||||
**Best for:** Teams already on GitHub who want a no-cost, zero-maintenance solution.
|
||||
|
||||
**Limitations:**
|
||||
- HTTP monitoring only (no TCP, DNS, SSL, Ping, gRPC, SQL)
|
||||
- 5-minute minimum check interval (GitHub Actions constraint)
|
||||
- No admin dashboard — everything managed through GitHub UI and config files
|
||||
- No subscriber notifications or alerting beyond GitHub integrations
|
||||
- No database-backed data storage
|
||||
|
||||
### Gatus {#gatus}
|
||||
|
||||
[Gatus](https://github.com/TwiN/gatus) is a developer-oriented monitoring tool written in Go with a lightweight status page UI.
|
||||
|
||||
**Best for:** DevOps teams who prefer YAML-based configuration and want minimal resource usage (10–30 MB RAM).
|
||||
|
||||
**Limitations:**
|
||||
- No admin UI — all configuration via YAML files
|
||||
- No incident management or maintenance scheduling
|
||||
- No subscriber notifications
|
||||
- No REST API for external integrations
|
||||
- No embeddable widgets or badges
|
||||
|
||||
### cState {#cstate}
|
||||
|
||||
[cState](https://github.com/cstate/cstate) is a static status page built with Hugo. It generates a fast, serverless site that can be hosted anywhere.
|
||||
|
||||
**Best for:** Teams that only need incident communication without active monitoring.
|
||||
|
||||
**Limitations:**
|
||||
- No active monitoring — it is purely a communication page
|
||||
- Incidents created via Markdown files or CMS
|
||||
- No alerting, no subscriber notifications
|
||||
- No admin dashboard
|
||||
- No database or API beyond read-only static JSON
|
||||
|
||||
### Cachet {#cachet}
|
||||
|
||||
[Cachet](https://github.com/cachethq/cachet) is a PHP/Laravel status page with a web dashboard, metric graphs, and subscriber notifications.
|
||||
|
||||
**Best for:** PHP teams who want a traditional status page with a dashboard.
|
||||
|
||||
**Limitations:**
|
||||
- No built-in active monitoring (requires external tools)
|
||||
- Development has been slow — major rewrite has been in progress for years
|
||||
- PHP/Laravel dependency stack
|
||||
- No modern UI framework
|
||||
- Limited alerting capabilities
|
||||
|
||||
### Uptime Kuma {#uptime-kuma}
|
||||
|
||||
[Uptime Kuma](https://github.com/louislam/uptime-kuma) is a popular self-hosted monitoring tool with a polished UI and 78+ notification integrations.
|
||||
|
||||
**Best for:** Teams that need a monitoring dashboard with broad notification support.
|
||||
|
||||
**Limitations:**
|
||||
- Status page is secondary to the monitoring dashboard — limited customization
|
||||
- No incident lifecycle management (investigating → identified → resolved)
|
||||
- No subscriber notifications
|
||||
- No maintenance scheduling with recurring rules
|
||||
- No multi-database support (SQLite only)
|
||||
- No REST API for automation
|
||||
- No RBAC or multi-user team management
|
||||
- No embeddable status widgets beyond basic badges
|
||||
|
||||
## SaaS Alternatives {#saas}
|
||||
|
||||
### Atlassian Statuspage {#statuspage}
|
||||
|
||||
[Statuspage.io](https://www.atlassian.com/software/statuspage) is the most widely recognized SaaS status page, used by major companies.
|
||||
|
||||
| Plan | Price | Subscribers | Team Members |
|
||||
|---|---|---|---|
|
||||
| Free | $0/mo | 100 | 2 |
|
||||
| Hobby | $29/mo | 250 | 5 |
|
||||
| Startup | $99/mo | 1,000 | 10 |
|
||||
| Business | $399/mo | 5,000 | 25 |
|
||||
| Enterprise | $1,499/mo | 25,000 | 50 |
|
||||
|
||||
**Key differences from Kener:**
|
||||
- No built-in active monitoring — requires external tools (PagerDuty, Datadog, etc.)
|
||||
- Subscriber limits per plan (Kener has no subscriber caps)
|
||||
- Limited customization on lower tiers (custom CSS only on Startup+)
|
||||
- No self-hosting option — data stays on Atlassian infrastructure
|
||||
|
||||
### Better Stack {#betterstack}
|
||||
|
||||
[Better Stack](https://betterstack.com) combines uptime monitoring, status pages, incident management, and log management.
|
||||
|
||||
**Key differences from Kener:**
|
||||
- Free tier includes 10 monitors with 3-minute intervals
|
||||
- Paid plans can get expensive for teams
|
||||
- Includes log management (beyond status page scope)
|
||||
- No self-hosting — fully managed SaaS
|
||||
- Vendor lock-in for monitoring data
|
||||
|
||||
### Instatus {#instatus}
|
||||
|
||||
[Instatus](https://instatus.com) focuses on fast, beautifully designed status pages using Jamstack architecture.
|
||||
|
||||
**Key differences from Kener:**
|
||||
- Starts at ~$15–20/mo
|
||||
- 30+ language support
|
||||
- No built-in monitoring — integrates with external providers (UptimeRobot, Datadog, Pingdom, etc.)
|
||||
- Static pages served via CDN (fast but no real-time updates)
|
||||
- No self-hosting option
|
||||
|
||||
### Hyperping {#hyperping}
|
||||
|
||||
[Hyperping](https://hyperping.com) offers uptime monitoring with status pages and on-call scheduling.
|
||||
|
||||
| Plan | Price | Monitors | Status Pages |
|
||||
|---|---|---|---|
|
||||
| Free | $0/mo | 5 | 1 (limited) |
|
||||
| Essentials | $24/mo | 50 | 1 |
|
||||
| Pro | $74/mo | 100 | Multiple |
|
||||
|
||||
**Key differences from Kener:**
|
||||
- 30-second check intervals on paid plans
|
||||
- Includes on-call scheduling (Kener does not)
|
||||
- No self-hosting option
|
||||
- Monitor limits per plan
|
||||
|
||||
### PagerDuty Status Page {#pagerduty}
|
||||
|
||||
[PagerDuty](https://www.pagerduty.com/platform/business-ops/status-pages/) offers status pages as an add-on to their incident management platform.
|
||||
|
||||
**Key differences from Kener:**
|
||||
- Status page is a $89/mo add-on to existing PagerDuty plans ($21–41/user/mo)
|
||||
- Subscriber limits (250–500 depending on plan)
|
||||
- Primarily an incident management tool — status page is secondary
|
||||
- No self-hosting, significant vendor lock-in
|
||||
- Annual renewal price increases reported (10–15%)
|
||||
|
||||
## Why Kener {#why-kener}
|
||||
|
||||
| Capability | Kener | Typical SaaS | Typical OSS |
|
||||
|---|---|---|---|
|
||||
| **11 monitor types** (API, TCP, DNS, SSL, Ping, SQL, gRPC, Heartbeat, GameDig, Group) | Yes | Varies (often HTTP-only or external) | 1–5 types |
|
||||
| **Full incident lifecycle** (investigating → identified → monitoring → resolved) | Yes | Yes | Rare |
|
||||
| **Recurring maintenance** (RRULE patterns) | Yes | Basic scheduling | Rare |
|
||||
| **Auto-generated incidents** from alerts | Yes | Some | No |
|
||||
| **Subscriber email notifications** | Yes (unlimited) | Yes (capped by plan) | Rare |
|
||||
| **RBAC** (Admin, Editor, Member) | Yes | Yes (paid tiers) | Rare |
|
||||
| **Multi-database** (SQLite, PostgreSQL, MySQL) | Yes | N/A (managed) | Usually 1 |
|
||||
| **Embeddable widgets** (status bars, latency charts, badges) | Yes | Some | Basic badges |
|
||||
| **Custom CSS/JS injection** | Yes | Paid tiers only | Varies |
|
||||
| **Custom fonts and theming** | Yes | Limited | Varies |
|
||||
| **Multiple status pages** | Yes | Paid tiers only | Rare |
|
||||
| **REST API** (17+ endpoints) | Yes | Yes | Limited |
|
||||
| **i18n support** | Yes | Some | Varies |
|
||||
| **Self-hosted, your data** | Yes | No | Yes |
|
||||
| **Cost** | Free (open source) | $29–1,499+/mo | Free |
|
||||
|
||||
### Kener is a good fit if you need {#good-fit}
|
||||
|
||||
- **Built-in monitoring + status page** in one tool (no external monitoring service required)
|
||||
- **Full control** over your data and infrastructure
|
||||
- **No subscriber or monitor caps** — scale without upgrading plans
|
||||
- **Rich incident management** with auto-generation from alerts
|
||||
- **Recurring maintenance windows** with complex scheduling (RRULE)
|
||||
- **Multiple database options** to match your existing infrastructure
|
||||
- **Embeddable widgets** for external dashboards or documentation sites
|
||||
- **Team management** with role-based access control
|
||||
|
||||
> [!TIP]
|
||||
> Kener provides monitoring + status page + incident management + alerting in a single self-hosted package. Most open-source alternatives cover only one or two of these, and SaaS tools charge per feature tier.
|
||||
@@ -26,13 +26,13 @@ Connection errors and timeouts return **DOWN**.
|
||||
|
||||
## Configuration fields {#configuration-fields}
|
||||
|
||||
| Field | Type | Default | Notes |
|
||||
| :-------- | :-------- | :------ | :--------------------------------------------- |
|
||||
| `host` | `string` | — | Required |
|
||||
| `port` | `number` | `50051` | Required |
|
||||
| `service` | `string` | `""` | Fully qualified service name; empty = overall |
|
||||
| `tls` | `boolean` | `false` | Use TLS credentials |
|
||||
| `timeout` | `number` | `10000` | Request deadline in ms |
|
||||
| Field | Type | Default | Notes |
|
||||
| :-------- | :-------- | :------ | :-------------------------------------------- |
|
||||
| `host` | `string` | — | Required |
|
||||
| `port` | `number` | `50051` | Required |
|
||||
| `service` | `string` | `""` | Fully qualified service name; empty = overall |
|
||||
| `tls` | `boolean` | `false` | Use TLS credentials |
|
||||
| `timeout` | `number` | `10000` | Request deadline in ms |
|
||||
|
||||
## Example {#example}
|
||||
|
||||
|
||||
@@ -64,6 +64,23 @@ Choose one layout:
|
||||
|
||||
Click **Save Preferences** after changes.
|
||||
|
||||
## Social preview and SEO {#social-preview-and-seo}
|
||||
|
||||
Each page can override the site-level SEO defaults set in [Site Configuration](/docs/v4/setup/site-configuration#social-preview-and-seo).
|
||||
|
||||
| Field | Description |
|
||||
| ---------------------- | ------------------------------------------------------------- |
|
||||
| `Meta Title` | Custom `<title>` and `og:title` for this page. |
|
||||
| `Meta Description` | Custom `<meta name="description">` and `og:description`. |
|
||||
| `Social Preview Image` | Custom `og:image` and `twitter:image` shown in link previews. |
|
||||
|
||||
When a field is left empty, the site-level value is used. When the site-level value is also empty, the page title and header are used as automatic fallbacks.
|
||||
|
||||
**Override hierarchy**: Page setting → Site setting → Auto-generated from page title/header.
|
||||
|
||||
> [!TIP]
|
||||
> Upload a 1200×630 image for best results across social platforms.
|
||||
|
||||
## Delete a page {#delete-a-page}
|
||||
|
||||
Non-home pages can be deleted from **Danger Zone**.
|
||||
|
||||
@@ -178,6 +178,25 @@ NODE_ENV=production
|
||||
- Controls logging verbosity
|
||||
- Enables/disables development-only features
|
||||
|
||||
### BODY_SIZE_LIMIT {#body-size-limit}
|
||||
|
||||
**Purpose**: Maximum allowed request body size. This is a SvelteKit setting that controls the largest payload the server will accept.
|
||||
|
||||
**Default**: `3M` (3 megabytes)
|
||||
|
||||
**When to Change**: Increase if you need to upload larger images through the admin panel or send larger API payloads.
|
||||
|
||||
**Format**: Number followed by unit — `K` (kilobytes), `M` (megabytes), or `Infinity` for no limit.
|
||||
|
||||
**Example**:
|
||||
|
||||
```bash
|
||||
BODY_SIZE_LIMIT=10M
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> The Docker image sets `BODY_SIZE_LIMIT=3M` by default. Override it by passing the variable to your container.
|
||||
|
||||
## Integration Variables {#integration-variables}
|
||||
|
||||
For detailed configuration of these integrations, see their dedicated documentation pages.
|
||||
|
||||
@@ -12,18 +12,22 @@ Use **Manage → Site Configurations** to control identity, navigation, monitor
|
||||
3. Configure **Navigation Menu**.
|
||||
4. Set **Monitor Sub Menu Options**.
|
||||
5. Configure **Global Page Visibility Settings**.
|
||||
6. Configure **Data Retention Policy**.
|
||||
7. Configure **Event Display Settings**.
|
||||
6. Configure **Social Preview & SEO**.
|
||||
7. Configure **Data Retention Policy**.
|
||||
8. Configure **Event Display Settings**.
|
||||
9. Configure **Maintenance Notification Settings**.
|
||||
|
||||
## Runtime impact map {#runtime-impact-map}
|
||||
|
||||
| Setting area | Stored key | Runtime impact |
|
||||
| ---------------------------- | ------------------------------------ | ------------------------------------------------------------------- |
|
||||
| Site name / URL / logo / nav | `siteName`, `siteURL`, `logo`, `nav` | Rendered in top navbar branding and nav links |
|
||||
| Favicon | `favicon` | Used in `<head>` as page icon |
|
||||
| Monitor sub menu options | `subMenuOptions` | Gates monitor share actions (badges/embed) on public monitor pages |
|
||||
| Global page visibility | `globalPageVisibilitySettings` | Controls page switcher visibility and page-scoped navigation/events |
|
||||
| Data retention policy | `dataRetentionPolicy` | Controls daily cleanup of old `monitoring_data` |
|
||||
| Setting area | Stored key | Runtime impact |
|
||||
| ---------------------------- | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- |
|
||||
| Site name / URL / logo / nav | `siteName`, `siteURL`, `logo`, `nav` | Rendered in top navbar branding and nav links |
|
||||
| Favicon | `favicon` | Used in `<head>` as page icon |
|
||||
| Monitor sub menu options | `subMenuOptions` | Gates monitor share actions (badges/embed) on public monitor pages |
|
||||
| Global page visibility | `globalPageVisibilitySettings` | Controls page switcher visibility and page-scoped navigation/events |
|
||||
| Data retention policy | `dataRetentionPolicy` | Controls daily cleanup of old `monitoring_data` |
|
||||
| Maintenance notifications | `globalMaintenanceNotificationSettings` | Controls which maintenance lifecycle events notify subscribers and reminder buffer timing |
|
||||
| Social preview & SEO | `metaSiteTitle`, `metaSiteDescription`, `socialPreviewImage` | Default `<title>`, `og:title`, `<meta description>`, `og:description`, `og:image` for all pages |
|
||||
|
||||
## Monitor sub menu options {#monitor-sub-menu-options}
|
||||
|
||||
@@ -80,6 +84,40 @@ This affects:
|
||||
- event sections on status pages
|
||||
- notifications payload API used by the UI
|
||||
|
||||
## Maintenance notification settings {#maintenance-notification-settings}
|
||||
|
||||
`globalMaintenanceNotificationSettings` controls which maintenance event lifecycle transitions send subscriber notifications.
|
||||
|
||||
### Event types {#event-types}
|
||||
|
||||
| Event type | Trigger | Default |
|
||||
| ---------- | ------------------------------- | ------- |
|
||||
| `created` | New maintenance event generated | Off |
|
||||
| `reminder` | Event enters READY state | On |
|
||||
| `started` | Event enters ONGOING state | On |
|
||||
| `ended` | Event enters COMPLETED state | On |
|
||||
|
||||
### Reminder buffer {#reminder-buffer}
|
||||
|
||||
`reminder_buffer_hours` (default `1`, minimum `1`) sets how many hours before the event start time the status transitions from SCHEDULED to READY.
|
||||
|
||||
See [Maintenance Events → Subscriber Notifications](/docs/v4/maintenances/events#subscriber-notifications) for the full lifecycle flow.
|
||||
|
||||
## Social preview and SEO {#social-preview-and-seo}
|
||||
|
||||
The **Social Preview & SEO** card sets site-wide defaults for meta tags used in search engines and link previews.
|
||||
|
||||
| Field | Stored key | Meta tags affected |
|
||||
| ---------------------- | --------------------- | --------------------------------------------- |
|
||||
| `Meta Title` | `metaSiteTitle` | `<title>`, `og:title` |
|
||||
| `Meta Description` | `metaSiteDescription` | `<meta name="description">`, `og:description` |
|
||||
| `Social Preview Image` | `socialPreviewImage` | `og:image`, `twitter:image` |
|
||||
|
||||
These values are used as defaults for every page. Individual pages can override them — see [Pages → Social Preview & SEO](/docs/v4/pages#social-preview-and-seo).
|
||||
|
||||
> [!TIP]
|
||||
> Upload a 1200×630 image for best results across social platforms.
|
||||
|
||||
## Verify changes {#verify-changes}
|
||||
|
||||
- Update site name/logo/nav and refresh home page.
|
||||
@@ -90,3 +128,5 @@ This affects:
|
||||
- notifications calendar opens page-scoped events for the current month.
|
||||
- Change event display settings and verify incident/maintenance visibility.
|
||||
- Set retention policy and confirm scheduler logs in server output.
|
||||
- Toggle maintenance notification event types and verify subscribers receive (or don't receive) emails at each lifecycle stage.
|
||||
- Set meta title/description and social preview image, then check `<meta>` tags in page source.
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<Toaster />
|
||||
|
||||
<svelte:head>
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<title>Kener Status</title>
|
||||
<link rel="icon" href={data.favicon} />
|
||||
{#if data.font?.cssSrc}
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
<Toaster />
|
||||
|
||||
<svelte:head>
|
||||
<title>Kener Status</title>
|
||||
<link rel="icon" href={data.favicon} />
|
||||
{#if data.font?.cssSrc}
|
||||
<link rel="stylesheet" href={data.font.cssSrc} />
|
||||
@@ -44,11 +43,6 @@
|
||||
${data.customCSS || ""}
|
||||
</style>`}
|
||||
<script src={clientResolver(resolve, "/capture.js")}></script>
|
||||
<!-- social preview meta tags -->
|
||||
{#if data.socialPreviewImage}
|
||||
<meta property="og:image" content={clientResolver(resolve, data.socialPreviewImage)} />
|
||||
<meta name="twitter:image" content={clientResolver(resolve, data.socialPreviewImage)} />
|
||||
{/if}
|
||||
</svelte:head>
|
||||
<main class="kener-public">
|
||||
<!-- Nav -->
|
||||
|
||||
@@ -113,7 +113,32 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{(data.pageDetails?.page_title || "Status Page") + " - " + data.siteName}</title>
|
||||
{#if data.metaPageTitle}
|
||||
<title>{data.metaPageTitle}</title>
|
||||
<meta property="og:title" content={data.metaPageTitle} />
|
||||
{:else if data.pageDetails?.page_title}
|
||||
<title>{data.pageDetails.page_title} - {data.siteName}</title>
|
||||
<meta property="og:title" content={data.pageDetails.page_title + " - " + data.siteName} />
|
||||
{:else}
|
||||
<title>{data.siteName} - Status Page</title>
|
||||
<meta property="og:title" content={data.siteName + " - Status Page"} />
|
||||
{/if}
|
||||
{#if data.metaPageDescription}
|
||||
<meta name="description" content={data.metaPageDescription} />
|
||||
<meta property="og:description" content={data.metaPageDescription} />
|
||||
{:else if data.pageDetails?.page_header}
|
||||
<meta name="description" content={data.pageDetails.page_header} />
|
||||
<meta property="og:description" content={data.pageDetails.page_header} />
|
||||
{:else}
|
||||
<meta name="description" content={(data.pageDetails?.page_title || "Status Page") + " - Status Page"} />
|
||||
<meta property="og:description" content={(data.pageDetails?.page_title || "Status Page") + " - Status Page"} />
|
||||
{/if}
|
||||
<meta property="og:type" content="website" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
{#if data.socialPagePreviewImage}
|
||||
<meta property="og:image" content={clientResolver(resolve, data.socialPagePreviewImage)} />
|
||||
<meta name="twitter:image" content={clientResolver(resolve, data.socialPagePreviewImage)} />
|
||||
{/if}
|
||||
</svelte:head>
|
||||
|
||||
<!-- page title -->
|
||||
@@ -130,14 +155,16 @@
|
||||
<Item.Root class="px-0 py-0">
|
||||
<Item.Content>
|
||||
{#if data.pageDetails?.page_header}
|
||||
<Item.Title class="text-2xl sm:text-3xl">{data.pageDetails.page_header}</Item.Title>
|
||||
<h1>
|
||||
<Item.Title class="text-2xl sm:text-3xl">{data.pageDetails.page_header}</Item.Title>
|
||||
</h1>
|
||||
{/if}
|
||||
{#if data.pageDetails?.page_subheader}
|
||||
<div class="">
|
||||
<h2 class="">
|
||||
<div class="prose prose-sm dark:prose-invert max-w-none">
|
||||
<SveltePurify html={mdToHTML(data.pageDetails.page_subheader)} />
|
||||
</div>
|
||||
</div>
|
||||
</h2>
|
||||
{/if}
|
||||
</Item.Content>
|
||||
</Item.Root>
|
||||
@@ -162,6 +189,15 @@
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{#if data.upcomingMaintenances && data.upcomingMaintenances.length > 0}
|
||||
<div class="flex flex-col gap-3">
|
||||
{#each data.upcomingMaintenances as maintenance, i (maintenance.id ?? i)}
|
||||
<div class="rounded-3xl border p-3 sm:p-4">
|
||||
<MaintenanceItem {maintenance} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="overflow-hidden rounded-3xl border">
|
||||
<div class={`grid grid-cols-1 ${getGridContainerClass(viewType)}`}>
|
||||
{#each data.monitorTags as tag, i (tag)}
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
import { SveltePurify } from "@humanspeak/svelte-purify";
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
let pageSettings = $derived(data.pageDetails.page_settings);
|
||||
let barCount = $derived.by(() =>
|
||||
data.isMobile
|
||||
@@ -114,7 +113,32 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{(data.pageDetails?.page_title || "Status Page") + " - " + data.siteName}</title>
|
||||
{#if data.metaPageTitle}
|
||||
<title>{data.metaPageTitle}</title>
|
||||
<meta property="og:title" content={data.metaPageTitle} />
|
||||
{:else if data.pageDetails?.page_title}
|
||||
<title>{data.pageDetails.page_title} - {data.siteName}</title>
|
||||
<meta property="og:title" content={data.pageDetails.page_title + " - " + data.siteName} />
|
||||
{:else}
|
||||
<title>{data.siteName} - Status Page</title>
|
||||
<meta property="og:title" content={data.siteName + " - Status Page"} />
|
||||
{/if}
|
||||
{#if data.metaPageDescription}
|
||||
<meta name="description" content={data.metaPageDescription} />
|
||||
<meta property="og:description" content={data.metaPageDescription} />
|
||||
{:else if data.pageDetails?.page_header}
|
||||
<meta name="description" content={data.pageDetails.page_header} />
|
||||
<meta property="og:description" content={data.pageDetails.page_header} />
|
||||
{:else}
|
||||
<meta name="description" content={(data.pageDetails?.page_title || "Status Page") + " - Status Page"} />
|
||||
<meta property="og:description" content={(data.pageDetails?.page_title || "Status Page") + " - Status Page"} />
|
||||
{/if}
|
||||
<meta property="og:type" content="website" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
{#if data.socialPagePreviewImage}
|
||||
<meta property="og:image" content={clientResolver(resolve, data.socialPagePreviewImage)} />
|
||||
<meta name="twitter:image" content={clientResolver(resolve, data.socialPagePreviewImage)} />
|
||||
{/if}
|
||||
</svelte:head>
|
||||
|
||||
<!-- page title -->
|
||||
@@ -131,14 +155,16 @@
|
||||
<Item.Root class="px-0 py-0">
|
||||
<Item.Content>
|
||||
{#if data.pageDetails?.page_header}
|
||||
<Item.Title class="text-2xl sm:text-3xl">{data.pageDetails.page_header}</Item.Title>
|
||||
<h1>
|
||||
<Item.Title class="text-2xl sm:text-3xl">{data.pageDetails.page_header}</Item.Title>
|
||||
</h1>
|
||||
{/if}
|
||||
{#if data.pageDetails?.page_subheader}
|
||||
<div class="">
|
||||
<h2 class="">
|
||||
<div class="prose prose-sm dark:prose-invert max-w-none">
|
||||
<SveltePurify html={mdToHTML(data.pageDetails.page_subheader)} />
|
||||
</div>
|
||||
</div>
|
||||
</h2>
|
||||
{/if}
|
||||
</Item.Content>
|
||||
</Item.Root>
|
||||
@@ -163,6 +189,15 @@
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{#if data.upcomingMaintenances && data.upcomingMaintenances.length > 0}
|
||||
<div class="flex flex-col gap-3">
|
||||
{#each data.upcomingMaintenances as maintenance, i (maintenance.id ?? i)}
|
||||
<div class="rounded-3xl border p-3 sm:p-4">
|
||||
<MaintenanceItem {maintenance} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="overflow-hidden rounded-3xl border">
|
||||
<div class={`grid grid-cols-1 ${getGridContainerClass(viewType)}`}>
|
||||
{#each data.monitorTags as tag, i (tag)}
|
||||
|
||||
@@ -159,6 +159,15 @@
|
||||
|
||||
<svelte:head>
|
||||
<title>{currentMonth} - Maintenances & Incidents - {data.siteName}</title>
|
||||
<meta name="description" content={`${currentMonth} maintenances and incidents for ${data.siteName}`} />
|
||||
<meta property="og:title" content={`${currentMonth} - Maintenances & Incidents - ${data.siteName}`} />
|
||||
<meta property="og:description" content={`${currentMonth} maintenances and incidents for ${data.siteName}`} />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
{#if data.socialPreviewImage}
|
||||
<meta property="og:image" content={clientResolver(resolve, data.socialPreviewImage)} />
|
||||
<meta name="twitter:image" content={clientResolver(resolve, data.socialPreviewImage)} />
|
||||
{/if}
|
||||
</svelte:head>
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
|
||||
@@ -32,7 +32,15 @@ export const GET: RequestHandler = async ({ params, url }) => {
|
||||
|
||||
if (!!analyticsData["analytics.googleTagManager"]) {
|
||||
let id = analyticsData["analytics.googleTagManager"].requirements["Measurement ID"];
|
||||
captureScript = captureScript + ";\n" + gtm.replaceAll("{{id}}", id);
|
||||
let transportUrl = analyticsData["analytics.googleTagManager"].requirements["Transport URL"] || "";
|
||||
let scriptHost = analyticsData["analytics.googleTagManager"].requirements["Script Host"] || "";
|
||||
captureScript =
|
||||
captureScript +
|
||||
";\n" +
|
||||
gtm
|
||||
.replaceAll("{{id}}", id)
|
||||
.replaceAll("{{transport_url}}", transportUrl)
|
||||
.replaceAll("{{script_host}}", scriptHost);
|
||||
}
|
||||
|
||||
if (!!analyticsData["analytics.mixpanel"]) {
|
||||
|
||||
@@ -159,6 +159,15 @@
|
||||
|
||||
<svelte:head>
|
||||
<title>{currentMonth} - Maintenances & Incidents - {data.siteName}</title>
|
||||
<meta name="description" content={`${currentMonth} maintenances and incidents for ${data.siteName}`} />
|
||||
<meta property="og:title" content={`${currentMonth} - Maintenances & Incidents - ${data.siteName}`} />
|
||||
<meta property="og:description" content={`${currentMonth} maintenances and incidents for ${data.siteName}`} />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
{#if data.socialPreviewImage}
|
||||
<meta property="og:image" content={clientResolver(resolve, data.socialPreviewImage)} />
|
||||
<meta name="twitter:image" content={clientResolver(resolve, data.socialPreviewImage)} />
|
||||
{/if}
|
||||
</svelte:head>
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
|
||||
@@ -20,9 +20,16 @@
|
||||
|
||||
<svelte:head>
|
||||
<title>{data.incident.title + " - " + data.siteName}</title>
|
||||
<!-- meta description -->
|
||||
<meta property="og:title" content={data.incident.title + " - " + data.siteName} />
|
||||
<meta property="og:type" content="article" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
{#if data.comments.length > 0}
|
||||
<meta name="description" content={data.comments[0].comment} />
|
||||
<meta property="og:description" content={data.comments[0].comment} />
|
||||
{/if}
|
||||
{#if data.socialPreviewImage}
|
||||
<meta property="og:image" content={clientResolver(resolve, data.socialPreviewImage)} />
|
||||
<meta name="twitter:image" content={clientResolver(resolve, data.socialPreviewImage)} />
|
||||
{/if}
|
||||
</svelte:head>
|
||||
|
||||
@@ -31,7 +38,9 @@
|
||||
<div class="flex flex-col gap-2 px-4 py-2">
|
||||
<Item.Root class="mb-4 px-0">
|
||||
<Item.Content class="min-w-0 flex-1 px-0">
|
||||
<Item.Title class="text-3xl wrap-break-word">{data.incident.title}</Item.Title>
|
||||
<h1>
|
||||
<Item.Title class="text-3xl wrap-break-word">{data.incident.title}</Item.Title>
|
||||
</h1>
|
||||
</Item.Content>
|
||||
</Item.Root>
|
||||
</div>
|
||||
|
||||
@@ -65,9 +65,16 @@
|
||||
|
||||
<svelte:head>
|
||||
<title>{data.maintenance.title + " - " + data.siteName}</title>
|
||||
<!-- meta description -->
|
||||
<meta property="og:title" content={data.maintenance.title + " - " + data.siteName} />
|
||||
<meta property="og:type" content="article" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
{#if data.maintenance.description}
|
||||
<meta name="description" content={data.maintenance.description} />
|
||||
<meta property="og:description" content={data.maintenance.description} />
|
||||
{/if}
|
||||
{#if data.socialPreviewImage}
|
||||
<meta property="og:image" content={clientResolver(resolve, data.socialPreviewImage)} />
|
||||
<meta name="twitter:image" content={clientResolver(resolve, data.socialPreviewImage)} />
|
||||
{/if}
|
||||
</svelte:head>
|
||||
|
||||
@@ -75,11 +82,10 @@
|
||||
<ThemePlus />
|
||||
<div class="flex flex-col gap-2 px-4 py-2">
|
||||
<Item.Root class="mb-4 flex-col items-start px-0 sm:flex-row sm:items-center">
|
||||
<Item.Media>
|
||||
<MaintenanceIcon class="stroke-maintenance size-8" />
|
||||
</Item.Media>
|
||||
<Item.Content class="min-w-0 flex-1 px-0">
|
||||
<Item.Title class="text-3xl wrap-break-word">{data.maintenance.title}</Item.Title>
|
||||
<h1>
|
||||
<Item.Title class="text-3xl wrap-break-word">{data.maintenance.title}</Item.Title>
|
||||
</h1>
|
||||
</Item.Content>
|
||||
</Item.Root>
|
||||
</div>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user