mirror of
https://github.com/rajnandan1/kener.git
synced 2026-08-07 15:27:13 +00:00
Compare commits
176 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 | |||
| a5afd38520 | |||
| 5138f7fb6e | |||
| 1bed0538db | |||
| bd582eaad3 | |||
| 5803ccadca | |||
| 4f16b06a0f | |||
| fad23e2a01 | |||
| 4b84e47d89 | |||
| 9e020c10f4 | |||
| 6fd47963ec | |||
| df9b36b242 | |||
| f9fe74ef94 | |||
| 823ea6eeb7 | |||
| 6c7606d3b0 | |||
| d2e9437cfa | |||
| a549eb5d1b | |||
| 648f8180e7 | |||
| 9afb8947ac | |||
| 0c8338e2a5 | |||
| df94755c6b | |||
| 204419fccb | |||
| a973494bc8 | |||
| ba459e61ad | |||
| f92fc8ff73 | |||
| 91cb4850ca | |||
| fb7939a4dc | |||
| 1f352591a4 | |||
| 0085621900 | |||
| 0eb789d89a | |||
| 7f21b27bb8 | |||
| 7abd6788de | |||
| 681b116e09 | |||
| 04b1d0716b | |||
| 668f12a0eb | |||
| 394149a3d6 | |||
| 57c8ff28ab | |||
| 5b29af9e74 | |||
| 9f5b90cb93 | |||
| e4f001acf7 | |||
| 604dbccaea | |||
| 6b7d0335b7 | |||
| b55dd29f02 | |||
| dcd7918a08 | |||
| 38e3a538ef | |||
| 19c2275a7b | |||
| a53251454e | |||
| 8f39a0f649 | |||
| 674c310b67 | |||
| a550afca60 | |||
| e64d955ecb | |||
| 57fda67ed7 | |||
| 6d5a42aeb2 | |||
| 5d9160c36d | |||
| b465f48c6e | |||
| 8c16d99f55 | |||
| fdedbb78cf | |||
| 416beca1a6 | |||
| e261d3620e | |||
| 1089133b97 | |||
| 90de4ae4cb | |||
| 4e263284c8 | |||
| 343368631f | |||
| f5f40e1438 | |||
| 74f6601f5d | |||
| c4e03c1a83 | |||
| bb9124db0d | |||
| ce52506c95 | |||
| f20b74cb36 | |||
| f3a5839aae | |||
| 0896aed91e | |||
| d8ae54da53 | |||
| 66bf012c87 | |||
| 08fc54d2ea | |||
| b36e68f7c8 | |||
| 045f32b7ce | |||
| 7ae869d98a | |||
| efb04a4238 | |||
| e6e586f6de | |||
| e13d7fb1f4 | |||
| 23b0bae018 | |||
| e581346f84 | |||
| 555fd3a8a2 | |||
| 7a29d2f2ca | |||
| 7a3fe20083 | |||
| c16c119d65 | |||
| 048a12899d | |||
| 5fd0e691b3 | |||
| 28932f8df5 | |||
| 4da29ac4e1 | |||
| 396fc5e3c3 | |||
| af8daf98f6 | |||
| 2e91f90057 | |||
| 3213ab8efa | |||
| f2308d9bd1 | |||
| 623465ff50 | |||
| 7c224b3886 | |||
| 166073fbd4 | |||
| 373cbd5917 | |||
| 2c27859197 | |||
| bb5d58b675 | |||
| caca29d354 | |||
| 35f0adb235 | |||
| 8bcff45d87 | |||
| 3ad256a626 | |||
| c4b9181a0a | |||
| 1ba40fadf6 | |||
| 20f1481a01 | |||
| 828747876b | |||
| cbe0ea683f | |||
| 561864c625 | |||
| 3abdb5c17a | |||
| f2ef19e3d8 | |||
| 78b18a59ec | |||
| c28581b51d |
@@ -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"
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"enabledPlugins": {
|
||||
"frontend-design@claude-plugins-official": true
|
||||
}
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
---
|
||||
name: codearch
|
||||
description: |
|
||||
Persistent project memory via a `.codearch/` folder. Use this skill at the START and END of every coding session, agentic task, or multi-step file operation. Trigger whenever the user asks you to work on a codebase, fix a bug, refactor, review code, or continue work from a previous session. Also trigger if the user mentions "context," "memory," "remember this," "pick up where we left off," or references prior work. This skill tells you how to read relevant context before acting and how to compress + save what you learned after acting. Always use it — skipping it loses hard-won context.
|
||||
---
|
||||
|
||||
# codearch — Project Memory Skill
|
||||
|
||||
`.codearch/` is a folder that lives at the project root. It stores compressed context files so you can resume work without re-deriving what was already figured out.
|
||||
|
||||
---
|
||||
|
||||
## Before Every Run — Load Context
|
||||
|
||||
**Do this before writing any code or answering any question about the codebase.**
|
||||
|
||||
### Step 1: Scan the folder
|
||||
|
||||
```bash
|
||||
ls .codearch/
|
||||
```
|
||||
|
||||
If the folder doesn't exist, skip to the run. You'll create it after.
|
||||
|
||||
### Step 2: Find relevant files
|
||||
|
||||
Two approaches — use both if unsure:
|
||||
|
||||
**A. File name scan** — Read the file names. Match against the current task (e.g., working on auth? look for `auth.md`, `session.md`, `jwt.md`).
|
||||
|
||||
**B. Text search** — Search inside files for keywords from the task.
|
||||
|
||||
```bash
|
||||
grep -ril "<keyword>" .codearch/
|
||||
```
|
||||
|
||||
Run multiple greps if the task spans several concepts.
|
||||
|
||||
### Step 3: Read relevant files
|
||||
|
||||
Read only the files that match. Don't load the entire folder into context unnecessarily.
|
||||
|
||||
```bash
|
||||
cat .codearch/<relevant-file>.md
|
||||
```
|
||||
|
||||
### Step 4: Apply the context
|
||||
|
||||
Use what you read to inform your approach. Don't re-derive decisions already documented. If a file says "we chose X over Y because of Z," respect that unless the user explicitly overrides it.
|
||||
|
||||
---
|
||||
|
||||
## After Every Run — Save Context
|
||||
|
||||
**Do this after completing the task, before ending the session.**
|
||||
|
||||
### Step 1: Write a summary
|
||||
|
||||
Compress what happened into a short summary. Cover:
|
||||
|
||||
- What the task was
|
||||
- What you changed or built
|
||||
- Key decisions made (and why)
|
||||
- Gotchas, constraints, or warnings for next time
|
||||
- Files/modules touched
|
||||
- Anything left unresolved
|
||||
|
||||
Keep it under 200 words. Dense is better than verbose.
|
||||
|
||||
**Example summary:**
|
||||
|
||||
```
|
||||
Task: Refactored auth middleware to support refresh tokens.
|
||||
Changes: Modified src/middleware/auth.go and src/handlers/refresh.go.
|
||||
Decisions: Used Redis for token storage (not DB) — avoids write contention under load.
|
||||
Gotcha: JWT secret is loaded from env at startup only; restart required after rotation.
|
||||
Unresolved: Rate limiting on /refresh endpoint not yet implemented.
|
||||
```
|
||||
|
||||
### Step 2: Find the right file to update
|
||||
|
||||
Check if the summary fits an existing `.codearch/` file:
|
||||
|
||||
```bash
|
||||
ls .codearch/
|
||||
grep -ril "<topic keyword>" .codearch/
|
||||
```
|
||||
|
||||
**Match logic:**
|
||||
|
||||
| Situation | Action |
|
||||
| ---------------------------------- | --------------------------- |
|
||||
| File exists and topic matches | Append summary to that file |
|
||||
| File exists but topic is a stretch | Create a new file |
|
||||
| No file matches | Create a new file |
|
||||
|
||||
### Step 3: Write or update the file
|
||||
|
||||
**Appending to an existing file:**
|
||||
|
||||
```bash
|
||||
cat >> .codearch/<existing-file>.md << 'EOF'
|
||||
|
||||
---
|
||||
## Session: <brief date or task label>
|
||||
|
||||
<your summary here>
|
||||
EOF
|
||||
```
|
||||
|
||||
**Creating a new file:**
|
||||
|
||||
Name it after the topic, not the date. Good names: `auth.md`, `database-schema.md`, `api-design.md`, `deployment.md`. Bad names: `session-2024-01-15.md`, `notes.md`, `misc.md`.
|
||||
|
||||
```bash
|
||||
cat > .codearch/<topic>.md << 'EOF'
|
||||
# <Topic Title>
|
||||
|
||||
## Session: <brief task label>
|
||||
|
||||
<your summary here>
|
||||
EOF
|
||||
```
|
||||
|
||||
### Step 4: Create the folder if it doesn't exist
|
||||
|
||||
```bash
|
||||
mkdir -p .codearch
|
||||
```
|
||||
|
||||
Do this before writing if the folder is missing.
|
||||
|
||||
---
|
||||
|
||||
## File Naming Rules
|
||||
|
||||
- One file per logical domain (`auth`, `billing`, `database`, `api`, `deployment`, `testing`)
|
||||
- Use kebab-case for multi-word topics: `rate-limiting.md`, `background-jobs.md`
|
||||
- Never use dates as file names
|
||||
- Never use generic names: `notes`, `misc`, `temp`, `context`
|
||||
- If a file grows beyond ~300 lines, split it by subtopic
|
||||
|
||||
---
|
||||
|
||||
## What Makes a Good `.codearch` Entry
|
||||
|
||||
**Include:**
|
||||
|
||||
- Decisions and their rationale
|
||||
- Non-obvious constraints (env vars, external dependencies, config quirks)
|
||||
- What was tried and rejected
|
||||
- Known bugs or tech debt left behind
|
||||
- Key file paths and what they do
|
||||
|
||||
**Exclude:**
|
||||
|
||||
- Code snippets (link to files instead)
|
||||
- Step-by-step logs of what you typed
|
||||
- Anything that's obvious from reading the code
|
||||
|
||||
---
|
||||
|
||||
## Example `.codearch/` Structure
|
||||
|
||||
```
|
||||
.codearch/
|
||||
├── auth.md # JWT strategy, refresh token design, middleware notes
|
||||
├── database-schema.md # Table decisions, migration gotchas, index rationale
|
||||
├── api-design.md # REST conventions, versioning decisions, error formats
|
||||
├── deployment.md # Docker setup, env var requirements, CI notes
|
||||
└── background-jobs.md # Queue design, retry logic, failure handling
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| When | What to do |
|
||||
| ---------------------- | ------------------------------------------------------------ |
|
||||
| Start of task | `ls .codearch/` → grep for keywords → read matches → proceed |
|
||||
| End of task | Write summary → find matching file → append or create |
|
||||
| Folder missing | `mkdir -p .codearch` then proceed |
|
||||
| No relevant file found | Skip loading; create one after the run |
|
||||
| Ambiguous match | Read both files; create a new one if neither fits well |
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../.agents/skills/ss-shadcn-svelte
|
||||
@@ -1,8 +1,9 @@
|
||||
---
|
||||
name: tailwindcss
|
||||
displayName: Tailwind CSS
|
||||
description: Tailwind CSS v4 utility-first styling patterns including responsive design, dark mode, and custom configuration. Use when styling with Tailwind, adding utility classes, configuring Tailwind, setting up dark mode, or customizing the theme.
|
||||
version: 1.0.0
|
||||
user-invokable: false
|
||||
metadata:
|
||||
category: styling
|
||||
---
|
||||
|
||||
# Tailwind CSS v4 Development Guidelines
|
||||
@@ -358,11 +359,3 @@ Tailwind v4 delivers 3.5x faster full builds (~100ms) compared to v3 using moder
|
||||
6. **Enable Dark Mode**: Plan for dark mode from the start
|
||||
7. **Use Plugins**: Leverage official plugins for common needs
|
||||
8. **Optimize Production**: Ensure purge is configured correctly
|
||||
|
||||
## Additional Resources
|
||||
|
||||
For detailed information, see:
|
||||
|
||||
- [Utility Patterns](resources/utility-patterns.md)
|
||||
- [Component Library](resources/component-library.md)
|
||||
- [Configuration Guide](resources/configuration.md)
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
# Localization
|
||||
|
||||
## Session: locale-key refresh (2026-02-24)
|
||||
|
||||
Task: Updated 18 locale files under `src/lib/locales` for 8 specific UI keys.
|
||||
Changes: Replaced English placeholders with localized values in `de.json`, `dk.json`, `es.json`, `fa.json`, `fr.json`, `hi.json`, `it.json`, `ja.json`, `ko.json`, `nb-NO.json`, `nl.json`, `pl.json`, `pt-BR.json`, `ru.json`, `sk.json`, `tr.json`, `vi.json`, and `zh-CN.json`.
|
||||
Decisions: Kept formatting and structure intact (2-space JSON indentation, only target keys edited). Adjusted French `Notifications` to `Alertes` to satisfy non-English-equality validation.
|
||||
Validation: Ran a script checking the 8 target keys; final result reports no values equal to the original English keys.
|
||||
Gotcha: Shared terminal got stuck in heredoc/REPL during checks; recovered and reran validation with a robust one-liner.
|
||||
@@ -1,10 +0,0 @@
|
||||
# Versioning
|
||||
|
||||
## Session: startup banner version source
|
||||
|
||||
Task: Clarify runtime version source for startup figlet/banner and align with project architecture.
|
||||
Changes: Reverted `src/lib/server/startup.ts` to use shared `version()` from `src/lib/version.ts` instead of reading `package.json` directly with fs path logic.
|
||||
Decision: Keep `src/lib/version.ts` as the single source for version resolution across server/client contexts. This respects code architecture and avoids duplicate version loading paths.
|
||||
Gotcha: If version appears stale at runtime, ensure the app is rebuilt/restarted after bumping `package.json` so injected/runtime values update.
|
||||
Files touched: `src/lib/server/startup.ts`.
|
||||
Verification: `npm run check` passed with 0 errors (existing unrelated warnings only).
|
||||
@@ -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,11 +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.
|
||||
@@ -95,3 +95,4 @@ jobs:
|
||||
generate_release_notes: true
|
||||
make_latest: ${{ inputs.make_latest && 'true' || 'false' }}
|
||||
prerelease: ${{ inputs.prerelease }}
|
||||
token: ${{ secrets.RELEASE_TOKEN }}
|
||||
|
||||
@@ -83,6 +83,8 @@ jobs:
|
||||
BASE_SUFFIX=""
|
||||
fi
|
||||
|
||||
WITH_DOCS="false"
|
||||
|
||||
if [ "${{ matrix.variant }}" = "alpine" ]; then
|
||||
VARIANT_SUFFIX="-alpine"
|
||||
else
|
||||
@@ -94,6 +96,7 @@ jobs:
|
||||
echo "release_tag=${TAG}${FULL_SUFFIX}" >> "$GITHUB_OUTPUT"
|
||||
echo "release_norm_tag=${NORM_TAG}${FULL_SUFFIX}" >> "$GITHUB_OUTPUT"
|
||||
echo "latest_tag=latest${FULL_SUFFIX}" >> "$GITHUB_OUTPUT"
|
||||
echo "with_docs=${WITH_DOCS}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
if [ "${{ matrix.variant }}" = "debian" ]; then
|
||||
echo "release_tag_debian_alias=${TAG}${BASE_SUFFIX}-debian" >> "$GITHUB_OUTPUT"
|
||||
@@ -129,7 +132,7 @@ jobs:
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
VARIANT=${{ matrix.variant }}
|
||||
WITH_DOCS=true
|
||||
WITH_DOCS=${{ steps.vars.outputs.with_docs }}
|
||||
KENER_BASE_PATH=${{ matrix.base_path }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
cache-from: type=gha
|
||||
|
||||
+2
-1
@@ -19,4 +19,5 @@ config/static/*
|
||||
!config/static/.kener
|
||||
**/*.yaml
|
||||
**/*.yml
|
||||
.github/
|
||||
.github/
|
||||
src/lib/components/ui
|
||||
@@ -22,6 +22,15 @@ You MUST use this tool whenever writing Svelte code before sending it to the use
|
||||
Generates a Svelte Playground link with the provided code.
|
||||
After completing the code, ask the user if they want a playground link. Only call this tool after user confirmation and NEVER if code was written to files in their project.
|
||||
|
||||
## Database compatibility rule
|
||||
|
||||
All database operations — migrations, queries, and repository functions — **MUST** work across all three supported databases: **SQLite**, **PostgreSQL**, and **MySQL**. Use Knex.js schema builder and query builder abstractions; avoid raw SQL unless wrapped in dialect-safe helpers or guarded with `try/catch`. When writing migrations:
|
||||
|
||||
- Use `knex.schema.hasColumn` / `knex.schema.hasTable` guards for idempotency.
|
||||
- Use Knex column types (`.string()`, `.integer()`, `.text()`, etc.) — never raw `ALTER TABLE` unless necessary.
|
||||
- For data-seeding inside migrations, use standard Knex query builder (`.insert()`, `.update()`, `.orderBy()`, `.first()`).
|
||||
- Test that `defaultTo()` values and `notNullable()` constraints work on all three engines.
|
||||
|
||||
## Documentation writing skill
|
||||
|
||||
When the user asks to write or edit documentation, follow the skill file:
|
||||
@@ -29,9 +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 skill - Important for all coding tasks
|
||||
|
||||
Always try to use the code architecture skill at the start and end of coding sessions:
|
||||
|
||||
- `.claude/skills/code-architecture/SKILL.md`
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## What is Kener?
|
||||
|
||||
Kener is an open-source status page application built with **SvelteKit 2.x (Svelte 5)** and **Node.js/Express**. It is a **TypeScript-first** codebase providing real-time monitoring, uptime tracking, incident management, and customizable dashboards.
|
||||
|
||||
## Development Commands
|
||||
|
||||
```bash
|
||||
npm run dev # Start dev server (SvelteKit + cron scheduler in parallel)
|
||||
npm run build # Production build (SvelteKit then esbuild server bundle)
|
||||
npm run start # Run production build (node build/main.js)
|
||||
npm run check # Svelte + TypeScript type checking
|
||||
npm run prettify # Format all files with Prettier
|
||||
npm run migrate # Run database migrations via Knex
|
||||
npm run seed # Run database seeds (migrations run automatically first)
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Dual Process Model
|
||||
|
||||
In development, `npm run dev` runs two parallel processes:
|
||||
|
||||
1. **SvelteKit dev server** (`vite dev`) - serves the frontend
|
||||
2. **Cron scheduler** (`vite-node src/lib/server/startup.ts`) - runs monitor checks, maintenance scheduler, daily cleanup
|
||||
|
||||
In production, `scripts/main.ts` is the single entry point: Express server + SvelteKit handler + migrations + seeds + scheduler startup.
|
||||
|
||||
### SvelteKit Route Groups
|
||||
|
||||
- **`(kener)/`** - Public status page
|
||||
- **`(manage)/`** - Admin dashboard (authenticated)
|
||||
- **`(embed)/`** - Embeddable widgets
|
||||
- **`(docs)/`** - Documentation pages
|
||||
- **`(api)/`** - SvelteKit API routes; also `src/lib/server/api-server/` for Express-side API handlers (file-based routing: `./action/method.ts`)
|
||||
- **`(account)/`** - Account/auth pages
|
||||
- **`(ext)/`** - External integrations
|
||||
- **`(assets)/`** - Asset serving
|
||||
|
||||
### Database
|
||||
|
||||
- **Knex.js** for query building and migrations. Supports SQLite (default), PostgreSQL, MySQL
|
||||
- Connection configured via `DATABASE_URL` env var: `sqlite://./path`, `postgresql://...`, `mysql://...`
|
||||
- Migrations in `/migrations/`, seeds in `/seeds/`
|
||||
- Always use the db singleton: `import db from "$lib/server/db/db"`
|
||||
|
||||
### Monitor Services
|
||||
|
||||
Each monitor type has a dedicated implementation in `src/lib/server/services/`:
|
||||
|
||||
- Types: API, Ping, TCP, DNS, SSL, SQL, Heartbeat, GameDig, Group, gRPC, None
|
||||
- Scheduled via `src/lib/server/schedulers/` using `croner`
|
||||
- Job queues managed with **BullMQ** + **Redis** (`src/lib/server/queues/`)
|
||||
|
||||
### Build System
|
||||
|
||||
`npm run build` is a two-step process:
|
||||
|
||||
1. `scripts/build-sveltekit.js` - Vite build of SvelteKit app (optionally with `--with-docs`)
|
||||
2. `scripts/build-server.js` - esbuild bundles `scripts/main.ts` into `build/main.js`
|
||||
|
||||
## Key Conventions
|
||||
|
||||
### Svelte 5 + TypeScript
|
||||
|
||||
- Use **TypeScript** for new/modified code
|
||||
- Use **Svelte 5 runes** (`$state`, `$derived`, `$effect`, `$props()`) in new components
|
||||
- Use generated `$types` for SvelteKit route typing (`import type { PageServerLoad } from './$types'`)
|
||||
- Use `import type { ... }` for type imports
|
||||
|
||||
### UI Components
|
||||
|
||||
- **shadcn-svelte** components in `src/lib/components/ui/`
|
||||
- Import: `import { Button } from "$lib/components/ui/button"`
|
||||
- Styling: **Tailwind CSS v4** with HSL CSS variables for theming
|
||||
|
||||
### Timestamps
|
||||
|
||||
All timestamps are **UTC seconds** (not milliseconds). Use helpers from `src/lib/server/tool.ts`.
|
||||
|
||||
### Status Constants
|
||||
|
||||
Constants are exported as a **default export** from `src/lib/global-constants.ts`:
|
||||
|
||||
```typescript
|
||||
// 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
|
||||
|
||||
// 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/apiController"`
|
||||
|
||||
### Types Location
|
||||
|
||||
- `src/lib/types/` - Shared types (client + server)
|
||||
- `src/lib/server/types/` - Server-only types
|
||||
- `src/lib/client/types/` - Client-only types
|
||||
|
||||
### i18n
|
||||
|
||||
Locale files in `src/lib/locales/`. Add translations by creating `{code}.json` and updating `locales.json`.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Required: `KENER_SECRET_KEY`, `ORIGIN`, `REDIS_URL`
|
||||
Optional: `DATABASE_URL` (defaults to SQLite), `KENER_BASE_PATH`, `PORT` (default 3000), `RESEND_API_KEY`, `RESEND_SENDER_EMAIL`
|
||||
|
||||
## Skills
|
||||
|
||||
Read `.claude/skills/` for specialized instructions on:
|
||||
|
||||
- **svelte-code-writer** - Svelte component creation/editing
|
||||
- **documentation-writer** - Editing docs in `src/routes/(docs)/docs/content/`
|
||||
- **tailwindcss** - Tailwind CSS v4 patterns
|
||||
@@ -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"
|
||||
@@ -164,6 +165,9 @@ COPY --chown=node:node --from=builder /app/src/lib/server/db/seedMonitorData.ts
|
||||
COPY --chown=node:node --from=builder /app/src/lib/server/db/seedPagesData.ts ./src/lib/server/db/seedPagesData.ts
|
||||
COPY --chown=node:node --from=builder /app/src/lib/server/templates/general ./src/lib/server/templates/general
|
||||
|
||||
# Locale JSON files (read at runtime by server-side i18n)
|
||||
COPY --chown=node:node --from=builder /app/src/lib/locales ./src/lib/locales
|
||||
|
||||
# Build output (SvelteKit client/server + esbuild main.js) — changes most often
|
||||
COPY --chown=node:node --from=builder /app/build ./build
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Raj Nandan Sharma
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,10 +1,5 @@
|
||||
# Kener - Stunning Status Pages
|
||||
|
||||
<details>
|
||||
<summary>Upcoming Version 4.0.0</summary>
|
||||
Currently we are working on updating kener to the latest svelte version with typescript
|
||||
</details>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://kener.ing/og.jpg?v=1" width="100%" height="auto" class="rounded-lg shadow-lg" alt="kener example illustration">
|
||||
</p>
|
||||
@@ -25,6 +20,7 @@
|
||||
<a href="https://github.com/rajnandan1/kener/actions/workflows/publish-images.yml"><img alt="GitHub Workflow Status" src="https://img.shields.io/github/actions/workflow/status/rajnandan1/kener/publish-images.yml" /></a>
|
||||
<a href="https://github.com/rajnandan1/kener/commit/HEAD"><img src="https://img.shields.io/github/last-commit/rajnandan1/kener/main" alt="" /></a>
|
||||
<a href="https://github.com/rajnandan1/kener/issues"><img alt="GitHub issues" src="https://img.shields.io/github/issues/rajnandan1/kener.svg" /></a>
|
||||
<a href="https://deepwiki.com/rajnandan1/kener"><img alt="Ask DeepWiki" src="https://deepwiki.com/badge.svg" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -51,6 +47,14 @@
|
||||
| [🌍 Live Server](https://kener.ing) | [🎉 Quick Start](https://kener.ing/docs/v4/getting-started/quick-start) | [🗄 Documentation](https://kener.ing/docs/v4/getting-started/introduction) |
|
||||
| ----------------------------------- | ----------------------------------------------------------------------- | -------------------------------------------------------------------------- |
|
||||
|
||||
<p align="center">
|
||||
|
||||
[](https://railway.com/deploy/spSvic?referralCode=1Pn7vs&utm_medium=integration&utm_source=template&utm_campaign=generic)
|
||||
[](https://zeabur.com/templates/1YRTMI?referralCode=rajnandan1)
|
||||
[](https://render.com/deploy?repo=https%3A%2F%2Fgithub.com%2Frajnandan1%2Fkener)
|
||||
|
||||
</p>
|
||||
|
||||
## What is Kener?
|
||||
|
||||
**Kener** is a sleek and lightweight status page system built with **SvelteKit** and **NodeJS**. It’s not here to replace heavyweights like Datadog or Atlassian but rather to offer a simple, modern, and hassle-free way to set up a great-looking status page with minimal effort.
|
||||
@@ -173,11 +177,7 @@ PORT=3000
|
||||
|
||||
For the full quick start (including local Docker builds and dev mode), see the docs:
|
||||
|
||||
- https://kener.ing/docs/quick-start
|
||||
|
||||
## One Click Deployment
|
||||
|
||||
[](https://railway.com/template/spSvic?referralCode=1Pn7vs)
|
||||
- https://kener.ing/docs/v4/getting-started/quick-start
|
||||
|
||||
## Features
|
||||
|
||||
@@ -207,10 +207,6 @@ Kener combines public status page essentials with advanced admin workflows.
|
||||
- Integrate analytics providers like GA, Plausible, Mixpanel, Umami, and Clarity
|
||||
- Access the full REST API for incidents, monitors, and reporting
|
||||
|
||||
<div align="left">
|
||||
<img alt="Visitor Stats" src="https://widgetbite.com/stats/rajnandan"/>
|
||||
</div>
|
||||
|
||||
## Technologies Used
|
||||
|
||||
- [SvelteKit](https://kit.svelte.dev/)
|
||||
@@ -220,11 +216,9 @@ Kener combines public status page essentials with advanced admin workflows.
|
||||
|
||||
If you’re enjoying Kener and want to support its development, consider sponsoring me on GitHub or treating me to a coffee. Your support helps keep the project growing! 🚀
|
||||
|
||||
[Sponsor Me Using Github](https://github.com/sponsors/rajnandan1)
|
||||
- [Sponsor Me Using GitHub](https://github.com/sponsors/rajnandan1)
|
||||
|
||||
☕ [Buy Me a Coffee](https://www.buymeacoffee.com/rajnandan1)
|
||||
|
||||

|
||||
- [Buy Me a Coffee](https://www.buymeacoffee.com/rajnandan1)
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,19 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema
|
||||
.createTable("monitoring_data", (table) => {
|
||||
if (!(await knex.schema.hasTable("monitoring_data"))) {
|
||||
await knex.schema.createTable("monitoring_data", (table) => {
|
||||
table.string("monitor_tag", 255).notNullable();
|
||||
table.integer("timestamp").notNullable();
|
||||
table.text("status");
|
||||
table.float("latency", 8, 2);
|
||||
table.text("type");
|
||||
table.primary(["monitor_tag", "timestamp"]);
|
||||
})
|
||||
// Create monitor_alerts table
|
||||
.createTable("monitor_alerts", (table) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable("monitor_alerts"))) {
|
||||
await knex.schema.createTable("monitor_alerts", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("monitor_tag", 255).notNullable();
|
||||
table.string("monitor_status", 255).notNullable();
|
||||
@@ -20,18 +22,29 @@ export async function up(knex: Knex): Promise<void> {
|
||||
table.integer("incident_number").defaultTo(0);
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
})
|
||||
// Add index to monitor_alerts table
|
||||
.raw("CREATE INDEX idx_monitor_tag_created_at ON monitor_alerts (monitor_tag, created_at)")
|
||||
.createTable("site_data", (table) => {
|
||||
});
|
||||
}
|
||||
|
||||
// Add index (IF NOT EXISTS not supported by all DBs, so use try/catch)
|
||||
try {
|
||||
await knex.schema.raw("CREATE INDEX idx_monitor_tag_created_at ON monitor_alerts (monitor_tag, created_at)");
|
||||
} catch (_e) {
|
||||
// Index already exists
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable("site_data"))) {
|
||||
await knex.schema.createTable("site_data", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("key", 255).notNullable().unique();
|
||||
table.text("value").notNullable();
|
||||
table.string("data_type", 255).notNullable();
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
})
|
||||
.createTable("monitors", (table) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable("monitors"))) {
|
||||
await knex.schema.createTable("monitors", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("tag", 255).notNullable().unique();
|
||||
table.string("name", 255).notNullable().unique();
|
||||
@@ -50,8 +63,11 @@ export async function up(knex: Knex): Promise<void> {
|
||||
table.string("include_degraded_in_downtime", 255).defaultTo("NO");
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
})
|
||||
.createTable("triggers", (table) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable("triggers"))) {
|
||||
await knex.schema.createTable("triggers", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("name", 255).notNullable().unique();
|
||||
table.string("trigger_type", 255);
|
||||
@@ -60,8 +76,11 @@ export async function up(knex: Knex): Promise<void> {
|
||||
table.text("trigger_meta");
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
})
|
||||
.createTable("users", (table) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable("users"))) {
|
||||
await knex.schema.createTable("users", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("email", 255).notNullable().unique();
|
||||
table.string("name", 255).notNullable();
|
||||
@@ -71,8 +90,11 @@ export async function up(knex: Knex): Promise<void> {
|
||||
table.string("role", 255).defaultTo("user");
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
})
|
||||
.createTable("api_keys", (table) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable("api_keys"))) {
|
||||
await knex.schema.createTable("api_keys", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("name", 255).notNullable().unique();
|
||||
table.string("hashed_key", 255).notNullable().unique();
|
||||
@@ -80,8 +102,11 @@ export async function up(knex: Knex): Promise<void> {
|
||||
table.string("status", 255).defaultTo("ACTIVE");
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
})
|
||||
.createTable("incidents", (table) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable("incidents"))) {
|
||||
await knex.schema.createTable("incidents", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("title", 255).notNullable();
|
||||
table.integer("start_date_time").notNullable();
|
||||
@@ -90,8 +115,11 @@ export async function up(knex: Knex): Promise<void> {
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
table.string("status", 255).defaultTo("ACTIVE");
|
||||
table.string("state", 255).defaultTo("INVESTIGATING");
|
||||
})
|
||||
.createTable("incident_monitors", (table) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable("incident_monitors"))) {
|
||||
await knex.schema.createTable("incident_monitors", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("monitor_tag", 255).notNullable();
|
||||
table.string("monitor_impact", 255);
|
||||
@@ -99,8 +127,11 @@ export async function up(knex: Knex): Promise<void> {
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
table.integer("incident_id").notNullable();
|
||||
table.unique(["monitor_tag", "incident_id"]);
|
||||
})
|
||||
.createTable("incident_comments", (table) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable("incident_comments"))) {
|
||||
await knex.schema.createTable("incident_comments", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.text("comment").notNullable();
|
||||
table.integer("incident_id").notNullable();
|
||||
@@ -110,6 +141,7 @@ export async function up(knex: Knex): Promise<void> {
|
||||
table.string("status", 255).defaultTo("ACTIVE");
|
||||
table.string("state", 255).defaultTo("INVESTIGATING");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable("incidents", function (table) {
|
||||
table.text("incident_type").defaultTo("INCIDENT");
|
||||
});
|
||||
const hasCol = await knex.schema.hasColumn("incidents", "incident_type");
|
||||
if (!hasCol) {
|
||||
await knex.schema.alterTable("incidents", function (table) {
|
||||
table.text("incident_type").defaultTo("INCIDENT");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable("incidents", function (table) {
|
||||
table.text("incident_source").defaultTo("DASHBOARD");
|
||||
});
|
||||
const hasCol = await knex.schema.hasColumn("incidents", "incident_source");
|
||||
if (!hasCol) {
|
||||
await knex.schema.alterTable("incidents", function (table) {
|
||||
table.text("incident_source").defaultTo("DASHBOARD");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable("invitations")) return;
|
||||
|
||||
await knex.schema.createTable("invitations", (table) => {
|
||||
// Primary key
|
||||
table.increments("id").primary();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema
|
||||
.createTable("subscribers", (table) => {
|
||||
if (!(await knex.schema.hasTable("subscribers"))) {
|
||||
await knex.schema.createTable("subscribers", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("subscriber_send").notNullable();
|
||||
table.text("subscriber_meta").nullable();
|
||||
@@ -16,8 +16,11 @@ export async function up(knex: Knex): Promise<void> {
|
||||
|
||||
// Add index on subscriber_send for better query performance
|
||||
table.index(["subscriber_send"]);
|
||||
})
|
||||
.createTable("subscriptions", (table) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable("subscriptions"))) {
|
||||
await knex.schema.createTable("subscriptions", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.integer("subscriber_id").unsigned().notNullable();
|
||||
table.string("subscriptions_status").notNullable();
|
||||
@@ -32,8 +35,11 @@ export async function up(knex: Knex): Promise<void> {
|
||||
|
||||
// Add index to optimize queries filtering by status and monitors
|
||||
table.index(["subscriptions_status", "subscriptions_monitors"]);
|
||||
})
|
||||
.createTable("subscription_triggers", (table) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable("subscription_triggers"))) {
|
||||
await knex.schema.createTable("subscription_triggers", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("subscription_trigger_type").notNullable().unique();
|
||||
table.string("subscription_trigger_status").notNullable();
|
||||
@@ -41,6 +47,7 @@ export async function up(knex: Knex): Promise<void> {
|
||||
table.datetime("created_at").defaultTo(knex.fn.now());
|
||||
table.datetime("updated_at").defaultTo(knex.fn.now());
|
||||
});
|
||||
}
|
||||
}
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable("images")) return;
|
||||
|
||||
await knex.schema.createTable("images", (table) => {
|
||||
table.string("id", 32).primary(); // nanoid generated ID with prefix
|
||||
table.text("data").notNullable(); // base64 encoded image data
|
||||
|
||||
@@ -2,37 +2,49 @@ import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
// Create pages table
|
||||
await knex.schema.createTable("pages", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("page_path", 255).notNullable().unique(); // e.g., "/", "/api", "/infrastructure"
|
||||
table.string("page_title", 255).notNullable();
|
||||
table.string("page_header", 255);
|
||||
table.string("page_subheader", 255);
|
||||
table.string("page_logo", 255);
|
||||
table.text("page_settings_json"); // JSON settings for the page
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
});
|
||||
if (!(await knex.schema.hasTable("pages"))) {
|
||||
await knex.schema.createTable("pages", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("page_path", 255).notNullable().unique(); // e.g., "/", "/api", "/infrastructure"
|
||||
table.string("page_title", 255).notNullable();
|
||||
table.string("page_header", 255);
|
||||
table.string("page_subheader", 255);
|
||||
table.string("page_logo", 255);
|
||||
table.text("page_settings_json"); // JSON settings for the page
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
});
|
||||
}
|
||||
|
||||
// Create pages_monitors junction table
|
||||
await knex.schema.createTable("pages_monitors", (table) => {
|
||||
table.integer("page_id").unsigned().notNullable();
|
||||
table.string("monitor_tag", 255).notNullable();
|
||||
table.text("monitor_settings_json"); // JSON settings for monitor on this page (e.g., order, visibility)
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
if (!(await knex.schema.hasTable("pages_monitors"))) {
|
||||
await knex.schema.createTable("pages_monitors", (table) => {
|
||||
table.integer("page_id").unsigned().notNullable();
|
||||
table.string("monitor_tag", 255).notNullable();
|
||||
table.text("monitor_settings_json"); // JSON settings for monitor on this page (e.g., order, visibility)
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
|
||||
// Composite primary key
|
||||
table.primary(["page_id", "monitor_tag"]);
|
||||
// Composite primary key
|
||||
table.primary(["page_id", "monitor_tag"]);
|
||||
|
||||
// Foreign key constraints
|
||||
table.foreign("page_id").references("id").inTable("pages").onDelete("CASCADE");
|
||||
table.foreign("monitor_tag").references("tag").inTable("monitors").onDelete("CASCADE");
|
||||
});
|
||||
// Foreign key constraints
|
||||
table.foreign("page_id").references("id").inTable("pages").onDelete("CASCADE");
|
||||
table.foreign("monitor_tag").references("tag").inTable("monitors").onDelete("CASCADE");
|
||||
});
|
||||
}
|
||||
|
||||
// Add index for faster lookups
|
||||
await knex.schema.raw("CREATE INDEX idx_pages_monitors_page_id ON pages_monitors (page_id)");
|
||||
await knex.schema.raw("CREATE INDEX idx_pages_monitors_monitor_tag ON pages_monitors (monitor_tag)");
|
||||
// Add indexes (safe to fail if they already exist)
|
||||
try {
|
||||
await knex.schema.raw("CREATE INDEX idx_pages_monitors_page_id ON pages_monitors (page_id)");
|
||||
} catch (_e) {
|
||||
/* index already exists */
|
||||
}
|
||||
try {
|
||||
await knex.schema.raw("CREATE INDEX idx_pages_monitors_monitor_tag ON pages_monitors (monitor_tag)");
|
||||
} catch (_e) {
|
||||
/* index already exists */
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
|
||||
@@ -1,64 +1,69 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
// Create maintenances table - defines maintenance schedules using iCalendar RRULE
|
||||
// RRULE examples:
|
||||
// - ONE_TIME: FREQ=MINUTELY;COUNT=1 (single occurrence)
|
||||
// - RECURRING: FREQ=WEEKLY;BYDAY=SU;BYHOUR=2;BYMINUTE=0 (every Sunday at 2 AM)
|
||||
// Reference: http://www.kanzaki.com/docs/ical/rrule.html
|
||||
await knex.schema.createTable("maintenances", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("title", 255).notNullable();
|
||||
table.text("description").nullable(); // Maintenance details/description
|
||||
table.integer("start_date_time").notNullable(); // Unix timestamp - when the first occurrence starts
|
||||
table.string("rrule", 500).notNullable(); // iCalendar RRULE string (e.g., FREQ=WEEKLY;BYDAY=SU)
|
||||
table.integer("duration_seconds").notNullable(); // Duration of each maintenance window in seconds
|
||||
table.string("status", 50).notNullable().defaultTo("ACTIVE"); // ACTIVE or INACTIVE
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
});
|
||||
if (!(await knex.schema.hasTable("maintenances"))) {
|
||||
await knex.schema.createTable("maintenances", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("title", 255).notNullable();
|
||||
table.text("description").nullable();
|
||||
table.integer("start_date_time").notNullable();
|
||||
table.string("rrule", 500).notNullable();
|
||||
table.integer("duration_seconds").notNullable();
|
||||
table.string("status", 50).notNullable().defaultTo("ACTIVE");
|
||||
table.string("is_global", 15).notNullable().defaultTo("YES");
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
});
|
||||
}
|
||||
|
||||
// Create maintenance_monitors junction table - links monitors to maintenance schedules
|
||||
await knex.schema.createTable("maintenance_monitors", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.integer("maintenance_id").unsigned().notNullable();
|
||||
table.string("monitor_tag", 255).notNullable();
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
if (!(await knex.schema.hasTable("maintenance_monitors"))) {
|
||||
await knex.schema.createTable("maintenance_monitors", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.integer("maintenance_id").unsigned().notNullable();
|
||||
table.string("monitor_tag", 255).notNullable();
|
||||
table.string("monitor_impact").defaultTo("MAINTENANCE").notNullable();
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
|
||||
// Foreign key constraints
|
||||
table.foreign("maintenance_id").references("id").inTable("maintenances").onDelete("CASCADE");
|
||||
table.foreign("monitor_tag").references("tag").inTable("monitors").onDelete("CASCADE");
|
||||
table.foreign("maintenance_id").references("id").inTable("maintenances").onDelete("CASCADE");
|
||||
table.foreign("monitor_tag").references("tag").inTable("monitors").onDelete("CASCADE");
|
||||
|
||||
// Unique constraint to prevent duplicate monitor assignments
|
||||
table.unique(["maintenance_id", "monitor_tag"]);
|
||||
});
|
||||
table.unique(["maintenance_id", "monitor_tag"]);
|
||||
});
|
||||
}
|
||||
|
||||
// Create maintenances_events table - actual maintenance occurrences (generated by job)
|
||||
await knex.schema.createTable("maintenances_events", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.integer("maintenance_id").unsigned().notNullable();
|
||||
table.integer("start_date_time").notNullable(); // Unix timestamp
|
||||
table.integer("end_date_time").notNullable(); // Unix timestamp
|
||||
table.string("status", 50).notNullable().defaultTo("SCHEDULED"); // SCHEDULED, IN_PROGRESS, COMPLETED, CANCELLED
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
if (!(await knex.schema.hasTable("maintenances_events"))) {
|
||||
await knex.schema.createTable("maintenances_events", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.integer("maintenance_id").unsigned().notNullable();
|
||||
table.integer("start_date_time").notNullable();
|
||||
table.integer("end_date_time").notNullable();
|
||||
table.string("status", 50).notNullable().defaultTo("SCHEDULED");
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
|
||||
// Foreign key constraint
|
||||
table.foreign("maintenance_id").references("id").inTable("maintenances").onDelete("CASCADE");
|
||||
});
|
||||
table.foreign("maintenance_id").references("id").inTable("maintenances").onDelete("CASCADE");
|
||||
});
|
||||
}
|
||||
|
||||
// Add indexes for faster lookups
|
||||
await knex.schema.raw("CREATE INDEX idx_maintenances_status ON maintenances (status)");
|
||||
await knex.schema.raw("CREATE INDEX idx_maintenances_start_time ON maintenances (start_date_time)");
|
||||
await knex.schema.raw(
|
||||
// Add indexes (safe to fail if they already exist)
|
||||
const indexes = [
|
||||
"CREATE INDEX idx_maintenances_status ON maintenances (status)",
|
||||
"CREATE INDEX idx_maintenances_start_time ON maintenances (start_date_time)",
|
||||
"CREATE INDEX idx_maintenance_monitors_maintenance_id ON maintenance_monitors (maintenance_id)",
|
||||
);
|
||||
await knex.schema.raw("CREATE INDEX idx_maintenance_monitors_monitor_tag ON maintenance_monitors (monitor_tag)");
|
||||
await knex.schema.raw("CREATE INDEX idx_maintenances_events_maintenance_id ON maintenances_events (maintenance_id)");
|
||||
await knex.schema.raw("CREATE INDEX idx_maintenances_events_status ON maintenances_events (status)");
|
||||
await knex.schema.raw("CREATE INDEX idx_maintenances_events_start_time ON maintenances_events (start_date_time)");
|
||||
await knex.schema.raw("CREATE INDEX idx_maintenances_events_end_time ON maintenances_events (end_date_time)");
|
||||
"CREATE INDEX idx_maintenance_monitors_monitor_tag ON maintenance_monitors (monitor_tag)",
|
||||
"CREATE INDEX idx_maintenances_events_maintenance_id ON maintenances_events (maintenance_id)",
|
||||
"CREATE INDEX idx_maintenances_events_status ON maintenances_events (status)",
|
||||
"CREATE INDEX idx_maintenances_events_start_time ON maintenances_events (start_date_time)",
|
||||
"CREATE INDEX idx_maintenances_events_end_time ON maintenances_events (end_date_time)",
|
||||
];
|
||||
for (const sql of indexes) {
|
||||
try {
|
||||
await knex.schema.raw(sql);
|
||||
} catch (_e) {
|
||||
/* index already exists */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
// Remove unique constraint from monitors.name
|
||||
await knex.schema.alterTable("monitors", (table) => {
|
||||
table.dropUnique(["name"]);
|
||||
});
|
||||
// Remove unique constraint from monitors.name (safe to fail if already dropped)
|
||||
try {
|
||||
await knex.schema.alterTable("monitors", (table) => {
|
||||
table.dropUnique(["name"]);
|
||||
});
|
||||
} catch (_e) {
|
||||
// Constraint already removed
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable("monitors", (table) => {
|
||||
table.string("is_hidden").defaultTo("NO").notNullable();
|
||||
});
|
||||
const hasCol = await knex.schema.hasColumn("monitors", "is_hidden");
|
||||
if (!hasCol) {
|
||||
await knex.schema.alterTable("monitors", (table) => {
|
||||
table.string("is_hidden").defaultTo("NO").notNullable();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable("monitors", (table) => {
|
||||
table.text("monitor_settings_json").nullable();
|
||||
});
|
||||
const hasCol = await knex.schema.hasColumn("monitors", "monitor_settings_json");
|
||||
if (!hasCol) {
|
||||
await knex.schema.alterTable("monitors", (table) => {
|
||||
table.text("monitor_settings_json").nullable();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable("maintenance_monitors", (table) => {
|
||||
table.string("monitor_impact").defaultTo("MAINTENANCE").notNullable();
|
||||
});
|
||||
if (!(await knex.schema.hasTable("maintenance_monitors"))) return;
|
||||
const hasCol = await knex.schema.hasColumn("maintenance_monitors", "monitor_impact");
|
||||
if (!hasCol) {
|
||||
await knex.schema.alterTable("maintenance_monitors", (table) => {
|
||||
table.string("monitor_impact").defaultTo("MAINTENANCE").notNullable();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable("maintenance_monitors"))) return;
|
||||
await knex.schema.alterTable("maintenance_monitors", (table) => {
|
||||
table.dropColumn("monitor_impact");
|
||||
});
|
||||
|
||||
@@ -2,42 +2,54 @@ import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
// Create monitor_alerts_config table
|
||||
await knex.schema.createTable("monitor_alerts_config", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("monitor_tag", 255).notNullable();
|
||||
table.string("alert_for", 50).notNullable(); // STATUS, LATENCY, UPTIME
|
||||
table.string("alert_value", 255).notNullable(); // DOWN, DEGRADED, or numeric value like "1000" or "99"
|
||||
table.integer("failure_threshold").notNullable().defaultTo(1);
|
||||
table.integer("success_threshold").notNullable().defaultTo(1);
|
||||
table.text("alert_description");
|
||||
table.string("create_incident", 10).notNullable().defaultTo("NO"); // YES or NO
|
||||
table.string("is_active", 10).notNullable().defaultTo("YES"); // YES or NO
|
||||
table.string("severity", 50).notNullable().defaultTo("WARNING"); // CRITICAL or WARNING
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
if (!(await knex.schema.hasTable("monitor_alerts_config"))) {
|
||||
await knex.schema.createTable("monitor_alerts_config", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("monitor_tag", 255).notNullable();
|
||||
table.string("alert_for", 50).notNullable(); // STATUS, LATENCY, UPTIME
|
||||
table.string("alert_value", 255).notNullable(); // DOWN, DEGRADED, or numeric value like "1000" or "99"
|
||||
table.integer("failure_threshold").notNullable().defaultTo(1);
|
||||
table.integer("success_threshold").notNullable().defaultTo(1);
|
||||
table.text("alert_description");
|
||||
table.string("create_incident", 10).notNullable().defaultTo("NO"); // YES or NO
|
||||
table.string("is_active", 10).notNullable().defaultTo("YES"); // YES or NO
|
||||
table.string("severity", 50).notNullable().defaultTo("WARNING"); // CRITICAL or WARNING
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
|
||||
// Foreign key to monitors table
|
||||
table.foreign("monitor_tag").references("tag").inTable("monitors").onDelete("CASCADE");
|
||||
});
|
||||
// Foreign key to monitors table
|
||||
table.foreign("monitor_tag").references("tag").inTable("monitors").onDelete("CASCADE");
|
||||
});
|
||||
}
|
||||
|
||||
// Create index for faster lookups
|
||||
await knex.raw("CREATE INDEX idx_monitor_alerts_config_monitor_tag ON monitor_alerts_config (monitor_tag)");
|
||||
await knex.raw("CREATE INDEX idx_monitor_alerts_config_is_active ON monitor_alerts_config (is_active)");
|
||||
// Create indexes (safe to fail if they already exist)
|
||||
try {
|
||||
await knex.raw("CREATE INDEX idx_monitor_alerts_config_monitor_tag ON monitor_alerts_config (monitor_tag)");
|
||||
} catch (_e) {
|
||||
/* index already exists */
|
||||
}
|
||||
try {
|
||||
await knex.raw("CREATE INDEX idx_monitor_alerts_config_is_active ON monitor_alerts_config (is_active)");
|
||||
} catch (_e) {
|
||||
/* index already exists */
|
||||
}
|
||||
|
||||
// Create monitor_alerts_config_triggers junction table
|
||||
await knex.schema.createTable("monitor_alerts_config_triggers", (table) => {
|
||||
table.integer("monitor_alerts_id").unsigned().notNullable();
|
||||
table.integer("trigger_id").unsigned().notNullable();
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
if (!(await knex.schema.hasTable("monitor_alerts_config_triggers"))) {
|
||||
await knex.schema.createTable("monitor_alerts_config_triggers", (table) => {
|
||||
table.integer("monitor_alerts_id").unsigned().notNullable();
|
||||
table.integer("trigger_id").unsigned().notNullable();
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
|
||||
// Composite primary key
|
||||
table.primary(["monitor_alerts_id", "trigger_id"]);
|
||||
// Composite primary key
|
||||
table.primary(["monitor_alerts_id", "trigger_id"]);
|
||||
|
||||
// Foreign keys
|
||||
table.foreign("monitor_alerts_id").references("id").inTable("monitor_alerts_config").onDelete("CASCADE");
|
||||
table.foreign("trigger_id").references("id").inTable("triggers").onDelete("CASCADE");
|
||||
});
|
||||
// Foreign keys
|
||||
table.foreign("monitor_alerts_id").references("id").inTable("monitor_alerts_config").onDelete("CASCADE");
|
||||
table.foreign("trigger_id").references("id").inTable("triggers").onDelete("CASCADE");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
|
||||
@@ -1,19 +1,56 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.createTable("monitor_alerts_v2", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.integer("config_id").references("id").inTable("monitor_alerts_config").notNullable().onDelete("CASCADE");
|
||||
table.integer("incident_id").references("id").inTable("incidents").nullable().onDelete("SET NULL");
|
||||
table.string("alert_status", 255).notNullable();
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
if (!(await knex.schema.hasTable("monitor_alerts_v2"))) {
|
||||
await knex.schema.createTable("monitor_alerts_v2", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.integer("config_id").unsigned().notNullable();
|
||||
table.integer("incident_id").unsigned().nullable();
|
||||
table.string("alert_status", 255).notNullable();
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
|
||||
// Add index for faster queries on config_id and alert_status
|
||||
table.index(["config_id", "alert_status"]);
|
||||
});
|
||||
// Add index for faster queries on config_id and alert_status
|
||||
table.index(["config_id", "alert_status"]);
|
||||
});
|
||||
}
|
||||
|
||||
// Ensure config_id is unsigned (fix for MySQL users who had signed int from a prior failed run)
|
||||
try {
|
||||
await knex.schema.alterTable("monitor_alerts_v2", (table) => {
|
||||
table.integer("config_id").unsigned().notNullable().alter();
|
||||
});
|
||||
} catch (_e) {
|
||||
/* column may already be correct */
|
||||
}
|
||||
|
||||
// Ensure incident_id is unsigned
|
||||
try {
|
||||
await knex.schema.alterTable("monitor_alerts_v2", (table) => {
|
||||
table.integer("incident_id").unsigned().nullable().alter();
|
||||
});
|
||||
} catch (_e) {
|
||||
/* column may already be correct */
|
||||
}
|
||||
|
||||
// Add foreign key constraints (skip if they already exist)
|
||||
try {
|
||||
await knex.schema.alterTable("monitor_alerts_v2", (table) => {
|
||||
table.foreign("config_id").references("id").inTable("monitor_alerts_config").onDelete("CASCADE");
|
||||
});
|
||||
} catch (_e) {
|
||||
/* foreign key may already exist */
|
||||
}
|
||||
|
||||
try {
|
||||
await knex.schema.alterTable("monitor_alerts_v2", (table) => {
|
||||
table.foreign("incident_id").references("id").inTable("incidents").onDelete("SET NULL");
|
||||
});
|
||||
} catch (_e) {
|
||||
/* foreign key may already exist */
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTable("monitor_alerts_v2");
|
||||
await knex.schema.dropTableIfExists("monitor_alerts_v2");
|
||||
}
|
||||
|
||||
@@ -2,93 +2,138 @@ import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
// 1. Create subscriber_users table - the actual user identity
|
||||
await knex.schema.createTable("subscriber_users", (table) => {
|
||||
table.increments("id").primary();
|
||||
if (!(await knex.schema.hasTable("subscriber_users"))) {
|
||||
await knex.schema.createTable("subscriber_users", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("email", 255).notNullable().unique();
|
||||
table.string("status", 20).notNullable().defaultTo("PENDING");
|
||||
table.string("verification_code", 10).nullable();
|
||||
table.timestamp("verification_expires_at").nullable();
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
table.index(["status"]);
|
||||
table.index(["email"]);
|
||||
});
|
||||
}
|
||||
|
||||
// Email is the primary identifier for users
|
||||
table.string("email", 255).notNullable().unique();
|
||||
// 2. Create subscriber_methods table
|
||||
if (!(await knex.schema.hasTable("subscriber_methods"))) {
|
||||
await knex.schema.createTable("subscriber_methods", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.integer("subscriber_user_id").unsigned().notNullable();
|
||||
table.string("method_type", 50).notNullable();
|
||||
table.string("method_value", 500).notNullable();
|
||||
table.string("status", 20).notNullable().defaultTo("ACTIVE");
|
||||
table.text("meta").nullable();
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
});
|
||||
}
|
||||
|
||||
// User status: PENDING (awaiting verification), ACTIVE, INACTIVE
|
||||
table.string("status", 20).notNullable().defaultTo("PENDING");
|
||||
// Add indexes, unique constraints, and foreign keys for subscriber_methods (idempotent)
|
||||
try {
|
||||
await knex.schema.alterTable("subscriber_methods", (table) => {
|
||||
table.index(["subscriber_user_id"]);
|
||||
});
|
||||
} catch (_e) {
|
||||
/* index already exists */
|
||||
}
|
||||
try {
|
||||
await knex.schema.alterTable("subscriber_methods", (table) => {
|
||||
table.index(["method_type"]);
|
||||
});
|
||||
} catch (_e) {
|
||||
/* index already exists */
|
||||
}
|
||||
try {
|
||||
await knex.schema.alterTable("subscriber_methods", (table) => {
|
||||
table.index(["status"]);
|
||||
});
|
||||
} catch (_e) {
|
||||
/* index already exists */
|
||||
}
|
||||
try {
|
||||
await knex.schema.alterTable("subscriber_methods", (table) => {
|
||||
table.unique(["subscriber_user_id", "method_type", "method_value"], {
|
||||
indexName: "sub_methods_user_type_value_unique",
|
||||
});
|
||||
});
|
||||
} catch (_e) {
|
||||
/* unique constraint already exists */
|
||||
}
|
||||
try {
|
||||
await knex.schema.alterTable("subscriber_methods", (table) => {
|
||||
table.foreign("subscriber_user_id").references("id").inTable("subscriber_users").onDelete("CASCADE");
|
||||
});
|
||||
} catch (_e) {
|
||||
/* foreign key already exists */
|
||||
}
|
||||
|
||||
// Verification code for email verification (6 digit)
|
||||
table.string("verification_code", 10).nullable();
|
||||
table.timestamp("verification_expires_at").nullable();
|
||||
// 3. Create user_subscriptions_v2 table
|
||||
if (!(await knex.schema.hasTable("user_subscriptions_v2"))) {
|
||||
await knex.schema.createTable("user_subscriptions_v2", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.integer("subscriber_user_id").unsigned().notNullable();
|
||||
table.integer("subscriber_method_id").unsigned().notNullable();
|
||||
table.string("event_type", 50).notNullable();
|
||||
table.string("status", 20).notNullable().defaultTo("ACTIVE");
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
});
|
||||
}
|
||||
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
|
||||
// Indexes
|
||||
table.index(["status"]);
|
||||
table.index(["email"]);
|
||||
});
|
||||
|
||||
// 2. Create subscriber_methods table - methods a user has configured
|
||||
await knex.schema.createTable("subscriber_methods", (table) => {
|
||||
table.increments("id").primary();
|
||||
|
||||
// Link to subscriber_user
|
||||
table.integer("subscriber_user_id").unsigned().notNullable();
|
||||
|
||||
// Method type: email, webhook, slack, discord
|
||||
table.string("method_type", 50).notNullable();
|
||||
|
||||
// Method value: email address, webhook URL, slack webhook, discord webhook
|
||||
table.string("method_value", 500).notNullable();
|
||||
|
||||
// Status: ACTIVE, INACTIVE
|
||||
table.string("status", 20).notNullable().defaultTo("ACTIVE");
|
||||
|
||||
// For webhook methods, we might want to store additional config
|
||||
table.text("meta").nullable(); // JSON for extra config like headers
|
||||
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
|
||||
// Indexes
|
||||
table.index(["subscriber_user_id"]);
|
||||
table.index(["method_type"]);
|
||||
table.index(["status"]);
|
||||
|
||||
// Unique: one method type per value per user (can't have same webhook twice)
|
||||
table.unique(["subscriber_user_id", "method_type", "method_value"]);
|
||||
|
||||
// Foreign key
|
||||
table.foreign("subscriber_user_id").references("id").inTable("subscriber_users").onDelete("CASCADE");
|
||||
});
|
||||
|
||||
// 3. Create user_subscriptions_v2 table - what a user subscribes to
|
||||
await knex.schema.createTable("user_subscriptions_v2", (table) => {
|
||||
table.increments("id").primary();
|
||||
|
||||
// Link to subscriber_user
|
||||
table.integer("subscriber_user_id").unsigned().notNullable();
|
||||
|
||||
// Link to subscriber_method (which method to use for this subscription)
|
||||
table.integer("subscriber_method_id").unsigned().notNullable();
|
||||
|
||||
// What event type: incidents, maintenance
|
||||
table.string("event_type", 50).notNullable();
|
||||
|
||||
// Status: ACTIVE, INACTIVE
|
||||
table.string("status", 20).notNullable().defaultTo("ACTIVE");
|
||||
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
|
||||
// Indexes
|
||||
table.index(["subscriber_user_id"]);
|
||||
table.index(["subscriber_method_id"]);
|
||||
table.index(["event_type"]);
|
||||
table.index(["status"]);
|
||||
|
||||
// Unique: one subscription per user-method-event-entity
|
||||
table.unique(["subscriber_user_id", "subscriber_method_id", "event_type"]);
|
||||
|
||||
// Foreign keys
|
||||
table.foreign("subscriber_user_id").references("id").inTable("subscriber_users").onDelete("CASCADE");
|
||||
table.foreign("subscriber_method_id").references("id").inTable("subscriber_methods").onDelete("CASCADE");
|
||||
});
|
||||
// Add indexes, unique constraints, and foreign keys for user_subscriptions_v2 (idempotent)
|
||||
try {
|
||||
await knex.schema.alterTable("user_subscriptions_v2", (table) => {
|
||||
table.index(["subscriber_user_id"]);
|
||||
});
|
||||
} catch (_e) {
|
||||
/* index already exists */
|
||||
}
|
||||
try {
|
||||
await knex.schema.alterTable("user_subscriptions_v2", (table) => {
|
||||
table.index(["subscriber_method_id"]);
|
||||
});
|
||||
} catch (_e) {
|
||||
/* index already exists */
|
||||
}
|
||||
try {
|
||||
await knex.schema.alterTable("user_subscriptions_v2", (table) => {
|
||||
table.index(["event_type"]);
|
||||
});
|
||||
} catch (_e) {
|
||||
/* index already exists */
|
||||
}
|
||||
try {
|
||||
await knex.schema.alterTable("user_subscriptions_v2", (table) => {
|
||||
table.index(["status"]);
|
||||
});
|
||||
} catch (_e) {
|
||||
/* index already exists */
|
||||
}
|
||||
try {
|
||||
await knex.schema.alterTable("user_subscriptions_v2", (table) => {
|
||||
table.unique(["subscriber_user_id", "subscriber_method_id", "event_type"], {
|
||||
indexName: "sub_v2_user_method_event_unique",
|
||||
});
|
||||
});
|
||||
} catch (_e) {
|
||||
/* unique constraint already exists */
|
||||
}
|
||||
try {
|
||||
await knex.schema.alterTable("user_subscriptions_v2", (table) => {
|
||||
table.foreign("subscriber_user_id").references("id").inTable("subscriber_users").onDelete("CASCADE");
|
||||
});
|
||||
} catch (_e) {
|
||||
/* foreign key already exists */
|
||||
}
|
||||
try {
|
||||
await knex.schema.alterTable("user_subscriptions_v2", (table) => {
|
||||
table.foreign("subscriber_method_id").references("id").inTable("subscriber_methods").onDelete("CASCADE");
|
||||
});
|
||||
} catch (_e) {
|
||||
/* foreign key already exists */
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable("general_email_templates")) return;
|
||||
|
||||
await knex.schema.createTable("general_email_templates", (table) => {
|
||||
table.string("template_id").primary();
|
||||
table.string("template_subject");
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable("monitors", (table) => {
|
||||
table.text("external_url").nullable();
|
||||
});
|
||||
await knex.schema.alterTable("monitoring_data", (table) => {
|
||||
table.text("error_message").nullable();
|
||||
});
|
||||
if (!(await knex.schema.hasColumn("monitors", "external_url"))) {
|
||||
await knex.schema.alterTable("monitors", (table) => {
|
||||
table.text("external_url").nullable();
|
||||
});
|
||||
}
|
||||
if (!(await knex.schema.hasColumn("monitoring_data", "error_message"))) {
|
||||
await knex.schema.alterTable("monitoring_data", (table) => {
|
||||
table.text("error_message").nullable();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable("monitoring_data", (table) => {
|
||||
table.index(["timestamp"], "idx_monitoring_data_timestamp");
|
||||
table.index(["monitor_tag", "type", "timestamp"], "idx_monitoring_data_monitor_tag_type_timestamp");
|
||||
});
|
||||
try {
|
||||
await knex.schema.alterTable("monitoring_data", (table) => {
|
||||
table.index(["timestamp"], "idx_monitoring_data_timestamp");
|
||||
});
|
||||
} catch (_e) {
|
||||
/* index already exists */
|
||||
}
|
||||
try {
|
||||
await knex.schema.alterTable("monitoring_data", (table) => {
|
||||
table.index(["monitor_tag", "type", "timestamp"], "idx_monitoring_data_monitor_tag_type_timestamp");
|
||||
});
|
||||
} catch (_e) {
|
||||
/* index already exists */
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.table("incidents", (table) => {
|
||||
table.string("is_global", 15).notNullable().defaultTo("YES");
|
||||
});
|
||||
await knex.schema.table("maintenances", (table) => {
|
||||
table.string("is_global", 15).notNullable().defaultTo("YES");
|
||||
});
|
||||
if (!(await knex.schema.hasColumn("incidents", "is_global"))) {
|
||||
await knex.schema.table("incidents", (table) => {
|
||||
table.string("is_global", 15).notNullable().defaultTo("YES");
|
||||
});
|
||||
}
|
||||
if ((await knex.schema.hasTable("maintenances")) && !(await knex.schema.hasColumn("maintenances", "is_global"))) {
|
||||
await knex.schema.table("maintenances", (table) => {
|
||||
table.string("is_global", 15).notNullable().defaultTo("YES");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.table("incidents", (table) => {
|
||||
table.dropColumn("is_global");
|
||||
});
|
||||
await knex.schema.table("maintenances", (table) => {
|
||||
table.dropColumn("is_global");
|
||||
});
|
||||
if (await knex.schema.hasColumn("incidents", "is_global")) {
|
||||
await knex.schema.table("incidents", (table) => {
|
||||
table.dropColumn("is_global");
|
||||
});
|
||||
}
|
||||
if ((await knex.schema.hasTable("maintenances")) && (await knex.schema.hasColumn("maintenances", "is_global"))) {
|
||||
await knex.schema.table("maintenances", (table) => {
|
||||
table.dropColumn("is_global");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasCol = await knex.schema.hasColumn("users", "is_owner");
|
||||
if (!hasCol) {
|
||||
await knex.schema.alterTable("users", (table) => {
|
||||
table.string("is_owner").defaultTo("NO").notNullable();
|
||||
});
|
||||
|
||||
// Set the first user (by id) as owner, if any users exist.
|
||||
// This only runs when the column has just been added to avoid
|
||||
// overwriting an existing owner on migration re-run.
|
||||
const firstUser = await knex("users").orderBy("id", "asc").first();
|
||||
if (firstUser) {
|
||||
await knex("users").where("id", firstUser.id).update({ is_owner: "YES" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasCol = await knex.schema.hasColumn("users", "is_owner");
|
||||
if (hasCol) {
|
||||
await knex.schema.alterTable("users", (table) => {
|
||||
table.dropColumn("is_owner");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable("pages", (table) => {
|
||||
table.text("page_subheader").alter();
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable("pages", (table) => {
|
||||
table.string("page_subheader", 255).alter();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasColumn = await knex.schema.hasColumn("pages_monitors", "position");
|
||||
if (!hasColumn) {
|
||||
await knex.schema.alterTable("pages_monitors", (table) => {
|
||||
table.integer("position").unsigned().notNullable().defaultTo(0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasColumn = await knex.schema.hasColumn("pages_monitors", "position");
|
||||
if (hasColumn) {
|
||||
await knex.schema.alterTable("pages_monitors", (table) => {
|
||||
table.dropColumn("position");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
+405
-293
File diff suppressed because it is too large
Load Diff
+19
-7
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "kener",
|
||||
"version": "4.0.1",
|
||||
"version": "4.0.21",
|
||||
"type": "module",
|
||||
"private": false,
|
||||
"license": "MIT",
|
||||
@@ -40,10 +40,10 @@
|
||||
"devschedule": "vite-node src/lib/server/startup.ts",
|
||||
"generate-readme": "node scripts/generate-readme.js",
|
||||
"index-docs": "vite-node scripts/index-docs.ts",
|
||||
"migrate": "npx knex migrate:latest",
|
||||
"migrate": "vite-node scripts/fix-migration-ext.ts && npx knex migrate:latest",
|
||||
"predev": "npm run seed",
|
||||
"prepare": "svelte-kit sync || echo ''",
|
||||
"preseed": "npx knex migrate:latest",
|
||||
"preseed": "vite-node scripts/fix-migration-ext.ts && npx knex migrate:latest",
|
||||
"prettify": "prettier --write .",
|
||||
"preview": "vite preview",
|
||||
"schedule": "vite-node src/lib/server/startup.ts",
|
||||
@@ -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,7 +64,7 @@
|
||||
"@types/d3-shape": "^3.1.8",
|
||||
"@types/dns2": "^2.0.10",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/heic-convert": "^2.1.0",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/mustache": "^4.2.6",
|
||||
"@types/node": "^25.0.3",
|
||||
@@ -80,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",
|
||||
@@ -113,6 +113,8 @@
|
||||
"@codemirror/state": "^6.5.4",
|
||||
"@codemirror/view": "^6.39.11",
|
||||
"@formkit/auto-animate": "^0.9.0",
|
||||
"@grpc/grpc-js": "^1.14.3",
|
||||
"@grpc/proto-loader": "^0.8.0",
|
||||
"@humanspeak/svelte-purify": "^0.0.6",
|
||||
"@number-flow/svelte": "^0.3.9",
|
||||
"@scalar/express-api-reference": "^0.8.28",
|
||||
@@ -136,9 +138,9 @@
|
||||
"figlet": "^1.9.4",
|
||||
"flexsearch": "^0.8.212",
|
||||
"front-matter": "^4.0.2",
|
||||
"fs-extra": "^11.3.2",
|
||||
"gamedig": "^5.3.2",
|
||||
"glob": "^13.0.6",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
services:
|
||||
- type: web
|
||||
name: kener
|
||||
runtime: image
|
||||
image:
|
||||
url: docker.io/rajnandan1/kener:latest
|
||||
envVars:
|
||||
- key: DATABASE_URL
|
||||
fromDatabase:
|
||||
name: kener-db
|
||||
property: connectionString
|
||||
- key: KENER_SECRET_KEY
|
||||
generateValue: true
|
||||
- key: ORIGIN
|
||||
fromService:
|
||||
type: web
|
||||
name: kener
|
||||
envVarKey: RENDER_EXTERNAL_URL
|
||||
- key: REDIS_URL
|
||||
fromService:
|
||||
name: kener-redis
|
||||
type: keyvalue
|
||||
property: connectionString
|
||||
- type: keyvalue
|
||||
name: kener-redis
|
||||
plan: starter
|
||||
ipAllowList:
|
||||
- source: 0.0.0.0/0
|
||||
description: everywhere
|
||||
maxmemoryPolicy: allkeys-lru
|
||||
|
||||
databases:
|
||||
- name: kener-db
|
||||
plan: basic-256mb
|
||||
databaseName: kener
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Renames .js migration entries to .ts in the knex_migrations table.
|
||||
* This is needed because migration files were renamed from .js to .ts,
|
||||
* but existing databases still reference the old .js filenames.
|
||||
*
|
||||
* Idempotent — safe to run multiple times.
|
||||
*/
|
||||
import knex from "knex";
|
||||
import knexOb from "../knexfile.js";
|
||||
|
||||
const db = knex(knexOb);
|
||||
|
||||
async function fixMigrationExtensions() {
|
||||
try {
|
||||
const hasTable = await db.schema.hasTable("knex_migrations");
|
||||
if (!hasTable) {
|
||||
console.log("No knex_migrations table found, skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
const oldJsMigrations = await db("knex_migrations").where("name", "like", "%.js");
|
||||
if (oldJsMigrations.length === 0) {
|
||||
console.log("No .js migration entries found, nothing to rename.");
|
||||
return;
|
||||
}
|
||||
|
||||
for (const row of oldJsMigrations) {
|
||||
const newName = row.name.replace(/\.js$/, ".ts");
|
||||
await db("knex_migrations").where("id", row.id).update({ name: newName });
|
||||
console.log(`Renamed: ${row.name} -> ${newName}`);
|
||||
}
|
||||
|
||||
console.log(`Fixed ${oldJsMigrations.length} migration record(s).`);
|
||||
} catch (err) {
|
||||
console.error("Error fixing migration extensions:", err);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await db.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
fixMigrationExtensions();
|
||||
@@ -47,7 +47,8 @@ interface DocsSidebarGroup {
|
||||
|
||||
interface DocsNavTab {
|
||||
name: string;
|
||||
sidebar: DocsSidebarGroup[];
|
||||
url?: string;
|
||||
sidebar?: DocsSidebarGroup[];
|
||||
}
|
||||
|
||||
interface DocsVersion {
|
||||
@@ -164,14 +165,16 @@ async function main(): Promise<void> {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const primaryTabSidebar = latestVersion.content.navigation?.tabs?.[0]?.sidebar ?? [];
|
||||
const sidebar = normalizeSidebar(primaryTabSidebar);
|
||||
const tabs = latestVersion.content.navigation?.tabs ?? [];
|
||||
const documents: DocsSearchDocument[] = [];
|
||||
|
||||
// Collect all pages from sidebar
|
||||
// Collect all pages from all tabs' sidebars
|
||||
const allPages: Array<{ page: DocsPageSource; group: string }> = [];
|
||||
for (const sidebarGroup of sidebar) {
|
||||
collectPages(sidebarGroup.pages, sidebarGroup.group, allPages);
|
||||
for (const tab of tabs) {
|
||||
const sidebar = normalizeSidebar(tab.sidebar ?? []);
|
||||
for (const sidebarGroup of sidebar) {
|
||||
collectPages(sidebarGroup.pages, sidebarGroup.group, allPages);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[index-docs] Indexing version ${latestVersion.slug}`);
|
||||
|
||||
+83
-63
@@ -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,70 +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 {
|
||||
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);
|
||||
}
|
||||
app.use(handler);
|
||||
|
||||
//migrations
|
||||
async function runMigrations() {
|
||||
try {
|
||||
// Rename old .js migration entries to .ts in the knex_migrations table
|
||||
// so Knex can find the renamed files on disk
|
||||
const hasTable = await db.schema.hasTable("knex_migrations");
|
||||
if (hasTable) {
|
||||
const oldJsMigrations = await db("knex_migrations").where("name", "like", "%.js");
|
||||
for (const row of oldJsMigrations) {
|
||||
const newName = row.name.replace(/\.js$/, ".ts");
|
||||
await db("knex_migrations").where("id", row.id).update({ name: newName });
|
||||
console.log(`Renamed migration record: ${row.name} -> ${newName}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Running migrations...");
|
||||
await db.migrate.latest(); // Runs migrations to the latest state
|
||||
console.log("Migrations completed successfully!");
|
||||
} 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();
|
||||
|
||||
@@ -30,6 +30,7 @@ export async function seed(knex: Knex): Promise<void> {
|
||||
page_id: pageId,
|
||||
monitor_tag: "earth",
|
||||
monitor_settings_json: "",
|
||||
position: 0,
|
||||
created_at: knex.fn.now(),
|
||||
updated_at: knex.fn.now(),
|
||||
});
|
||||
@@ -42,6 +43,7 @@ export async function seed(knex: Knex): Promise<void> {
|
||||
page_id: pageId,
|
||||
monitor_tag: "kener",
|
||||
monitor_settings_json: "",
|
||||
position: 1,
|
||||
created_at: knex.fn.now(),
|
||||
updated_at: knex.fn.now(),
|
||||
});
|
||||
|
||||
@@ -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">
|
||||
|
||||
+35
-1
@@ -1,4 +1,5 @@
|
||||
import { json, type Handle } from "@sveltejs/kit";
|
||||
import { sequence } from "@sveltejs/kit/hooks";
|
||||
import { VerifyAPIKey } from "$lib/server/controllers/apiController";
|
||||
import db from "$lib/server/db/db";
|
||||
import type { UnauthorizedResponse, NotFoundResponse } from "$lib/types/api";
|
||||
@@ -58,7 +59,38 @@ function extractPagePath(pathname: string): string | null {
|
||||
return match ? decodeURIComponent(match[1]) : null;
|
||||
}
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
// Content types that indicate a form submission (mirrors SvelteKit's internal CSRF check scope)
|
||||
const FORM_CONTENT_TYPES = ["application/x-www-form-urlencoded", "multipart/form-data", "text/plain"];
|
||||
|
||||
function isFormContentType(request: Request): boolean {
|
||||
const type = request.headers.get("content-type")?.split(";", 1)[0].trim()?.toLowerCase() ?? "";
|
||||
return FORM_CONTENT_TYPES.includes(type);
|
||||
}
|
||||
|
||||
// Custom CSRF handler: validates Origin when present, allows requests when absent.
|
||||
// When Origin is absent (e.g. Referrer-Policy: no-referrer), security relies on
|
||||
// SameSite=Lax cookies which prevent cross-site POST from carrying auth cookies.
|
||||
const csrfHandle: Handle = async ({ event, resolve }) => {
|
||||
const { request } = event;
|
||||
|
||||
if (
|
||||
isFormContentType(request) &&
|
||||
(request.method === "POST" || request.method === "PUT" || request.method === "PATCH" || request.method === "DELETE")
|
||||
) {
|
||||
const requestOrigin = request.headers.get("origin");
|
||||
if (requestOrigin && requestOrigin !== "null") {
|
||||
const requestHost = new URL(requestOrigin).host;
|
||||
const expectedHost = event.url.host;
|
||||
if (requestHost !== expectedHost) {
|
||||
return new Response(`Cross-site ${request.method} form submissions are forbidden`, { status: 403 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resolve(event);
|
||||
};
|
||||
|
||||
const apiAuthHandle: Handle = async ({ event, resolve }) => {
|
||||
const { pathname } = event.url;
|
||||
|
||||
// Check if this is an API route that requires authentication
|
||||
@@ -160,3 +192,5 @@ export const handle: Handle = async ({ event, resolve }) => {
|
||||
response.headers.delete("Link");
|
||||
return response;
|
||||
};
|
||||
|
||||
export const handle = sequence(csrfHandle, apiAuthHandle);
|
||||
|
||||
@@ -1,188 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Badge } from "$lib/components/ui/badge/index.js";
|
||||
import Clock from "@lucide/svelte/icons/clock";
|
||||
import CalendarClock from "@lucide/svelte/icons/calendar-clock";
|
||||
import Timer from "@lucide/svelte/icons/timer";
|
||||
import { t } from "$lib/stores/i18n";
|
||||
import { formatDate, formatDuration } from "$lib/stores/datetime";
|
||||
import { resolve } from "$app/paths";
|
||||
import clientResolver from "$lib/client/resolver.js";
|
||||
|
||||
interface Maintenance {
|
||||
id: number;
|
||||
title: string;
|
||||
description?: string | null;
|
||||
start_date_time: number;
|
||||
end_date_time: number;
|
||||
monitor_tag?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
ongoingMaintenances?: Maintenance[];
|
||||
upcomingMaintenances?: Maintenance[];
|
||||
pastMaintenances?: Maintenance[];
|
||||
class?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
ongoingMaintenances = [],
|
||||
upcomingMaintenances = [],
|
||||
pastMaintenances = [],
|
||||
class: className = ""
|
||||
}: Props = $props();
|
||||
|
||||
// Deduplicate maintenances by id (can have duplicates due to multiple monitors)
|
||||
function deduplicateMaintenances(maintenances: Maintenance[]): Maintenance[] {
|
||||
const seen = new Set<number>();
|
||||
return maintenances.filter((m) => {
|
||||
if (seen.has(m.id)) return false;
|
||||
seen.add(m.id);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
// Deduplicated arrays
|
||||
let uniqueOngoing = $derived(deduplicateMaintenances(ongoingMaintenances));
|
||||
let uniqueUpcoming = $derived(deduplicateMaintenances(upcomingMaintenances));
|
||||
let uniquePast = $derived(deduplicateMaintenances(pastMaintenances));
|
||||
|
||||
// Check if there's any maintenance data
|
||||
let hasAnyData = $derived(uniqueOngoing.length > 0 || uniqueUpcoming.length > 0 || uniquePast.length > 0);
|
||||
</script>
|
||||
|
||||
{#if hasAnyData}
|
||||
<div class="bg-background rounded-3xl border {className}">
|
||||
<div class=" flex items-center justify-between p-4">
|
||||
<Badge variant="secondary" class="gap-1">{$t("Maintenances")}</Badge>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-0 lg:grid-cols-3">
|
||||
<!-- Ongoing Maintenances -->
|
||||
<div class="flex flex-col lg:border-r">
|
||||
<div class="text-muted-foreground bg-secondary flex items-center justify-between gap-2 p-4 text-sm font-medium">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="bg-maintenance h-2 w-2 rounded-full"></div>
|
||||
{$t("Ongoing")}
|
||||
</div>
|
||||
<div class="text-maintenance">
|
||||
<span>{uniqueOngoing.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="scrollbar-hidden max-h-64 overflow-y-auto">
|
||||
{#if uniqueOngoing.length === 0}
|
||||
<p class="text-muted-foreground py-4 text-center text-xs">{$t("No ongoing maintenances")}</p>
|
||||
{:else}
|
||||
{#each uniqueOngoing as maintenance (maintenance.id)}
|
||||
<a
|
||||
href={clientResolver(resolve, `/maintenances/${maintenance.id}`)}
|
||||
class="hover:bg-muted/50 block border-b p-3 transition-colors last:border-0"
|
||||
>
|
||||
<h4 class="line-clamp-2 text-sm leading-tight font-medium">{maintenance.title}</h4>
|
||||
{#if maintenance.description}
|
||||
<p class="text-muted-foreground mt-1 line-clamp-3 text-xs leading-relaxed">
|
||||
{maintenance.description}
|
||||
</p>
|
||||
{/if}
|
||||
<div class="text-muted-foreground mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs">
|
||||
<span class="flex items-center gap-1">
|
||||
<Clock class="h-3 w-3" />
|
||||
{$formatDate(maintenance.start_date_time, "MMM d, HH:mm")}
|
||||
</span>
|
||||
<span class="flex items-center gap-1">
|
||||
<Timer class="h-3 w-3" />
|
||||
{$formatDuration(maintenance.start_date_time, maintenance.end_date_time)}
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upcoming Maintenances -->
|
||||
<div class="flex flex-col lg:border-r">
|
||||
<div class="text-muted-foreground bg-secondary flex items-center justify-between gap-2 p-4 text-sm font-medium">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="bg-primary h-2 w-2 rounded-full"></div>
|
||||
{$t("Upcoming")}
|
||||
</div>
|
||||
<div>
|
||||
{uniqueUpcoming.length}
|
||||
</div>
|
||||
</div>
|
||||
<div class="scrollbar-hidden max-h-64 overflow-y-auto">
|
||||
{#if uniqueUpcoming.length === 0}
|
||||
<p class="text-muted-foreground py-4 text-center text-xs">{$t("No upcoming maintenances")}</p>
|
||||
{:else}
|
||||
{#each uniqueUpcoming as maintenance (maintenance.id)}
|
||||
<a
|
||||
href={clientResolver(resolve, `/maintenances/${maintenance.id}`)}
|
||||
class="hover:bg-muted/50 block border-b p-3 transition-colors last:border-0"
|
||||
>
|
||||
<h4 class="line-clamp-2 text-sm leading-tight font-medium">{maintenance.title}</h4>
|
||||
{#if maintenance.description}
|
||||
<p class="text-muted-foreground mt-1 line-clamp-3 text-xs leading-relaxed">
|
||||
{maintenance.description}
|
||||
</p>
|
||||
{/if}
|
||||
<div class="text-muted-foreground mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs">
|
||||
<span class="flex items-center gap-1">
|
||||
<CalendarClock class="h-3 w-3" />
|
||||
{$formatDate(maintenance.start_date_time, "MMM d, HH:mm")}
|
||||
</span>
|
||||
<span class="flex items-center gap-1">
|
||||
<Timer class="h-3 w-3" />
|
||||
{$formatDuration(maintenance.start_date_time, maintenance.end_date_time)}
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Past Maintenances -->
|
||||
<div class="flex flex-col">
|
||||
<div class="text-muted-foreground bg-secondary flex items-center justify-between gap-2 p-4 text-sm font-medium">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="bg-muted-foreground h-2 w-2 rounded-full"></div>
|
||||
|
||||
{$t("Past")}
|
||||
</div>
|
||||
<div>
|
||||
{uniquePast.length}
|
||||
</div>
|
||||
</div>
|
||||
<div class="scrollbar-hidden max-h-64 overflow-y-auto">
|
||||
{#if uniquePast.length === 0}
|
||||
<p class="text-muted-foreground py-4 text-center text-xs">{$t("No past maintenances")}</p>
|
||||
{:else}
|
||||
{#each uniquePast as maintenance (maintenance.id)}
|
||||
<a
|
||||
href={clientResolver(resolve, `/maintenances/${maintenance.id}`)}
|
||||
class="hover:bg-muted/50 block border-b p-3 transition-colors last:border-0"
|
||||
>
|
||||
<h4 class="line-clamp-2 text-sm leading-tight font-medium">{maintenance.title}</h4>
|
||||
{#if maintenance.description}
|
||||
<p class="text-muted-foreground mt-1 line-clamp-3 text-xs leading-relaxed">
|
||||
{maintenance.description}
|
||||
</p>
|
||||
{/if}
|
||||
<div class="text-muted-foreground mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs">
|
||||
<span class="flex items-center gap-1">
|
||||
<Clock class="h-3 w-3" />
|
||||
{$formatDate(maintenance.start_date_time, "MMM d, HH:mm")}
|
||||
</span>
|
||||
<span class="flex items-center gap-1">
|
||||
<Timer class="h-3 w-3" />
|
||||
{$formatDuration(maintenance.start_date_time, maintenance.end_date_time)}
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -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}
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
<Item.Root class="items-start p-0 {className} sm:items-center">
|
||||
<Item.Content class="min-w-0 flex-1">
|
||||
<div class="flex flex-col items-start justify-start gap-0.5">
|
||||
<span class="text-xs font-medium text-{incident.state.toLowerCase()}">{incident.state}</span>
|
||||
<span class="text-xs font-medium text-{incident.state.toLowerCase()}">{$t(incident.state)}</span>
|
||||
<Item.Title class="min-w-0 text-base wrap-break-word break-all">
|
||||
<a {target} class="hover:underline" href={clientResolver(resolve, `/incidents/${incident.id}`)}>
|
||||
{incident.title}
|
||||
@@ -105,7 +105,7 @@
|
||||
class="mt-2 flex w-full flex-col gap-2 text-xs font-medium sm:flex-row sm:items-center sm:justify-between"
|
||||
>
|
||||
<span class="max-w-full rounded-full border px-3 py-2 wrap-break-word">
|
||||
{$formatDate(incident.start_date_time, "PPp")}
|
||||
{$formatDate(incident.start_date_time, page.data.dateAndTimeFormat.datePlusTime)}
|
||||
</span>
|
||||
<span class="relative w-full text-center sm:flex-1">
|
||||
<span
|
||||
@@ -117,7 +117,7 @@
|
||||
</span>
|
||||
{#if incident.end_date_time}
|
||||
<span class="max-w-full rounded-full border px-3 py-2 wrap-break-word">
|
||||
{$formatDate(incident.end_date_time, "PPp")}
|
||||
{$formatDate(incident.end_date_time, page.data.dateAndTimeFormat.datePlusTime)}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="max-w-full rounded-full border px-3 py-2 wrap-break-word">
|
||||
@@ -128,14 +128,14 @@
|
||||
{#if showSummary}
|
||||
<div class="my-2 grid grid-cols-1 gap-4 text-xs font-medium sm:grid-cols-3">
|
||||
<div class="text-muted-foreground bg-secondary flex items-center justify-between rounded-full border p-2 px-4">
|
||||
<span>Last Updated</span>
|
||||
<span>{$formatDate(incident.updated_at, "PPp")}</span>
|
||||
<span>{$t("Last Updated")}</span>
|
||||
<span>{$formatDate(incident.updated_at, page.data.dateAndTimeFormat.datePlusTime)}</span>
|
||||
</div>
|
||||
<div class="text-muted-foreground bg-secondary flex items-center justify-between rounded-full border p-2 px-4">
|
||||
<span>Status</span>
|
||||
<span>{$t("Status")}</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-{incident.state.toLowerCase()}">
|
||||
{incident.state}
|
||||
{$t(incident.state)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -167,7 +167,7 @@
|
||||
{$t(comment.state)}
|
||||
</Badge>
|
||||
<span class="text-muted-foreground text-xs">
|
||||
{$formatDate(comment.commented_at, "PPp")}
|
||||
{$formatDate(comment.commented_at, page.data.dateAndTimeFormat.datePlusTime)}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -11,7 +11,15 @@
|
||||
|
||||
let { data } = page;
|
||||
const navItems: { name: string; url: string; iconURL: string }[] = data.navItems || [];
|
||||
const { siteName, siteUrl, logo } = data;
|
||||
const { siteName, logo, globalPageVisibilitySettings } = data;
|
||||
|
||||
const brandPath = $derived.by(() => {
|
||||
if (globalPageVisibilitySettings?.forceExclusivity) {
|
||||
const currentPagePath = page.params?.page_path?.trim();
|
||||
return currentPagePath ? `/${currentPagePath}` : "/";
|
||||
}
|
||||
return "/";
|
||||
});
|
||||
|
||||
function trackBrandClick() {
|
||||
trackEvent("nav_brand_clicked", { name: siteName });
|
||||
@@ -29,7 +37,7 @@
|
||||
>
|
||||
<!-- Brand -->
|
||||
<a
|
||||
href={clientResolver(resolve, siteUrl)}
|
||||
href={clientResolver(resolve, brandPath)}
|
||||
class="{navigationMenuTriggerStyle()} hover:border-border border border-transparent bg-transparent text-xs hover:bg-transparent"
|
||||
style="border-radius: var(--radius-3xl)"
|
||||
onclick={trackBrandClick}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { t } from "$lib/stores/i18n";
|
||||
import { ParseLatency } from "$lib/clientTools";
|
||||
import { formatDate } from "$lib/stores/datetime";
|
||||
import { page } from "$app/state";
|
||||
|
||||
interface ChartPoint {
|
||||
date: Date;
|
||||
@@ -82,7 +83,9 @@
|
||||
></div>
|
||||
<div class="flex flex-1 flex-col items-start justify-between gap-1 leading-none">
|
||||
<span class="text-muted-foreground text-xs"
|
||||
>{item.payload?.date ? $formatDate(item.payload.date, "MMM d") : ""}</span
|
||||
>{item.payload?.date
|
||||
? $formatDate(item.payload.date, page.data.dateAndTimeFormat.dateOnly)
|
||||
: ""}</span
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-foreground font-mono font-medium tabular-nums">
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<Item.Root class="items-start p-0 {className} sm:items-center">
|
||||
<Item.Content class="min-w-0 flex-1">
|
||||
<div class="flex flex-col items-start justify-start gap-0.5">
|
||||
<span class="text-xs font-medium text-{maintenance.status.toLowerCase()}">{maintenance.status}</span>
|
||||
<span class="text-xs font-medium text-{maintenance.status.toLowerCase()}">{$t(maintenance.status)}</span>
|
||||
<Item.Title class="min-w-0 text-base wrap-break-word break-all">
|
||||
<a {target} class="hover:underline" href={clientResolver(resolve, `/maintenances/${maintenance.id}`)}
|
||||
>{maintenance.title}</a
|
||||
@@ -97,7 +97,7 @@
|
||||
class="mt-2 flex w-full flex-col gap-2 text-xs font-medium sm:flex-row sm:items-center sm:justify-between"
|
||||
>
|
||||
<span class="max-w-full rounded-full border px-3 py-2 wrap-break-word">
|
||||
{$formatDate(maintenance.start_date_time, "PPp")}
|
||||
{$formatDate(maintenance.start_date_time, page.data.dateAndTimeFormat.datePlusTime)}
|
||||
</span>
|
||||
<span class="relative w-full text-center sm:flex-1">
|
||||
<span
|
||||
@@ -108,7 +108,7 @@
|
||||
</span>
|
||||
</span>
|
||||
<span class="max-w-full rounded-full border px-3 py-2 wrap-break-word">
|
||||
{$formatDate(maintenance.end_date_time, "PPp")}
|
||||
{$formatDate(maintenance.end_date_time, page.data.dateAndTimeFormat.datePlusTime)}
|
||||
</span>
|
||||
</Item.Description>
|
||||
</Item.Content>
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
import TrendingUp from "@lucide/svelte/icons/trending-up";
|
||||
import { t } from "$lib/stores/i18n";
|
||||
import { formatDate } from "$lib/stores/datetime";
|
||||
import { selectedTimezone } from "$lib/stores/timezone";
|
||||
import { toZonedTime } from "date-fns-tz";
|
||||
import * as Tooltip from "$lib/components/ui/tooltip/index.js";
|
||||
|
||||
interface MinuteData {
|
||||
@@ -74,7 +76,7 @@
|
||||
const minutesByHour: Map<number, MinuteData[]> = new Map();
|
||||
|
||||
for (const minute of minutes) {
|
||||
const date = new Date(minute.timestamp * 1000);
|
||||
const date = toZonedTime(minute.timestamp * 1000, $selectedTimezone);
|
||||
const hour = date.getHours();
|
||||
|
||||
if (!minutesByHour.has(hour)) {
|
||||
@@ -319,7 +321,10 @@
|
||||
style={tooltipStyle}
|
||||
>
|
||||
<span class="text-{hoveredMinute.data.status.toLowerCase()}">
|
||||
{$t(hoveredMinute.data.status)} @ {$formatDate(hoveredMinute.data.timestamp, "HH:mm")}
|
||||
{$t(hoveredMinute.data.status)} @ {$formatDate(
|
||||
hoveredMinute.data.timestamp,
|
||||
page.data.dateAndTimeFormat.timeOnly
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import * as Item from "$lib/components/ui/item/index.js";
|
||||
import { Button } from "$lib/components/ui/button/index.js";
|
||||
import { Skeleton } from "$lib/components/ui/skeleton/index.js";
|
||||
import * as Avatar from "$lib/components/ui/avatar/index.js";
|
||||
import ICONS from "$lib/icons";
|
||||
@@ -12,6 +11,7 @@
|
||||
import { GetInitials } from "$lib/clientTools.js";
|
||||
import GroupMonitorPopover from "./GroupMonitorPopover.svelte";
|
||||
import { t } from "$lib/stores/i18n";
|
||||
import { page } from "$app/state";
|
||||
|
||||
interface Props {
|
||||
tag: string;
|
||||
@@ -136,11 +136,11 @@
|
||||
<StatusBarCalendar data={data.uptimeData} monitorTag={tag} barHeight={40} radius={8} />
|
||||
<div class="flex min-w-0 justify-between gap-3">
|
||||
<p class="text-muted-foreground min-w-0 truncate text-xs font-medium">
|
||||
{$formatDate(new Date(data.fromTimeStamp * 1000), "MMM d, yyyy")}
|
||||
{$formatDate(new Date(data.fromTimeStamp * 1000), page.data.dateAndTimeFormat.dateOnly)}
|
||||
</p>
|
||||
|
||||
<p class="text-muted-foreground min-w-0 truncate text-right text-xs font-medium">
|
||||
{$formatDate(new Date(data.toTimeStamp * 1000), "MMM d, yyyy")}
|
||||
{$formatDate(new Date(data.toTimeStamp * 1000), page.data.dateAndTimeFormat.dateOnly)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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}
|
||||
|
||||
@@ -3,24 +3,20 @@
|
||||
import * as Tooltip from "$lib/components/ui/tooltip/index.js";
|
||||
|
||||
import { resolve } from "$app/paths";
|
||||
import TrendingUp from "@lucide/svelte/icons/trending-up";
|
||||
import Clock from "@lucide/svelte/icons/clock";
|
||||
import Activity from "@lucide/svelte/icons/activity";
|
||||
import { Button } from "$lib/components/ui/button/index.js";
|
||||
import * as Dialog from "$lib/components/ui/dialog/index.js";
|
||||
import { Badge } from "$lib/components/ui/badge/index.js";
|
||||
import { Skeleton } from "$lib/components/ui/skeleton/index.js";
|
||||
import LoaderBoxes from "$lib/components/loaderbox.svelte";
|
||||
import constants from "$lib/global-constants.js";
|
||||
import * as Tabs from "$lib/components/ui/tabs/index.js";
|
||||
import { page } from "$app/state";
|
||||
import { AreaChart, Area, LinearGradient } from "layerchart";
|
||||
import { curveCatmullRom } from "d3-shape";
|
||||
import { scaleOrdinal, scaleSequential, scaleTime } from "d3-scale";
|
||||
import { scaleTime } from "d3-scale";
|
||||
import IncidentItem from "$lib/components/IncidentItem.svelte";
|
||||
import MaintenanceItem from "$lib/components/MaintenanceItem.svelte";
|
||||
import MinuteGrid from "$lib/components/MinuteGrid.svelte";
|
||||
import clientResolver from "$lib/client/resolver.js";
|
||||
import { ParseLatency } from "$lib/clientTools";
|
||||
|
||||
import * as Chart from "$lib/components/ui/chart/index.js";
|
||||
import type { IncidentForMonitorListWithComments, MaintenanceEventsMonitorList } from "$lib/server/types/db";
|
||||
@@ -213,9 +209,8 @@
|
||||
<Dialog.Content class="max-h-[90vh] overflow-y-auto rounded-3xl p-4 sm:max-w-[46.5rem] sm:p-6">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title class="flex items-center gap-2 text-base sm:text-lg">
|
||||
<Activity class="h-4 w-4 shrink-0 sm:h-5 sm:w-5" />
|
||||
<span class="truncate">
|
||||
{selectedDay ? $formatDate(new Date(selectedDay.timestamp * 1000), "EEEE, MMMM do, yyyy") : ""}
|
||||
{selectedDay ? $formatDate(new Date(selectedDay.timestamp * 1000), page.data.dateAndTimeFormat.dateOnly) : ""}
|
||||
</span>
|
||||
</Dialog.Title>
|
||||
<Dialog.Description class="text-xs sm:text-sm"
|
||||
@@ -316,7 +311,7 @@
|
||||
line: { class: "stroke-1" }
|
||||
},
|
||||
xAxis: {
|
||||
format: (d: Date) => $formatDate(d, "HH:mm")
|
||||
format: (d: Date) => $formatDate(d, page.data.dateAndTimeFormat.timeOnly)
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -342,11 +337,13 @@
|
||||
></div>
|
||||
<div class="flex flex-1 flex-col items-start justify-between gap-1 leading-none">
|
||||
<span class="text-muted-foreground text-xs">
|
||||
{item.payload?.date ? $formatDate(item.payload.date, "HH:mm") : ""}
|
||||
{item.payload?.date
|
||||
? $formatDate(item.payload.date, page.data.dateAndTimeFormat.timeOnly)
|
||||
: ""}
|
||||
</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-foreground font-mono font-medium tabular-nums">
|
||||
{Math.round(Number(value))} ms
|
||||
{ParseLatency(Math.round(Number(value)))}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { onMount, untrack } from "svelte";
|
||||
import * as Card from "$lib/components/ui/card/index.js";
|
||||
import { Skeleton } from "$lib/components/ui/skeleton/index.js";
|
||||
import { Badge } from "$lib/components/ui/badge/index.js";
|
||||
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
|
||||
import StatusBarCalendar from "$lib/components/StatusBarCalendar.svelte";
|
||||
import LatencyTrendChart from "$lib/components/LatencyTrendChart.svelte";
|
||||
@@ -18,6 +17,7 @@
|
||||
import * as Popover from "$lib/components/ui/popover/index.js";
|
||||
import * as ToggleGroup from "$lib/components/ui/toggle-group/index.js";
|
||||
import GroupMonitorPopover from "$lib/components/GroupMonitorPopover.svelte";
|
||||
import { page } from "$app/state";
|
||||
|
||||
interface Props {
|
||||
monitorTag: string;
|
||||
@@ -205,12 +205,12 @@
|
||||
<div class="flex justify-between">
|
||||
<p class="text-muted-foreground text-xs font-medium">
|
||||
{#if displayData.length > 0}
|
||||
{$formatDate(displayData[0].ts, "d MMM yyyy")}
|
||||
{$formatDate(displayData[0].ts, page.data.dateAndTimeFormat.dateOnly)}
|
||||
{/if}
|
||||
</p>
|
||||
<p class="text-muted-foreground text-xs font-medium">
|
||||
{#if displayData.length > 0}
|
||||
{$formatDate(displayData[displayData.length - 1].ts, "d MMM yyyy")}
|
||||
{$formatDate(displayData[displayData.length - 1].ts, page.data.dateAndTimeFormat.dateOnly)}
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
@@ -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>
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from "$app/paths";
|
||||
import { page } from "$app/state";
|
||||
import { Button } from "$lib/components/ui/button/index.js";
|
||||
import * as Popover from "$lib/components/ui/popover/index.js";
|
||||
import { Spinner } from "$lib/components/ui/spinner/index.js";
|
||||
import ICONS from "$lib/icons";
|
||||
import clientResolver from "$lib/client/resolver.js";
|
||||
import { formatDate } from "$lib/stores/datetime";
|
||||
import { formatDate, formatDuration } from "$lib/stores/datetime";
|
||||
import { t } from "$lib/stores/i18n";
|
||||
import type { NotificationEvent } from "$lib/server/controllers/dashboardController.js";
|
||||
import Calendar from "@lucide/svelte/icons/calendar-1";
|
||||
import { format } from "date-fns";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
interface Props {
|
||||
@@ -22,6 +24,17 @@
|
||||
let notifications = $state<NotificationEvent[]>([]);
|
||||
let loading = $state(false);
|
||||
|
||||
const defaultEventsPath = $derived(`/events/${format(new Date(), "MMMM-yyyy")}`);
|
||||
|
||||
const resolvedEventsPath = $derived.by(() => {
|
||||
const finalEventsPath = eventsPath || defaultEventsPath;
|
||||
if (page.data?.globalPageVisibilitySettings?.forceExclusivity) {
|
||||
const currentPagePath = page.params?.page_path?.trim();
|
||||
return currentPagePath ? `/${currentPagePath}${finalEventsPath}` : finalEventsPath;
|
||||
}
|
||||
return finalEventsPath;
|
||||
});
|
||||
|
||||
async function fetchNotifications() {
|
||||
loading = true;
|
||||
try {
|
||||
@@ -76,7 +89,7 @@
|
||||
>
|
||||
<div class="flex items-center justify-between border-b px-4 py-3">
|
||||
<h4 class="text-sm font-semibold">{$t("Events")}</h4>
|
||||
<Button variant="outline" href={clientResolver(resolve, eventsPath)} size="icon-sm" class="rounded-btn">
|
||||
<Button variant="outline" href={clientResolver(resolve, resolvedEventsPath)} size="icon-sm" class="rounded-btn">
|
||||
<Calendar class="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -93,17 +106,17 @@
|
||||
class="hover:bg-muted/60 block border-b px-4 py-3 last:border-b-0"
|
||||
>
|
||||
<div class="my-0.5 flex items-center justify-between gap-2 text-xs">
|
||||
<span class="text-muted-foreground text-[11px] uppercase">{item.eventType}</span>
|
||||
<span class="text-{item.eventStatus.toLowerCase()}">{item.eventStatus}</span>
|
||||
<span class="text-muted-foreground text-[11px] uppercase">{$t(item.eventType)}</span>
|
||||
<span class="text-{item.eventStatus.toLowerCase()}">{$t(item.eventStatus)}</span>
|
||||
</div>
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<p class="line-clamp-2 text-sm">{item.eventTitle}</p>
|
||||
</div>
|
||||
<div class="text-muted-foreground mt-1 flex flex-wrap items-center gap-2 text-xs">
|
||||
<span>{$formatDate(item.eventDate, "PPp")}</span>
|
||||
<span>{$formatDate(item.eventDate, page.data.dateAndTimeFormat.datePlusTime)}</span>
|
||||
|
||||
<span>•</span>
|
||||
<span>{item.eventDuration}</span>
|
||||
<span>{$formatDuration(item.eventStartDateTime, item.eventEndDateTime, $t("Ongoing"))}</span>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button/index.js";
|
||||
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
|
||||
import { Spinner } from "$lib/components/ui/spinner/index.js";
|
||||
import { resolve } from "$app/paths";
|
||||
import clientResolver from "$lib/client/resolver.js";
|
||||
import { onMount } from "svelte";
|
||||
import type { PageNavItem } from "$lib/server/controllers/dashboardController.js";
|
||||
import { page } from "$app/state";
|
||||
import * as Item from "$lib/components/ui/item/index.js";
|
||||
import ChevronRight from "@lucide/svelte/icons/chevron-right";
|
||||
|
||||
let currentPath = $derived(page.params.page_path);
|
||||
|
||||
let pages = $state<PageNavItem[]>([]);
|
||||
let pagesLoading = $state(false);
|
||||
const defaultHomePage = $derived(pages.find((p) => p.page_path == ""));
|
||||
const currentPage = $derived(pages.find((p) => p.page_path === currentPath) || defaultHomePage);
|
||||
|
||||
async function fetchPages() {
|
||||
pagesLoading = true;
|
||||
try {
|
||||
const response = await fetch(clientResolver(resolve, "/dashboard-apis/pages"));
|
||||
if (response.ok) {
|
||||
pages = await response.json();
|
||||
console.log(">>>>>>---- PageList:25 ", pages);
|
||||
}
|
||||
} catch {
|
||||
// silently fail, pages dropdown will just not show
|
||||
} finally {
|
||||
pagesLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
fetchPages();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex shrink-0 items-center gap-2">
|
||||
{#if pagesLoading}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="bg-background/80 dark:bg-background/70 border-foreground/10 flex items-center justify-center rounded-full border text-xs shadow-none backdrop-blur-md"
|
||||
disabled
|
||||
>
|
||||
<Spinner class="h-4 w-4" />
|
||||
</Button>
|
||||
{:else if pages.length > 0}
|
||||
<!-- loop through pages -->
|
||||
<div class="flex w-full flex-col gap-2">
|
||||
{#each pages as page}
|
||||
<Item.Root variant="outline" class="rounded-3xl">
|
||||
{#snippet child({ props })}
|
||||
<a href={clientResolver(resolve, `/${page.page_path}`)} {...props}>
|
||||
{#if page.page_logo}
|
||||
<Item.Media variant="image">
|
||||
<img
|
||||
src={page.page_logo}
|
||||
alt={page.page_title}
|
||||
width="32"
|
||||
height="32"
|
||||
class="size-8 rounded object-cover"
|
||||
/>
|
||||
</Item.Media>
|
||||
{/if}
|
||||
<Item.Content>
|
||||
<Item.Title class="line-clamp-1">
|
||||
{page.page_title}
|
||||
</Item.Title>
|
||||
<Item.Description>{page.page_header}</Item.Description>
|
||||
</Item.Content>
|
||||
<Item.Actions>
|
||||
<ChevronRight class="size-4" />
|
||||
</Item.Actions>
|
||||
</a>
|
||||
{/snippet}
|
||||
</Item.Root>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -13,8 +13,8 @@
|
||||
|
||||
let pages = $state<PageNavItem[]>([]);
|
||||
let pagesLoading = $state(false);
|
||||
|
||||
const currentPage = $derived(pages.find((p) => p.page_path === currentPath) || pages[0]);
|
||||
const defaultHomePage = $derived(pages.find((p) => p.page_path == ""));
|
||||
const currentPage = $derived(pages.find((p) => p.page_path === currentPath) || defaultHomePage);
|
||||
|
||||
async function fetchPages() {
|
||||
pagesLoading = true;
|
||||
|
||||
@@ -375,7 +375,7 @@
|
||||
>
|
||||
<span class={getStatusColor(hoveredBar.data)}>{$t(GetStatusSummary(hoveredBar.data))}</span>
|
||||
<span class="text-muted-foreground">@</span>
|
||||
{$formatDate(hoveredBar.data.ts, "d MMM yyyy")}
|
||||
{$formatDate(hoveredBar.data.ts, page.data.dateAndTimeFormat.dateOnly)}
|
||||
{#if hoveredBar.data.avgLatency > 0}
|
||||
<span class="text-muted-foreground ml-1">|</span>
|
||||
<span class="ml-1">{ParseLatency(hoveredBar.data.avgLatency)}</span>
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
import Sun from "@lucide/svelte/icons/sun";
|
||||
import Moon from "@lucide/svelte/icons/moon";
|
||||
import Share from "@lucide/svelte/icons/share-2";
|
||||
import ChevronLeft from "@lucide/svelte/icons/chevron-left";
|
||||
import { format } from "date-fns";
|
||||
import SubscribeMenu from "$lib/components/SubscribeMenu.svelte";
|
||||
import CopyButton from "$lib/components/CopyButton.svelte";
|
||||
@@ -41,22 +40,22 @@
|
||||
|
||||
if (page.route.id === "/(kener)/monitors/[monitor_tag]") {
|
||||
return {
|
||||
label: "Edit Monitor",
|
||||
label: $t("Edit Monitor"),
|
||||
url: clientResolver(resolve, "/manage/app/monitors/" + page.params.monitor_tag)
|
||||
};
|
||||
} else if (page.route.id === "/(kener)/incidents/[incident_id]") {
|
||||
return {
|
||||
label: "Update Incident",
|
||||
label: $t("Update Incident"),
|
||||
url: clientResolver(resolve, "/manage/app/incidents/" + page.params.incident_id)
|
||||
};
|
||||
} else if (page.route.id === "/(kener)/maintenances/[maintenance_id]") {
|
||||
return {
|
||||
label: "Update Maintenance",
|
||||
label: $t("Update Maintenance"),
|
||||
url: clientResolver(resolve, "/manage/app/maintenances/" + page.data.maintenance.id)
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
label: "Manage Site",
|
||||
label: $t("Manage Site"),
|
||||
url: clientResolver(resolve, "/manage/app/site-configurations")
|
||||
};
|
||||
}
|
||||
@@ -79,7 +78,10 @@
|
||||
</script>
|
||||
|
||||
<div class="theme-plus-bar scrollbar-hidden sticky top-18 z-20 flex w-full items-center gap-2 rounded py-2">
|
||||
<PageSelector />
|
||||
<!-- Show the switcher if not forced exclusivity and switcher is enabled -->
|
||||
{#if !!!page.data.globalPageVisibilitySettings.forceExclusivity && page.data.globalPageVisibilitySettings.showSwitcher}
|
||||
<PageSelector />
|
||||
{/if}
|
||||
<div class="ml-auto flex shrink-0 items-center gap-2">
|
||||
{#if page.data.isSubsEnabled && page.data.canSendEmail}
|
||||
<ButtonGroup.Root class="hidden shrink-0 sm:flex">
|
||||
@@ -168,6 +170,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if !!page.data.announcement && !!page.data.announcement.title && !!page.data.announcement.message}
|
||||
<SiteBanner announcement={page.data.announcement} />
|
||||
{/if}
|
||||
|
||||
+74
-36
@@ -2,107 +2,149 @@
|
||||
"name": "Čeština",
|
||||
"mappings": {
|
||||
"%latency %metric latency": "%latency %metric latence",
|
||||
"24 Hours": "24 hodin",
|
||||
"30 Days": "30 dní",
|
||||
"7 Days": "7 dní",
|
||||
"90 Days": "90 dní",
|
||||
"Affected Monitors (%count)": "Zasažené monitory (%count)",
|
||||
"All Systems Operational": "Všechny systémy jsou v provozu",
|
||||
"average": "průměrná",
|
||||
"Average Latency": "Průměrná latence",
|
||||
"Avg Latency": "Prům. latence",
|
||||
"Back": "Zpět",
|
||||
"Badges": "Odznaky",
|
||||
"CANCELLED": "ZRUŠENO",
|
||||
"COMPLETED": "DOKONČENO",
|
||||
"Continue": "Pokračovat",
|
||||
"Copied": "Zkopírováno",
|
||||
"Current": "Aktuální",
|
||||
"Dark": "Tmavý",
|
||||
"Day": "Den",
|
||||
"Day Uptime": "Denní dostupnost",
|
||||
"Days": "Dny",
|
||||
"Degraded Performance": "Zhoršený výkon",
|
||||
"Didn't receive the code? Resend": "Nepřišel vám kód? Odeslat znovu",
|
||||
"Days": "Dní",
|
||||
"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ánované údržby.",
|
||||
"Get notified about incidents and scheduled maintenance.": "Dostávejte upozornění na incidenty a plánovanou údržbu",
|
||||
"Get notified about incidents updates": "Dostávejte upozornění na aktualizace incidentů",
|
||||
"Get notified about scheduled maintenance": "Dostávejte upozornění na plánovanou údržbu",
|
||||
"Home": "Domů",
|
||||
"IDENTIFIED": "IDENTIFIKOVÁNO",
|
||||
"iFrame": "iFrame",
|
||||
"Impact": "Dopad",
|
||||
"incident": "incident",
|
||||
"Incident": "Incident",
|
||||
"Incident Updates": "Aktualizace incidentů",
|
||||
"Incidents": "Incidenty",
|
||||
"Included Monitors": "Included Monitors",
|
||||
"Included Monitors (%count)": "Zahrnuté monitory (%count)",
|
||||
"INVESTIGATING": "VYŠETŘOVÁNÍ",
|
||||
"Last Updated": "Naposledy aktualizováno",
|
||||
"Latency": "Latence",
|
||||
"Latency Embed": "Vložená latence",
|
||||
"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": "Živý stav",
|
||||
"Loading your preferences...": "Načítám vaše nastavení...",
|
||||
"Live Status": "Aktuální stav",
|
||||
"Loading your preferences...": "Načítání nastavení...",
|
||||
"maintenance": "údržba",
|
||||
"Maintenance": "Údržba",
|
||||
"MAINTENANCE": "ÚDRŽBA",
|
||||
"Maintenance Updates": "Aktualizace údržby",
|
||||
"Maintenances": "Údržby",
|
||||
"Major System Outage": "Závažný výpadek systému",
|
||||
"Manage your notification preferences.": "Spravujte svá nastavení oznámení.",
|
||||
"Manage Site": "Spravovat stránku",
|
||||
"Manage your notification preferences.": "Spravujte nastavení oznámení",
|
||||
"Max Latency": "Max. latence",
|
||||
"Maximum Latency": "Max. latence",
|
||||
"maximum": "maximální",
|
||||
"Maximum Latency": "Maximální latence",
|
||||
"Min Latency": "Min. latence",
|
||||
"Minimum Latency": "Min. latence",
|
||||
"minimum": "minimální",
|
||||
"Minimum Latency": "Minimální latence",
|
||||
"Minute-by-minute status data for this day": "Minutová data stavu pro tento den",
|
||||
"Network error. Please try again.": "Chyba sítě. Zkuste to prosím znovu.",
|
||||
"No Events in %currentMonth": "V měsíci %currentMonth nejsou žádné události",
|
||||
"MONITORING": "MONITOROVÁNÍ",
|
||||
"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 žádné incidenty",
|
||||
"No latency data available for this day": "Pro tento den nejsou k dispozici data latence",
|
||||
"No maintenances for this day": "Pro tento den nejsou naplánované žádné údržby",
|
||||
"No incidents for this day": "Pro tento den nejsou evidovány žádné incidenty",
|
||||
"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 ongoing maintenances": "Žádné probíhající údržby",
|
||||
"No past maintenances": "Žádné minulé údržby",
|
||||
"No monitors available.": "Žádné dostupné monitory",
|
||||
"No ongoing maintenances": "Žádná probíhající údržba",
|
||||
"No past maintenances": "Žádná minulá údržba",
|
||||
"No Status Available": "Stav není k dispozici",
|
||||
"No upcoming maintenances": "Žádné nadcházející údržby",
|
||||
"No 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í",
|
||||
"Partial Degraded Performance": "Částečně zhoršený výkon",
|
||||
"ONGOING": "PROBÍHAJÍCÍ",
|
||||
"Ongoing Maintenances": "Probíhající údržby",
|
||||
"Operational": "V provozu",
|
||||
"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",
|
||||
"Recent Incidents": "Nedávné incidenty",
|
||||
"Recurring": "Opakující se",
|
||||
"RESOLVED": "VYŘEŠENO",
|
||||
"SCHEDULED": "NAPLÁNOVÁNO",
|
||||
"Scheduled Events (%count)": "Plánované události (%count)",
|
||||
"Scheduled Windows": "Naplánované úlohy",
|
||||
"Script": "Skript",
|
||||
"Select Language": "Vyberte jazyk",
|
||||
"Select latency metric to display": "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": "Vložený stav",
|
||||
"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",
|
||||
"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.": "Na tento okamžik nejsou žádné probíhající incidenty ani údržby.",
|
||||
"Theme": "Motiv",
|
||||
"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",
|
||||
"Under Maintenance": "Probíhá údržba",
|
||||
"Unknown impact": "Neznámý dopad",
|
||||
"UP": "V PROVOZU",
|
||||
"Upcoming": "Nadcházející",
|
||||
"Upcoming Maintenances": "Nadcházející údržby",
|
||||
"Update Incident": "Aktualizovat incident",
|
||||
"Update Maintenance": "Aktualizovat údržbu",
|
||||
"Updates": "Aktualizace",
|
||||
"Updates (%count)": "Aktualizace (%count)",
|
||||
"Uptime": "Dostupnost",
|
||||
@@ -110,10 +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",
|
||||
"INVESTIGATING": "VYŠETŘOVÁNÍ",
|
||||
"IDENTIFIED": "IDENTIFIKOVÁNO",
|
||||
"MONITORING": "MONITOROVÁNÍ",
|
||||
"RESOLVED": "ŘEŠENO"
|
||||
"We sent a 6-digit code to": "Odeslali jsme 6místný kód na"
|
||||
}
|
||||
}
|
||||
|
||||
+26
-8
@@ -8,14 +8,21 @@
|
||||
"Avg Latency": "Durchschnittliche Latenz",
|
||||
"Back": "Zurück",
|
||||
"Badges": "Abzeichen",
|
||||
"CANCELLED": "ABGESAGT",
|
||||
"COMPLETED": "ABGESCHLOSSEN",
|
||||
"Continue": "Weitermachen",
|
||||
"Copied": "Kopiert",
|
||||
"Current": "Aktuell",
|
||||
"Dark": "Dunkel",
|
||||
"Day": "Tag",
|
||||
"Day Uptime": "Tagesverfügbarkeit",
|
||||
"Days": "Tage",
|
||||
"Degraded": "Beeinträchtigt",
|
||||
"DEGRADED": "BEEINTRÄCHTIGT",
|
||||
"Degraded Performance": "Beeinträchtigte Leistung",
|
||||
"Didn't receive the code? Resend": "Sie haben den Code nicht erhalten? ",
|
||||
"Down": "Ausgefallen",
|
||||
"DOWN": "AUSGEFALLEN",
|
||||
"Duration": "Dauer",
|
||||
"Email address": "E-Mail-Adresse",
|
||||
"Embed Monitor": "Monitor einbetten",
|
||||
@@ -32,11 +39,14 @@
|
||||
"Get notified about incidents and scheduled maintenance.": "Lassen Sie sich über Vorfälle und geplante Wartungsarbeiten benachrichtigen.",
|
||||
"Get notified about incidents updates": "Lassen Sie sich über Aktualisierungen von Vorfällen benachrichtigen",
|
||||
"Get notified about scheduled maintenance": "Lassen Sie sich über geplante Wartungsarbeiten benachrichtigen",
|
||||
"IDENTIFIED": "IDENTIFIZIERT",
|
||||
"iFrame": "iFrame",
|
||||
"Impact": "Auswirkungen",
|
||||
"incident": "Vorfall",
|
||||
"Incident Updates": "Vorfallaktualisierungen",
|
||||
"Incidents": "Vorfälle",
|
||||
"Included Monitors": "Included Monitors",
|
||||
"Included Monitors (%count)": "Enthaltene Monitore (%count)",
|
||||
"INVESTIGATING": "WIRD UNTERSUCHT",
|
||||
"Last Updated": "Zuletzt aktualisiert",
|
||||
"Latency": "Latenz",
|
||||
"Latency Embed": "Latenz-Einbettung",
|
||||
@@ -44,18 +54,22 @@
|
||||
"Latency Trend": "Latenztrend",
|
||||
"Latest Latency": "Neueste Latenz",
|
||||
"Latest Status": "Neuester Stand",
|
||||
"Light": "Licht",
|
||||
"Light": "Hell",
|
||||
"Live Status": "Live-Status",
|
||||
"Loading your preferences...": "Deine Einstellungen werden geladen...",
|
||||
"maintenance": "Wartung",
|
||||
"MAINTENANCE": "WARTUNG",
|
||||
"Maintenance Updates": "Wartungsaktualisierungen",
|
||||
"Maintenances": "Wartungen",
|
||||
"Major System Outage": "Schwerwiegender Systemausfall",
|
||||
"Manage Site": "Seite verwalten",
|
||||
"Manage your notification preferences.": "Verwalten Sie Ihre Benachrichtigungseinstellungen.",
|
||||
"Max Latency": "Maximale Latenz",
|
||||
"Maximum Latency": "Maximale Latenz",
|
||||
"Min Latency": "Min. Latenz",
|
||||
"Minimum Latency": "Min. Latenz",
|
||||
"Minute-by-minute status data for this day": "Minutenweise Statusdaten für diesen Tag",
|
||||
"MONITORING": "WIRD ÜBERWACHT",
|
||||
"Network error. Please try again.": "Netzwerkfehler. ",
|
||||
"No Events in %currentMonth": "Keine Ereignisse in %currentMonth",
|
||||
"No events to show": "Keine Ereignisse zum Anzeigen",
|
||||
@@ -73,6 +87,7 @@
|
||||
"Notifications": "Benachrichtigungen",
|
||||
"One-time": "Einmalig",
|
||||
"Ongoing": "Laufend",
|
||||
"Operational": "Betriebsbereit",
|
||||
"Partial Degraded Performance": "Teilweise beeinträchtigte Leistung",
|
||||
"Partial System Outage": "Teilweiser Systemausfall",
|
||||
"Past": "Vergangenheit",
|
||||
@@ -82,7 +97,11 @@
|
||||
"Please enter the 6-digit verification code": "Bitte geben Sie den 6-stelligen Bestätigungscode ein",
|
||||
"Read less": "Weniger lesen",
|
||||
"Read more": "Mehr lesen",
|
||||
"READY": "BEREIT",
|
||||
"Recurring": "Wiederkehrend",
|
||||
"RESOLVED": "BEHOBEN",
|
||||
"SCHEDULED": "GEPLANT",
|
||||
"Scheduled Events (%count)": "Geplante Ereignisse (%count)",
|
||||
"Script": "Skript",
|
||||
"Select Language": "Wählen Sie Sprache aus",
|
||||
"Select latency metric to display": "Latenzmetrik zur Anzeige auswählen",
|
||||
@@ -95,14 +114,17 @@
|
||||
"Status Embed": "Status einbetten",
|
||||
"Status history and latency trend": "Statusverlauf und Latenztrend",
|
||||
"Subscribe": "Abonnieren",
|
||||
"Subscribe to Updates": "Abonnieren Sie Updates",
|
||||
"Subscribe to Updates": "Benachrichtigungen erhalten",
|
||||
"There are no incidents or maintenances scheduled for this month.": "Für diesen Monat sind keine Vorfälle oder Wartungsarbeiten geplant.",
|
||||
"There are no ongoing incidents or maintenance events.": "Es gibt keine laufenden Vorfälle oder Wartungsereignisse.",
|
||||
"Total Incidents": "Gesamtzahl der Vorfälle",
|
||||
"Total Maintenances": "Gesamtwartungen",
|
||||
"Under Maintenance": "Unter Wartung",
|
||||
"Unknown impact": "Unbekannte Auswirkung",
|
||||
"UP": "AKTIV",
|
||||
"Upcoming": "Demnächst",
|
||||
"Update Incident": "Vorfall aktualisieren",
|
||||
"Update Maintenance": "Wartung aktualisieren",
|
||||
"Updates": "Aktualisierungen",
|
||||
"Updates (%count)": "Aktualisierungen (%count)",
|
||||
"Uptime": "Betriebszeit",
|
||||
@@ -110,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": "INVESTIGATING",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"MONITORING": "MONITORING",
|
||||
"RESOLVED": "RESOLVED"
|
||||
"We sent a 6-digit code to": "Wir haben einen 6-stelligen Code an gesendet"
|
||||
}
|
||||
}
|
||||
|
||||
+24
-6
@@ -8,14 +8,21 @@
|
||||
"Avg Latency": "Gns. latens",
|
||||
"Back": "Tilbage",
|
||||
"Badges": "Badges",
|
||||
"CANCELLED": "ANNULLERET",
|
||||
"COMPLETED": "AFSLUTTET",
|
||||
"Continue": "Fortsætte",
|
||||
"Copied": "Kopieret",
|
||||
"Current": "Aktuel",
|
||||
"Dark": "Mørk",
|
||||
"Day": "Dag",
|
||||
"Day Uptime": "Dag oppetid",
|
||||
"Days": "dage",
|
||||
"Degraded": "Forringet",
|
||||
"DEGRADED": "FORRINGET",
|
||||
"Degraded Performance": "Nedsat ydeevne",
|
||||
"Didn't receive the code? Resend": "Modtog du ikke koden? ",
|
||||
"Down": "Nede",
|
||||
"DOWN": "NEDE",
|
||||
"Duration": "Varighed",
|
||||
"Email address": "E-mailadresse",
|
||||
"Embed Monitor": "Integrer skærm",
|
||||
@@ -32,11 +39,14 @@
|
||||
"Get notified about incidents and scheduled maintenance.": "Få besked om hændelser og planlagt vedligeholdelse.",
|
||||
"Get notified about incidents updates": "Få besked om opdateringer om hændelser",
|
||||
"Get notified about scheduled maintenance": "Få besked om planlagt vedligeholdelse",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"iFrame": "iFrame",
|
||||
"Impact": "Indvirkning",
|
||||
"incident": "Hændelse",
|
||||
"Incident Updates": "Hændelsesopdateringer",
|
||||
"Incidents": "Hændelser",
|
||||
"Included Monitors": "Included Monitors",
|
||||
"Included Monitors (%count)": "Included Monitors (%count)",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"Last Updated": "Sidst opdateret",
|
||||
"Latency": "Latency",
|
||||
"Latency Embed": "Latency Embed",
|
||||
@@ -47,15 +57,19 @@
|
||||
"Light": "Lys",
|
||||
"Live Status": "Live status",
|
||||
"Loading your preferences...": "Indlæser dine præferencer...",
|
||||
"maintenance": "Vedligeholdelse",
|
||||
"MAINTENANCE": "VEDLIGEHOLDELSE",
|
||||
"Maintenance Updates": "Vedligeholdelsesopdateringer",
|
||||
"Maintenances": "Vedligeholdelse",
|
||||
"Major System Outage": "Større systemnedbrud",
|
||||
"Manage Site": "Administrer side",
|
||||
"Manage your notification preferences.": "Administrer dine meddelelsespræferencer.",
|
||||
"Max Latency": "Max latens",
|
||||
"Maximum Latency": "Max latens",
|
||||
"Min Latency": "Min latens",
|
||||
"Minimum Latency": "Min latens",
|
||||
"Minute-by-minute status data for this day": "Minut for minut statusdata for denne dag",
|
||||
"MONITORING": "MONITORING",
|
||||
"Network error. Please try again.": "Netværksfejl. ",
|
||||
"No Events in %currentMonth": "Ingen begivenheder i %currentMonth",
|
||||
"No events to show": "Ingen hændelser at vise",
|
||||
@@ -73,6 +87,7 @@
|
||||
"Notifications": "Notifikationer",
|
||||
"One-time": "Engangs",
|
||||
"Ongoing": "Løbende",
|
||||
"Operational": "Operationel",
|
||||
"Partial Degraded Performance": "Delvist nedsat ydeevne",
|
||||
"Partial System Outage": "Delvist systemnedbrud",
|
||||
"Past": "Forbi",
|
||||
@@ -82,7 +97,11 @@
|
||||
"Please enter the 6-digit verification code": "Indtast venligst den 6-cifrede bekræftelseskode",
|
||||
"Read less": "Læs mindre",
|
||||
"Read more": "Læs mere",
|
||||
"READY": "KLAR",
|
||||
"Recurring": "Tilbagevendende",
|
||||
"RESOLVED": "RESOLVED",
|
||||
"SCHEDULED": "PLANLAGT",
|
||||
"Scheduled Events (%count)": "Planlagte begivenheder (%count)",
|
||||
"Script": "Manuskript",
|
||||
"Select Language": "Vælg sprog",
|
||||
"Select latency metric to display": "Vælg latenstidsmåling, der skal vises",
|
||||
@@ -102,7 +121,10 @@
|
||||
"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)",
|
||||
"Uptime": "Oppetid",
|
||||
@@ -110,10 +132,6 @@
|
||||
"Verification failed": "Bekræftelse mislykkedes",
|
||||
"Verify": "Verificere",
|
||||
"Verifying": "Bekræfter",
|
||||
"We sent a 6-digit code to": "Vi sendte en 6-cifret kode til",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"MONITORING": "MONITORING",
|
||||
"RESOLVED": "RESOLVED"
|
||||
"We sent a 6-digit code to": "Vi sendte en 6-cifret kode til"
|
||||
}
|
||||
}
|
||||
|
||||
+24
-6
@@ -8,14 +8,21 @@
|
||||
"Avg Latency": "Avg Latency",
|
||||
"Back": "Back",
|
||||
"Badges": "Badges",
|
||||
"CANCELLED": "CANCELLED",
|
||||
"COMPLETED": "COMPLETED",
|
||||
"Continue": "Continue",
|
||||
"Copied": "Copied",
|
||||
"Current": "Current",
|
||||
"Dark": "Dark",
|
||||
"Day": "Day",
|
||||
"Day Uptime": "Day Uptime",
|
||||
"Days": "Days",
|
||||
"Degraded": "Degraded",
|
||||
"DEGRADED": "DEGRADED",
|
||||
"Degraded Performance": "Degraded Performance",
|
||||
"Didn't receive the code? Resend": "Didn't receive the code? Resend",
|
||||
"Down": "Down",
|
||||
"DOWN": "DOWN",
|
||||
"Duration": "Duration",
|
||||
"Email address": "Email address",
|
||||
"Embed Monitor": "Embed Monitor",
|
||||
@@ -32,11 +39,14 @@
|
||||
"Get notified about incidents and scheduled maintenance.": "Get notified about incidents and scheduled maintenance.",
|
||||
"Get notified about incidents updates": "Get notified about incidents updates",
|
||||
"Get notified about scheduled maintenance": "Get notified about scheduled maintenance",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"iFrame": "iFrame",
|
||||
"Impact": "Impact",
|
||||
"incident": "Incident",
|
||||
"Incident Updates": "Incident Updates",
|
||||
"Incidents": "Incidents",
|
||||
"Included Monitors": "Included Monitors",
|
||||
"Included Monitors (%count)": "Included Monitors (%count)",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"Last Updated": "Last Updated",
|
||||
"Latency": "Latency",
|
||||
"Latency Embed": "Latency Embed",
|
||||
@@ -47,15 +57,19 @@
|
||||
"Light": "Light",
|
||||
"Live Status": "Live Status",
|
||||
"Loading your preferences...": "Loading your preferences...",
|
||||
"maintenance": "Maintenance",
|
||||
"MAINTENANCE": "MAINTENANCE",
|
||||
"Maintenance Updates": "Maintenance Updates",
|
||||
"Maintenances": "Maintenances",
|
||||
"Major System Outage": "Major System Outage",
|
||||
"Manage Site": "Manage Site",
|
||||
"Manage your notification preferences.": "Manage your notification preferences.",
|
||||
"Max Latency": "Max Latency",
|
||||
"Maximum Latency": "Max Latency",
|
||||
"Min Latency": "Min Latency",
|
||||
"Minimum Latency": "Min Latency",
|
||||
"Minute-by-minute status data for this day": "Minute-by-minute status data for this day",
|
||||
"MONITORING": "MONITORING",
|
||||
"Network error. Please try again.": "Network error. Please try again.",
|
||||
"No Events in %currentMonth": "No Events in %currentMonth",
|
||||
"No events to show": "No events to show",
|
||||
@@ -73,6 +87,7 @@
|
||||
"Notifications": "Notifications",
|
||||
"One-time": "One-time",
|
||||
"Ongoing": "Ongoing",
|
||||
"Operational": "Operational",
|
||||
"Partial Degraded Performance": "Partial Degraded Performance",
|
||||
"Partial System Outage": "Partial System Outage",
|
||||
"Past": "Past",
|
||||
@@ -82,7 +97,11 @@
|
||||
"Please enter the 6-digit verification code": "Please enter the 6-digit verification code",
|
||||
"Read less": "Read less",
|
||||
"Read more": "Read more",
|
||||
"READY": "READY",
|
||||
"Recurring": "Recurring",
|
||||
"RESOLVED": "RESOLVED",
|
||||
"SCHEDULED": "SCHEDULED",
|
||||
"Scheduled Events (%count)": "Scheduled Events (%count)",
|
||||
"Script": "Script",
|
||||
"Select Language": "Select Language",
|
||||
"Select latency metric to display": "Select latency metric to display",
|
||||
@@ -102,7 +121,10 @@
|
||||
"Total Maintenances": "Total Maintenances",
|
||||
"Under Maintenance": "Under Maintenance",
|
||||
"Unknown impact": "Unknown impact",
|
||||
"UP": "UP",
|
||||
"Upcoming": "Upcoming",
|
||||
"Update Incident": "Update Incident",
|
||||
"Update Maintenance": "Update Maintenance",
|
||||
"Updates": "Updates",
|
||||
"Updates (%count)": "Updates (%count)",
|
||||
"Uptime": "Uptime",
|
||||
@@ -110,10 +132,6 @@
|
||||
"Verification failed": "Verification failed",
|
||||
"Verify": "Verify",
|
||||
"Verifying": "Verifying",
|
||||
"We sent a 6-digit code to": "We sent a 6-digit code to",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"MONITORING": "MONITORING",
|
||||
"RESOLVED": "RESOLVED"
|
||||
"We sent a 6-digit code to": "We sent a 6-digit code to"
|
||||
}
|
||||
}
|
||||
|
||||
+24
-6
@@ -8,14 +8,21 @@
|
||||
"Avg Latency": "Latencia promedio",
|
||||
"Back": "Atrás",
|
||||
"Badges": "Insignias",
|
||||
"CANCELLED": "CANCELADO",
|
||||
"COMPLETED": "COMPLETADO",
|
||||
"Continue": "Continuar",
|
||||
"Copied": "Copiado",
|
||||
"Current": "Actual",
|
||||
"Dark": "Oscuro",
|
||||
"Day": "Día",
|
||||
"Day Uptime": "Tiempo de actividad del día",
|
||||
"Days": "Días",
|
||||
"Degraded": "Degradado",
|
||||
"DEGRADED": "DEGRADADO",
|
||||
"Degraded Performance": "Rendimiento degradado",
|
||||
"Didn't receive the code? Resend": "¿No recibiste el código? ",
|
||||
"Down": "Caído",
|
||||
"DOWN": "CAÍDO",
|
||||
"Duration": "Duración",
|
||||
"Email address": "Dirección de correo electrónico",
|
||||
"Embed Monitor": "Monitor integrado",
|
||||
@@ -32,11 +39,14 @@
|
||||
"Get notified about incidents and scheduled maintenance.": "Recibe notificaciones sobre incidencias y mantenimientos programados.",
|
||||
"Get notified about incidents updates": "Recibir notificaciones sobre actualizaciones de incidentes",
|
||||
"Get notified about scheduled maintenance": "Recibir notificaciones sobre el mantenimiento programado",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"iFrame": "marco flotante",
|
||||
"Impact": "Impacto",
|
||||
"incident": "Incidente",
|
||||
"Incident Updates": "Actualizaciones de incidentes",
|
||||
"Incidents": "Incidentes",
|
||||
"Included Monitors": "Included Monitors",
|
||||
"Included Monitors (%count)": "Included Monitors (%count)",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"Last Updated": "Última actualización",
|
||||
"Latency": "Estado latente",
|
||||
"Latency Embed": "Incrustación de latencia",
|
||||
@@ -47,15 +57,19 @@
|
||||
"Light": "Luz",
|
||||
"Live Status": "Estado en vivo",
|
||||
"Loading your preferences...": "Cargando tus preferencias...",
|
||||
"maintenance": "Mantenimiento",
|
||||
"MAINTENANCE": "MANTENIMIENTO",
|
||||
"Maintenance Updates": "Actualizaciones de mantenimiento",
|
||||
"Maintenances": "Mantenimientos",
|
||||
"Major System Outage": "Interrupción importante del sistema",
|
||||
"Manage Site": "Gestionar sitio",
|
||||
"Manage your notification preferences.": "Administre sus preferencias de notificación.",
|
||||
"Max Latency": "Latencia máxima",
|
||||
"Maximum Latency": "Latencia máxima",
|
||||
"Min Latency": "Latencia mínima",
|
||||
"Minimum Latency": "Latencia mínima",
|
||||
"Minute-by-minute status data for this day": "Datos del estado minuto a minuto de este día",
|
||||
"MONITORING": "MONITORING",
|
||||
"Network error. Please try again.": "Error de red. ",
|
||||
"No Events in %currentMonth": "No hay eventos en %currentMonth",
|
||||
"No events to show": "No hay eventos para mostrar",
|
||||
@@ -73,6 +87,7 @@
|
||||
"Notifications": "Notificaciones",
|
||||
"One-time": "una sola vez",
|
||||
"Ongoing": "En curso",
|
||||
"Operational": "Operativo",
|
||||
"Partial Degraded Performance": "Rendimiento parcialmente degradado",
|
||||
"Partial System Outage": "Interrupción parcial del sistema",
|
||||
"Past": "Pasado",
|
||||
@@ -82,7 +97,11 @@
|
||||
"Please enter the 6-digit verification code": "Por favor ingresa el código de verificación de 6 dígitos",
|
||||
"Read less": "Leer menos",
|
||||
"Read more": "Leer más",
|
||||
"READY": "LISTO",
|
||||
"Recurring": "Recurrente",
|
||||
"RESOLVED": "RESOLVED",
|
||||
"SCHEDULED": "PROGRAMADO",
|
||||
"Scheduled Events (%count)": "Eventos programados (%count)",
|
||||
"Script": "Guion",
|
||||
"Select Language": "Seleccionar idioma",
|
||||
"Select latency metric to display": "Selecciona la métrica de latencia para mostrar",
|
||||
@@ -102,7 +121,10 @@
|
||||
"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)",
|
||||
"Uptime": "tiempo de actividad",
|
||||
@@ -110,10 +132,6 @@
|
||||
"Verification failed": "La verificación falló",
|
||||
"Verify": "Verificar",
|
||||
"Verifying": "Verificando",
|
||||
"We sent a 6-digit code to": "Enviamos un código de 6 dígitos a",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"MONITORING": "MONITORING",
|
||||
"RESOLVED": "RESOLVED"
|
||||
"We sent a 6-digit code to": "Enviamos un código de 6 dígitos a"
|
||||
}
|
||||
}
|
||||
|
||||
+24
-6
@@ -8,14 +8,21 @@
|
||||
"Avg Latency": "تأخیر متوسط",
|
||||
"Back": "برگشت",
|
||||
"Badges": "نشان ها",
|
||||
"CANCELLED": "لغو شده",
|
||||
"COMPLETED": "تکمیل شده",
|
||||
"Continue": "ادامه دهید",
|
||||
"Copied": "کپی شده",
|
||||
"Current": "فعلی",
|
||||
"Dark": "تاریک",
|
||||
"Day": "روز",
|
||||
"Day Uptime": "تایم روز",
|
||||
"Days": "روزها",
|
||||
"Degraded": "کاهش یافته",
|
||||
"DEGRADED": "کاهش یافته",
|
||||
"Degraded Performance": "عملکرد کاهشیافته",
|
||||
"Didn't receive the code? Resend": "کد را دریافت نکردید؟ ",
|
||||
"Down": "از کار افتاده",
|
||||
"DOWN": "قطع",
|
||||
"Duration": "مدت",
|
||||
"Email address": "آدرس ایمیل",
|
||||
"Embed Monitor": "تعبیه مانیتور",
|
||||
@@ -32,11 +39,14 @@
|
||||
"Get notified about incidents and scheduled maintenance.": "در مورد حوادث و تعمیر و نگهداری برنامه ریزی شده مطلع شوید.",
|
||||
"Get notified about incidents updates": "در مورد به روز رسانی حوادث مطلع شوید",
|
||||
"Get notified about scheduled maintenance": "در مورد تعمیر و نگهداری برنامه ریزی شده مطلع شوید",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"iFrame": "آی فریم",
|
||||
"Impact": "تاثیر",
|
||||
"incident": "رخداد",
|
||||
"Incident Updates": "به روز رسانی حادثه",
|
||||
"Incidents": "حوادث",
|
||||
"Included Monitors": "Included Monitors",
|
||||
"Included Monitors (%count)": "Included Monitors (%count)",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"Last Updated": "آخرین به روز رسانی",
|
||||
"Latency": "تأخیر",
|
||||
"Latency Embed": "تعبیه تاخیر",
|
||||
@@ -47,15 +57,19 @@
|
||||
"Light": "نور",
|
||||
"Live Status": "وضعیت زنده",
|
||||
"Loading your preferences...": "در حال بارگیری تنظیمات برگزیده شما...",
|
||||
"maintenance": "نگهداری",
|
||||
"MAINTENANCE": "تعمیر و نگهداری",
|
||||
"Maintenance Updates": "به روز رسانی های تعمیر و نگهداری",
|
||||
"Maintenances": "تعمیر و نگهداری",
|
||||
"Major System Outage": "قطعی عمده سیستم",
|
||||
"Manage Site": "مدیریت سایت",
|
||||
"Manage your notification preferences.": "تنظیمات برگزیده اعلان خود را مدیریت کنید.",
|
||||
"Max Latency": "حداکثر تاخیر",
|
||||
"Maximum Latency": "حداکثر تاخیر",
|
||||
"Min Latency": "حداقل تاخیر",
|
||||
"Minimum Latency": "حداقل تاخیر",
|
||||
"Minute-by-minute status data for this day": "داده های وضعیت دقیقه به دقیقه برای این روز",
|
||||
"MONITORING": "MONITORING",
|
||||
"Network error. Please try again.": "خطای شبکه ",
|
||||
"No Events in %currentMonth": "هیچ رویدادی در %currentMonth وجود ندارد",
|
||||
"No events to show": "رویدادی برای نمایش وجود ندارد",
|
||||
@@ -73,6 +87,7 @@
|
||||
"Notifications": "اعلانها",
|
||||
"One-time": "یک بار",
|
||||
"Ongoing": "در حال انجام است",
|
||||
"Operational": "عملیاتی",
|
||||
"Partial Degraded Performance": "کاهش عملکرد جزئی",
|
||||
"Partial System Outage": "قطعی جزئی سیستم",
|
||||
"Past": "گذشته",
|
||||
@@ -82,7 +97,11 @@
|
||||
"Please enter the 6-digit verification code": "لطفا کد تایید 6 رقمی را وارد کنید",
|
||||
"Read less": "کمتر بخوانید",
|
||||
"Read more": "ادامه مطلب",
|
||||
"READY": "آماده",
|
||||
"Recurring": "دورهای",
|
||||
"RESOLVED": "RESOLVED",
|
||||
"SCHEDULED": "زمانبندی شده",
|
||||
"Scheduled Events (%count)": "رویدادهای زمانبندی شده (%count)",
|
||||
"Script": "اسکریپت",
|
||||
"Select Language": "زبان را انتخاب کنید",
|
||||
"Select latency metric to display": "معیار تأخیر برای نمایش را انتخاب کنید",
|
||||
@@ -102,7 +121,10 @@
|
||||
"Total Maintenances": "کل تعمیر و نگهداری",
|
||||
"Under Maintenance": "تحت تعمیر و نگهداری",
|
||||
"Unknown impact": "تاثیر نامعلوم",
|
||||
"UP": "فعال",
|
||||
"Upcoming": "آینده",
|
||||
"Update Incident": "بهروزرسانی رویداد",
|
||||
"Update Maintenance": "بهروزرسانی نگهداری",
|
||||
"Updates": "بهروزرسانیها",
|
||||
"Updates (%count)": "بهروزرسانیها (%count)",
|
||||
"Uptime": "آپتایم",
|
||||
@@ -110,10 +132,6 @@
|
||||
"Verification failed": "تأیید ناموفق بود",
|
||||
"Verify": "تأیید کنید",
|
||||
"Verifying": "در حال تأیید",
|
||||
"We sent a 6-digit code to": "ما یک کد 6 رقمی ارسال کردیم",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"MONITORING": "MONITORING",
|
||||
"RESOLVED": "RESOLVED"
|
||||
"We sent a 6-digit code to": "ما یک کد 6 رقمی ارسال کردیم"
|
||||
}
|
||||
}
|
||||
|
||||
+24
-6
@@ -8,14 +8,21 @@
|
||||
"Avg Latency": "Latence moyenne",
|
||||
"Back": "Dos",
|
||||
"Badges": "Insignes",
|
||||
"CANCELLED": "ANNULÉ",
|
||||
"COMPLETED": "TERMINÉ",
|
||||
"Continue": "Continuer",
|
||||
"Copied": "Copié",
|
||||
"Current": "En cours",
|
||||
"Dark": "Sombre",
|
||||
"Day": "Jour",
|
||||
"Day Uptime": "Disponibilité journalière",
|
||||
"Days": "Jours",
|
||||
"Degraded": "Dégradé",
|
||||
"DEGRADED": "DÉGRADÉ",
|
||||
"Degraded Performance": "Performance dégradée",
|
||||
"Didn't receive the code? Resend": "Vous n'avez pas reçu le code ? ",
|
||||
"Down": "Hors service",
|
||||
"DOWN": "EN PANNE",
|
||||
"Duration": "Durée",
|
||||
"Email address": "Adresse email",
|
||||
"Embed Monitor": "Intégrer le moniteur",
|
||||
@@ -32,11 +39,14 @@
|
||||
"Get notified about incidents and scheduled maintenance.": "Soyez informé des incidents et des maintenances planifiées.",
|
||||
"Get notified about incidents updates": "Soyez informé des mises à jour des incidents",
|
||||
"Get notified about scheduled maintenance": "Soyez informé de la maintenance planifiée",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"iFrame": "iFrame",
|
||||
"Impact": "Impact",
|
||||
"incident": "Incident",
|
||||
"Incident Updates": "Mises à jour des incidents",
|
||||
"Incidents": "Incidents",
|
||||
"Included Monitors": "Included Monitors",
|
||||
"Included Monitors (%count)": "Included Monitors (%count)",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"Last Updated": "Dernière mise à jour",
|
||||
"Latency": "Latence",
|
||||
"Latency Embed": "Latence intégrée",
|
||||
@@ -47,15 +57,19 @@
|
||||
"Light": "Lumière",
|
||||
"Live Status": "Statut en direct",
|
||||
"Loading your preferences...": "Chargement de vos préférences...",
|
||||
"maintenance": "Maintenance",
|
||||
"MAINTENANCE": "MAINTENANCE",
|
||||
"Maintenance Updates": "Mises à jour de maintenance",
|
||||
"Maintenances": "Entretiens",
|
||||
"Major System Outage": "Panne majeure du système",
|
||||
"Manage Site": "Gérer le site",
|
||||
"Manage your notification preferences.": "Gérez vos préférences de notification.",
|
||||
"Max Latency": "Latence maximale",
|
||||
"Maximum Latency": "Latence maximale",
|
||||
"Min Latency": "Latence minimale",
|
||||
"Minimum Latency": "Latence minimale",
|
||||
"Minute-by-minute status data for this day": "Données d'état minute par minute pour cette journée",
|
||||
"MONITORING": "MONITORING",
|
||||
"Network error. Please try again.": "Erreur réseau. ",
|
||||
"No Events in %currentMonth": "Aucun événement dans %currentMonth",
|
||||
"No events to show": "Aucun événement à afficher",
|
||||
@@ -73,6 +87,7 @@
|
||||
"Notifications": "Alertes",
|
||||
"One-time": "Une fois",
|
||||
"Ongoing": "En cours",
|
||||
"Operational": "Opérationnel",
|
||||
"Partial Degraded Performance": "Performance partiellement dégradée",
|
||||
"Partial System Outage": "Panne partielle du système",
|
||||
"Past": "Passé",
|
||||
@@ -82,7 +97,11 @@
|
||||
"Please enter the 6-digit verification code": "Veuillez saisir le code de vérification à 6 chiffres",
|
||||
"Read less": "Lire moins",
|
||||
"Read more": "En savoir plus",
|
||||
"READY": "PRÊT",
|
||||
"Recurring": "Récurrent",
|
||||
"RESOLVED": "RESOLVED",
|
||||
"SCHEDULED": "PLANIFIÉ",
|
||||
"Scheduled Events (%count)": "Événements planifiés (%count)",
|
||||
"Script": "Scénario",
|
||||
"Select Language": "Sélectionnez la langue",
|
||||
"Select latency metric to display": "Sélectionnez la métrique de latence à afficher",
|
||||
@@ -102,7 +121,10 @@
|
||||
"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)",
|
||||
"Uptime": "Temps de disponibilité",
|
||||
@@ -110,10 +132,6 @@
|
||||
"Verification failed": "La vérification a échoué",
|
||||
"Verify": "Vérifier",
|
||||
"Verifying": "Vérification",
|
||||
"We sent a 6-digit code to": "Nous avons envoyé un code à 6 chiffres à",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"MONITORING": "MONITORING",
|
||||
"RESOLVED": "RESOLVED"
|
||||
"We sent a 6-digit code to": "Nous avons envoyé un code à 6 chiffres à"
|
||||
}
|
||||
}
|
||||
|
||||
+32
-14
@@ -8,14 +8,21 @@
|
||||
"Avg Latency": "औसत लेटेंसी",
|
||||
"Back": "वापस",
|
||||
"Badges": "बैज",
|
||||
"CANCELLED": "रद्द",
|
||||
"COMPLETED": "पूर्ण",
|
||||
"Continue": "जारी रखें",
|
||||
"Copied": "कॉपी किया गया",
|
||||
"Current": "वर्तमान",
|
||||
"Dark": "डार्क",
|
||||
"Day": "दिन",
|
||||
"Day Uptime": "दिन का अपटाइम",
|
||||
"Days": "दिन",
|
||||
"Degraded": "अवनत",
|
||||
"DEGRADED": "अवक्रमित",
|
||||
"Degraded Performance": "प्रदर्शन में गिरावट",
|
||||
"Didn't receive the code? Resend": "कोड नहीं मिला? फिर से भेजें",
|
||||
"Down": "बंद",
|
||||
"DOWN": "बंद",
|
||||
"Duration": "अवधि",
|
||||
"Email address": "ईमेल पता",
|
||||
"Embed Monitor": "मॉनिटर एम्बेड करें",
|
||||
@@ -32,11 +39,14 @@
|
||||
"Get notified about incidents and scheduled maintenance.": "घटनाओं और निर्धारित रखरखाव के बारे में सूचित हों।",
|
||||
"Get notified about incidents updates": "घटना अपडेट के बारे में सूचित हों",
|
||||
"Get notified about scheduled maintenance": "निर्धारित रखरखाव के बारे में सूचित हों",
|
||||
"IDENTIFIED": "पहचाना गया",
|
||||
"iFrame": "iFrame",
|
||||
"Impact": "प्रभाव",
|
||||
"incident": "घटना",
|
||||
"Incident Updates": "घटना अपडेट",
|
||||
"Incidents": "घटनाएँ",
|
||||
"Included Monitors": "Included Monitors",
|
||||
"Included Monitors (%count)": "शामिल मॉनिटर (%count)",
|
||||
"INVESTIGATING": "जाँच कर रहे हैं",
|
||||
"Last Updated": "अंतिम अपडेट",
|
||||
"Latency": "लेटेंसी",
|
||||
"Latency Embed": "लेटेंसी एम्बेड",
|
||||
@@ -47,15 +57,19 @@
|
||||
"Light": "लाइट",
|
||||
"Live Status": "लाइव स्थिति",
|
||||
"Loading your preferences...": "आपकी प्राथमिकताएँ लोड हो रही हैं...",
|
||||
"maintenance": "रखरखाव",
|
||||
"MAINTENANCE": "रखरखाव",
|
||||
"Maintenance Updates": "रखरखाव अपडेट",
|
||||
"Maintenances": "रखरखाव",
|
||||
"Major System Outage": "सिस्टम का बड़ा आउटेज",
|
||||
"Manage Site": "साइट प्रबंधित करें",
|
||||
"Manage your notification preferences.": "अपनी सूचना प्राथमिकताएँ प्रबंधित करें।",
|
||||
"Max Latency": "Max Latency",
|
||||
"Maximum Latency": "Max Latency",
|
||||
"Min Latency": "Min Latency",
|
||||
"Minimum Latency": "Min Latency",
|
||||
"Max Latency": "अधिकतम लेटेंसी",
|
||||
"Maximum Latency": "अधिकतम लेटेंसी",
|
||||
"Min Latency": "न्यूनतम लेटेंसी",
|
||||
"Minimum Latency": "न्यूनतम लेटेंसी",
|
||||
"Minute-by-minute status data for this day": "इस दिन के लिए मिनट-दर-मिनट स्थिति डेटा",
|
||||
"MONITORING": "मॉनिटरिंग",
|
||||
"Network error. Please try again.": "नेटवर्क त्रुटि। कृपया पुनः प्रयास करें।",
|
||||
"No Events in %currentMonth": "%currentMonth में कोई घटना नहीं",
|
||||
"No events to show": "दिखाने के लिए कोई इवेंट नहीं",
|
||||
@@ -73,6 +87,7 @@
|
||||
"Notifications": "सूचनाएँ",
|
||||
"One-time": "एक बार",
|
||||
"Ongoing": "चल रहा है",
|
||||
"Operational": "चालू",
|
||||
"Partial Degraded Performance": "आंशिक प्रदर्शन गिरावट",
|
||||
"Partial System Outage": "सिस्टम का आंशिक आउटेज.",
|
||||
"Past": "बीता हुआ",
|
||||
@@ -82,7 +97,11 @@
|
||||
"Please enter the 6-digit verification code": "कृपया 6 अंकों का सत्यापन कोड दर्ज करें",
|
||||
"Read less": "कम पढ़ें",
|
||||
"Read more": "और पढ़ें",
|
||||
"READY": "तैयार",
|
||||
"Recurring": "आवर्ती",
|
||||
"RESOLVED": "सुलझा हुआ",
|
||||
"SCHEDULED": "अनुसूचित",
|
||||
"Scheduled Events (%count)": "अनुसूचित कार्यक्रम (%count)",
|
||||
"Script": "स्क्रिप्ट",
|
||||
"Select Language": "भाषा चुनें",
|
||||
"Select latency metric to display": "दिखाने के लिए विलंबता मेट्रिक चुनें",
|
||||
@@ -98,22 +117,21 @@
|
||||
"Subscribe to Updates": "अपडेट के लिए सब्सक्राइब करें",
|
||||
"There are no incidents or maintenances scheduled for this month.": "इस महीने के लिए कोई घटना या रखरखाव निर्धारित नहीं है।",
|
||||
"There are no ongoing incidents or maintenance events.": "कोई चल रही घटना या रखरखाव इवेंट नहीं है।",
|
||||
"Total Incidents": "Total Incidents",
|
||||
"Total Maintenances": "Total Maintenances",
|
||||
"Total Incidents": "कुल घटनाएँ",
|
||||
"Total Maintenances": "कुल रखरखाव",
|
||||
"Under Maintenance": "रखरखाव जारी है",
|
||||
"Unknown impact": "अज्ञात प्रभाव",
|
||||
"UP": "चालू",
|
||||
"Upcoming": "आने वाला",
|
||||
"Update Incident": "घटना अपडेट करें",
|
||||
"Update Maintenance": "रखरखाव अपडेट करें",
|
||||
"Updates": "अपडेट्स",
|
||||
"Updates (%count)": "अपडेट (%count)",
|
||||
"Uptime": "अपटाइम",
|
||||
"Uptime Badge": "अपटाइम बैज",
|
||||
"Verification failed": "सत्यापन विफल रहा",
|
||||
"Verify": "Verify",
|
||||
"Verifying": "Verifying",
|
||||
"We sent a 6-digit code to": "हमने 6 अंकों का कोड भेजा है",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"MONITORING": "MONITORING",
|
||||
"RESOLVED": "RESOLVED"
|
||||
"Verify": "सत्यापित करें",
|
||||
"Verifying": "सत्यापित हो रहा है",
|
||||
"We sent a 6-digit code to": "हमने 6 अंकों का कोड भेजा है"
|
||||
}
|
||||
}
|
||||
|
||||
+24
-6
@@ -8,14 +8,21 @@
|
||||
"Avg Latency": "Latenza media",
|
||||
"Back": "Indietro",
|
||||
"Badges": "Distintivi",
|
||||
"CANCELLED": "ANNULLATO",
|
||||
"COMPLETED": "COMPLETATO",
|
||||
"Continue": "Continuare",
|
||||
"Copied": "Copiato",
|
||||
"Current": "Corrente",
|
||||
"Dark": "Buio",
|
||||
"Day": "Giorno",
|
||||
"Day Uptime": "Tempo di attività giornaliero",
|
||||
"Days": "Giorni",
|
||||
"Degraded": "Degradato",
|
||||
"DEGRADED": "DEGRADATO",
|
||||
"Degraded Performance": "Prestazioni degradate",
|
||||
"Didn't receive the code? Resend": "Non hai ricevuto il codice? ",
|
||||
"Down": "Non disponibile",
|
||||
"DOWN": "NON DISPONIBILE",
|
||||
"Duration": "Durata",
|
||||
"Email address": "Indirizzo e-mail",
|
||||
"Embed Monitor": "Incorpora monitoraggio",
|
||||
@@ -32,11 +39,14 @@
|
||||
"Get notified about incidents and scheduled maintenance.": "Ricevi notifiche sugli incidenti e sulla manutenzione programmata.",
|
||||
"Get notified about incidents updates": "Ricevi notifiche sugli aggiornamenti degli incidenti",
|
||||
"Get notified about scheduled maintenance": "Ricevi notifiche sulla manutenzione programmata",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"iFrame": "iFrame",
|
||||
"Impact": "Impatto",
|
||||
"incident": "Incidente",
|
||||
"Incident Updates": "Aggiornamenti sugli incidenti",
|
||||
"Incidents": "Incidenti",
|
||||
"Included Monitors": "Included Monitors",
|
||||
"Included Monitors (%count)": "Included Monitors (%count)",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"Last Updated": "Ultimo aggiornamento",
|
||||
"Latency": "Latenza",
|
||||
"Latency Embed": "Incorporamento latenza",
|
||||
@@ -47,15 +57,19 @@
|
||||
"Light": "Leggero",
|
||||
"Live Status": "Stato in tempo reale",
|
||||
"Loading your preferences...": "Caricamento delle tue preferenze...",
|
||||
"maintenance": "Manutenzione",
|
||||
"MAINTENANCE": "MANUTENZIONE",
|
||||
"Maintenance Updates": "Aggiornamenti sulla manutenzione",
|
||||
"Maintenances": "Manutenzioni",
|
||||
"Major System Outage": "Interruzione grave del sistema",
|
||||
"Manage Site": "Gestisci sito",
|
||||
"Manage your notification preferences.": "Gestisci le tue preferenze di notifica.",
|
||||
"Max Latency": "Latenza massima",
|
||||
"Maximum Latency": "Latenza massima",
|
||||
"Min Latency": "Latenza minima",
|
||||
"Minimum Latency": "Latenza minima",
|
||||
"Minute-by-minute status data for this day": "Dati sullo stato minuto per minuto per questo giorno",
|
||||
"MONITORING": "MONITORING",
|
||||
"Network error. Please try again.": "Errore di rete. ",
|
||||
"No Events in %currentMonth": "Nessun evento in %currentMonth",
|
||||
"No events to show": "Nessun evento da mostrare",
|
||||
@@ -73,6 +87,7 @@
|
||||
"Notifications": "Notifiche",
|
||||
"One-time": "Una volta",
|
||||
"Ongoing": "In corso",
|
||||
"Operational": "Operativo",
|
||||
"Partial Degraded Performance": "Prestazioni parzialmente degradate",
|
||||
"Partial System Outage": "Interruzione parziale del sistema",
|
||||
"Past": "Passato",
|
||||
@@ -82,7 +97,11 @@
|
||||
"Please enter the 6-digit verification code": "Inserisci il codice di verifica di 6 cifre",
|
||||
"Read less": "Leggi di meno",
|
||||
"Read more": "Per saperne di più",
|
||||
"READY": "PRONTO",
|
||||
"Recurring": "Ricorrente",
|
||||
"RESOLVED": "RESOLVED",
|
||||
"SCHEDULED": "PROGRAMMATO",
|
||||
"Scheduled Events (%count)": "Eventi programmati (%count)",
|
||||
"Script": "Copione",
|
||||
"Select Language": "Seleziona lingua",
|
||||
"Select latency metric to display": "Seleziona la metrica di latenza da visualizzare",
|
||||
@@ -102,7 +121,10 @@
|
||||
"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)",
|
||||
"Uptime": "Tempo di attività",
|
||||
@@ -110,10 +132,6 @@
|
||||
"Verification failed": "Verifica non riuscita",
|
||||
"Verify": "Verificare",
|
||||
"Verifying": "Verifica",
|
||||
"We sent a 6-digit code to": "Abbiamo inviato un codice di 6 cifre a",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"MONITORING": "MONITORING",
|
||||
"RESOLVED": "RESOLVED"
|
||||
"We sent a 6-digit code to": "Abbiamo inviato un codice di 6 cifre a"
|
||||
}
|
||||
}
|
||||
|
||||
+24
-6
@@ -8,14 +8,21 @@
|
||||
"Avg Latency": "平均レイテンシー",
|
||||
"Back": "戻る",
|
||||
"Badges": "バッジ",
|
||||
"CANCELLED": "キャンセル",
|
||||
"COMPLETED": "完了",
|
||||
"Continue": "続く",
|
||||
"Copied": "コピーされました",
|
||||
"Current": "現在",
|
||||
"Dark": "暗い",
|
||||
"Day": "日",
|
||||
"Day Uptime": "一日の稼働時間",
|
||||
"Days": "日数",
|
||||
"Degraded": "低下",
|
||||
"DEGRADED": "劣化",
|
||||
"Degraded Performance": "パフォーマンスの低下",
|
||||
"Didn't receive the code? Resend": "コードを受け取っていませんか?",
|
||||
"Down": "停止",
|
||||
"DOWN": "障害",
|
||||
"Duration": "間隔",
|
||||
"Email address": "電子メールアドレス",
|
||||
"Embed Monitor": "埋め込みモニター",
|
||||
@@ -32,11 +39,14 @@
|
||||
"Get notified about incidents and scheduled maintenance.": "インシデントや定期メンテナンスに関する通知を受け取ります。",
|
||||
"Get notified about incidents updates": "インシデントの更新に関する通知を受け取る",
|
||||
"Get notified about scheduled maintenance": "定期メンテナンスに関する通知を受け取る",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"iFrame": "iFrame",
|
||||
"Impact": "インパクト",
|
||||
"incident": "インシデント",
|
||||
"Incident Updates": "インシデントの最新情報",
|
||||
"Incidents": "事件",
|
||||
"Included Monitors": "Included Monitors",
|
||||
"Included Monitors (%count)": "Included Monitors (%count)",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"Last Updated": "最終更新日",
|
||||
"Latency": "レイテンシー",
|
||||
"Latency Embed": "レイテンシの埋め込み",
|
||||
@@ -47,15 +57,19 @@
|
||||
"Light": "ライト",
|
||||
"Live Status": "ライブステータス",
|
||||
"Loading your preferences...": "設定を読み込んでいます...",
|
||||
"maintenance": "メンテナンス",
|
||||
"MAINTENANCE": "メンテナンス",
|
||||
"Maintenance Updates": "メンテナンスアップデート",
|
||||
"Maintenances": "メンテナンス",
|
||||
"Major System Outage": "大規模なシステム障害",
|
||||
"Manage Site": "サイト管理",
|
||||
"Manage your notification preferences.": "通知設定を管理します。",
|
||||
"Max Latency": "最大レイテンシー",
|
||||
"Maximum Latency": "最大レイテンシー",
|
||||
"Min Latency": "最小レイテンシ",
|
||||
"Minimum Latency": "最小レイテンシ",
|
||||
"Minute-by-minute status data for this day": "この日の分単位のステータスデータ",
|
||||
"MONITORING": "MONITORING",
|
||||
"Network error. Please try again.": "ネットワークエラー。",
|
||||
"No Events in %currentMonth": "%currentMonth にはイベントがありません",
|
||||
"No events to show": "表示するイベントはありません",
|
||||
@@ -73,6 +87,7 @@
|
||||
"Notifications": "通知",
|
||||
"One-time": "一度",
|
||||
"Ongoing": "進行中",
|
||||
"Operational": "稼働中",
|
||||
"Partial Degraded Performance": "部分的なパフォーマンス低下",
|
||||
"Partial System Outage": "部分的なシステム障害。",
|
||||
"Past": "過去",
|
||||
@@ -82,7 +97,11 @@
|
||||
"Please enter the 6-digit verification code": "6桁の認証コードを入力してください",
|
||||
"Read less": "読む量を減らす",
|
||||
"Read more": "続きを読む",
|
||||
"READY": "準備完了",
|
||||
"Recurring": "繰り返し",
|
||||
"RESOLVED": "RESOLVED",
|
||||
"SCHEDULED": "予定済み",
|
||||
"Scheduled Events (%count)": "予定イベント (%count)",
|
||||
"Script": "スクリプト",
|
||||
"Select Language": "言語の選択",
|
||||
"Select latency metric to display": "表示するレイテンシ指標を選択",
|
||||
@@ -102,7 +121,10 @@
|
||||
"Total Maintenances": "トータルメンテナンス",
|
||||
"Under Maintenance": "メンテナンス中",
|
||||
"Unknown impact": "未知の影響",
|
||||
"UP": "正常",
|
||||
"Upcoming": "今後の予定",
|
||||
"Update Incident": "インシデントを更新",
|
||||
"Update Maintenance": "メンテナンスを更新",
|
||||
"Updates": "更新",
|
||||
"Updates (%count)": "アップデート (%count)",
|
||||
"Uptime": "稼働時間",
|
||||
@@ -110,10 +132,6 @@
|
||||
"Verification failed": "検証に失敗しました",
|
||||
"Verify": "確認する",
|
||||
"Verifying": "検証中",
|
||||
"We sent a 6-digit code to": "6桁のコードを送信しました",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"MONITORING": "MONITORING",
|
||||
"RESOLVED": "RESOLVED"
|
||||
"We sent a 6-digit code to": "6桁のコードを送信しました"
|
||||
}
|
||||
}
|
||||
|
||||
+24
-6
@@ -8,14 +8,21 @@
|
||||
"Avg Latency": "평균 지연 시간",
|
||||
"Back": "뒤쪽에",
|
||||
"Badges": "배지",
|
||||
"CANCELLED": "취소됨",
|
||||
"COMPLETED": "완료됨",
|
||||
"Continue": "계속하다",
|
||||
"Copied": "복사됨",
|
||||
"Current": "현재",
|
||||
"Dark": "어두운",
|
||||
"Day": "낮",
|
||||
"Day Uptime": "일일 가동 시간",
|
||||
"Days": "날",
|
||||
"Degraded": "저하",
|
||||
"DEGRADED": "성능 저하",
|
||||
"Degraded Performance": "성능 저하",
|
||||
"Didn't receive the code? Resend": "코드를 받지 못하셨나요? 재전송",
|
||||
"Down": "중단",
|
||||
"DOWN": "장애",
|
||||
"Duration": "지속",
|
||||
"Email address": "이메일 주소",
|
||||
"Embed Monitor": "모니터 내장",
|
||||
@@ -32,11 +39,14 @@
|
||||
"Get notified about incidents and scheduled maintenance.": "사고 및 예정된 유지 관리에 대한 알림을 받으세요.",
|
||||
"Get notified about incidents updates": "사건 업데이트에 대한 알림 받기",
|
||||
"Get notified about scheduled maintenance": "예정된 유지 관리에 대한 알림 받기",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"iFrame": "아이프레임",
|
||||
"Impact": "영향",
|
||||
"incident": "인시던트",
|
||||
"Incident Updates": "사고 업데이트",
|
||||
"Incidents": "사건",
|
||||
"Included Monitors": "Included Monitors",
|
||||
"Included Monitors (%count)": "Included Monitors (%count)",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"Last Updated": "마지막 업데이트",
|
||||
"Latency": "숨어 있음",
|
||||
"Latency Embed": "지연 시간 포함",
|
||||
@@ -47,15 +57,19 @@
|
||||
"Light": "빛",
|
||||
"Live Status": "실시간 현황",
|
||||
"Loading your preferences...": "환경설정 로드 중...",
|
||||
"maintenance": "유지보수",
|
||||
"MAINTENANCE": "점검 중",
|
||||
"Maintenance Updates": "유지보수 업데이트",
|
||||
"Maintenances": "유지보수",
|
||||
"Major System Outage": "대규모 시스템 장애",
|
||||
"Manage Site": "사이트 관리",
|
||||
"Manage your notification preferences.": "알림 기본 설정을 관리하세요.",
|
||||
"Max Latency": "최대 대기 시간",
|
||||
"Maximum Latency": "최대 대기 시간",
|
||||
"Min Latency": "최소 지연 시간",
|
||||
"Minimum Latency": "최소 지연 시간",
|
||||
"Minute-by-minute status data for this day": "오늘의 분별 상태 데이터",
|
||||
"MONITORING": "MONITORING",
|
||||
"Network error. Please try again.": "네트워크 오류입니다. 다시 시도해 주세요.",
|
||||
"No Events in %currentMonth": "%currentMonth에 이벤트가 없습니다.",
|
||||
"No events to show": "표시할 이벤트가 없습니다",
|
||||
@@ -73,6 +87,7 @@
|
||||
"Notifications": "알림",
|
||||
"One-time": "일회성",
|
||||
"Ongoing": "전진",
|
||||
"Operational": "정상",
|
||||
"Partial Degraded Performance": "부분적 성능 저하",
|
||||
"Partial System Outage": "부분적 시스템 장애.",
|
||||
"Past": "과거",
|
||||
@@ -82,7 +97,11 @@
|
||||
"Please enter the 6-digit verification code": "인증번호 6자리를 입력해주세요",
|
||||
"Read less": "덜 읽으세요",
|
||||
"Read more": "더 읽어보세요",
|
||||
"READY": "준비됨",
|
||||
"Recurring": "반복",
|
||||
"RESOLVED": "RESOLVED",
|
||||
"SCHEDULED": "예정됨",
|
||||
"Scheduled Events (%count)": "예정된 이벤트 (%count)",
|
||||
"Script": "스크립트",
|
||||
"Select Language": "언어 선택",
|
||||
"Select latency metric to display": "표시할 지연 시간 지표를 선택하세요",
|
||||
@@ -102,7 +121,10 @@
|
||||
"Total Maintenances": "총 유지보수",
|
||||
"Under Maintenance": "유지보수 중",
|
||||
"Unknown impact": "알 수 없는 영향",
|
||||
"UP": "정상",
|
||||
"Upcoming": "예정",
|
||||
"Update Incident": "인시던트 업데이트",
|
||||
"Update Maintenance": "유지보수 업데이트",
|
||||
"Updates": "업데이트",
|
||||
"Updates (%count)": "업데이트(%count)",
|
||||
"Uptime": "가동 시간",
|
||||
@@ -110,10 +132,6 @@
|
||||
"Verification failed": "확인 실패",
|
||||
"Verify": "확인하다",
|
||||
"Verifying": "확인 중",
|
||||
"We sent a 6-digit code to": "6자리 코드를 다음 주소로 보냈습니다.",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"MONITORING": "MONITORING",
|
||||
"RESOLVED": "RESOLVED"
|
||||
"We sent a 6-digit code to": "6자리 코드를 다음 주소로 보냈습니다."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,14 +8,21 @@
|
||||
"Avg Latency": "Gj.sn. ventetid",
|
||||
"Back": "Tilbake",
|
||||
"Badges": "Merker",
|
||||
"CANCELLED": "AVLYST",
|
||||
"COMPLETED": "FULLFØRT",
|
||||
"Continue": "Fortsette",
|
||||
"Copied": "Kopiert",
|
||||
"Current": "Nåværende",
|
||||
"Dark": "Mørk",
|
||||
"Day": "Dag",
|
||||
"Day Uptime": "Dag oppetid",
|
||||
"Days": "Dager",
|
||||
"Degraded": "Degradert",
|
||||
"DEGRADED": "FORRINGET",
|
||||
"Degraded Performance": "Redusert ytelse",
|
||||
"Didn't receive the code? Resend": "Fikk du ikke koden? Send på nytt",
|
||||
"Down": "Nede",
|
||||
"DOWN": "NEDE",
|
||||
"Duration": "Varighet",
|
||||
"Email address": "E-postadresse",
|
||||
"Embed Monitor": "Bygg inn monitor",
|
||||
@@ -32,11 +39,14 @@
|
||||
"Get notified about incidents and scheduled maintenance.": "Bli varslet om hendelser og planlagt vedlikehold.",
|
||||
"Get notified about incidents updates": "Bli varslet om hendelsesoppdateringer",
|
||||
"Get notified about scheduled maintenance": "Bli varslet om planlagt vedlikehold",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"iFrame": "iFrame",
|
||||
"Impact": "Påvirkning",
|
||||
"incident": "Hendelse",
|
||||
"Incident Updates": "Hendelsesoppdateringer",
|
||||
"Incidents": "Hendelser",
|
||||
"Included Monitors": "Included Monitors",
|
||||
"Included Monitors (%count)": "Included Monitors (%count)",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"Last Updated": "Sist oppdatert",
|
||||
"Latency": "Latens",
|
||||
"Latency Embed": "Innebygging av ventetid",
|
||||
@@ -47,15 +57,19 @@
|
||||
"Light": "Lys",
|
||||
"Live Status": "Live-status",
|
||||
"Loading your preferences...": "Laster inn innstillingene dine ...",
|
||||
"maintenance": "Vedlikehold",
|
||||
"MAINTENANCE": "VEDLIKEHOLD",
|
||||
"Maintenance Updates": "Vedlikeholdsoppdateringer",
|
||||
"Maintenances": "Vedlikehold",
|
||||
"Major System Outage": "Større systemutfall",
|
||||
"Manage Site": "Administrer nettsted",
|
||||
"Manage your notification preferences.": "Administrer varslingspreferansene dine.",
|
||||
"Max Latency": "Maks ventetid",
|
||||
"Maximum Latency": "Maks ventetid",
|
||||
"Min Latency": "Min latens",
|
||||
"Minimum Latency": "Min latens",
|
||||
"Minute-by-minute status data for this day": "Statusdata fra minutt for minutt for denne dagen",
|
||||
"MONITORING": "MONITORING",
|
||||
"Network error. Please try again.": "Nettverksfeil. Vennligst prøv igjen.",
|
||||
"No Events in %currentMonth": "Ingen hendelser i %currentMonth",
|
||||
"No events to show": "Ingen hendelser å vise",
|
||||
@@ -73,6 +87,7 @@
|
||||
"Notifications": "Varsler",
|
||||
"One-time": "En gang",
|
||||
"Ongoing": "Pågående",
|
||||
"Operational": "Operativ",
|
||||
"Partial Degraded Performance": "Delvis redusert ytelse",
|
||||
"Partial System Outage": "Delvis systemutfall",
|
||||
"Past": "Forbi",
|
||||
@@ -82,7 +97,11 @@
|
||||
"Please enter the 6-digit verification code": "Vennligst skriv inn den 6-sifrede bekreftelseskoden",
|
||||
"Read less": "Les mindre",
|
||||
"Read more": "Les mer",
|
||||
"READY": "KLAR",
|
||||
"Recurring": "Gjentakende",
|
||||
"RESOLVED": "RESOLVED",
|
||||
"SCHEDULED": "PLANLAGT",
|
||||
"Scheduled Events (%count)": "Planlagte hendelser (%count)",
|
||||
"Script": "Manus",
|
||||
"Select Language": "Velg Språk",
|
||||
"Select latency metric to display": "Velg latensmåling som skal vises",
|
||||
@@ -102,7 +121,10 @@
|
||||
"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)",
|
||||
"Uptime": "Oppetid",
|
||||
@@ -110,10 +132,6 @@
|
||||
"Verification failed": "Bekreftelsen mislyktes",
|
||||
"Verify": "Verifisere",
|
||||
"Verifying": "Bekrefter",
|
||||
"We sent a 6-digit code to": "Vi sendte en 6-sifret kode til",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"MONITORING": "MONITORING",
|
||||
"RESOLVED": "RESOLVED"
|
||||
"We sent a 6-digit code to": "Vi sendte en 6-sifret kode til"
|
||||
}
|
||||
}
|
||||
|
||||
+24
-6
@@ -8,14 +8,21 @@
|
||||
"Avg Latency": "Gem. latentie",
|
||||
"Back": "Rug",
|
||||
"Badges": "Insignes",
|
||||
"CANCELLED": "GEANNULEERD",
|
||||
"COMPLETED": "VOLTOOID",
|
||||
"Continue": "Doorgaan",
|
||||
"Copied": "Gekopieerd",
|
||||
"Current": "Huidig",
|
||||
"Dark": "Donker",
|
||||
"Day": "Dag",
|
||||
"Day Uptime": "Dag uptime",
|
||||
"Days": "Dagen",
|
||||
"Degraded": "Verstoord",
|
||||
"DEGRADED": "VERSLECHTERD",
|
||||
"Degraded Performance": "Verminderde prestaties",
|
||||
"Didn't receive the code? Resend": "Heb je de code niet ontvangen? ",
|
||||
"Down": "Niet bereikbaar",
|
||||
"DOWN": "OFFLINE",
|
||||
"Duration": "Duur",
|
||||
"Email address": "E-mailadres",
|
||||
"Embed Monitor": "Monitor insluiten",
|
||||
@@ -32,11 +39,14 @@
|
||||
"Get notified about incidents and scheduled maintenance.": "Ontvang meldingen over incidenten en gepland onderhoud.",
|
||||
"Get notified about incidents updates": "Ontvang meldingen over incidentupdates",
|
||||
"Get notified about scheduled maintenance": "Ontvang meldingen over gepland onderhoud",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"iFrame": "iFrame",
|
||||
"Impact": "Invloed",
|
||||
"incident": "Incident",
|
||||
"Incident Updates": "Incidentupdates",
|
||||
"Incidents": "Incidenten",
|
||||
"Included Monitors": "Included Monitors",
|
||||
"Included Monitors (%count)": "Included Monitors (%count)",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"Last Updated": "Laatst bijgewerkt",
|
||||
"Latency": "Latentie",
|
||||
"Latency Embed": "Latentie insluiten",
|
||||
@@ -47,15 +57,19 @@
|
||||
"Light": "Licht",
|
||||
"Live Status": "Live-status",
|
||||
"Loading your preferences...": "Uw voorkeuren laden...",
|
||||
"maintenance": "Onderhoud",
|
||||
"MAINTENANCE": "ONDERHOUD",
|
||||
"Maintenance Updates": "Onderhoudsupdates",
|
||||
"Maintenances": "Onderhouden",
|
||||
"Major System Outage": "Grote systeemstoring",
|
||||
"Manage Site": "Site beheren",
|
||||
"Manage your notification preferences.": "Beheer uw meldingsvoorkeuren.",
|
||||
"Max Latency": "Maximale latentie",
|
||||
"Maximum Latency": "Maximale latentie",
|
||||
"Min Latency": "Minimale latentie",
|
||||
"Minimum Latency": "Minimale latentie",
|
||||
"Minute-by-minute status data for this day": "Statusgegevens van minuut tot minuut voor deze dag",
|
||||
"MONITORING": "MONITORING",
|
||||
"Network error. Please try again.": "Netwerkfout. ",
|
||||
"No Events in %currentMonth": "Geen evenementen in %currentMonth",
|
||||
"No events to show": "Geen gebeurtenissen om weer te geven",
|
||||
@@ -73,6 +87,7 @@
|
||||
"Notifications": "Meldingen",
|
||||
"One-time": "Eenmalig",
|
||||
"Ongoing": "Lopend",
|
||||
"Operational": "Operationeel",
|
||||
"Partial Degraded Performance": "Gedeeltelijk verminderde prestaties",
|
||||
"Partial System Outage": "Gedeeltelijke systeemstoring",
|
||||
"Past": "Verleden",
|
||||
@@ -82,7 +97,11 @@
|
||||
"Please enter the 6-digit verification code": "Voer de 6-cijferige verificatiecode in",
|
||||
"Read less": "Lees minder",
|
||||
"Read more": "Lees meer",
|
||||
"READY": "GEREED",
|
||||
"Recurring": "Terugkerend",
|
||||
"RESOLVED": "RESOLVED",
|
||||
"SCHEDULED": "GEPLAND",
|
||||
"Scheduled Events (%count)": "Geplande evenementen (%count)",
|
||||
"Script": "Script",
|
||||
"Select Language": "Selecteer Taal",
|
||||
"Select latency metric to display": "Selecteer de latentie-metriek die moet worden weergegeven",
|
||||
@@ -102,7 +121,10 @@
|
||||
"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)",
|
||||
"Uptime": "Uptime",
|
||||
@@ -110,10 +132,6 @@
|
||||
"Verification failed": "Verificatie mislukt",
|
||||
"Verify": "Verifiëren",
|
||||
"Verifying": "Verifiëren",
|
||||
"We sent a 6-digit code to": "We hebben een 6-cijferige code gestuurd naar",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"MONITORING": "MONITORING",
|
||||
"RESOLVED": "RESOLVED"
|
||||
"We sent a 6-digit code to": "We hebben een 6-cijferige code gestuurd naar"
|
||||
}
|
||||
}
|
||||
|
||||
+24
-6
@@ -8,14 +8,21 @@
|
||||
"Avg Latency": "Średnie opóźnienie",
|
||||
"Back": "Z powrotem",
|
||||
"Badges": "Odznaki",
|
||||
"CANCELLED": "ANULOWANE",
|
||||
"COMPLETED": "ZAKOŃCZONE",
|
||||
"Continue": "Kontynuować",
|
||||
"Copied": "Skopiowano",
|
||||
"Current": "Bieżące",
|
||||
"Dark": "Ciemny",
|
||||
"Day": "Dzień",
|
||||
"Day Uptime": "Dzień sprawności",
|
||||
"Days": "Dni",
|
||||
"Degraded": "Pogorszony",
|
||||
"DEGRADED": "POGORSZONY",
|
||||
"Degraded Performance": "Obniżona wydajność",
|
||||
"Didn't receive the code? Resend": "Nie otrzymałeś kodu? ",
|
||||
"Down": "Niedostępny",
|
||||
"DOWN": "AWARIA",
|
||||
"Duration": "Czas trwania",
|
||||
"Email address": "Adres e-mail",
|
||||
"Embed Monitor": "Wstaw monitor",
|
||||
@@ -32,11 +39,14 @@
|
||||
"Get notified about incidents and scheduled maintenance.": "Otrzymuj powiadomienia o incydentach i zaplanowanych konserwacjach.",
|
||||
"Get notified about incidents updates": "Otrzymuj powiadomienia o aktualizacjach incydentów",
|
||||
"Get notified about scheduled maintenance": "Otrzymuj powiadomienia o zaplanowanej konserwacji",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"iFrame": "iFramka",
|
||||
"Impact": "Uderzenie",
|
||||
"incident": "Incydent",
|
||||
"Incident Updates": "Aktualizacje incydentów",
|
||||
"Incidents": "Incydenty",
|
||||
"Included Monitors": "Included Monitors",
|
||||
"Included Monitors (%count)": "Included Monitors (%count)",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"Last Updated": "Ostatnia aktualizacja",
|
||||
"Latency": "Utajenie",
|
||||
"Latency Embed": "Osadzanie opóźnień",
|
||||
@@ -47,15 +57,19 @@
|
||||
"Light": "Światło",
|
||||
"Live Status": "Stan na żywo",
|
||||
"Loading your preferences...": "Ładowanie Twoich preferencji...",
|
||||
"maintenance": "Konserwacja",
|
||||
"MAINTENANCE": "KONSERWACJA",
|
||||
"Maintenance Updates": "Aktualizacje konserwacyjne",
|
||||
"Maintenances": "Konserwacje",
|
||||
"Major System Outage": "Poważna awaria systemu",
|
||||
"Manage Site": "Zarządzaj stroną",
|
||||
"Manage your notification preferences.": "Zarządzaj preferencjami powiadomień.",
|
||||
"Max Latency": "Maksymalne opóźnienie",
|
||||
"Maximum Latency": "Maksymalne opóźnienie",
|
||||
"Min Latency": "Minimalne opóźnienie",
|
||||
"Minimum Latency": "Minimalne opóźnienie",
|
||||
"Minute-by-minute status data for this day": "Dane o stanie minuta po minucie na ten dzień",
|
||||
"MONITORING": "MONITORING",
|
||||
"Network error. Please try again.": "Błąd sieci. ",
|
||||
"No Events in %currentMonth": "Brak wydarzeń w %currentMonth",
|
||||
"No events to show": "Brak zdarzeń do wyświetlenia",
|
||||
@@ -73,6 +87,7 @@
|
||||
"Notifications": "Powiadomienia",
|
||||
"One-time": "Jednorazowy",
|
||||
"Ongoing": "Bieżący",
|
||||
"Operational": "Operacyjny",
|
||||
"Partial Degraded Performance": "Częściowo obniżona wydajność",
|
||||
"Partial System Outage": "Częściowa awaria systemu",
|
||||
"Past": "Przeszłość",
|
||||
@@ -82,7 +97,11 @@
|
||||
"Please enter the 6-digit verification code": "Wprowadź 6-cyfrowy kod weryfikacyjny",
|
||||
"Read less": "Czytaj mniej",
|
||||
"Read more": "Przeczytaj więcej",
|
||||
"READY": "GOTOWE",
|
||||
"Recurring": "Cykliczne",
|
||||
"RESOLVED": "RESOLVED",
|
||||
"SCHEDULED": "ZAPLANOWANE",
|
||||
"Scheduled Events (%count)": "Zaplanowane wydarzenia (%count)",
|
||||
"Script": "Scenariusz",
|
||||
"Select Language": "Wybierz Język",
|
||||
"Select latency metric to display": "Wybierz metrykę opóźnienia do wyświetlenia",
|
||||
@@ -102,7 +121,10 @@
|
||||
"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)",
|
||||
"Uptime": "Czas pracy",
|
||||
@@ -110,10 +132,6 @@
|
||||
"Verification failed": "Weryfikacja nie powiodła się",
|
||||
"Verify": "Zweryfikować",
|
||||
"Verifying": "Weryfikacja",
|
||||
"We sent a 6-digit code to": "Wysłaliśmy 6-cyfrowy kod na adres",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"MONITORING": "MONITORING",
|
||||
"RESOLVED": "RESOLVED"
|
||||
"We sent a 6-digit code to": "Wysłaliśmy 6-cyfrowy kod na adres"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,14 +8,21 @@
|
||||
"Avg Latency": "Latência Média",
|
||||
"Back": "Voltar",
|
||||
"Badges": "Emblemas",
|
||||
"CANCELLED": "CANCELADO",
|
||||
"COMPLETED": "CONCLUÍDO",
|
||||
"Continue": "Continuar",
|
||||
"Copied": "Copiado",
|
||||
"Current": "Atual",
|
||||
"Dark": "Escuro",
|
||||
"Day": "Dia",
|
||||
"Day Uptime": "Tempo de atividade diurno",
|
||||
"Days": "Dias",
|
||||
"Degraded": "Degradado",
|
||||
"DEGRADED": "DEGRADADO",
|
||||
"Degraded Performance": "Desempenho degradado",
|
||||
"Didn't receive the code? Resend": "Não recebeu o código? ",
|
||||
"Down": "Fora do ar",
|
||||
"DOWN": "FORA DO AR",
|
||||
"Duration": "Duração",
|
||||
"Email address": "Endereço de email",
|
||||
"Embed Monitor": "Incorporar monitor",
|
||||
@@ -32,11 +39,14 @@
|
||||
"Get notified about incidents and scheduled maintenance.": "Seja notificado sobre incidentes e manutenções programadas.",
|
||||
"Get notified about incidents updates": "Seja notificado sobre atualizações de incidentes",
|
||||
"Get notified about scheduled maintenance": "Seja notificado sobre manutenções programadas",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"iFrame": "iFrame",
|
||||
"Impact": "Impacto",
|
||||
"incident": "Incidente",
|
||||
"Incident Updates": "Atualizações de incidentes",
|
||||
"Incidents": "Incidentes",
|
||||
"Included Monitors": "Included Monitors",
|
||||
"Included Monitors (%count)": "Included Monitors (%count)",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"Last Updated": "Última atualização",
|
||||
"Latency": "Latência",
|
||||
"Latency Embed": "Incorporação de latência",
|
||||
@@ -47,15 +57,19 @@
|
||||
"Light": "Luz",
|
||||
"Live Status": "Status ao vivo",
|
||||
"Loading your preferences...": "Carregando suas preferências...",
|
||||
"maintenance": "Manutenção",
|
||||
"MAINTENANCE": "MANUTENÇÃO",
|
||||
"Maintenance Updates": "Atualizações de manutenção",
|
||||
"Maintenances": "Manutenção",
|
||||
"Major System Outage": "Indisponibilidade grave do sistema",
|
||||
"Manage Site": "Gerenciar site",
|
||||
"Manage your notification preferences.": "Gerencie suas preferências de notificação.",
|
||||
"Max Latency": "Latência máxima",
|
||||
"Maximum Latency": "Latência máxima",
|
||||
"Min Latency": "Latência mínima",
|
||||
"Minimum Latency": "Latência mínima",
|
||||
"Minute-by-minute status data for this day": "Dados de status minuto a minuto para este dia",
|
||||
"MONITORING": "MONITORING",
|
||||
"Network error. Please try again.": "Erro de rede. ",
|
||||
"No Events in %currentMonth": "Nenhum evento em %currentMonth",
|
||||
"No events to show": "Não há eventos para exibir",
|
||||
@@ -73,6 +87,7 @@
|
||||
"Notifications": "Notificações",
|
||||
"One-time": "Único",
|
||||
"Ongoing": "Em andamento",
|
||||
"Operational": "Operacional",
|
||||
"Partial Degraded Performance": "Desempenho parcialmente degradado",
|
||||
"Partial System Outage": "Indisponibilidade parcial do sistema",
|
||||
"Past": "Passado",
|
||||
@@ -82,7 +97,11 @@
|
||||
"Please enter the 6-digit verification code": "Insira o código de verificação de 6 dígitos",
|
||||
"Read less": "Leia menos",
|
||||
"Read more": "Leia mais",
|
||||
"READY": "PRONTO",
|
||||
"Recurring": "Recorrente",
|
||||
"RESOLVED": "RESOLVED",
|
||||
"SCHEDULED": "AGENDADO",
|
||||
"Scheduled Events (%count)": "Eventos agendados (%count)",
|
||||
"Script": "Roteiro",
|
||||
"Select Language": "Selecione o idioma",
|
||||
"Select latency metric to display": "Selecione a métrica de latência para exibir",
|
||||
@@ -102,7 +121,10 @@
|
||||
"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)",
|
||||
"Uptime": "Tempo de atividade",
|
||||
@@ -110,10 +132,6 @@
|
||||
"Verification failed": "Falha na verificação",
|
||||
"Verify": "Verificar",
|
||||
"Verifying": "Verificando",
|
||||
"We sent a 6-digit code to": "Enviamos um código de 6 dígitos para",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"MONITORING": "MONITORING",
|
||||
"RESOLVED": "RESOLVED"
|
||||
"We sent a 6-digit code to": "Enviamos um código de 6 dígitos para"
|
||||
}
|
||||
}
|
||||
|
||||
+24
-6
@@ -9,14 +9,21 @@
|
||||
"Avg Latency": "Средняя задержка",
|
||||
"Back": "Назад",
|
||||
"Badges": "Значки",
|
||||
"CANCELLED": "ОТМЕНЕНО",
|
||||
"COMPLETED": "ЗАВЕРШЕНО",
|
||||
"Continue": "Продолжить",
|
||||
"Copied": "Скопировано",
|
||||
"Current": "Текущие",
|
||||
"Dark": "Темная",
|
||||
"Day": "Day",
|
||||
"Day Uptime": "Время работы за день",
|
||||
"Days": "Days",
|
||||
"Degraded": "Деградация",
|
||||
"DEGRADED": "УХУДШЕНИЕ",
|
||||
"Degraded Performance": "Снижение производительности",
|
||||
"Didn't receive the code? Resend": "Не получили код? Отправить повторно",
|
||||
"Down": "Недоступен",
|
||||
"DOWN": "НЕ РАБОТАЕТ",
|
||||
"Duration": "Продолжительность",
|
||||
"Email address": "Адрес электронной почты",
|
||||
"Embed Monitor": "Встроить монитор",
|
||||
@@ -33,11 +40,14 @@
|
||||
"Get notified about incidents and scheduled maintenance.": "Получайте уведомления об инцидентах и плановом обслуживании.",
|
||||
"Get notified about incidents updates": "Получайте уведомления об обновлениях инцидентов",
|
||||
"Get notified about scheduled maintenance": "Получайте уведомления о плановом обслуживании",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"iFrame": "iFrame",
|
||||
"Impact": "Влияние",
|
||||
"incident": "Инцидент",
|
||||
"Incident Updates": "Обновления инцидентов",
|
||||
"Incidents": "Инциденты",
|
||||
"Included Monitors": "Included Monitors",
|
||||
"Included Monitors (%count)": "Included Monitors (%count)",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"Last Updated": "Последнее обновление",
|
||||
"Latency": "Задержка",
|
||||
"Latency Embed": "Встроенная задержка",
|
||||
@@ -48,15 +58,19 @@
|
||||
"Light": "Светлая",
|
||||
"Live Status": "Статус в реальном времени",
|
||||
"Loading your preferences...": "Загрузка настроек...",
|
||||
"maintenance": "Обслуживание",
|
||||
"MAINTENANCE": "ОБСЛУЖИВАНИЕ",
|
||||
"Maintenance Updates": "Обновления обслуживания",
|
||||
"Maintenances": "Обслуживание",
|
||||
"Major System Outage": "Крупный сбой системы",
|
||||
"Manage Site": "Управление сайтом",
|
||||
"Manage your notification preferences.": "Управляйте настройками уведомлений.",
|
||||
"Max Latency": "Max Latency",
|
||||
"Maximum Latency": "Max Latency",
|
||||
"Min Latency": "Min Latency",
|
||||
"Minimum Latency": "Min Latency",
|
||||
"Minute-by-minute status data for this day": "Поминутные данные о статусе за этот день",
|
||||
"MONITORING": "MONITORING",
|
||||
"Network error. Please try again.": "Ошибка сети. Пожалуйста, попробуйте снова.",
|
||||
"No Events in %currentMonth": "Нет событий в %currentMonth",
|
||||
"No events to show": "Нет событий для отображения",
|
||||
@@ -74,6 +88,7 @@
|
||||
"Notifications": "Уведомления",
|
||||
"One-time": "Одноразовое",
|
||||
"Ongoing": "Текущие",
|
||||
"Operational": "Работает",
|
||||
"Partial Degraded Performance": "Частичное снижение производительности",
|
||||
"Partial System Outage": "Частичный сбой системы",
|
||||
"Past": "Прошедшие",
|
||||
@@ -83,7 +98,11 @@
|
||||
"Please enter the 6-digit verification code": "Пожалуйста, введите 6-значный код подтверждения",
|
||||
"Read less": "Читать меньше",
|
||||
"Read more": "Читать далее",
|
||||
"READY": "ГОТОВО",
|
||||
"Recurring": "Повторяющийся",
|
||||
"RESOLVED": "RESOLVED",
|
||||
"SCHEDULED": "ЗАПЛАНИРОВАНО",
|
||||
"Scheduled Events (%count)": "Запланированные события (%count)",
|
||||
"Script": "Скрипт",
|
||||
"Select Language": "Выберите язык",
|
||||
"Select latency metric to display": "Выберите метрику задержки для отображения",
|
||||
@@ -103,7 +122,10 @@
|
||||
"Total Maintenances": "Total Maintenances",
|
||||
"Under Maintenance": "На обслуживании",
|
||||
"Unknown impact": "Неизвестное влияние",
|
||||
"UP": "РАБОТАЕТ",
|
||||
"Upcoming": "Предстоящие",
|
||||
"Update Incident": "Обновить инцидент",
|
||||
"Update Maintenance": "Обновить обслуживание",
|
||||
"Updates": "Обновления",
|
||||
"Updates (%count)": "Обновления (%count)",
|
||||
"Uptime": "Время работы",
|
||||
@@ -111,10 +133,6 @@
|
||||
"Verification failed": "Проверка не удалась",
|
||||
"Verify": "Verify",
|
||||
"Verifying": "Verifying",
|
||||
"We sent a 6-digit code to": "Мы отправили 6-значный код на",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"MONITORING": "MONITORING",
|
||||
"RESOLVED": "RESOLVED"
|
||||
"We sent a 6-digit code to": "Мы отправили 6-значный код на"
|
||||
}
|
||||
}
|
||||
|
||||
+108
-70
@@ -2,118 +2,156 @@
|
||||
"name": "Slovenčina",
|
||||
"mappings": {
|
||||
"%latency %metric latency": "%latency %metric latencia",
|
||||
"24 Hours": "24 hodín",
|
||||
"30 Days": "30 dní",
|
||||
"7 Days": "7 dní",
|
||||
"90 Days": "90 dní",
|
||||
"Affected Monitors (%count)": "Ovplyvnené monitory (%count)",
|
||||
"All Systems Operational": "Všetky systémy sú funkčné",
|
||||
"All Systems Operational": "Všetky systémy sú v prevádzke",
|
||||
"average": "priemerná",
|
||||
"Average Latency": "Priemerná latencia",
|
||||
"Avg Latency": "Priem. latencia",
|
||||
"Back": "Späť",
|
||||
"Badges": "Odznaky",
|
||||
"CANCELLED": "ZRUŠENÉ",
|
||||
"COMPLETED": "DOKONČENÉ",
|
||||
"Continue": "Pokračovať",
|
||||
"Copied": "Skopírované",
|
||||
"Current": "Aktuálne",
|
||||
"Dark": "Tmavý",
|
||||
"Day": "deň",
|
||||
"Day Uptime": "Day Uptime",
|
||||
"Days": "dní",
|
||||
"Day": "Deň",
|
||||
"Day Uptime": "Denná dostupnosť",
|
||||
"Days": "Dní",
|
||||
"Degraded": "Obmedzený",
|
||||
"DEGRADED": "OBMEDZENÝ",
|
||||
"Degraded Performance": "Znížený výkon",
|
||||
"Didn't receive the code? Resend": "Nedostali ste kód? ",
|
||||
"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",
|
||||
"End Time": "Čas ukončenia",
|
||||
"Enter the verification code sent to your email.": "Zadajte overovací kód odoslaný na váš e-mail.",
|
||||
"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 e-mail",
|
||||
"Events": "Udalosti",
|
||||
"Failed to load data": "Nepodarilo sa načítať údaje",
|
||||
"Failed to load latency data": "Nepodarilo sa načítať údaje o latencii",
|
||||
"Failed to load status data for this day": "Nepodarilo sa načítať údaje o stave pre tento deň",
|
||||
"Failed to send verification code": "Odoslanie overovacieho kódu zlyhalo",
|
||||
"Failed to update preference": "Predvoľbu sa nepodarilo aktualizovať",
|
||||
"Get badges for this monitor": "Získajte odznaky pre tento monitor",
|
||||
"Get notified about incidents and scheduled maintenance.": "Získajte upozornenia na incidenty a plánovanú údržbu.",
|
||||
"Get notified about incidents updates": "Získajte upozornenia na aktualizácie incidentov",
|
||||
"Get notified about scheduled maintenance": "Získajte upozornenia na plánovanú údržbu",
|
||||
"Failed to load data": "Nepodarilo sa načítať dáta",
|
||||
"Failed to load latency data": "Nepodarilo sa načítať dáta latencie",
|
||||
"Failed to load status data for this day": "Nepodarilo sa načítať stavové dáta pre tento deň",
|
||||
"Failed to send verification code": "Nepodarilo sa odoslať overovací kód",
|
||||
"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 updates": "Dostávajte upozornenia na aktualizácie incidentov",
|
||||
"Get notified about scheduled maintenance": "Dostávajte upozornenia na plánovanú údržbu",
|
||||
"Home": "Domov",
|
||||
"IDENTIFIED": "IDENTIFIKOVANÉ",
|
||||
"iFrame": "iFrame",
|
||||
"Impact": "Vplyv",
|
||||
"Impact": "Dopad",
|
||||
"incident": "incident",
|
||||
"Incident": "Incident",
|
||||
"Incident Updates": "Aktualizácie incidentov",
|
||||
"Incidents": "Incidenty",
|
||||
"Included Monitors": "Included Monitors",
|
||||
"Last Updated": "Posledná aktualizácia",
|
||||
"Included Monitors (%count)": "Zahrnuté monitory (%count)",
|
||||
"INVESTIGATING": "PREBIEHA VYŠETROVANIE",
|
||||
"Last Updated": "Naposledy aktualizované",
|
||||
"Latency": "Latencia",
|
||||
"Latency Embed": "Latency Embed",
|
||||
"Latency Over Time": "Latencia v priebehu času",
|
||||
"Latency Embed": "Vložená latencia",
|
||||
"Latency Over Time": "Latencia v čase",
|
||||
"Latency Trend": "Trend latencie",
|
||||
"Latest Latency": "Najnovšia latencia",
|
||||
"Latest Status": "Najnovší stav",
|
||||
"Light": "Svetlo",
|
||||
"Live Status": "Živý stav",
|
||||
"Loading your preferences...": "Načítavajú sa vaše preferencie...",
|
||||
"Latest Latency": "Posledná latencia",
|
||||
"Latest Status": "Naposledy zistený stav",
|
||||
"Light": "Svetlý",
|
||||
"Live Status": "Aktuálny stav",
|
||||
"Loading your preferences...": "Načítavanie nastavení...",
|
||||
"maintenance": "údržba",
|
||||
"Maintenance": "Údržba",
|
||||
"MAINTENANCE": "ÚDRŽBA",
|
||||
"Maintenance Updates": "Aktualizácie údržby",
|
||||
"Maintenances": "Údržba",
|
||||
"Major System Outage": "Veľký výpadok systému",
|
||||
"Manage your notification preferences.": "Spravujte svoje predvoľby upozornení.",
|
||||
"Max Latency": "Maximálna latencia",
|
||||
"Maintenances": "Údržby",
|
||||
"Major System Outage": "Závažný výpadok systému",
|
||||
"Manage Site": "Spravovať stránku",
|
||||
"Manage your notification preferences.": "Spravujte nastavenia upozornení",
|
||||
"Max Latency": "Max. latencia",
|
||||
"maximum": "maximálna",
|
||||
"Maximum Latency": "Maximálna latencia",
|
||||
"Min Latency": "Minimálna latencia",
|
||||
"Min Latency": "Min. latencia",
|
||||
"minimum": "minimálna",
|
||||
"Minimum Latency": "Minimálna latencia",
|
||||
"Minute-by-minute status data for this day": "Údaje o stave z minúty na minútu pre tento deň",
|
||||
"Network error. Please try again.": "Chyba siete. ",
|
||||
"No Events in %currentMonth": "Žiadne udalosti v %currentMonth",
|
||||
"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ú žiadne udalosti",
|
||||
"No events to show": "Žiadne udalosti na zobrazenie",
|
||||
"No incidents for this day": "Žiadne incidenty pre tento deň",
|
||||
"No latency data available for this day": "Pre tento deň nie sú k dispozícii žiadne údaje o latencii",
|
||||
"No maintenances for this day": "Na tento deň nie sú žiadne údržby",
|
||||
"No monitors affected": "Nie sú ovplyvnené žiadne monitory",
|
||||
"No monitors available.": "Nie sú k dispozícii žiadne monitory.",
|
||||
"No ongoing maintenances": "Žiadna priebežná údržba",
|
||||
"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ú dostupné dáta latencie",
|
||||
"No maintenances for this day": "Pre tento deň nie je naplánovaná žiadna údržba",
|
||||
"No monitors affected": "Žiadne ovplyvnené monitory",
|
||||
"No monitors available.": "Žiadne dostupné monitory",
|
||||
"No ongoing maintenances": "Žiadna prebiehajúca ú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ľ žiadne aktualizácie",
|
||||
"NO_DATA": "Žiadne dáta",
|
||||
"Notifications": "Upozornenia",
|
||||
"One-time": "Jednorazovo",
|
||||
"Ongoing": "Prebieha",
|
||||
"Partial Degraded Performance": "Čiastočne znížený výkon",
|
||||
"Ongoing": "Prebiehajúce",
|
||||
"ONGOING": "PREBIEHAJÚCE",
|
||||
"Ongoing Maintenances": "Prebiehajúce údržby",
|
||||
"Operational": "V prevádzke",
|
||||
"Partial Degraded Performance": "Čiastočne obmedzený výkon",
|
||||
"Partial System Outage": "Čiastočný výpadok systému",
|
||||
"Past": "Minulosť",
|
||||
"Per-Minute Status": "Stav za minútu",
|
||||
"Pinging": "Ping",
|
||||
"Past": "Minulé",
|
||||
"Per-Minute Status": "Stav po minútach",
|
||||
"Pinging": "Pingovanie",
|
||||
"Please enter a valid email address": "Zadajte platnú e-mailovú adresu",
|
||||
"Please enter the 6-digit verification code": "Zadajte 6-miestny overovací kód",
|
||||
"Read less": "Čítajte menej",
|
||||
"Read more": "Prečítajte si viac",
|
||||
"Read less": "Zobraziť menej",
|
||||
"Read more": "Zobraziť viac",
|
||||
"READY": "PRIPRAVENÉ",
|
||||
"Recent Incidents": "Nedávne incidenty",
|
||||
"Recurring": "Opakujúce sa",
|
||||
"RESOLVED": "VYRIEŠENÉ",
|
||||
"SCHEDULED": "NAPLÁNOVANÉ",
|
||||
"Scheduled Events (%count)": "Plánované udalosti (%count)",
|
||||
"Scheduled Windows": "Naplánované úlohy",
|
||||
"Script": "Skript",
|
||||
"Select Language": "Vyberte Jazyk",
|
||||
"Select latency metric to display": "Vyberte metriku latencie na zobrazenie",
|
||||
"Select Range": "Vyberte položku Rozsah",
|
||||
"Sending...": "Odosiela sa...",
|
||||
"Standard": "Štandardné",
|
||||
"Start Time": "Čas začiatku",
|
||||
"Select Language": "Vyberte jazyk",
|
||||
"Select latency metric to display": "Vyberte metriku latencie",
|
||||
"Select Range": "Vyberte rozsah",
|
||||
"Sending...": "Odosielanie...",
|
||||
"Standard": "Štandardný",
|
||||
"Start Time": "Začiatok",
|
||||
"Status": "Stav",
|
||||
"Status Badge": "Stavový odznak",
|
||||
"Status Embed": "Stav Vložiť",
|
||||
"Status Badge": "Odznak stavu",
|
||||
"Status Embed": "Stav na vloženie",
|
||||
"Status history and latency trend": "História stavu a trend latencie",
|
||||
"Subscribe": "Prihlásiť sa na odber",
|
||||
"Subscribe to Updates": "Prihláste sa na odber aktualizácií",
|
||||
"There are no incidents or maintenances scheduled for this month.": "Na tento mesiac nie sú naplánované žiadne incidenty ani údržba.",
|
||||
"There are no ongoing incidents or maintenance events.": "Neexistujú žiadne prebiehajúce incidenty ani udalosti údržby.",
|
||||
"Total Incidents": "Celkový počet incidentov",
|
||||
"Total Maintenances": "Celková údržba",
|
||||
"Under Maintenance": "V časti Údržba",
|
||||
"Unknown impact": "Neznámy vplyv",
|
||||
"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é ž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",
|
||||
"Under Maintenance": "Prebieha údržba",
|
||||
"Unknown impact": "Neznámy dopad",
|
||||
"UP": "V PREVÁDZKE",
|
||||
"Upcoming": "Nadchádzajúce",
|
||||
"Upcoming Maintenances": "Nadchádzajúce údržby",
|
||||
"Update Incident": "Aktualizovať incident",
|
||||
"Update Maintenance": "Aktualizovať údržbu",
|
||||
"Updates": "Aktualizácie",
|
||||
"Updates (%count)": "Aktualizácie (%count)",
|
||||
"Uptime": "Uptime",
|
||||
"Uptime": "Dostupnosť",
|
||||
"Uptime Badge": "Odznak dostupnosti",
|
||||
"Verification failed": "Overenie zlyhalo",
|
||||
"Verify": "Overiť",
|
||||
"Verifying": "Overuje sa",
|
||||
"We sent a 6-digit code to": "Poslali sme 6-miestny kód na",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"MONITORING": "MONITORING",
|
||||
"RESOLVED": "RESOLVED"
|
||||
"Verifying": "Overovanie",
|
||||
"We sent a 6-digit code to": "Odoslali sme 6-miestny kód na"
|
||||
}
|
||||
}
|
||||
|
||||
+24
-6
@@ -8,14 +8,21 @@
|
||||
"Avg Latency": "Ortalama Gecikme",
|
||||
"Back": "Geri",
|
||||
"Badges": "Rozetler",
|
||||
"CANCELLED": "İPTAL EDİLMİŞ",
|
||||
"COMPLETED": "TAMAMLANMIŞ",
|
||||
"Continue": "Devam etmek",
|
||||
"Copied": "Kopyalandı",
|
||||
"Current": "Güncel",
|
||||
"Dark": "Karanlık",
|
||||
"Day": "Gün",
|
||||
"Day Uptime": "Gün Çalışma Süresi",
|
||||
"Days": "Günler",
|
||||
"Degraded": "Bozulmuş",
|
||||
"DEGRADED": "DÜŞÜK PERFORMANS",
|
||||
"Degraded Performance": "Düşük performans",
|
||||
"Didn't receive the code? Resend": "Kodu almadınız mı? ",
|
||||
"Down": "Çevrimdışı",
|
||||
"DOWN": "ÇÖKME",
|
||||
"Duration": "Süre",
|
||||
"Email address": "E-posta adresi",
|
||||
"Embed Monitor": "Gömülü Monitör",
|
||||
@@ -32,11 +39,14 @@
|
||||
"Get notified about incidents and scheduled maintenance.": "Olaylar ve planlı bakım hakkında bildirim alın.",
|
||||
"Get notified about incidents updates": "Olay güncellemeleri hakkında bildirim alın",
|
||||
"Get notified about scheduled maintenance": "Planlanmış bakım hakkında bildirim alın",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"iFrame": "iFrame",
|
||||
"Impact": "Darbe",
|
||||
"incident": "Olay",
|
||||
"Incident Updates": "Olay Güncellemeleri",
|
||||
"Incidents": "Olaylar",
|
||||
"Included Monitors": "Included Monitors",
|
||||
"Included Monitors (%count)": "Included Monitors (%count)",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"Last Updated": "Son Güncelleme",
|
||||
"Latency": "Gecikme",
|
||||
"Latency Embed": "Gecikme Yerleştirme",
|
||||
@@ -47,15 +57,19 @@
|
||||
"Light": "Işık",
|
||||
"Live Status": "Canlı Durum",
|
||||
"Loading your preferences...": "Tercihleriniz yükleniyor...",
|
||||
"maintenance": "Bakım",
|
||||
"MAINTENANCE": "BAKIM",
|
||||
"Maintenance Updates": "Bakım Güncellemeleri",
|
||||
"Maintenances": "Bakımlar",
|
||||
"Major System Outage": "Büyük sistem kesintisi",
|
||||
"Manage Site": "Siteyi yönet",
|
||||
"Manage your notification preferences.": "Bildirim tercihlerinizi yönetin.",
|
||||
"Max Latency": "Maksimum Gecikme",
|
||||
"Maximum Latency": "Maksimum Gecikme",
|
||||
"Min Latency": "Minimum Gecikme",
|
||||
"Minimum Latency": "Minimum Gecikme",
|
||||
"Minute-by-minute status data for this day": "Bu günün dakika dakika durum verileri",
|
||||
"MONITORING": "MONITORING",
|
||||
"Network error. Please try again.": "Ağ hatası. ",
|
||||
"No Events in %currentMonth": "%currentMonth Bölgesinde Etkinlik Yok",
|
||||
"No events to show": "Gösterilecek olay yok",
|
||||
@@ -73,6 +87,7 @@
|
||||
"Notifications": "Bildirimler",
|
||||
"One-time": "Bir kerelik",
|
||||
"Ongoing": "devam ediyor",
|
||||
"Operational": "Çalışıyor",
|
||||
"Partial Degraded Performance": "Kısmi düşük performans",
|
||||
"Partial System Outage": "Kısmi sistem kesintisi",
|
||||
"Past": "Geçmiş",
|
||||
@@ -82,7 +97,11 @@
|
||||
"Please enter the 6-digit verification code": "Lütfen 6 haneli doğrulama kodunu girin",
|
||||
"Read less": "Daha az oku",
|
||||
"Read more": "Devamını oku",
|
||||
"READY": "HAZIR",
|
||||
"Recurring": "Tekrarlayan",
|
||||
"RESOLVED": "RESOLVED",
|
||||
"SCHEDULED": "PLANLANMIŞ",
|
||||
"Scheduled Events (%count)": "Planlanan etkinlikler (%count)",
|
||||
"Script": "Senaryo",
|
||||
"Select Language": "Dil Seçiniz",
|
||||
"Select latency metric to display": "Gösterilecek gecikme metriğini seçin",
|
||||
@@ -102,7 +121,10 @@
|
||||
"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)",
|
||||
"Uptime": "Çalışma süresi",
|
||||
@@ -110,10 +132,6 @@
|
||||
"Verification failed": "Doğrulama başarısız oldu",
|
||||
"Verify": "Doğrulamak",
|
||||
"Verifying": "Doğrulanıyor",
|
||||
"We sent a 6-digit code to": "6 haneli kodu gönderdik",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"MONITORING": "MONITORING",
|
||||
"RESOLVED": "RESOLVED"
|
||||
"We sent a 6-digit code to": "6 haneli kodu gönderdik"
|
||||
}
|
||||
}
|
||||
|
||||
+24
-6
@@ -8,14 +8,21 @@
|
||||
"Avg Latency": "Độ trễ trung bình",
|
||||
"Back": "Mặt sau",
|
||||
"Badges": "Huy hiệu",
|
||||
"CANCELLED": "ĐÃ HỦY",
|
||||
"COMPLETED": "HOÀN THÀNH",
|
||||
"Continue": "Tiếp tục",
|
||||
"Copied": "Đã sao chép",
|
||||
"Current": "Hiện tại",
|
||||
"Dark": "Tối tăm",
|
||||
"Day": "Ngày",
|
||||
"Day Uptime": "Thời gian hoạt động trong ngày",
|
||||
"Days": "Ngày",
|
||||
"Degraded": "Suy giảm",
|
||||
"DEGRADED": "SUY GIẢM",
|
||||
"Degraded Performance": "Hiệu suất suy giảm",
|
||||
"Didn't receive the code? Resend": "Không nhận được mã? ",
|
||||
"Down": "Ngừng hoạt động",
|
||||
"DOWN": "NGỪNG HOẠT ĐỘNG",
|
||||
"Duration": "Khoảng thời gian",
|
||||
"Email address": "Địa chỉ email",
|
||||
"Embed Monitor": "Màn hình nhúng",
|
||||
@@ -32,11 +39,14 @@
|
||||
"Get notified about incidents and scheduled maintenance.": "Nhận thông báo về sự cố và bảo trì theo lịch trình.",
|
||||
"Get notified about incidents updates": "Nhận thông báo về cập nhật sự cố",
|
||||
"Get notified about scheduled maintenance": "Nhận thông báo về bảo trì theo lịch trình",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"iFrame": "iFrame",
|
||||
"Impact": "Sự va chạm",
|
||||
"incident": "Sự cố",
|
||||
"Incident Updates": "Cập nhật sự cố",
|
||||
"Incidents": "Sự cố",
|
||||
"Included Monitors": "Included Monitors",
|
||||
"Included Monitors (%count)": "Included Monitors (%count)",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"Last Updated": "Cập nhật lần cuối",
|
||||
"Latency": "Độ trễ",
|
||||
"Latency Embed": "Nhúng độ trễ",
|
||||
@@ -47,15 +57,19 @@
|
||||
"Light": "Ánh sáng",
|
||||
"Live Status": "Trạng thái trực tiếp",
|
||||
"Loading your preferences...": "Đang tải tùy chọn của bạn...",
|
||||
"maintenance": "Bảo trì",
|
||||
"MAINTENANCE": "BẢO TRÌ",
|
||||
"Maintenance Updates": "Cập nhật bảo trì",
|
||||
"Maintenances": "Bảo trì",
|
||||
"Major System Outage": "Sự cố hệ thống nghiêm trọng",
|
||||
"Manage Site": "Quản lý trang",
|
||||
"Manage your notification preferences.": "Quản lý tùy chọn thông báo của bạn.",
|
||||
"Max Latency": "Độ trễ tối đa",
|
||||
"Maximum Latency": "Độ trễ tối đa",
|
||||
"Min Latency": "Độ trễ tối thiểu",
|
||||
"Minimum Latency": "Độ trễ tối thiểu",
|
||||
"Minute-by-minute status data for this day": "Dữ liệu trạng thái từng phút cho ngày này",
|
||||
"MONITORING": "MONITORING",
|
||||
"Network error. Please try again.": "Lỗi mạng. ",
|
||||
"No Events in %currentMonth": "Không có sự kiện nào trong %currentMonth",
|
||||
"No events to show": "Không có sự kiện nào để hiển thị",
|
||||
@@ -73,6 +87,7 @@
|
||||
"Notifications": "Thông báo",
|
||||
"One-time": "Một lần",
|
||||
"Ongoing": "Đang thực hiện",
|
||||
"Operational": "Hoạt động",
|
||||
"Partial Degraded Performance": "Hiệu suất suy giảm một phần",
|
||||
"Partial System Outage": "Sự cố hệ thống một phần",
|
||||
"Past": "Quá khứ",
|
||||
@@ -82,7 +97,11 @@
|
||||
"Please enter the 6-digit verification code": "Vui lòng nhập mã xác minh gồm 6 chữ số",
|
||||
"Read less": "Đọc ít hơn",
|
||||
"Read more": "Đọc thêm",
|
||||
"READY": "SẴN SÀNG",
|
||||
"Recurring": "Định kỳ",
|
||||
"RESOLVED": "RESOLVED",
|
||||
"SCHEDULED": "ĐÃ LÊN LỊCH",
|
||||
"Scheduled Events (%count)": "Sự kiện đã lên lịch (%count)",
|
||||
"Script": "Kịch bản",
|
||||
"Select Language": "Chọn ngôn ngữ",
|
||||
"Select latency metric to display": "Chọn chỉ số độ trễ để hiển thị",
|
||||
@@ -102,7 +121,10 @@
|
||||
"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)",
|
||||
"Uptime": "Thời gian hoạt động",
|
||||
@@ -110,10 +132,6 @@
|
||||
"Verification failed": "Xác minh không thành công",
|
||||
"Verify": "Xác minh",
|
||||
"Verifying": "Đang xác minh",
|
||||
"We sent a 6-digit code to": "Chúng tôi đã gửi mã gồm 6 chữ số tới",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"MONITORING": "MONITORING",
|
||||
"RESOLVED": "RESOLVED"
|
||||
"We sent a 6-digit code to": "Chúng tôi đã gửi mã gồm 6 chữ số tới"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,14 +8,21 @@
|
||||
"Avg Latency": "平均延迟",
|
||||
"Back": "后退",
|
||||
"Badges": "徽章",
|
||||
"CANCELLED": "已取消",
|
||||
"COMPLETED": "已完成",
|
||||
"Continue": "继续",
|
||||
"Copied": "已复制",
|
||||
"Current": "当前",
|
||||
"Dark": "黑暗的",
|
||||
"Day": "天",
|
||||
"Day Uptime": "日间正常运行时间",
|
||||
"Days": "天",
|
||||
"Degraded": "降级",
|
||||
"DEGRADED": "降级",
|
||||
"Degraded Performance": "性能下降",
|
||||
"Didn't receive the code? Resend": "没有收到代码?",
|
||||
"Down": "宕机",
|
||||
"DOWN": "故障",
|
||||
"Duration": "期间",
|
||||
"Email address": "电子邮件",
|
||||
"Embed Monitor": "嵌入监视器",
|
||||
@@ -32,11 +39,14 @@
|
||||
"Get notified about incidents and scheduled maintenance.": "获取有关事件和定期维护的通知。",
|
||||
"Get notified about incidents updates": "获取有关事件更新的通知",
|
||||
"Get notified about scheduled maintenance": "获取有关定期维护的通知",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"iFrame": "框架",
|
||||
"Impact": "影响",
|
||||
"incident": "事件",
|
||||
"Incident Updates": "事件更新",
|
||||
"Incidents": "事件",
|
||||
"Included Monitors": "Included Monitors",
|
||||
"Included Monitors (%count)": "Included Monitors (%count)",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"Last Updated": "最后更新",
|
||||
"Latency": "延迟",
|
||||
"Latency Embed": "延迟嵌入",
|
||||
@@ -47,15 +57,19 @@
|
||||
"Light": "光",
|
||||
"Live Status": "实时状态",
|
||||
"Loading your preferences...": "正在加载您的偏好设置...",
|
||||
"maintenance": "维护",
|
||||
"MAINTENANCE": "维护中",
|
||||
"Maintenance Updates": "维护更新",
|
||||
"Maintenances": "维护保养",
|
||||
"Major System Outage": "重大系统故障",
|
||||
"Manage Site": "管理站点",
|
||||
"Manage your notification preferences.": "管理您的通知首选项。",
|
||||
"Max Latency": "最大延迟",
|
||||
"Maximum Latency": "最大延迟",
|
||||
"Min Latency": "最短延迟",
|
||||
"Minimum Latency": "最短延迟",
|
||||
"Minute-by-minute status data for this day": "当日每分钟的状态数据",
|
||||
"MONITORING": "MONITORING",
|
||||
"Network error. Please try again.": "网络错误。",
|
||||
"No Events in %currentMonth": "%currentMonth 没有活动",
|
||||
"No events to show": "没有可显示的事件",
|
||||
@@ -73,6 +87,7 @@
|
||||
"Notifications": "通知",
|
||||
"One-time": "一度",
|
||||
"Ongoing": "进行中",
|
||||
"Operational": "正常运行",
|
||||
"Partial Degraded Performance": "部分性能下降",
|
||||
"Partial System Outage": "部分系统故障.",
|
||||
"Past": "过去的",
|
||||
@@ -82,7 +97,11 @@
|
||||
"Please enter the 6-digit verification code": "请输入6位验证码",
|
||||
"Read less": "少读书",
|
||||
"Read more": "阅读更多",
|
||||
"READY": "就绪",
|
||||
"Recurring": "周期性",
|
||||
"RESOLVED": "RESOLVED",
|
||||
"SCHEDULED": "已安排",
|
||||
"Scheduled Events (%count)": "计划事件 (%count)",
|
||||
"Script": "脚本",
|
||||
"Select Language": "选择语言",
|
||||
"Select latency metric to display": "选择要显示的延迟指标",
|
||||
@@ -102,7 +121,10 @@
|
||||
"Total Maintenances": "全面维护",
|
||||
"Under Maintenance": "维护中",
|
||||
"Unknown impact": "未知影响",
|
||||
"UP": "正常",
|
||||
"Upcoming": "即将推出",
|
||||
"Update Incident": "更新事件",
|
||||
"Update Maintenance": "更新维护",
|
||||
"Updates": "更新",
|
||||
"Updates (%count)": "更新 (%count)",
|
||||
"Uptime": "正常运行时间",
|
||||
@@ -110,10 +132,6 @@
|
||||
"Verification failed": "验证失败",
|
||||
"Verify": "核实",
|
||||
"Verifying": "正在验证",
|
||||
"We sent a 6-digit code to": "我们发送了一个 6 位代码至",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"MONITORING": "MONITORING",
|
||||
"RESOLVED": "RESOLVED"
|
||||
"We sent a 6-digit code to": "我们发送了一个 6 位代码至"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
{
|
||||
"name": "繁體中文(台灣)",
|
||||
"mappings": {
|
||||
"%latency %metric latency": "%latency %metric 延遲",
|
||||
"Affected Monitors (%count)": "受影響的監控項目 (%count)",
|
||||
"All Systems Operational": "所有系統正常運作",
|
||||
"Average Latency": "平均延遲",
|
||||
"Avg Latency": "平均延遲",
|
||||
"Back": "返回",
|
||||
"Badges": "徽章",
|
||||
"CANCELLED": "已取消",
|
||||
"COMPLETED": "已完成",
|
||||
"Continue": "繼續",
|
||||
"Copied": "已複製",
|
||||
"Current": "目前",
|
||||
"Dark": "深色",
|
||||
"Day": "天",
|
||||
"Day Uptime": "當日正常運作時間",
|
||||
"Days": "天",
|
||||
"DEGRADED": "降級",
|
||||
"Degraded Performance": "效能降低",
|
||||
"Didn't receive the code? Resend": "沒有收到驗證碼?重新傳送",
|
||||
"DOWN": "故障",
|
||||
"Duration": "持續時間",
|
||||
"Edit Monitor": "編輯監控",
|
||||
"Email address": "電子郵件地址",
|
||||
"Embed Monitor": "嵌入監控",
|
||||
"Embed this monitor in your website or app": "將此監控嵌入您的網站或應用程式",
|
||||
"End Time": "結束時間",
|
||||
"Enter the verification code sent to your email.": "請輸入傳送至您電子郵件的驗證碼。",
|
||||
"Events": "事件",
|
||||
"Failed to load data": "載入資料失敗",
|
||||
"Failed to load latency data": "載入延遲資料失敗",
|
||||
"Failed to load status data for this day": "無法載入當日的狀態資料",
|
||||
"Failed to send verification code": "傳送驗證碼失敗",
|
||||
"Failed to update preference": "更新偏好設定失敗",
|
||||
"Get badges for this monitor": "取得此監控的徽章",
|
||||
"Get notified about incidents and scheduled maintenance.": "接收事件與排程維護的通知。",
|
||||
"Get notified about incidents updates": "接收事件更新通知",
|
||||
"Get notified about scheduled maintenance": "接收排程維護通知",
|
||||
"IDENTIFIED": "已確認",
|
||||
"iFrame": "iFrame",
|
||||
"Impact": "影響",
|
||||
"Incident Updates": "事件更新",
|
||||
"Incidents": "事件",
|
||||
"Included Monitors (%count)": "包含的監控項目 (%count)",
|
||||
"INVESTIGATING": "調查中",
|
||||
"Last Updated": "最後更新",
|
||||
"Latency": "延遲",
|
||||
"Latency Embed": "延遲嵌入",
|
||||
"Latency Over Time": "延遲趨勢圖",
|
||||
"Latency Trend": "延遲趨勢",
|
||||
"Latest Latency": "最新延遲",
|
||||
"Latest Status": "最新狀態",
|
||||
"Light": "淺色",
|
||||
"Live Status": "即時狀態",
|
||||
"Loading your preferences...": "正在載入您的偏好設定……",
|
||||
"MAINTENANCE": "維護中",
|
||||
"Maintenance Updates": "維護更新",
|
||||
"Maintenances": "維護作業",
|
||||
"Major System Outage": "重大系統中斷",
|
||||
"Manage Site": "管理網站",
|
||||
"Manage your notification preferences.": "管理您的通知偏好設定。",
|
||||
"Max Latency": "最大延遲",
|
||||
"Maximum Latency": "最大延遲",
|
||||
"Min Latency": "最小延遲",
|
||||
"Minimum Latency": "最小延遲",
|
||||
"Minute-by-minute status data for this day": "當日逐分鐘狀態資料",
|
||||
"MONITORING": "監控中",
|
||||
"Network error. Please try again.": "網路錯誤,請重試。",
|
||||
"No Events in %currentMonth": "%currentMonth 沒有事件",
|
||||
"No events to show": "沒有可顯示的事件",
|
||||
"No incidents for this day": "當日無事件",
|
||||
"No latency data available for this day": "當日無延遲資料",
|
||||
"No maintenances for this day": "當日無維護作業",
|
||||
"No monitors affected": "沒有受影響的監控項目",
|
||||
"No monitors available.": "沒有可用的監控項目。",
|
||||
"No ongoing maintenances": "沒有進行中的維護作業",
|
||||
"No past maintenances": "沒有過去的維護作業",
|
||||
"No Status Available": "無可用狀態",
|
||||
"No upcoming maintenances": "沒有即將進行的維護作業",
|
||||
"No Updates": "沒有更新",
|
||||
"No updates yet": "尚無更新",
|
||||
"Notifications": "通知",
|
||||
"One-time": "單次",
|
||||
"Ongoing": "進行中",
|
||||
"Partial Degraded Performance": "部分效能降低",
|
||||
"Partial System Outage": "部分系統中斷",
|
||||
"Past": "過去",
|
||||
"Per-Minute Status": "逐分鐘狀態",
|
||||
"Pinging": "偵測中",
|
||||
"Please enter a valid email address": "請輸入有效的電子郵件地址",
|
||||
"Please enter the 6-digit verification code": "請輸入 6 位數驗證碼",
|
||||
"Read less": "收合",
|
||||
"Read more": "展開",
|
||||
"READY": "就緒",
|
||||
"Recurring": "週期性",
|
||||
"RESOLVED": "已解決",
|
||||
"SCHEDULED": "已排程",
|
||||
"Scheduled Events (%count)": "排程事件 (%count)",
|
||||
"Script": "程式碼",
|
||||
"Select Language": "選擇語言",
|
||||
"Select latency metric to display": "選擇要顯示的延遲指標",
|
||||
"Select Range": "選擇範圍",
|
||||
"Sending...": "傳送中……",
|
||||
"Standard": "標準",
|
||||
"Start Time": "開始時間",
|
||||
"Status": "狀態",
|
||||
"Status Badge": "狀態徽章",
|
||||
"Status Embed": "狀態嵌入",
|
||||
"Status history and latency trend": "狀態歷程與延遲趨勢",
|
||||
"Subscribe": "訂閱",
|
||||
"Subscribe to Updates": "訂閱更新",
|
||||
"There are no incidents or maintenances scheduled for this month.": "本月沒有排程的事件或維護作業。",
|
||||
"There are no ongoing incidents or maintenance events.": "目前沒有進行中的事件或維護作業。",
|
||||
"Total Incidents": "事件總數",
|
||||
"Total Maintenances": "維護總數",
|
||||
"Under Maintenance": "維護中",
|
||||
"Unknown impact": "影響不明",
|
||||
"UP": "正常",
|
||||
"Upcoming": "即將進行",
|
||||
"Update Incident": "更新事件",
|
||||
"Update Maintenance": "更新維護",
|
||||
"Updates": "更新",
|
||||
"Updates (%count)": "更新 (%count)",
|
||||
"Uptime": "正常運作時間",
|
||||
"Uptime Badge": "正常運作時間徽章",
|
||||
"Verification failed": "驗證失敗",
|
||||
"Verify": "驗證",
|
||||
"Verifying": "驗證中",
|
||||
"We sent a 6-digit code to": "我們已傳送 6 位數驗證碼至"
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { json, error } from "@sveltejs/kit";
|
||||
import type { APIServerRequest } from "$lib/server/types/api-server";
|
||||
import type { IncidentForMonitorListWithComments, MaintenanceEventsMonitorList } from "$lib/server/types/db";
|
||||
import db from "$lib/server/db/db";
|
||||
import { GetAllSiteData } from "$lib/server/controllers/siteDataController";
|
||||
|
||||
interface EventsByMonthRequest {
|
||||
start_ts: number;
|
||||
@@ -15,6 +16,13 @@ export interface EventsByMonthResponse {
|
||||
|
||||
export default async function post(req: APIServerRequest): Promise<Response> {
|
||||
const body = req.body as EventsByMonthRequest;
|
||||
const queryParams = req.query;
|
||||
const rawPagePath = queryParams.get("page_path");
|
||||
let pagePath = rawPagePath?.trim() || null;
|
||||
|
||||
if (pagePath && ["undefined", "null"].includes(pagePath.toLowerCase())) {
|
||||
pagePath = null;
|
||||
}
|
||||
|
||||
if (!body.start_ts || typeof body.start_ts !== "number") {
|
||||
return error(400, { message: "start_ts is required and must be a number" });
|
||||
@@ -24,9 +32,28 @@ export default async function post(req: APIServerRequest): Promise<Response> {
|
||||
return error(400, { message: "end_ts is required and must be a number" });
|
||||
}
|
||||
|
||||
// get site data
|
||||
const siteData = await GetAllSiteData();
|
||||
const { globalPageVisibilitySettings } = siteData;
|
||||
const isExclusivePageEnabled = !!globalPageVisibilitySettings?.forceExclusivity;
|
||||
|
||||
if (isExclusivePageEnabled && !pagePath) {
|
||||
pagePath = "";
|
||||
}
|
||||
|
||||
let pathMonitors: string[] | undefined = undefined;
|
||||
if (pagePath !== null) {
|
||||
const pageData = await db.getPageByPath(pagePath);
|
||||
if (!pageData) {
|
||||
return error(404, { message: "Page not found" });
|
||||
}
|
||||
const monitorsForPage = await db.getPageMonitorsExcludeHidden(pageData.id);
|
||||
pathMonitors = monitorsForPage.map((m) => m.monitor_tag);
|
||||
}
|
||||
|
||||
const [incidents, maintenances] = await Promise.all([
|
||||
db.getIncidentsForEventsByDateRange(body.start_ts, body.end_ts),
|
||||
db.getMaintenanceEventsForEventsByDateRange(body.start_ts, body.end_ts),
|
||||
db.getIncidentsForEventsByDateRange(body.start_ts, body.end_ts, pathMonitors),
|
||||
db.getMaintenanceEventsForEventsByDateRange(body.start_ts, body.end_ts, pathMonitors),
|
||||
]);
|
||||
|
||||
const response: EventsByMonthResponse = {
|
||||
|
||||
@@ -1,17 +1,40 @@
|
||||
import { json } from "@sveltejs/kit";
|
||||
import type { APIServerRequest } from "$lib/server/types/api-server";
|
||||
import { GetAllPages } from "$lib/server/controllers/pagesController";
|
||||
import { GetSiteDataByKey } from "$lib/server/controllers/siteDataController";
|
||||
import type { PageNavItem } from "$lib/server/controllers/dashboardController";
|
||||
import type { PageOrderingSettings } from "$lib/types/site";
|
||||
|
||||
/**
|
||||
* GET /dashboard-apis/pages
|
||||
* Returns all pages as PageNavItem[] (page_title, page_path)
|
||||
* Respects pageOrderingSettings if enabled
|
||||
*/
|
||||
export default async function get(_req: APIServerRequest): Promise<Response> {
|
||||
const allPagesData = await GetAllPages();
|
||||
const pages: PageNavItem[] = allPagesData.map((p) => ({
|
||||
const pageOrderingSettings = (await GetSiteDataByKey("pageOrderingSettings")) as PageOrderingSettings | null;
|
||||
|
||||
let orderedPages = allPagesData;
|
||||
|
||||
if (pageOrderingSettings?.enabled && pageOrderingSettings.order?.length > 0) {
|
||||
const orderMap = new Map(pageOrderingSettings.order.map((id, idx) => [id, idx]));
|
||||
orderedPages = [...allPagesData].sort((a, b) => {
|
||||
const aIdx = orderMap.get(a.id);
|
||||
const bIdx = orderMap.get(b.id);
|
||||
// Pages in the order list come first, sorted by their position
|
||||
if (aIdx !== undefined && bIdx !== undefined) return aIdx - bIdx;
|
||||
if (aIdx !== undefined) return -1;
|
||||
if (bIdx !== undefined) return 1;
|
||||
// Pages not in the order list keep their default order (by id)
|
||||
return a.id - b.id;
|
||||
});
|
||||
}
|
||||
|
||||
const pages: PageNavItem[] = orderedPages.map((p) => ({
|
||||
page_title: p.page_title,
|
||||
page_path: p.page_path,
|
||||
page_header: p.page_header,
|
||||
page_logo: p.page_logo,
|
||||
}));
|
||||
return json(pages);
|
||||
}
|
||||
|
||||
Vendored
+5
@@ -13,6 +13,11 @@ export async function setCache<T>(key: string, value: T | null | undefined, ttlS
|
||||
await redis.set(getCacheKey(key), payload, "EX", ttl);
|
||||
}
|
||||
|
||||
export async function deleteCache(key: string): Promise<void> {
|
||||
const redis = redisIOConnection();
|
||||
await redis.del(getCacheKey(key));
|
||||
}
|
||||
|
||||
export async function getCache<T>(
|
||||
key: string,
|
||||
fetcher?: () => Promise<T | null | undefined> | T | null | undefined,
|
||||
|
||||
Vendored
+6
-1
@@ -1,5 +1,5 @@
|
||||
import type { MonitoringData } from "../types/db.js";
|
||||
import { setCache, getCache } from "./cache.js";
|
||||
import { setCache, getCache, deleteCache } from "./cache.js";
|
||||
export async function SetLastMonitoringValue(tag: string, value: MonitoringData): Promise<void> {
|
||||
await setCache<MonitoringData>(tag + ":last_status", value, 86400); //set ttl to 1 day
|
||||
}
|
||||
@@ -20,3 +20,8 @@ export async function SetLastHeartbeat(tag: string, timestamp: number): Promise<
|
||||
export async function GetLastHeartbeat(tag: string): Promise<{ timestamp: number } | null> {
|
||||
return await getCache<{ timestamp: number }>("last_heartbeat:" + tag, undefined, 45 * 86400);
|
||||
}
|
||||
|
||||
export async function DeleteMonitorCaches(tag: string): Promise<void> {
|
||||
await deleteCache(tag + ":last_status");
|
||||
await deleteCache("last_heartbeat:" + tag);
|
||||
}
|
||||
|
||||
@@ -55,12 +55,14 @@ export const VerifyToken = async (token: string): Promise<TokenPayload | undefin
|
||||
|
||||
export const GetSMTPFromENV = (): SMTPConfiguration | null => {
|
||||
//if variables are not return null
|
||||
const smtpPassword = process.env.SMTP_PASS || process.env.SMTP_PASSWORD;
|
||||
const fromEmail = process.env.SMTP_FROM_EMAIL || process.env.SMTP_SENDER;
|
||||
if (
|
||||
!!!process.env.SMTP_HOST ||
|
||||
!!!process.env.SMTP_PORT ||
|
||||
!!!process.env.SMTP_USER ||
|
||||
!!!process.env.SMTP_FROM_EMAIL ||
|
||||
!!!process.env.SMTP_PASS
|
||||
!!!fromEmail ||
|
||||
!!!smtpPassword
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
@@ -69,8 +71,8 @@ export const GetSMTPFromENV = (): SMTPConfiguration | null => {
|
||||
smtp_host: process.env.SMTP_HOST,
|
||||
smtp_port: Number(process.env.SMTP_PORT),
|
||||
smtp_user: process.env.SMTP_USER,
|
||||
smtp_sender: process.env.SMTP_FROM_EMAIL,
|
||||
smtp_pass: process.env.SMTP_PASS,
|
||||
smtp_sender: fromEmail,
|
||||
smtp_pass: smtpPassword,
|
||||
smtp_secure: !!Number(process.env.SMTP_SECURE),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import { GetMinuteStartNowTimestampUTC, BeginningOfMinute, BeginningOfDay } from
|
||||
import { GetPageByPathWithMonitors, GetLatestMonitoringDataAllActive } from "./controller.js";
|
||||
import { GetMonitorsParsed } from "./monitorsController.js";
|
||||
import { GetStatusSummary, GetStatusBgColor } from "../../clientTools";
|
||||
import { formatDistanceStrict } from "date-fns";
|
||||
|
||||
import type {
|
||||
IncidentRecord,
|
||||
IncidentCommentRecord,
|
||||
@@ -32,7 +32,8 @@ export interface NotificationEvent {
|
||||
eventTitle: string;
|
||||
eventDate: string;
|
||||
eventType: string;
|
||||
eventDuration: string;
|
||||
eventStartDateTime: number;
|
||||
eventEndDateTime: number | null;
|
||||
eventStatus: string;
|
||||
}
|
||||
|
||||
@@ -150,15 +151,21 @@ export const GetAffectedMonitorsByIncidentId = async (
|
||||
export interface PageNavItem {
|
||||
page_title: string;
|
||||
page_path: string;
|
||||
page_header: string;
|
||||
page_logo: string | null;
|
||||
}
|
||||
|
||||
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) => {
|
||||
@@ -218,11 +225,6 @@ export const BuildNotificationPayload = (
|
||||
): NotificationPayload => {
|
||||
const notifications: Array<NotificationEvent & { sortTs: number }> = [];
|
||||
|
||||
const durationFromRange = (startTs: number, endTs: number | null): string => {
|
||||
const effectiveEnd = Math.max(startTs, endTs ?? nowTs);
|
||||
return formatDistanceStrict(new Date(startTs * 1000), new Date(effectiveEnd * 1000));
|
||||
};
|
||||
|
||||
for (const incident of ongoingIncidents) {
|
||||
const ts = incident.start_date_time;
|
||||
notifications.push({
|
||||
@@ -231,7 +233,8 @@ export const BuildNotificationPayload = (
|
||||
eventTitle: incident.title,
|
||||
eventDate: new Date(ts * 1000).toISOString(),
|
||||
eventType: "incident",
|
||||
eventDuration: durationFromRange(incident.start_date_time, incident.end_date_time),
|
||||
eventStartDateTime: incident.start_date_time,
|
||||
eventEndDateTime: incident.end_date_time,
|
||||
eventStatus: incident.state,
|
||||
});
|
||||
}
|
||||
@@ -244,7 +247,8 @@ export const BuildNotificationPayload = (
|
||||
eventTitle: incident.title,
|
||||
eventDate: new Date(ts * 1000).toISOString(),
|
||||
eventType: "incident",
|
||||
eventDuration: durationFromRange(incident.start_date_time, incident.end_date_time),
|
||||
eventStartDateTime: incident.start_date_time,
|
||||
eventEndDateTime: incident.end_date_time,
|
||||
eventStatus: incident.state,
|
||||
});
|
||||
}
|
||||
@@ -257,7 +261,8 @@ export const BuildNotificationPayload = (
|
||||
eventTitle: maintenance.title,
|
||||
eventDate: new Date(ts * 1000).toISOString(),
|
||||
eventType: "maintenance",
|
||||
eventDuration: durationFromRange(maintenance.start_date_time, maintenance.end_date_time),
|
||||
eventStartDateTime: maintenance.start_date_time,
|
||||
eventEndDateTime: maintenance.end_date_time,
|
||||
eventStatus: GC.ONGOING,
|
||||
});
|
||||
}
|
||||
@@ -270,7 +275,8 @@ export const BuildNotificationPayload = (
|
||||
eventTitle: maintenance.title,
|
||||
eventDate: new Date(ts * 1000).toISOString(),
|
||||
eventType: "maintenance",
|
||||
eventDuration: durationFromRange(maintenance.start_date_time, maintenance.end_date_time),
|
||||
eventStartDateTime: maintenance.start_date_time,
|
||||
eventEndDateTime: maintenance.end_date_time,
|
||||
eventStatus: GC.SCHEDULED,
|
||||
});
|
||||
}
|
||||
@@ -283,7 +289,8 @@ export const BuildNotificationPayload = (
|
||||
eventTitle: maintenance.title,
|
||||
eventDate: new Date(ts * 1000).toISOString(),
|
||||
eventType: "maintenance",
|
||||
eventDuration: durationFromRange(maintenance.start_date_time, maintenance.end_date_time),
|
||||
eventStartDateTime: maintenance.start_date_time,
|
||||
eventEndDateTime: maintenance.end_date_time,
|
||||
eventStatus: GC.COMPLETED,
|
||||
});
|
||||
}
|
||||
@@ -339,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
|
||||
@@ -360,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);
|
||||
@@ -378,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 };
|
||||
};
|
||||
|
||||
@@ -2,8 +2,15 @@ import MobileDetect from "mobile-detect";
|
||||
import type { Cookies } from "@sveltejs/kit";
|
||||
import type { UserRecordPublic } from "$lib/server/types/db";
|
||||
import seedSiteData from "$lib/server/db/seedSiteData";
|
||||
import { GetAllSiteData, GetLoggedInSession, GetLocaleFromCookie, GetUsersCount, IsEmailSetup } from "./controller.js";
|
||||
import type { EventDisplaySettings } from "$lib/types/site.js";
|
||||
import {
|
||||
GetAllSiteData,
|
||||
GetLoggedInSession,
|
||||
GetLocaleFromCookie,
|
||||
GetUsersCount,
|
||||
IsEmailSetup,
|
||||
IsSetupComplete,
|
||||
} from "./controller.js";
|
||||
import type { EventDisplaySettings, GlobalPageVisibilitySettings, SiteDateTimeFormat } from "$lib/types/site.js";
|
||||
|
||||
export interface LayoutServerData {
|
||||
isMobile: boolean;
|
||||
@@ -62,6 +69,10 @@ export interface LayoutServerData {
|
||||
eventDisplaySettings: EventDisplaySettings;
|
||||
socialPreviewImage?: string;
|
||||
customCSS?: string;
|
||||
globalPageVisibilitySettings: GlobalPageVisibilitySettings;
|
||||
dateAndTimeFormat: SiteDateTimeFormat;
|
||||
metaSiteTitle?: string;
|
||||
metaSiteDescription?: string;
|
||||
}
|
||||
|
||||
export async function GetLayoutServerData(cookies: Cookies, request: Request): Promise<LayoutServerData> {
|
||||
@@ -75,7 +86,7 @@ export async function GetLayoutServerData(cookies: Cookies, request: Request): P
|
||||
GetUsersCount(),
|
||||
]);
|
||||
|
||||
const isSetupComplete = process.env.KENER_SECRET_KEY !== undefined && Object.keys(siteData).length > 0;
|
||||
const isSetupComplete = await IsSetupComplete();
|
||||
|
||||
const selectedLang = GetLocaleFromCookie(siteData, cookies);
|
||||
const siteStatusColors = siteData.colors;
|
||||
@@ -128,5 +139,9 @@ export async function GetLayoutServerData(cookies: Cookies, request: Request): P
|
||||
eventDisplaySettings: siteData.eventDisplaySettings || seedSiteData.eventDisplaySettings,
|
||||
socialPreviewImage: siteData.socialPreviewImage,
|
||||
customCSS: siteData.customCSS,
|
||||
globalPageVisibilitySettings: siteData.globalPageVisibilitySettings || seedSiteData.globalPageVisibilitySettings,
|
||||
dateAndTimeFormat: siteData.dateAndTimeFormat || seedSiteData.dateAndTimeFormat,
|
||||
metaSiteTitle: siteData.metaSiteTitle,
|
||||
metaSiteDescription: siteData.metaSiteDescription,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,9 +12,11 @@ import type {
|
||||
MaintenanceEventFilter,
|
||||
} from "../types/db.js";
|
||||
import { GetMinuteStartNowTimestampUTC } from "../tool.js";
|
||||
import { maintenanceToVariables } from "../notification/notification_utils.js";
|
||||
import { maintenanceToVariables, siteDataToVariables } from "../notification/notification_utils.js";
|
||||
import { GetAllSiteData } from "./controller.js";
|
||||
import subscriberQueue from "../queues/subscriberQueue.js";
|
||||
import GC from "../../global-constants";
|
||||
import seedSiteData from "../db/seedSiteData.js";
|
||||
|
||||
// ============ Input Interfaces ============
|
||||
|
||||
@@ -77,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);
|
||||
|
||||
@@ -86,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
|
||||
@@ -126,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);
|
||||
@@ -152,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);
|
||||
}
|
||||
}
|
||||
@@ -201,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,
|
||||
@@ -335,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) {
|
||||
@@ -344,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -376,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;
|
||||
@@ -533,17 +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})`);
|
||||
@@ -553,14 +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",
|
||||
);
|
||||
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
|
||||
@@ -570,14 +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",
|
||||
);
|
||||
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
|
||||
@@ -587,14 +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",
|
||||
);
|
||||
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
|
||||
@@ -604,14 +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",
|
||||
);
|
||||
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);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user