mirror of
https://github.com/rajnandan1/kener.git
synced 2026-08-07 15:27:13 +00:00
Compare commits
199 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1750e2a341 | |||
| 560c87219b | |||
| 94e24eec04 | |||
| 15680a58aa | |||
| 59f0eaef27 | |||
| 8362a73058 | |||
| 52f8c50f50 | |||
| 60868d55ca | |||
| f7e657ee95 | |||
| 63e5ec2886 | |||
| 2aef97c1ed | |||
| 51b2da97e0 | |||
| 50bddcd9a3 | |||
| bd36533b05 | |||
| 17500a0b43 | |||
| 0050cd810b | |||
| 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 | |||
| f9486927de | |||
| 86645d9ea3 | |||
| feea1d76cd | |||
| 2e95f31d93 | |||
| 18cf8f51b7 | |||
| 9af842ebc0 | |||
| 3d157f1c20 |
@@ -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
|
||||
}
|
||||
}
|
||||
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)
|
||||
|
||||
@@ -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.
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Create Release (Deterministic)
|
||||
name: Create Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
- name: Bump package version
|
||||
run: |
|
||||
VERSION="${{ inputs.version }}"
|
||||
CURRENT_VERSION="$(node -p \"require('./package.json').version\")"
|
||||
CURRENT_VERSION=$(node -p 'require("./package.json").version')
|
||||
|
||||
if [ "$CURRENT_VERSION" != "$VERSION" ]; then
|
||||
npm version "$VERSION" --no-git-tag-version --allow-same-version
|
||||
@@ -95,3 +95,4 @@ jobs:
|
||||
generate_release_notes: true
|
||||
make_latest: ${{ inputs.make_latest && 'true' || 'false' }}
|
||||
prerelease: ${{ inputs.prerelease }}
|
||||
token: ${{ secrets.RELEASE_TOKEN }}
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
run: |
|
||||
TAG="${{ github.event.release.tag_name || github.ref_name }}"
|
||||
EXPECTED_VERSION="${TAG#v}"
|
||||
PACKAGE_VERSION="$(node -p \"require('./package.json').version\")"
|
||||
PACKAGE_VERSION=$(node -p 'require("./package.json").version')
|
||||
|
||||
if [ "$PACKAGE_VERSION" != "$EXPECTED_VERSION" ]; then
|
||||
echo "package.json version mismatch"
|
||||
@@ -83,6 +83,8 @@ jobs:
|
||||
BASE_SUFFIX=""
|
||||
fi
|
||||
|
||||
WITH_DOCS="false"
|
||||
|
||||
if [ "${{ matrix.variant }}" = "alpine" ]; then
|
||||
VARIANT_SUFFIX="-alpine"
|
||||
else
|
||||
@@ -94,6 +96,7 @@ jobs:
|
||||
echo "release_tag=${TAG}${FULL_SUFFIX}" >> "$GITHUB_OUTPUT"
|
||||
echo "release_norm_tag=${NORM_TAG}${FULL_SUFFIX}" >> "$GITHUB_OUTPUT"
|
||||
echo "latest_tag=latest${FULL_SUFFIX}" >> "$GITHUB_OUTPUT"
|
||||
echo "with_docs=${WITH_DOCS}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
if [ "${{ matrix.variant }}" = "debian" ]; then
|
||||
echo "release_tag_debian_alias=${TAG}${BASE_SUFFIX}-debian" >> "$GITHUB_OUTPUT"
|
||||
@@ -129,7 +132,7 @@ jobs:
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
VARIANT=${{ matrix.variant }}
|
||||
WITH_DOCS=true
|
||||
WITH_DOCS=${{ steps.vars.outputs.with_docs }}
|
||||
KENER_BASE_PATH=${{ matrix.base_path }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
cache-from: type=gha
|
||||
|
||||
+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:
|
||||
|
||||
@@ -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"
|
||||
@@ -162,8 +163,12 @@ COPY --chown=node:node --from=builder /app/seeds ./seeds
|
||||
COPY --chown=node:node --from=builder /app/src/lib/server/db/seedSiteData.ts ./src/lib/server/db/seedSiteData.ts
|
||||
COPY --chown=node:node --from=builder /app/src/lib/server/db/seedMonitorData.ts ./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/allPerms.ts ./src/lib/allPerms.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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
// 1. Roles table
|
||||
if (!(await knex.schema.hasTable("roles"))) {
|
||||
await knex.schema.createTable("roles", (table) => {
|
||||
table.string("id", 100).primary();
|
||||
table.text("role_name").notNullable();
|
||||
table.integer("readonly").notNullable().defaultTo(0);
|
||||
table.string("status", 20).notNullable().defaultTo("ACTIVE");
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Permissions table
|
||||
if (!(await knex.schema.hasTable("permissions"))) {
|
||||
await knex.schema.createTable("permissions", (table) => {
|
||||
table.string("id", 100).primary();
|
||||
table.text("permission_name").notNullable();
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Roles ↔ Permissions junction table
|
||||
if (!(await knex.schema.hasTable("roles_permissions"))) {
|
||||
await knex.schema.createTable("roles_permissions", (table) => {
|
||||
table.string("roles_id", 100).notNullable().references("id").inTable("roles").onDelete("CASCADE");
|
||||
table.string("permissions_id", 100).notNullable().references("id").inTable("permissions").onDelete("CASCADE");
|
||||
table.string("status", 20).notNullable().defaultTo("ACTIVE");
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
|
||||
table.primary(["roles_id", "permissions_id"]);
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Users ↔ Roles junction table
|
||||
if (!(await knex.schema.hasTable("users_roles"))) {
|
||||
await knex.schema.createTable("users_roles", (table) => {
|
||||
table.string("roles_id", 100).notNullable().references("id").inTable("roles").onDelete("CASCADE");
|
||||
table.integer("users_id").unsigned().notNullable().references("id").inTable("users").onDelete("CASCADE");
|
||||
table.timestamp("created_at").defaultTo(knex.fn.now());
|
||||
table.timestamp("updated_at").defaultTo(knex.fn.now());
|
||||
|
||||
table.primary(["roles_id", "users_id"]);
|
||||
table.index("users_id", "idx_users_roles_users_id");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists("users_roles");
|
||||
await knex.schema.dropTableIfExists("roles_permissions");
|
||||
await knex.schema.dropTableIfExists("permissions");
|
||||
await knex.schema.dropTableIfExists("roles");
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import type { Knex } from "knex";
|
||||
|
||||
// Maps the legacy users.role string to the new roles.id value.
|
||||
// The old default was "user"; everything unmapped falls back to "member".
|
||||
const ROLE_MAP: Record<string, string> = {
|
||||
admin: "admin",
|
||||
editor: "editor",
|
||||
member: "member",
|
||||
user: "member",
|
||||
};
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasColumn = await knex.schema.hasColumn("users", "role");
|
||||
if (!hasColumn) return;
|
||||
|
||||
// 1. Ensure the three target roles exist so FK inserts succeed.
|
||||
// Seeds will reconcile permissions later; we only need the rows.
|
||||
const rolesToEnsure = [
|
||||
{ id: "admin", role_name: "Administrator" },
|
||||
{ id: "editor", role_name: "Editor" },
|
||||
{ id: "member", role_name: "Member" },
|
||||
];
|
||||
for (const role of rolesToEnsure) {
|
||||
const exists = await knex("roles").where("id", role.id).first();
|
||||
if (!exists) {
|
||||
await knex("roles").insert({
|
||||
id: role.id,
|
||||
role_name: role.role_name,
|
||||
readonly: 1,
|
||||
status: "ACTIVE",
|
||||
created_at: knex.fn.now(),
|
||||
updated_at: knex.fn.now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Read users.role into memory BEFORE dropping the column.
|
||||
// On SQLite, dropColumn recreates the table (create → copy → drop → rename),
|
||||
// which can discard DML inserts to tables with FKs pointing at users.
|
||||
const users: Array<{ id: number; role: string }> = await knex("users").select("id", "role");
|
||||
|
||||
// 3. Drop the column first.
|
||||
await knex.schema.alterTable("users", (table) => {
|
||||
table.dropColumn("role");
|
||||
});
|
||||
|
||||
// 4. Now populate users_roles from the in-memory snapshot.
|
||||
for (const user of users) {
|
||||
const newRoleId = ROLE_MAP[user.role] ?? "member";
|
||||
|
||||
const alreadyAssigned = await knex("users_roles").where({ roles_id: newRoleId, users_id: user.id }).first();
|
||||
|
||||
if (!alreadyAssigned) {
|
||||
await knex("users_roles").insert({
|
||||
roles_id: newRoleId,
|
||||
users_id: user.id,
|
||||
created_at: knex.fn.now(),
|
||||
updated_at: knex.fn.now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reverse map: pick the highest-precedence role when backfilling.
|
||||
const REVERSE_ROLE_PRECEDENCE: string[] = ["admin", "editor", "member"];
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasColumn = await knex.schema.hasColumn("users", "role");
|
||||
if (!hasColumn) {
|
||||
await knex.schema.alterTable("users", (table) => {
|
||||
table.string("role").defaultTo("member");
|
||||
});
|
||||
}
|
||||
|
||||
// Backfill users.role from users_roles using deterministic precedence
|
||||
const assignments: Array<{ users_id: number; roles_id: string }> = await knex("users_roles").select(
|
||||
"users_id",
|
||||
"roles_id",
|
||||
);
|
||||
|
||||
// Group roles by user
|
||||
const userRolesMap = new Map<number, string[]>();
|
||||
for (const row of assignments) {
|
||||
const list = userRolesMap.get(row.users_id) || [];
|
||||
list.push(row.roles_id);
|
||||
userRolesMap.set(row.users_id, list);
|
||||
}
|
||||
|
||||
// Pick highest-precedence role for each user
|
||||
for (const [userId, roleIds] of userRolesMap) {
|
||||
const bestRole = REVERSE_ROLE_PRECEDENCE.find((r) => roleIds.includes(r)) || roleIds[0] || "member";
|
||||
await knex("users").where("id", userId).update({ role: bestRole });
|
||||
}
|
||||
}
|
||||
Generated
+405
-293
File diff suppressed because it is too large
Load Diff
+19
-7
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "kener",
|
||||
"version": "4.0.0",
|
||||
"version": "4.0.23",
|
||||
"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,28 @@
|
||||
import type { Knex } from "knex";
|
||||
import { permissions } from "../src/lib/allPerms.ts";
|
||||
|
||||
export async function seed(knex: Knex): Promise<void> {
|
||||
const permissionIds = new Set(permissions.map((p) => p.id));
|
||||
|
||||
// Get all existing permissions
|
||||
const existing: Array<{ id: string }> = await knex("permissions").select("id");
|
||||
const existingIds = new Set(existing.map((e) => e.id));
|
||||
|
||||
// Insert missing permissions
|
||||
for (const perm of permissions) {
|
||||
if (!existingIds.has(perm.id)) {
|
||||
await knex("permissions").insert({
|
||||
id: perm.id,
|
||||
permission_name: perm.permission_name,
|
||||
created_at: knex.fn.now(),
|
||||
updated_at: knex.fn.now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Delete permissions that are no longer in the seed list
|
||||
const toDelete = existing.filter((e) => !permissionIds.has(e.id)).map((e) => e.id);
|
||||
if (toDelete.length > 0) {
|
||||
await knex("permissions").whereIn("id", toDelete).del();
|
||||
}
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
import type { Knex } from "knex";
|
||||
import { permissions } from "../src/lib/allPerms.ts";
|
||||
|
||||
/**
|
||||
* Seeds the three readonly roles (admin, editor, member),
|
||||
* assigns permissions to each role in roles_permissions,
|
||||
* and migrates existing users.role → users_roles.
|
||||
*
|
||||
* Permission mapping derived from src/routes/(manage)/manage/api/+server.ts:
|
||||
*
|
||||
* admin → all permissions
|
||||
* editor → all except api_keys.delete (AdminCan-only)
|
||||
* member → all .read permissions only
|
||||
*/
|
||||
|
||||
const readonlyRoles = [
|
||||
{ id: "admin", role_name: "Administrator" },
|
||||
{ id: "editor", role_name: "Editor" },
|
||||
{ id: "member", role_name: "Member" },
|
||||
];
|
||||
|
||||
const allPermissionIds = permissions.map((p) => p.id);
|
||||
const readPermissionIds = allPermissionIds.filter((id) => id.endsWith(".read"));
|
||||
|
||||
const rolePermissions: Record<string, string[]> = {
|
||||
admin: allPermissionIds,
|
||||
editor: allPermissionIds.filter((id) => id !== "api_keys.delete"),
|
||||
member: readPermissionIds,
|
||||
};
|
||||
|
||||
export async function seed(knex: Knex): Promise<void> {
|
||||
// 1. Ensure readonly roles exist
|
||||
for (const role of readonlyRoles) {
|
||||
const existing = await knex("roles").where("id", role.id).first();
|
||||
if (!existing) {
|
||||
await knex("roles").insert({
|
||||
id: role.id,
|
||||
role_name: role.role_name,
|
||||
readonly: 1,
|
||||
status: "ACTIVE",
|
||||
created_at: knex.fn.now(),
|
||||
updated_at: knex.fn.now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Seed roles_permissions for readonly roles
|
||||
// Only insert permissions that actually exist in the permissions table
|
||||
// to avoid FK constraint errors if permissions seed hasn't run yet.
|
||||
const existingPermRows: Array<{ id: string }> = await knex("permissions").select("id");
|
||||
const existingPermIds = new Set(existingPermRows.map((p) => p.id));
|
||||
|
||||
for (const [roleId, permissionIds] of Object.entries(rolePermissions)) {
|
||||
const validPermissionIds = permissionIds.filter((id) => existingPermIds.has(id));
|
||||
|
||||
const existingPerms: Array<{ permissions_id: string }> = await knex("roles_permissions")
|
||||
.where("roles_id", roleId)
|
||||
.select("permissions_id");
|
||||
const existingSet = new Set(existingPerms.map((e) => e.permissions_id));
|
||||
|
||||
// Insert missing permissions
|
||||
for (const permId of validPermissionIds) {
|
||||
if (!existingSet.has(permId)) {
|
||||
await knex("roles_permissions").insert({
|
||||
roles_id: roleId,
|
||||
permissions_id: permId,
|
||||
status: "ACTIVE",
|
||||
created_at: knex.fn.now(),
|
||||
updated_at: knex.fn.now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Remove permissions no longer assigned to this role
|
||||
const desiredSet = new Set(validPermissionIds);
|
||||
const toRemove = existingPerms.filter((e) => !desiredSet.has(e.permissions_id)).map((e) => e.permissions_id);
|
||||
if (toRemove.length > 0) {
|
||||
await knex("roles_permissions").where("roles_id", roleId).whereIn("permissions_id", toRemove).del();
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Migrate existing users: read users.role → insert into users_roles
|
||||
const hasRoleColumn = await knex.schema.hasColumn("users", "role");
|
||||
if (hasRoleColumn) {
|
||||
const users: Array<{ id: number; role: string }> = await knex("users").select("id", "role");
|
||||
|
||||
for (const user of users) {
|
||||
if (!user.role) continue;
|
||||
|
||||
// Only migrate if a matching role exists
|
||||
const roleExists = await knex("roles").where("id", user.role).first();
|
||||
if (!roleExists) continue;
|
||||
|
||||
// Skip if already assigned
|
||||
const existing = await knex("users_roles").where({ roles_id: user.role, users_id: user.id }).first();
|
||||
if (!existing) {
|
||||
await knex("users_roles").insert({
|
||||
roles_id: user.role,
|
||||
users_id: user.id,
|
||||
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);
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
/**
|
||||
* Permissions derived from src/routes/(manage)/manage/api/+server.ts actions.
|
||||
* Grouped by domain with read/write granularity.
|
||||
*
|
||||
* Mapping from actions → permissions:
|
||||
*
|
||||
* monitors.read → getMonitors, getMonitoringDataPaginated
|
||||
* monitors.write → storeMonitorData, updateMonitoringData, deleteMonitor, deleteMonitorData, cloneMonitor, testMonitor
|
||||
*
|
||||
* incidents.read → getIncidents, getIncident, getComments
|
||||
* incidents.write → createIncident, updateIncident, deleteIncident, addMonitor, removeMonitor, addComment, deleteComment, updateComment
|
||||
*
|
||||
* maintenances.read → getMaintenances, getMaintenance, getMaintenanceEvents, getMaintenanceEvent, getMaintenanceMonitors
|
||||
* maintenances.write → createMaintenance, updateMaintenance, deleteMaintenance, createMaintenanceEvent, updateMaintenanceEvent, deleteMaintenanceEvent, addMonitorToMaintenance, removeMonitorFromMaintenance, updateMaintenanceMonitorImpact
|
||||
*
|
||||
* pages.read → getPages
|
||||
* pages.write → createPage, updatePage, deletePage, addMonitorToPage, removeMonitorFromPage, reorderPageMonitors
|
||||
*
|
||||
* triggers.read → getTriggers
|
||||
* triggers.write → createUpdateTrigger, updateMonitorTriggers, deleteTrigger, testTrigger
|
||||
*
|
||||
* alerts.read → getMonitorAlertConfig, getMonitorAlertConfigById, getMonitorAlertConfigsByMonitorTag, getAlertConfigsPaginated, getAllAlertsPaginated
|
||||
* alerts.write → createMonitorAlertConfig, updateMonitorAlertConfig, deleteMonitorAlertConfig, toggleMonitorAlertConfigStatus, deleteMonitorAlertV2, updateMonitorAlertV2Status
|
||||
*
|
||||
* api_keys.read → getAPIKeys
|
||||
* api_keys.write → createNewApiKey, updateApiKeyStatus
|
||||
* api_keys.delete → deleteApiKey (admin-only today)
|
||||
*
|
||||
* users.read → getUsers
|
||||
* users.write → manualUpdate, createNewUser, resendInvitation, sendVerificationEmail
|
||||
*
|
||||
* settings.read → getAllSiteData, getSiteDataByKey, getSubscriptionsConfig
|
||||
* settings.write → storeSiteData, updateSubscriptionsConfig
|
||||
*
|
||||
* subscribers.read → getSubscribersByMethod, getSubscriberWithSubscriptions, getSubscriberCountsByMethod, getAdminSubscribers
|
||||
* subscribers.write → deleteUserSubscription, updateUserSubscriptionStatus, adminUpdateSubscriptionStatus, adminDeleteSubscriber, adminAddSubscriber
|
||||
*
|
||||
* email_templates.read → getGeneralEmailTemplates, getGeneralEmailTemplateById
|
||||
* email_templates.write → updateGeneralEmailTemplate
|
||||
*
|
||||
* images.write → uploadImage, deleteImage
|
||||
*/
|
||||
export const permissions: Array<{ id: string; permission_name: string }> = [
|
||||
// Monitors
|
||||
{ id: "monitors.read", permission_name: "View monitors and monitoring data" },
|
||||
{ id: "monitors.write", permission_name: "Create, update, delete, and clone monitors" },
|
||||
|
||||
// Incidents
|
||||
{ id: "incidents.read", permission_name: "View incidents and comments" },
|
||||
{ id: "incidents.write", permission_name: "Create, update, and delete incidents and comments" },
|
||||
|
||||
// Maintenances
|
||||
{ id: "maintenances.read", permission_name: "View maintenances and events" },
|
||||
{ id: "maintenances.write", permission_name: "Create, update, and delete maintenances and events" },
|
||||
|
||||
// Pages
|
||||
{ id: "pages.read", permission_name: "View pages" },
|
||||
{ id: "pages.write", permission_name: "Create, update, and delete pages" },
|
||||
|
||||
// Triggers
|
||||
{ id: "triggers.read", permission_name: "View triggers" },
|
||||
{ id: "triggers.write", permission_name: "Create, update, delete, and test triggers" },
|
||||
|
||||
// Alerts
|
||||
{ id: "alerts.read", permission_name: "View alert configurations and alert history" },
|
||||
{ id: "alerts.write", permission_name: "Create, update, and delete alert configurations" },
|
||||
|
||||
// API Keys
|
||||
{ id: "api_keys.read", permission_name: "View API keys" },
|
||||
{ id: "api_keys.write", permission_name: "Create and update API keys" },
|
||||
{ id: "api_keys.delete", permission_name: "Delete API keys" },
|
||||
|
||||
// Users
|
||||
{ id: "users.read", permission_name: "View users" },
|
||||
{ id: "users.write", permission_name: "Manage users, invitations, and verification" },
|
||||
|
||||
// Settings (site data + subscriptions config)
|
||||
{ id: "settings.read", permission_name: "View site settings and subscriptions config" },
|
||||
{ id: "settings.write", permission_name: "Update site settings and subscriptions config" },
|
||||
|
||||
// Subscribers
|
||||
{ id: "subscribers.read", permission_name: "View subscribers" },
|
||||
{ id: "subscribers.write", permission_name: "Manage subscribers and subscriptions" },
|
||||
|
||||
// Email Templates
|
||||
{ id: "email_templates.read", permission_name: "View email templates" },
|
||||
{ id: "email_templates.write", permission_name: "Update email templates" },
|
||||
|
||||
// Images
|
||||
{ id: "images.write", permission_name: "Upload and delete images" },
|
||||
|
||||
// Roles
|
||||
{ id: "roles.read", permission_name: "View roles, permissions, and user assignments" },
|
||||
{ id: "roles.write", permission_name: "Create, update, and delete roles" },
|
||||
{ id: "roles.assign_permissions", permission_name: "Add and remove permissions from roles" },
|
||||
{ id: "roles.assign_users", permission_name: "Add and remove users to and from roles" },
|
||||
];
|
||||
|
||||
export const ACTION_PERMISSION_MAP: Record<string, string | null> = {
|
||||
// Self-actions — no permission needed beyond being logged in
|
||||
updateUser: null,
|
||||
updatePassword: null,
|
||||
sendVerificationEmail: null, // controller has its own self-vs-other check
|
||||
|
||||
// Settings
|
||||
getAllSiteData: "settings.read",
|
||||
getSiteDataByKey: "settings.read",
|
||||
getSubscriptionsConfig: "settings.read",
|
||||
storeSiteData: "settings.write",
|
||||
updateSubscriptionsConfig: "settings.write",
|
||||
|
||||
// Users
|
||||
getUsers: "users.read",
|
||||
manualUpdate: "users.write",
|
||||
createNewUser: "users.write",
|
||||
resendInvitation: "users.write",
|
||||
|
||||
// Monitors
|
||||
getMonitors: "monitors.read",
|
||||
getMonitoringDataPaginated: "monitors.read",
|
||||
storeMonitorData: "monitors.write",
|
||||
updateMonitoringData: "monitors.write",
|
||||
deleteMonitor: "monitors.write",
|
||||
deleteMonitorData: "monitors.write",
|
||||
cloneMonitor: "monitors.write",
|
||||
testMonitor: "monitors.write",
|
||||
|
||||
// Incidents
|
||||
getIncidents: "incidents.read",
|
||||
getIncident: "incidents.read",
|
||||
getComments: "incidents.read",
|
||||
createIncident: "incidents.write",
|
||||
updateIncident: "incidents.write",
|
||||
deleteIncident: "incidents.write",
|
||||
addMonitor: "incidents.write",
|
||||
removeMonitor: "incidents.write",
|
||||
addComment: "incidents.write",
|
||||
deleteComment: "incidents.write",
|
||||
updateComment: "incidents.write",
|
||||
|
||||
// Maintenances
|
||||
getMaintenances: "maintenances.read",
|
||||
getMaintenance: "maintenances.read",
|
||||
getMaintenanceEvents: "maintenances.read",
|
||||
getMaintenanceEvent: "maintenances.read",
|
||||
getMaintenanceMonitors: "maintenances.read",
|
||||
createMaintenance: "maintenances.write",
|
||||
updateMaintenance: "maintenances.write",
|
||||
deleteMaintenance: "maintenances.write",
|
||||
createMaintenanceEvent: "maintenances.write",
|
||||
updateMaintenanceEvent: "maintenances.write",
|
||||
deleteMaintenanceEvent: "maintenances.write",
|
||||
addMonitorToMaintenance: "maintenances.write",
|
||||
removeMonitorFromMaintenance: "maintenances.write",
|
||||
updateMaintenanceMonitorImpact: "maintenances.write",
|
||||
|
||||
// Pages
|
||||
getPages: "pages.read",
|
||||
createPage: "pages.write",
|
||||
updatePage: "pages.write",
|
||||
deletePage: "pages.write",
|
||||
addMonitorToPage: "pages.write",
|
||||
removeMonitorFromPage: "pages.write",
|
||||
reorderPageMonitors: "pages.write",
|
||||
|
||||
// Triggers
|
||||
getTriggers: "triggers.read",
|
||||
createUpdateTrigger: "triggers.write",
|
||||
updateMonitorTriggers: "triggers.write",
|
||||
deleteTrigger: "triggers.write",
|
||||
testTrigger: "triggers.write",
|
||||
|
||||
// Alerts
|
||||
getAllAlertsPaginated: "alerts.read",
|
||||
getMonitorAlertConfig: "alerts.read",
|
||||
getMonitorAlertConfigById: "alerts.read",
|
||||
getMonitorAlertConfigsByMonitorTag: "alerts.read",
|
||||
getAlertConfigsPaginated: "alerts.read",
|
||||
createMonitorAlertConfig: "alerts.write",
|
||||
updateMonitorAlertConfig: "alerts.write",
|
||||
deleteMonitorAlertConfig: "alerts.write",
|
||||
toggleMonitorAlertConfigStatus: "alerts.write",
|
||||
deleteMonitorAlertV2: "alerts.write",
|
||||
updateMonitorAlertV2Status: "alerts.write",
|
||||
|
||||
// API Keys
|
||||
getAPIKeys: "api_keys.read",
|
||||
createNewApiKey: "api_keys.write",
|
||||
updateApiKeyStatus: "api_keys.write",
|
||||
deleteApiKey: "api_keys.delete",
|
||||
|
||||
// Subscribers
|
||||
getSubscribersByMethod: "subscribers.read",
|
||||
getSubscriberWithSubscriptions: "subscribers.read",
|
||||
getSubscriberCountsByMethod: "subscribers.read",
|
||||
getAdminSubscribers: "subscribers.read",
|
||||
deleteUserSubscription: "subscribers.write",
|
||||
updateUserSubscriptionStatus: "subscribers.write",
|
||||
adminUpdateSubscriptionStatus: "subscribers.write",
|
||||
adminDeleteSubscriber: "subscribers.write",
|
||||
adminAddSubscriber: "subscribers.write",
|
||||
|
||||
// Email Templates
|
||||
getGeneralEmailTemplates: "email_templates.read",
|
||||
getGeneralEmailTemplateById: "email_templates.read",
|
||||
updateGeneralEmailTemplate: "email_templates.write",
|
||||
|
||||
// Images
|
||||
uploadImage: "images.write",
|
||||
deleteImage: "images.write",
|
||||
|
||||
// Roles
|
||||
getRoles: "roles.read",
|
||||
getAllPermissions: "roles.read",
|
||||
getRolePermissions: "roles.read",
|
||||
getRoleUsers: "roles.read",
|
||||
createRole: "roles.write",
|
||||
updateRole: "roles.write",
|
||||
deleteRole: "roles.write",
|
||||
updateRolePermissions: "roles.assign_permissions",
|
||||
addUserToRole: "roles.assign_users",
|
||||
removeUserFromRole: "roles.assign_users",
|
||||
};
|
||||
|
||||
export const ROUTE_PERMISSION_MAP: Record<string, string | null> = {
|
||||
// Monitors
|
||||
"/(manage)/manage/app/monitors": "monitors.read",
|
||||
"/(manage)/manage/app/monitors/[tag]": "monitors.read",
|
||||
"/(manage)/manage/app/monitoring-data": "monitors.read",
|
||||
|
||||
// Incidents
|
||||
"/(manage)/manage/app/incidents": "incidents.read",
|
||||
"/(manage)/manage/app/incidents/[incident_id]": "incidents.read",
|
||||
|
||||
// Maintenances
|
||||
"/(manage)/manage/app/maintenances": "maintenances.read",
|
||||
"/(manage)/manage/app/maintenances/[id]": "maintenances.read",
|
||||
|
||||
// Pages
|
||||
"/(manage)/manage/app/pages": "pages.read",
|
||||
"/(manage)/manage/app/pages/[page_id]": "pages.read",
|
||||
|
||||
// Triggers
|
||||
"/(manage)/manage/app/triggers": "triggers.read",
|
||||
"/(manage)/manage/app/triggers/[trigger_id]": "triggers.read",
|
||||
|
||||
// Alerts
|
||||
"/(manage)/manage/app/alerts": "alerts.read",
|
||||
"/(manage)/manage/app/alerts/[alert_config_id]": "alerts.read",
|
||||
"/(manage)/manage/app/alerts/logs/[alert_config_id]": "alerts.read",
|
||||
|
||||
// API Keys
|
||||
"/(manage)/manage/app/api-keys": "api_keys.read",
|
||||
|
||||
// Users
|
||||
"/(manage)/manage/app/users": "users.read",
|
||||
|
||||
// Settings
|
||||
"/(manage)/manage/app/site-configurations": "settings.read",
|
||||
"/(manage)/manage/app/customizations": "settings.read",
|
||||
"/(manage)/manage/app/internationalization": "settings.read",
|
||||
"/(manage)/manage/app/analytics-providers": "settings.read",
|
||||
"/(manage)/manage/app/badges": "settings.read",
|
||||
"/(manage)/manage/app/embed": "settings.read",
|
||||
|
||||
// Subscribers
|
||||
"/(manage)/manage/app/subscriptions": "subscribers.read",
|
||||
|
||||
// Email Templates
|
||||
"/(manage)/manage/app/templates": "email_templates.read",
|
||||
|
||||
// Roles
|
||||
"/(manage)/manage/app/roles": "roles.read",
|
||||
};
|
||||
@@ -31,3 +31,39 @@ export default function urlResolve(resolve: ResolveFn, path: string, params?: Re
|
||||
}
|
||||
return resolve(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a path to an absolute URL by prefixing the site URL.
|
||||
* Required for meta tags like og:image and twitter:image that need absolute URLs.
|
||||
* @param resolve - The resolve function from $app/paths
|
||||
* @param siteUrl - The site URL (e.g., "https://status.example.com")
|
||||
* @param path - The route path or absolute URL
|
||||
* @param params - Optional parameters for dynamic route segments
|
||||
* @returns An absolute URL, or the resolved relative URL if siteUrl is empty
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* absoluteResolve(resolve, "https://status.example.com", "/uploads/preview.png")
|
||||
* // => "https://status.example.com/uploads/preview.png"
|
||||
* ```
|
||||
*/
|
||||
export function absoluteResolve(
|
||||
resolve: ResolveFn,
|
||||
siteUrl: string,
|
||||
path: string,
|
||||
params?: Record<string, string>
|
||||
): string {
|
||||
// Normalize relative paths like "./assets/..." to "/assets/..." so the
|
||||
// final URL doesn't contain "/./" segments (crawlers don't normalize these)
|
||||
const normalizedPath = path.startsWith("./") ? path.slice(1) : path;
|
||||
const resolved = urlResolve(resolve, normalizedPath, params);
|
||||
// Already absolute, return as-is
|
||||
if (resolved.startsWith("http://") || resolved.startsWith("https://")) {
|
||||
return resolved;
|
||||
}
|
||||
if (!siteUrl) {
|
||||
return resolved;
|
||||
}
|
||||
const trimmedSiteUrl = siteUrl.replace(/\/+$/, "");
|
||||
return trimmedSiteUrl + (resolved.startsWith("/") ? resolved : "/" + resolved);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
+79
-37
@@ -2,114 +2,156 @@
|
||||
"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",
|
||||
"No events to show": "No events to show",
|
||||
"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",
|
||||
"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 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.": "No monitors available.",
|
||||
"No ongoing maintenances": "Žádné probíhající údržby",
|
||||
"No past maintenances": "Žádné minulé údržby",
|
||||
"No monitors available.": "Žádné dostupné monitory",
|
||||
"No ongoing maintenances": "Žádná probíhající údržba",
|
||||
"No past maintenances": "Žádná minulá údržba",
|
||||
"No Status Available": "Stav není k dispozici",
|
||||
"No upcoming maintenances": "Žádné nadcházející údržby",
|
||||
"No Updates": "No Updates",
|
||||
"No updates yet": "Zatím bez aktualizací",
|
||||
"Notifications": "Notifications",
|
||||
"No upcoming maintenances": "Žádná nadcházející údržba",
|
||||
"No Updates": "Žádné aktualizace",
|
||||
"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",
|
||||
"Recurring": "Recurring",
|
||||
"READY": "PŘIPRAVENO",
|
||||
"Recent Incidents": "Nedávné incidenty",
|
||||
"Recurring": "Opakující se",
|
||||
"RESOLVED": "VYŘEŠENO",
|
||||
"SCHEDULED": "NAPLÁNOVÁNO",
|
||||
"Scheduled Events (%count)": "Plánované události (%count)",
|
||||
"Scheduled Windows": "Naplánované úlohy",
|
||||
"Script": "Skript",
|
||||
"Select Language": "Vyberte jazyk",
|
||||
"Select latency metric to display": "Select latency metric to display",
|
||||
"Select latency metric to display": "Vyberte metriku latence",
|
||||
"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.": "There are no ongoing incidents or maintenance events.",
|
||||
"Theme": "Motiv",
|
||||
"There are no incidents or maintenances scheduled for this month.": "Na tento měsíc nejsou naplánované žá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í",
|
||||
"Updates": "Updates",
|
||||
"Upcoming Maintenances": "Nadcházející údržby",
|
||||
"Update Incident": "Aktualizovat incident",
|
||||
"Update Maintenance": "Aktualizovat údržbu",
|
||||
"Updates": "Aktualizace",
|
||||
"Updates (%count)": "Aktualizace (%count)",
|
||||
"Uptime": "Dostupnost",
|
||||
"Uptime Badge": "Odznak dostupnosti",
|
||||
"Verification failed": "Ověření se nezdařilo",
|
||||
"Verify": "Ověřit",
|
||||
"Verifying": "Ověřování",
|
||||
"We sent a 6-digit code to": "Poslali jsme 6místný kód na"
|
||||
"We sent a 6-digit code to": "Odeslali jsme 6místný kód na"
|
||||
}
|
||||
}
|
||||
|
||||
+52
-30
@@ -5,38 +5,48 @@
|
||||
"Affected Monitors (%count)": "Betroffene Monitore (%count)",
|
||||
"All Systems Operational": "Alle Systeme betriebsbereit",
|
||||
"Average Latency": "Durchschnittliche Latenz",
|
||||
"Avg Latency": "Durchschnittliche Latenz",
|
||||
"Avg Latency": "Durchschn. Latenz",
|
||||
"Back": "Zurück",
|
||||
"Badges": "Abzeichen",
|
||||
"Continue": "Weitermachen",
|
||||
"Badges": "Anzeigen",
|
||||
"CANCELLED": "ABGESAGT",
|
||||
"COMPLETED": "ABGESCHLOSSEN",
|
||||
"Continue": "Fortsetzen",
|
||||
"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? ",
|
||||
"Didn't receive the code? Resend": "Sie haben den Code nicht erhalten? Erneut senden",
|
||||
"Down": "Ausgefallen",
|
||||
"DOWN": "AUSGEFALLEN",
|
||||
"Duration": "Dauer",
|
||||
"Email address": "E-Mail-Adresse",
|
||||
"Embed Monitor": "Monitor einbetten",
|
||||
"Embed this monitor in your website or app": "Betten Sie diesen Monitor in Ihre Website oder App ein",
|
||||
"End Time": "Endzeit",
|
||||
"Enter the verification code sent to your email.": "Geben Sie den Bestätigungscode ein, der an Ihre E-Mail-Adresse gesendet wurde.",
|
||||
"Events": "Veranstaltungen",
|
||||
"Events": "Ereignisse",
|
||||
"Failed to load data": "Daten konnten nicht geladen werden",
|
||||
"Failed to load latency data": "Latenzdaten konnten nicht geladen werden",
|
||||
"Failed to load status data for this day": "Statusdaten für diesen Tag konnten nicht geladen werden",
|
||||
"Failed to send verification code": "Der Bestätigungscode konnte nicht gesendet werden",
|
||||
"Failed to update preference": "Die Präferenz konnte nicht aktualisiert werden",
|
||||
"Get badges for this monitor": "Erhalten Sie Abzeichen für diesen Monitor",
|
||||
"Get badges for this monitor": "Erhalten Sie Statusanzeigen für diesen Monitor",
|
||||
"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 Updates": "Vorfallaktualisierungen",
|
||||
"incident": "Vorfall",
|
||||
"Incident Updates": "Vorfallsaktualisierungen",
|
||||
"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,35 +54,40 @@
|
||||
"Latency Trend": "Latenztrend",
|
||||
"Latest Latency": "Neueste Latenz",
|
||||
"Latest Status": "Neuester Stand",
|
||||
"Light": "Licht",
|
||||
"Light": "Hell",
|
||||
"Live Status": "Live-Status",
|
||||
"Loading your preferences...": "Deine Einstellungen werden geladen...",
|
||||
"maintenance": "Wartung",
|
||||
"MAINTENANCE": "WARTUNG",
|
||||
"Maintenance Updates": "Wartungsaktualisierungen",
|
||||
"Maintenances": "Wartungen",
|
||||
"Major System Outage": "Schwerwiegender Systemausfall",
|
||||
"Manage Site": "Seite verwalten",
|
||||
"Manage your notification preferences.": "Verwalten Sie Ihre Benachrichtigungseinstellungen.",
|
||||
"Max Latency": "Maximale Latenz",
|
||||
"Max Latency": "Max. Latenz",
|
||||
"Maximum Latency": "Maximale Latenz",
|
||||
"Min Latency": "Min. Latenz",
|
||||
"Minimum Latency": "Min. Latenz",
|
||||
"Minimum Latency": "Minimale Latenz",
|
||||
"Minute-by-minute status data for this day": "Minutenweise Statusdaten für diesen Tag",
|
||||
"Network error. Please try again.": "Netzwerkfehler. ",
|
||||
"MONITORING": "WIRD ÜBERWACHT",
|
||||
"Network error. Please try again.": "Netzwerkfehler. Bitte erneut versuchen.",
|
||||
"No Events in %currentMonth": "Keine Ereignisse in %currentMonth",
|
||||
"No events to show": "No events to show",
|
||||
"No events to show": "Keine Ereignisse zum Anzeigen",
|
||||
"No incidents for this day": "Keine Vorfälle für diesen Tag",
|
||||
"No latency data available for this day": "Für diesen Tag sind keine Latenzdaten verfügbar",
|
||||
"No maintenances for this day": "An diesem Tag finden keine Wartungsarbeiten statt",
|
||||
"No monitors affected": "Keine Monitore betroffen",
|
||||
"No monitors available.": "No monitors available.",
|
||||
"No monitors available.": "Keine Monitore verfügbar.",
|
||||
"No ongoing maintenances": "Keine laufenden Wartungsarbeiten",
|
||||
"No past maintenances": "Keine früheren Wartungsarbeiten",
|
||||
"No Status Available": "Kein Status verfügbar",
|
||||
"No upcoming maintenances": "Keine bevorstehenden Wartungsarbeiten",
|
||||
"No Updates": "No Updates",
|
||||
"No updates yet": "Noch keine Updates",
|
||||
"Notifications": "Notifications",
|
||||
"No Updates": "Keine Aktualisierungen",
|
||||
"No updates yet": "Noch keine Aktualisierungen",
|
||||
"Notifications": "Benachrichtigungen",
|
||||
"One-time": "Einmalig",
|
||||
"Ongoing": "Laufend",
|
||||
"Operational": "Betriebsbereit",
|
||||
"Partial Degraded Performance": "Teilweise beeinträchtigte Leistung",
|
||||
"Partial System Outage": "Teilweiser Systemausfall",
|
||||
"Past": "Vergangenheit",
|
||||
@@ -82,34 +97,41 @@
|
||||
"Please enter the 6-digit verification code": "Bitte geben Sie den 6-stelligen Bestätigungscode ein",
|
||||
"Read less": "Weniger lesen",
|
||||
"Read more": "Mehr lesen",
|
||||
"Recurring": "Recurring",
|
||||
"READY": "BEREIT",
|
||||
"Recurring": "Wiederkehrend",
|
||||
"RESOLVED": "BEHOBEN",
|
||||
"SCHEDULED": "GEPLANT",
|
||||
"Scheduled Events (%count)": "Geplante Ereignisse (%count)",
|
||||
"Script": "Skript",
|
||||
"Select Language": "Wählen Sie Sprache aus",
|
||||
"Select latency metric to display": "Select latency metric to display",
|
||||
"Select Range": "Wählen Sie Bereich aus",
|
||||
"Select Language": "Sprache auswählen",
|
||||
"Select latency metric to display": "Anzuzeigende Latenzmetrik auswählen",
|
||||
"Select Range": "Bereich auswählen",
|
||||
"Sending...": "Senden...",
|
||||
"Standard": "Standard",
|
||||
"Start Time": "Startzeit",
|
||||
"Status": "Status",
|
||||
"Status Badge": "Statusabzeichen",
|
||||
"Status Badge": "Statusanzeige",
|
||||
"Status Embed": "Status einbetten",
|
||||
"Status history and latency trend": "Statusverlauf und Latenztrend",
|
||||
"Subscribe": "Abonnieren",
|
||||
"Subscribe to Updates": "Abonnieren Sie Updates",
|
||||
"Subscribe to Updates": "Benachrichtigungen abonnieren",
|
||||
"There are no incidents or maintenances scheduled for this month.": "Für diesen Monat sind keine Vorfälle oder Wartungsarbeiten geplant.",
|
||||
"There are no ongoing incidents or maintenance events.": "There are no ongoing incidents or maintenance events.",
|
||||
"There are no ongoing incidents or maintenance events.": "Es gibt keine laufenden Vorfälle oder Wartungsarbeiten.",
|
||||
"Total Incidents": "Gesamtzahl der Vorfälle",
|
||||
"Total Maintenances": "Gesamtwartungen",
|
||||
"Total Maintenances": "Gesamtzahl der Wartungen",
|
||||
"Under Maintenance": "Unter Wartung",
|
||||
"Unknown impact": "Unbekannte Auswirkung",
|
||||
"Upcoming": "Demnächst",
|
||||
"Updates": "Updates",
|
||||
"UP": "AKTIV",
|
||||
"Upcoming": "Anstehend",
|
||||
"Update Incident": "Vorfall aktualisieren",
|
||||
"Update Maintenance": "Wartung aktualisieren",
|
||||
"Updates": "Aktualisierungen",
|
||||
"Updates (%count)": "Aktualisierungen (%count)",
|
||||
"Uptime": "Betriebszeit",
|
||||
"Uptime Badge": "Verfügbarkeitsabzeichen",
|
||||
"Verification failed": "Die Überprüfung ist fehlgeschlagen",
|
||||
"Uptime Badge": "Verfügbarkeitsanzeige",
|
||||
"Verification failed": "Überprüfung fehlgeschlagen",
|
||||
"Verify": "Verifizieren",
|
||||
"Verifying": "Verifizieren",
|
||||
"We sent a 6-digit code to": "Wir haben einen 6-stelligen Code an gesendet"
|
||||
"We sent a 6-digit code to": "Wir haben einen 6-stelligen Code gesendet an"
|
||||
}
|
||||
}
|
||||
|
||||
+31
-9
@@ -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,32 +57,37 @@
|
||||
"Light": "Lys",
|
||||
"Live Status": "Live status",
|
||||
"Loading your preferences...": "Indlæser dine præferencer...",
|
||||
"maintenance": "Vedligeholdelse",
|
||||
"MAINTENANCE": "VEDLIGEHOLDELSE",
|
||||
"Maintenance Updates": "Vedligeholdelsesopdateringer",
|
||||
"Maintenances": "Vedligeholdelse",
|
||||
"Major System Outage": "Større systemnedbrud",
|
||||
"Manage Site": "Administrer side",
|
||||
"Manage your notification preferences.": "Administrer dine meddelelsespræferencer.",
|
||||
"Max Latency": "Max latens",
|
||||
"Maximum Latency": "Max latens",
|
||||
"Min Latency": "Min latens",
|
||||
"Minimum Latency": "Min latens",
|
||||
"Minute-by-minute status data for this day": "Minut for minut statusdata for denne dag",
|
||||
"MONITORING": "MONITORING",
|
||||
"Network error. Please try again.": "Netværksfejl. ",
|
||||
"No Events in %currentMonth": "Ingen begivenheder i %currentMonth",
|
||||
"No events to show": "No events to show",
|
||||
"No events to show": "Ingen hændelser at vise",
|
||||
"No incidents for this day": "Ingen hændelser denne dag",
|
||||
"No latency data available for this day": "Ingen forsinkelsesdata tilgængelige for denne dag",
|
||||
"No maintenances for this day": "Ingen vedligeholdelse denne dag",
|
||||
"No monitors affected": "Ingen skærme påvirket",
|
||||
"No monitors available.": "No monitors available.",
|
||||
"No monitors available.": "Ingen monitorer tilgængelige.",
|
||||
"No ongoing maintenances": "Ingen løbende vedligeholdelse",
|
||||
"No past maintenances": "Ingen tidligere vedligeholdelse",
|
||||
"No Status Available": "Ingen status tilgængelig",
|
||||
"No upcoming maintenances": "Ingen kommende vedligeholdelse",
|
||||
"No Updates": "No Updates",
|
||||
"No Updates": "Ingen opdateringer",
|
||||
"No updates yet": "Ingen opdateringer endnu",
|
||||
"Notifications": "Notifications",
|
||||
"Notifications": "Notifikationer",
|
||||
"One-time": "Engangs",
|
||||
"Ongoing": "Løbende",
|
||||
"Operational": "Operationel",
|
||||
"Partial Degraded Performance": "Delvist nedsat ydeevne",
|
||||
"Partial System Outage": "Delvist systemnedbrud",
|
||||
"Past": "Forbi",
|
||||
@@ -82,10 +97,14 @@
|
||||
"Please enter the 6-digit verification code": "Indtast venligst den 6-cifrede bekræftelseskode",
|
||||
"Read less": "Læs mindre",
|
||||
"Read more": "Læs mere",
|
||||
"Recurring": "Recurring",
|
||||
"READY": "KLAR",
|
||||
"Recurring": "Tilbagevendende",
|
||||
"RESOLVED": "RESOLVED",
|
||||
"SCHEDULED": "PLANLAGT",
|
||||
"Scheduled Events (%count)": "Planlagte begivenheder (%count)",
|
||||
"Script": "Manuskript",
|
||||
"Select Language": "Vælg sprog",
|
||||
"Select latency metric to display": "Select latency metric to display",
|
||||
"Select latency metric to display": "Vælg latenstidsmåling, der skal vises",
|
||||
"Select Range": "Vælg Område",
|
||||
"Sending...": "Sender...",
|
||||
"Standard": "Standard",
|
||||
@@ -97,13 +116,16 @@
|
||||
"Subscribe": "Abonner",
|
||||
"Subscribe to Updates": "Abonner på opdateringer",
|
||||
"There are no incidents or maintenances scheduled for this month.": "Der er ingen hændelser eller vedligeholdelse planlagt i denne måned.",
|
||||
"There are no ongoing incidents or maintenance events.": "There are no ongoing incidents or maintenance events.",
|
||||
"There are no ongoing incidents or maintenance events.": "Der er ingen igangværende hændelser eller vedligeholdelsesaktiviteter.",
|
||||
"Total Incidents": "Samlede hændelser",
|
||||
"Total Maintenances": "Samlet vedligeholdelse",
|
||||
"Under Maintenance": "Under Vedligeholdelse",
|
||||
"Unknown impact": "Ukendt påvirkning",
|
||||
"UP": "OPPE",
|
||||
"Upcoming": "Kommende",
|
||||
"Updates": "Updates",
|
||||
"Update Incident": "Opdater hændelse",
|
||||
"Update Maintenance": "Opdater vedligeholdelse",
|
||||
"Updates": "Opdateringer",
|
||||
"Updates (%count)": "Opdateringer (%count)",
|
||||
"Uptime": "Oppetid",
|
||||
"Uptime Badge": "Oppetidsmærke",
|
||||
|
||||
+23
-1
@@ -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",
|
||||
|
||||
+31
-9
@@ -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,32 +57,37 @@
|
||||
"Light": "Luz",
|
||||
"Live Status": "Estado en vivo",
|
||||
"Loading your preferences...": "Cargando tus preferencias...",
|
||||
"maintenance": "Mantenimiento",
|
||||
"MAINTENANCE": "MANTENIMIENTO",
|
||||
"Maintenance Updates": "Actualizaciones de mantenimiento",
|
||||
"Maintenances": "Mantenimientos",
|
||||
"Major System Outage": "Interrupción importante del sistema",
|
||||
"Manage Site": "Gestionar sitio",
|
||||
"Manage your notification preferences.": "Administre sus preferencias de notificación.",
|
||||
"Max Latency": "Latencia máxima",
|
||||
"Maximum Latency": "Latencia máxima",
|
||||
"Min Latency": "Latencia mínima",
|
||||
"Minimum Latency": "Latencia mínima",
|
||||
"Minute-by-minute status data for this day": "Datos del estado minuto a minuto de este día",
|
||||
"MONITORING": "MONITORING",
|
||||
"Network error. Please try again.": "Error de red. ",
|
||||
"No Events in %currentMonth": "No hay eventos en %currentMonth",
|
||||
"No events to show": "No events to show",
|
||||
"No events to show": "No hay eventos para mostrar",
|
||||
"No incidents for this day": "No hay incidencias para este día",
|
||||
"No latency data available for this day": "No hay datos de latencia disponibles para este día",
|
||||
"No maintenances for this day": "No hay mantenimientos para este día.",
|
||||
"No monitors affected": "Ningún monitor afectado",
|
||||
"No monitors available.": "No monitors available.",
|
||||
"No monitors available.": "No hay monitores disponibles.",
|
||||
"No ongoing maintenances": "Sin mantenimientos continuos",
|
||||
"No past maintenances": "Sin mantenimientos pasados",
|
||||
"No Status Available": "Estado no disponible",
|
||||
"No upcoming maintenances": "No hay mantenimientos próximos",
|
||||
"No Updates": "No Updates",
|
||||
"No Updates": "Sin actualizaciones",
|
||||
"No updates yet": "Aún no hay actualizaciones",
|
||||
"Notifications": "Notifications",
|
||||
"Notifications": "Notificaciones",
|
||||
"One-time": "una sola vez",
|
||||
"Ongoing": "En curso",
|
||||
"Operational": "Operativo",
|
||||
"Partial Degraded Performance": "Rendimiento parcialmente degradado",
|
||||
"Partial System Outage": "Interrupción parcial del sistema",
|
||||
"Past": "Pasado",
|
||||
@@ -82,10 +97,14 @@
|
||||
"Please enter the 6-digit verification code": "Por favor ingresa el código de verificación de 6 dígitos",
|
||||
"Read less": "Leer menos",
|
||||
"Read more": "Leer más",
|
||||
"Recurring": "Recurring",
|
||||
"READY": "LISTO",
|
||||
"Recurring": "Recurrente",
|
||||
"RESOLVED": "RESOLVED",
|
||||
"SCHEDULED": "PROGRAMADO",
|
||||
"Scheduled Events (%count)": "Eventos programados (%count)",
|
||||
"Script": "Guion",
|
||||
"Select Language": "Seleccionar idioma",
|
||||
"Select latency metric to display": "Select latency metric to display",
|
||||
"Select latency metric to display": "Selecciona la métrica de latencia para mostrar",
|
||||
"Select Range": "Seleccionar rango",
|
||||
"Sending...": "Envío...",
|
||||
"Standard": "Estándar",
|
||||
@@ -97,13 +116,16 @@
|
||||
"Subscribe": "Suscribir",
|
||||
"Subscribe to Updates": "Suscríbete a las actualizaciones",
|
||||
"There are no incidents or maintenances scheduled for this month.": "No hay incidencias ni mantenimientos programados para este mes.",
|
||||
"There are no ongoing incidents or maintenance events.": "There are no ongoing incidents or maintenance events.",
|
||||
"There are no ongoing incidents or maintenance events.": "No hay incidentes ni mantenimientos en curso.",
|
||||
"Total Incidents": "Incidentes totales",
|
||||
"Total Maintenances": "Mantenimientos totales",
|
||||
"Under Maintenance": "En mantenimiento",
|
||||
"Unknown impact": "Impacto desconocido",
|
||||
"UP": "ACTIVO",
|
||||
"Upcoming": "Próximo",
|
||||
"Updates": "Updates",
|
||||
"Update Incident": "Actualizar incidente",
|
||||
"Update Maintenance": "Actualizar mantenimiento",
|
||||
"Updates": "Actualizaciones",
|
||||
"Updates (%count)": "Actualizaciones (%count)",
|
||||
"Uptime": "tiempo de actividad",
|
||||
"Uptime Badge": "Insignia de tiempo de actividad",
|
||||
|
||||
+31
-9
@@ -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,32 +57,37 @@
|
||||
"Light": "نور",
|
||||
"Live Status": "وضعیت زنده",
|
||||
"Loading your preferences...": "در حال بارگیری تنظیمات برگزیده شما...",
|
||||
"maintenance": "نگهداری",
|
||||
"MAINTENANCE": "تعمیر و نگهداری",
|
||||
"Maintenance Updates": "به روز رسانی های تعمیر و نگهداری",
|
||||
"Maintenances": "تعمیر و نگهداری",
|
||||
"Major System Outage": "قطعی عمده سیستم",
|
||||
"Manage Site": "مدیریت سایت",
|
||||
"Manage your notification preferences.": "تنظیمات برگزیده اعلان خود را مدیریت کنید.",
|
||||
"Max Latency": "حداکثر تاخیر",
|
||||
"Maximum Latency": "حداکثر تاخیر",
|
||||
"Min Latency": "حداقل تاخیر",
|
||||
"Minimum Latency": "حداقل تاخیر",
|
||||
"Minute-by-minute status data for this day": "داده های وضعیت دقیقه به دقیقه برای این روز",
|
||||
"MONITORING": "MONITORING",
|
||||
"Network error. Please try again.": "خطای شبکه ",
|
||||
"No Events in %currentMonth": "هیچ رویدادی در %currentMonth وجود ندارد",
|
||||
"No events to show": "No events to show",
|
||||
"No events to show": "رویدادی برای نمایش وجود ندارد",
|
||||
"No incidents for this day": "هیچ حادثه ای برای این روز وجود ندارد",
|
||||
"No latency data available for this day": "هیچ داده تاخیری برای این روز در دسترس نیست",
|
||||
"No maintenances for this day": "بدون تعمیر و نگهداری برای این روز",
|
||||
"No monitors affected": "هیچ مانیتوری تحت تأثیر قرار نگرفت",
|
||||
"No monitors available.": "No monitors available.",
|
||||
"No monitors available.": "هیچ مانیتوری در دسترس نیست.",
|
||||
"No ongoing maintenances": "بدون تعمیر و نگهداری مداوم",
|
||||
"No past maintenances": "بدون تعمیر و نگهداری قبلی",
|
||||
"No Status Available": "وضعیتی در دسترس نیست",
|
||||
"No upcoming maintenances": "بدون تعمیر و نگهداری آینده",
|
||||
"No Updates": "No Updates",
|
||||
"No Updates": "بدون بهروزرسانی",
|
||||
"No updates yet": "هنوز به روز رسانی نشده است",
|
||||
"Notifications": "Notifications",
|
||||
"Notifications": "اعلانها",
|
||||
"One-time": "یک بار",
|
||||
"Ongoing": "در حال انجام است",
|
||||
"Operational": "عملیاتی",
|
||||
"Partial Degraded Performance": "کاهش عملکرد جزئی",
|
||||
"Partial System Outage": "قطعی جزئی سیستم",
|
||||
"Past": "گذشته",
|
||||
@@ -82,10 +97,14 @@
|
||||
"Please enter the 6-digit verification code": "لطفا کد تایید 6 رقمی را وارد کنید",
|
||||
"Read less": "کمتر بخوانید",
|
||||
"Read more": "ادامه مطلب",
|
||||
"Recurring": "Recurring",
|
||||
"READY": "آماده",
|
||||
"Recurring": "دورهای",
|
||||
"RESOLVED": "RESOLVED",
|
||||
"SCHEDULED": "زمانبندی شده",
|
||||
"Scheduled Events (%count)": "رویدادهای زمانبندی شده (%count)",
|
||||
"Script": "اسکریپت",
|
||||
"Select Language": "زبان را انتخاب کنید",
|
||||
"Select latency metric to display": "Select latency metric to display",
|
||||
"Select latency metric to display": "معیار تأخیر برای نمایش را انتخاب کنید",
|
||||
"Select Range": "Range را انتخاب کنید",
|
||||
"Sending...": "ارسال...",
|
||||
"Standard": "استاندارد",
|
||||
@@ -97,13 +116,16 @@
|
||||
"Subscribe": "مشترک شوید",
|
||||
"Subscribe to Updates": "مشترک شدن در به روز رسانی",
|
||||
"There are no incidents or maintenances scheduled for this month.": "هیچ حادثه یا تعمیراتی برای این ماه برنامه ریزی نشده است.",
|
||||
"There are no ongoing incidents or maintenance events.": "There are no ongoing incidents or maintenance events.",
|
||||
"There are no ongoing incidents or maintenance events.": "هیچ رخداد یا رویداد نگهداری فعالی وجود ندارد.",
|
||||
"Total Incidents": "مجموع حوادث",
|
||||
"Total Maintenances": "کل تعمیر و نگهداری",
|
||||
"Under Maintenance": "تحت تعمیر و نگهداری",
|
||||
"Unknown impact": "تاثیر نامعلوم",
|
||||
"UP": "فعال",
|
||||
"Upcoming": "آینده",
|
||||
"Updates": "Updates",
|
||||
"Update Incident": "بهروزرسانی رویداد",
|
||||
"Update Maintenance": "بهروزرسانی نگهداری",
|
||||
"Updates": "بهروزرسانیها",
|
||||
"Updates (%count)": "بهروزرسانیها (%count)",
|
||||
"Uptime": "آپتایم",
|
||||
"Uptime Badge": "نشان Uptime",
|
||||
|
||||
+31
-9
@@ -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,32 +57,37 @@
|
||||
"Light": "Lumière",
|
||||
"Live Status": "Statut en direct",
|
||||
"Loading your preferences...": "Chargement de vos préférences...",
|
||||
"maintenance": "Maintenance",
|
||||
"MAINTENANCE": "MAINTENANCE",
|
||||
"Maintenance Updates": "Mises à jour de maintenance",
|
||||
"Maintenances": "Entretiens",
|
||||
"Major System Outage": "Panne majeure du système",
|
||||
"Manage Site": "Gérer le site",
|
||||
"Manage your notification preferences.": "Gérez vos préférences de notification.",
|
||||
"Max Latency": "Latence maximale",
|
||||
"Maximum Latency": "Latence maximale",
|
||||
"Min Latency": "Latence minimale",
|
||||
"Minimum Latency": "Latence minimale",
|
||||
"Minute-by-minute status data for this day": "Données d'état minute par minute pour cette journée",
|
||||
"MONITORING": "MONITORING",
|
||||
"Network error. Please try again.": "Erreur réseau. ",
|
||||
"No Events in %currentMonth": "Aucun événement dans %currentMonth",
|
||||
"No events to show": "No events to show",
|
||||
"No events to show": "Aucun événement à afficher",
|
||||
"No incidents for this day": "Aucun incident pour cette journée",
|
||||
"No latency data available for this day": "Aucune donnée de latence disponible pour ce jour",
|
||||
"No maintenances for this day": "Aucune maintenance pour cette journée",
|
||||
"No monitors affected": "Aucun moniteur affecté",
|
||||
"No monitors available.": "No monitors available.",
|
||||
"No monitors available.": "Aucun moniteur disponible.",
|
||||
"No ongoing maintenances": "Aucune maintenance en cours",
|
||||
"No past maintenances": "Aucune maintenance passée",
|
||||
"No Status Available": "Aucun statut disponible",
|
||||
"No upcoming maintenances": "Aucune maintenance à venir",
|
||||
"No Updates": "No Updates",
|
||||
"No Updates": "Aucune mise à jour",
|
||||
"No updates yet": "Aucune mise à jour pour l'instant",
|
||||
"Notifications": "Notifications",
|
||||
"Notifications": "Alertes",
|
||||
"One-time": "Une fois",
|
||||
"Ongoing": "En cours",
|
||||
"Operational": "Opérationnel",
|
||||
"Partial Degraded Performance": "Performance partiellement dégradée",
|
||||
"Partial System Outage": "Panne partielle du système",
|
||||
"Past": "Passé",
|
||||
@@ -82,10 +97,14 @@
|
||||
"Please enter the 6-digit verification code": "Veuillez saisir le code de vérification à 6 chiffres",
|
||||
"Read less": "Lire moins",
|
||||
"Read more": "En savoir plus",
|
||||
"Recurring": "Recurring",
|
||||
"READY": "PRÊT",
|
||||
"Recurring": "Récurrent",
|
||||
"RESOLVED": "RESOLVED",
|
||||
"SCHEDULED": "PLANIFIÉ",
|
||||
"Scheduled Events (%count)": "Événements planifiés (%count)",
|
||||
"Script": "Scénario",
|
||||
"Select Language": "Sélectionnez la langue",
|
||||
"Select latency metric to display": "Select latency metric to display",
|
||||
"Select latency metric to display": "Sélectionnez la métrique de latence à afficher",
|
||||
"Select Range": "Sélectionner une plage",
|
||||
"Sending...": "Envoi...",
|
||||
"Standard": "Standard",
|
||||
@@ -97,13 +116,16 @@
|
||||
"Subscribe": "S'abonner",
|
||||
"Subscribe to Updates": "Abonnez-vous aux mises à jour",
|
||||
"There are no incidents or maintenances scheduled for this month.": "Il n’y a aucun incident ou maintenance prévu pour ce mois.",
|
||||
"There are no ongoing incidents or maintenance events.": "There are no ongoing incidents or maintenance events.",
|
||||
"There are no ongoing incidents or maintenance events.": "Il n’y a aucun incident ni aucune maintenance en cours.",
|
||||
"Total Incidents": "Nombre total d'incidents",
|
||||
"Total Maintenances": "Entretiens totaux",
|
||||
"Under Maintenance": "En maintenance",
|
||||
"Unknown impact": "Impact inconnu",
|
||||
"UP": "OPÉRATIONNEL",
|
||||
"Upcoming": "Prochain",
|
||||
"Updates": "Updates",
|
||||
"Update Incident": "Mettre à jour l'incident",
|
||||
"Update Maintenance": "Mettre à jour la maintenance",
|
||||
"Updates": "Mises à jour",
|
||||
"Updates (%count)": "Mises à jour (%count)",
|
||||
"Uptime": "Temps de disponibilité",
|
||||
"Uptime Badge": "Badge de disponibilité",
|
||||
|
||||
+39
-17
@@ -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,32 +57,37 @@
|
||||
"Light": "लाइट",
|
||||
"Live Status": "लाइव स्थिति",
|
||||
"Loading your preferences...": "आपकी प्राथमिकताएँ लोड हो रही हैं...",
|
||||
"maintenance": "रखरखाव",
|
||||
"MAINTENANCE": "रखरखाव",
|
||||
"Maintenance Updates": "रखरखाव अपडेट",
|
||||
"Maintenances": "रखरखाव",
|
||||
"Major System Outage": "सिस्टम का बड़ा आउटेज",
|
||||
"Manage Site": "साइट प्रबंधित करें",
|
||||
"Manage your notification preferences.": "अपनी सूचना प्राथमिकताएँ प्रबंधित करें।",
|
||||
"Max Latency": "Max Latency",
|
||||
"Maximum Latency": "Max Latency",
|
||||
"Min Latency": "Min Latency",
|
||||
"Minimum Latency": "Min Latency",
|
||||
"Max Latency": "अधिकतम लेटेंसी",
|
||||
"Maximum Latency": "अधिकतम लेटेंसी",
|
||||
"Min Latency": "न्यूनतम लेटेंसी",
|
||||
"Minimum Latency": "न्यूनतम लेटेंसी",
|
||||
"Minute-by-minute status data for this day": "इस दिन के लिए मिनट-दर-मिनट स्थिति डेटा",
|
||||
"MONITORING": "मॉनिटरिंग",
|
||||
"Network error. Please try again.": "नेटवर्क त्रुटि। कृपया पुनः प्रयास करें।",
|
||||
"No Events in %currentMonth": "%currentMonth में कोई घटना नहीं",
|
||||
"No events to show": "No events to show",
|
||||
"No events to show": "दिखाने के लिए कोई इवेंट नहीं",
|
||||
"No incidents for this day": "इस दिन के लिए कोई घटना नहीं",
|
||||
"No latency data available for this day": "इस दिन के लिए कोई लेटेंसी डेटा उपलब्ध नहीं",
|
||||
"No maintenances for this day": "इस दिन के लिए कोई रखरखाव नहीं",
|
||||
"No monitors affected": "कोई मॉनिटर प्रभावित नहीं",
|
||||
"No monitors available.": "No monitors available.",
|
||||
"No monitors available.": "कोई मॉनिटर उपलब्ध नहीं है।",
|
||||
"No ongoing maintenances": "कोई चल रहा रखरखाव नहीं",
|
||||
"No past maintenances": "कोई बीता हुआ रखरखाव नहीं",
|
||||
"No Status Available": "स्थिति उपलब्ध नहीं है",
|
||||
"No upcoming maintenances": "कोई आने वाला रखरखाव नहीं",
|
||||
"No Updates": "No Updates",
|
||||
"No Updates": "कोई अपडेट नहीं",
|
||||
"No updates yet": "अभी तक कोई अपडेट नहीं",
|
||||
"Notifications": "Notifications",
|
||||
"Notifications": "सूचनाएँ",
|
||||
"One-time": "एक बार",
|
||||
"Ongoing": "चल रहा है",
|
||||
"Operational": "चालू",
|
||||
"Partial Degraded Performance": "आंशिक प्रदर्शन गिरावट",
|
||||
"Partial System Outage": "सिस्टम का आंशिक आउटेज.",
|
||||
"Past": "बीता हुआ",
|
||||
@@ -82,10 +97,14 @@
|
||||
"Please enter the 6-digit verification code": "कृपया 6 अंकों का सत्यापन कोड दर्ज करें",
|
||||
"Read less": "कम पढ़ें",
|
||||
"Read more": "और पढ़ें",
|
||||
"Recurring": "Recurring",
|
||||
"READY": "तैयार",
|
||||
"Recurring": "आवर्ती",
|
||||
"RESOLVED": "सुलझा हुआ",
|
||||
"SCHEDULED": "अनुसूचित",
|
||||
"Scheduled Events (%count)": "अनुसूचित कार्यक्रम (%count)",
|
||||
"Script": "स्क्रिप्ट",
|
||||
"Select Language": "भाषा चुनें",
|
||||
"Select latency metric to display": "Select latency metric to display",
|
||||
"Select latency metric to display": "दिखाने के लिए विलंबता मेट्रिक चुनें",
|
||||
"Select Range": "रेंज चुनें",
|
||||
"Sending...": "भेजा जा रहा है...",
|
||||
"Standard": "मानक",
|
||||
@@ -97,19 +116,22 @@
|
||||
"Subscribe": "सब्सक्राइब करें",
|
||||
"Subscribe to Updates": "अपडेट के लिए सब्सक्राइब करें",
|
||||
"There are no incidents or maintenances scheduled for this month.": "इस महीने के लिए कोई घटना या रखरखाव निर्धारित नहीं है।",
|
||||
"There are no ongoing incidents or maintenance events.": "There are no ongoing incidents or maintenance events.",
|
||||
"Total Incidents": "Total Incidents",
|
||||
"Total Maintenances": "Total Maintenances",
|
||||
"There are no ongoing incidents or maintenance events.": "कोई चल रही घटना या रखरखाव इवेंट नहीं है।",
|
||||
"Total Incidents": "कुल घटनाएँ",
|
||||
"Total Maintenances": "कुल रखरखाव",
|
||||
"Under Maintenance": "रखरखाव जारी है",
|
||||
"Unknown impact": "अज्ञात प्रभाव",
|
||||
"UP": "चालू",
|
||||
"Upcoming": "आने वाला",
|
||||
"Updates": "Updates",
|
||||
"Update Incident": "घटना अपडेट करें",
|
||||
"Update Maintenance": "रखरखाव अपडेट करें",
|
||||
"Updates": "अपडेट्स",
|
||||
"Updates (%count)": "अपडेट (%count)",
|
||||
"Uptime": "अपटाइम",
|
||||
"Uptime Badge": "अपटाइम बैज",
|
||||
"Verification failed": "सत्यापन विफल रहा",
|
||||
"Verify": "Verify",
|
||||
"Verifying": "Verifying",
|
||||
"Verify": "सत्यापित करें",
|
||||
"Verifying": "सत्यापित हो रहा है",
|
||||
"We sent a 6-digit code to": "हमने 6 अंकों का कोड भेजा है"
|
||||
}
|
||||
}
|
||||
|
||||
+31
-9
@@ -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,32 +57,37 @@
|
||||
"Light": "Leggero",
|
||||
"Live Status": "Stato in tempo reale",
|
||||
"Loading your preferences...": "Caricamento delle tue preferenze...",
|
||||
"maintenance": "Manutenzione",
|
||||
"MAINTENANCE": "MANUTENZIONE",
|
||||
"Maintenance Updates": "Aggiornamenti sulla manutenzione",
|
||||
"Maintenances": "Manutenzioni",
|
||||
"Major System Outage": "Interruzione grave del sistema",
|
||||
"Manage Site": "Gestisci sito",
|
||||
"Manage your notification preferences.": "Gestisci le tue preferenze di notifica.",
|
||||
"Max Latency": "Latenza massima",
|
||||
"Maximum Latency": "Latenza massima",
|
||||
"Min Latency": "Latenza minima",
|
||||
"Minimum Latency": "Latenza minima",
|
||||
"Minute-by-minute status data for this day": "Dati sullo stato minuto per minuto per questo giorno",
|
||||
"MONITORING": "MONITORING",
|
||||
"Network error. Please try again.": "Errore di rete. ",
|
||||
"No Events in %currentMonth": "Nessun evento in %currentMonth",
|
||||
"No events to show": "No events to show",
|
||||
"No events to show": "Nessun evento da mostrare",
|
||||
"No incidents for this day": "Nessun incidente per questa giornata",
|
||||
"No latency data available for this day": "Nessun dato sulla latenza disponibile per questo giorno",
|
||||
"No maintenances for this day": "Nessuna manutenzione per oggi",
|
||||
"No monitors affected": "Nessun monitor interessato",
|
||||
"No monitors available.": "No monitors available.",
|
||||
"No monitors available.": "Nessun monitor disponibile.",
|
||||
"No ongoing maintenances": "Nessuna manutenzione continua",
|
||||
"No past maintenances": "Nessuna manutenzione passata",
|
||||
"No Status Available": "Nessuno stato disponibile",
|
||||
"No upcoming maintenances": "Nessuna manutenzione imminente",
|
||||
"No Updates": "No Updates",
|
||||
"No Updates": "Nessun aggiornamento",
|
||||
"No updates yet": "Nessun aggiornamento ancora",
|
||||
"Notifications": "Notifications",
|
||||
"Notifications": "Notifiche",
|
||||
"One-time": "Una volta",
|
||||
"Ongoing": "In corso",
|
||||
"Operational": "Operativo",
|
||||
"Partial Degraded Performance": "Prestazioni parzialmente degradate",
|
||||
"Partial System Outage": "Interruzione parziale del sistema",
|
||||
"Past": "Passato",
|
||||
@@ -82,10 +97,14 @@
|
||||
"Please enter the 6-digit verification code": "Inserisci il codice di verifica di 6 cifre",
|
||||
"Read less": "Leggi di meno",
|
||||
"Read more": "Per saperne di più",
|
||||
"Recurring": "Recurring",
|
||||
"READY": "PRONTO",
|
||||
"Recurring": "Ricorrente",
|
||||
"RESOLVED": "RESOLVED",
|
||||
"SCHEDULED": "PROGRAMMATO",
|
||||
"Scheduled Events (%count)": "Eventi programmati (%count)",
|
||||
"Script": "Copione",
|
||||
"Select Language": "Seleziona lingua",
|
||||
"Select latency metric to display": "Select latency metric to display",
|
||||
"Select latency metric to display": "Seleziona la metrica di latenza da visualizzare",
|
||||
"Select Range": "Seleziona Intervallo",
|
||||
"Sending...": "Invio...",
|
||||
"Standard": "Standard",
|
||||
@@ -97,13 +116,16 @@
|
||||
"Subscribe": "Iscriviti",
|
||||
"Subscribe to Updates": "Iscriviti agli aggiornamenti",
|
||||
"There are no incidents or maintenances scheduled for this month.": "Non ci sono incidenti o manutenzioni programmate per questo mese.",
|
||||
"There are no ongoing incidents or maintenance events.": "There are no ongoing incidents or maintenance events.",
|
||||
"There are no ongoing incidents or maintenance events.": "Non ci sono incidenti o manutenzioni in corso.",
|
||||
"Total Incidents": "Incidenti totali",
|
||||
"Total Maintenances": "Manutenzioni totali",
|
||||
"Under Maintenance": "In manutenzione",
|
||||
"Unknown impact": "Impatto sconosciuto",
|
||||
"UP": "ATTIVO",
|
||||
"Upcoming": "Prossimamente",
|
||||
"Updates": "Updates",
|
||||
"Update Incident": "Aggiorna incidente",
|
||||
"Update Maintenance": "Aggiorna manutenzione",
|
||||
"Updates": "Aggiornamenti",
|
||||
"Updates (%count)": "Aggiornamenti (%count)",
|
||||
"Uptime": "Tempo di attività",
|
||||
"Uptime Badge": "Badge di operatività",
|
||||
|
||||
+31
-9
@@ -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,32 +57,37 @@
|
||||
"Light": "ライト",
|
||||
"Live Status": "ライブステータス",
|
||||
"Loading your preferences...": "設定を読み込んでいます...",
|
||||
"maintenance": "メンテナンス",
|
||||
"MAINTENANCE": "メンテナンス",
|
||||
"Maintenance Updates": "メンテナンスアップデート",
|
||||
"Maintenances": "メンテナンス",
|
||||
"Major System Outage": "大規模なシステム障害",
|
||||
"Manage Site": "サイト管理",
|
||||
"Manage your notification preferences.": "通知設定を管理します。",
|
||||
"Max Latency": "最大レイテンシー",
|
||||
"Maximum Latency": "最大レイテンシー",
|
||||
"Min Latency": "最小レイテンシ",
|
||||
"Minimum Latency": "最小レイテンシ",
|
||||
"Minute-by-minute status data for this day": "この日の分単位のステータスデータ",
|
||||
"MONITORING": "MONITORING",
|
||||
"Network error. Please try again.": "ネットワークエラー。",
|
||||
"No Events in %currentMonth": "%currentMonth にはイベントがありません",
|
||||
"No events to show": "No events to show",
|
||||
"No events to show": "表示するイベントはありません",
|
||||
"No incidents for this day": "この日は事件はありませんでした",
|
||||
"No latency data available for this day": "この日のレイテンシ データはありません",
|
||||
"No maintenances for this day": "この日はメンテナンスはありません",
|
||||
"No monitors affected": "影響を受けるモニターはありません",
|
||||
"No monitors available.": "No monitors available.",
|
||||
"No monitors available.": "利用可能なモニターはありません。",
|
||||
"No ongoing maintenances": "継続的なメンテナンスはありません",
|
||||
"No past maintenances": "過去のメンテナンスはありません",
|
||||
"No Status Available": "ステータス情報はありません",
|
||||
"No upcoming maintenances": "今後のメンテナンスはありません",
|
||||
"No Updates": "No Updates",
|
||||
"No Updates": "更新はありません",
|
||||
"No updates yet": "まだ更新はありません",
|
||||
"Notifications": "Notifications",
|
||||
"Notifications": "通知",
|
||||
"One-time": "一度",
|
||||
"Ongoing": "進行中",
|
||||
"Operational": "稼働中",
|
||||
"Partial Degraded Performance": "部分的なパフォーマンス低下",
|
||||
"Partial System Outage": "部分的なシステム障害。",
|
||||
"Past": "過去",
|
||||
@@ -82,10 +97,14 @@
|
||||
"Please enter the 6-digit verification code": "6桁の認証コードを入力してください",
|
||||
"Read less": "読む量を減らす",
|
||||
"Read more": "続きを読む",
|
||||
"Recurring": "Recurring",
|
||||
"READY": "準備完了",
|
||||
"Recurring": "繰り返し",
|
||||
"RESOLVED": "RESOLVED",
|
||||
"SCHEDULED": "予定済み",
|
||||
"Scheduled Events (%count)": "予定イベント (%count)",
|
||||
"Script": "スクリプト",
|
||||
"Select Language": "言語の選択",
|
||||
"Select latency metric to display": "Select latency metric to display",
|
||||
"Select latency metric to display": "表示するレイテンシ指標を選択",
|
||||
"Select Range": "範囲の選択",
|
||||
"Sending...": "送信中...",
|
||||
"Standard": "標準",
|
||||
@@ -97,13 +116,16 @@
|
||||
"Subscribe": "購読する",
|
||||
"Subscribe to Updates": "アップデートを購読する",
|
||||
"There are no incidents or maintenances scheduled for this month.": "今月は予定されているインシデントやメンテナンスはありません。",
|
||||
"There are no ongoing incidents or maintenance events.": "There are no ongoing incidents or maintenance events.",
|
||||
"There are no ongoing incidents or maintenance events.": "進行中のインシデントまたはメンテナンスイベントはありません。",
|
||||
"Total Incidents": "総インシデント数",
|
||||
"Total Maintenances": "トータルメンテナンス",
|
||||
"Under Maintenance": "メンテナンス中",
|
||||
"Unknown impact": "未知の影響",
|
||||
"UP": "正常",
|
||||
"Upcoming": "今後の予定",
|
||||
"Updates": "Updates",
|
||||
"Update Incident": "インシデントを更新",
|
||||
"Update Maintenance": "メンテナンスを更新",
|
||||
"Updates": "更新",
|
||||
"Updates (%count)": "アップデート (%count)",
|
||||
"Uptime": "稼働時間",
|
||||
"Uptime Badge": "稼働時間バッジ",
|
||||
|
||||
+31
-9
@@ -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,32 +57,37 @@
|
||||
"Light": "빛",
|
||||
"Live Status": "실시간 현황",
|
||||
"Loading your preferences...": "환경설정 로드 중...",
|
||||
"maintenance": "유지보수",
|
||||
"MAINTENANCE": "점검 중",
|
||||
"Maintenance Updates": "유지보수 업데이트",
|
||||
"Maintenances": "유지보수",
|
||||
"Major System Outage": "대규모 시스템 장애",
|
||||
"Manage Site": "사이트 관리",
|
||||
"Manage your notification preferences.": "알림 기본 설정을 관리하세요.",
|
||||
"Max Latency": "최대 대기 시간",
|
||||
"Maximum Latency": "최대 대기 시간",
|
||||
"Min Latency": "최소 지연 시간",
|
||||
"Minimum Latency": "최소 지연 시간",
|
||||
"Minute-by-minute status data for this day": "오늘의 분별 상태 데이터",
|
||||
"MONITORING": "MONITORING",
|
||||
"Network error. Please try again.": "네트워크 오류입니다. 다시 시도해 주세요.",
|
||||
"No Events in %currentMonth": "%currentMonth에 이벤트가 없습니다.",
|
||||
"No events to show": "No events to show",
|
||||
"No events to show": "표시할 이벤트가 없습니다",
|
||||
"No incidents for this day": "오늘은 사건이 없습니다.",
|
||||
"No latency data available for this day": "이 날에는 지연 시간 데이터가 없습니다.",
|
||||
"No maintenances for this day": "이날은 유지보수가 없습니다",
|
||||
"No monitors affected": "영향을 받는 모니터 없음",
|
||||
"No monitors available.": "No monitors available.",
|
||||
"No monitors available.": "사용 가능한 모니터가 없습니다.",
|
||||
"No ongoing maintenances": "지속적인 유지 관리가 필요하지 않습니다.",
|
||||
"No past maintenances": "과거 유지보수 없음",
|
||||
"No Status Available": "사용 가능한 상태 정보 없음",
|
||||
"No upcoming maintenances": "예정된 유지 관리가 없습니다.",
|
||||
"No Updates": "No Updates",
|
||||
"No Updates": "업데이트 없음",
|
||||
"No updates yet": "아직 업데이트가 없습니다",
|
||||
"Notifications": "Notifications",
|
||||
"Notifications": "알림",
|
||||
"One-time": "일회성",
|
||||
"Ongoing": "전진",
|
||||
"Operational": "정상",
|
||||
"Partial Degraded Performance": "부분적 성능 저하",
|
||||
"Partial System Outage": "부분적 시스템 장애.",
|
||||
"Past": "과거",
|
||||
@@ -82,10 +97,14 @@
|
||||
"Please enter the 6-digit verification code": "인증번호 6자리를 입력해주세요",
|
||||
"Read less": "덜 읽으세요",
|
||||
"Read more": "더 읽어보세요",
|
||||
"Recurring": "Recurring",
|
||||
"READY": "준비됨",
|
||||
"Recurring": "반복",
|
||||
"RESOLVED": "RESOLVED",
|
||||
"SCHEDULED": "예정됨",
|
||||
"Scheduled Events (%count)": "예정된 이벤트 (%count)",
|
||||
"Script": "스크립트",
|
||||
"Select Language": "언어 선택",
|
||||
"Select latency metric to display": "Select latency metric to display",
|
||||
"Select latency metric to display": "표시할 지연 시간 지표를 선택하세요",
|
||||
"Select Range": "범위 선택",
|
||||
"Sending...": "배상...",
|
||||
"Standard": "기준",
|
||||
@@ -97,13 +116,16 @@
|
||||
"Subscribe": "구독하다",
|
||||
"Subscribe to Updates": "업데이트 구독",
|
||||
"There are no incidents or maintenances scheduled for this month.": "이번 달에는 예정된 사고나 점검이 없습니다.",
|
||||
"There are no ongoing incidents or maintenance events.": "There are no ongoing incidents or maintenance events.",
|
||||
"There are no ongoing incidents or maintenance events.": "진행 중인 인시던트 또는 유지보수 이벤트가 없습니다.",
|
||||
"Total Incidents": "총 사고",
|
||||
"Total Maintenances": "총 유지보수",
|
||||
"Under Maintenance": "유지보수 중",
|
||||
"Unknown impact": "알 수 없는 영향",
|
||||
"UP": "정상",
|
||||
"Upcoming": "예정",
|
||||
"Updates": "Updates",
|
||||
"Update Incident": "인시던트 업데이트",
|
||||
"Update Maintenance": "유지보수 업데이트",
|
||||
"Updates": "업데이트",
|
||||
"Updates (%count)": "업데이트(%count)",
|
||||
"Uptime": "가동 시간",
|
||||
"Uptime Badge": "가동 시간 배지",
|
||||
|
||||
@@ -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,32 +57,37 @@
|
||||
"Light": "Lys",
|
||||
"Live Status": "Live-status",
|
||||
"Loading your preferences...": "Laster inn innstillingene dine ...",
|
||||
"maintenance": "Vedlikehold",
|
||||
"MAINTENANCE": "VEDLIKEHOLD",
|
||||
"Maintenance Updates": "Vedlikeholdsoppdateringer",
|
||||
"Maintenances": "Vedlikehold",
|
||||
"Major System Outage": "Større systemutfall",
|
||||
"Manage Site": "Administrer nettsted",
|
||||
"Manage your notification preferences.": "Administrer varslingspreferansene dine.",
|
||||
"Max Latency": "Maks ventetid",
|
||||
"Maximum Latency": "Maks ventetid",
|
||||
"Min Latency": "Min latens",
|
||||
"Minimum Latency": "Min latens",
|
||||
"Minute-by-minute status data for this day": "Statusdata fra minutt for minutt for denne dagen",
|
||||
"MONITORING": "MONITORING",
|
||||
"Network error. Please try again.": "Nettverksfeil. Vennligst prøv igjen.",
|
||||
"No Events in %currentMonth": "Ingen hendelser i %currentMonth",
|
||||
"No events to show": "No events to show",
|
||||
"No events to show": "Ingen hendelser å vise",
|
||||
"No incidents for this day": "Ingen hendelser denne dagen",
|
||||
"No latency data available for this day": "Ingen forsinkelsesdata tilgjengelig for denne dagen",
|
||||
"No maintenances for this day": "Ingen vedlikehold denne dagen",
|
||||
"No monitors affected": "Ingen monitorer er berørt",
|
||||
"No monitors available.": "No monitors available.",
|
||||
"No monitors available.": "Ingen monitorer tilgjengelige.",
|
||||
"No ongoing maintenances": "Ingen løpende vedlikehold",
|
||||
"No past maintenances": "Ingen tidligere vedlikehold",
|
||||
"No Status Available": "Ingen status tilgjengelig",
|
||||
"No upcoming maintenances": "Ingen kommende vedlikehold",
|
||||
"No Updates": "No Updates",
|
||||
"No Updates": "Ingen oppdateringer",
|
||||
"No updates yet": "Ingen oppdateringer ennå",
|
||||
"Notifications": "Notifications",
|
||||
"Notifications": "Varsler",
|
||||
"One-time": "En gang",
|
||||
"Ongoing": "Pågående",
|
||||
"Operational": "Operativ",
|
||||
"Partial Degraded Performance": "Delvis redusert ytelse",
|
||||
"Partial System Outage": "Delvis systemutfall",
|
||||
"Past": "Forbi",
|
||||
@@ -82,10 +97,14 @@
|
||||
"Please enter the 6-digit verification code": "Vennligst skriv inn den 6-sifrede bekreftelseskoden",
|
||||
"Read less": "Les mindre",
|
||||
"Read more": "Les mer",
|
||||
"Recurring": "Recurring",
|
||||
"READY": "KLAR",
|
||||
"Recurring": "Gjentakende",
|
||||
"RESOLVED": "RESOLVED",
|
||||
"SCHEDULED": "PLANLAGT",
|
||||
"Scheduled Events (%count)": "Planlagte hendelser (%count)",
|
||||
"Script": "Manus",
|
||||
"Select Language": "Velg Språk",
|
||||
"Select latency metric to display": "Select latency metric to display",
|
||||
"Select latency metric to display": "Velg latensmåling som skal vises",
|
||||
"Select Range": "Velg Område",
|
||||
"Sending...": "Sender...",
|
||||
"Standard": "Standard",
|
||||
@@ -97,13 +116,16 @@
|
||||
"Subscribe": "Abonner",
|
||||
"Subscribe to Updates": "Abonner på oppdateringer",
|
||||
"There are no incidents or maintenances scheduled for this month.": "Det er ingen hendelser eller vedlikehold planlagt denne måneden.",
|
||||
"There are no ongoing incidents or maintenance events.": "There are no ongoing incidents or maintenance events.",
|
||||
"There are no ongoing incidents or maintenance events.": "Det er ingen pågående hendelser eller vedlikeholdsaktiviteter.",
|
||||
"Total Incidents": "Totale hendelser",
|
||||
"Total Maintenances": "Totalt vedlikehold",
|
||||
"Under Maintenance": "Under Vedlikehold",
|
||||
"Unknown impact": "Ukjent påvirkning",
|
||||
"UP": "OPPE",
|
||||
"Upcoming": "Kommende",
|
||||
"Updates": "Updates",
|
||||
"Update Incident": "Oppdater hendelse",
|
||||
"Update Maintenance": "Oppdater vedlikehold",
|
||||
"Updates": "Oppdateringer",
|
||||
"Updates (%count)": "Oppdateringer (%count)",
|
||||
"Uptime": "Oppetid",
|
||||
"Uptime Badge": "Oppetidsmerke",
|
||||
|
||||
+31
-9
@@ -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,32 +57,37 @@
|
||||
"Light": "Licht",
|
||||
"Live Status": "Live-status",
|
||||
"Loading your preferences...": "Uw voorkeuren laden...",
|
||||
"maintenance": "Onderhoud",
|
||||
"MAINTENANCE": "ONDERHOUD",
|
||||
"Maintenance Updates": "Onderhoudsupdates",
|
||||
"Maintenances": "Onderhouden",
|
||||
"Major System Outage": "Grote systeemstoring",
|
||||
"Manage Site": "Site beheren",
|
||||
"Manage your notification preferences.": "Beheer uw meldingsvoorkeuren.",
|
||||
"Max Latency": "Maximale latentie",
|
||||
"Maximum Latency": "Maximale latentie",
|
||||
"Min Latency": "Minimale latentie",
|
||||
"Minimum Latency": "Minimale latentie",
|
||||
"Minute-by-minute status data for this day": "Statusgegevens van minuut tot minuut voor deze dag",
|
||||
"MONITORING": "MONITORING",
|
||||
"Network error. Please try again.": "Netwerkfout. ",
|
||||
"No Events in %currentMonth": "Geen evenementen in %currentMonth",
|
||||
"No events to show": "No events to show",
|
||||
"No events to show": "Geen gebeurtenissen om weer te geven",
|
||||
"No incidents for this day": "Geen incidenten deze dag",
|
||||
"No latency data available for this day": "Er zijn geen latentiegegevens beschikbaar voor deze dag",
|
||||
"No maintenances for this day": "Geen onderhoud voor deze dag",
|
||||
"No monitors affected": "Er zijn geen monitoren getroffen",
|
||||
"No monitors available.": "No monitors available.",
|
||||
"No monitors available.": "Geen monitoren beschikbaar.",
|
||||
"No ongoing maintenances": "Geen doorlopend onderhoud",
|
||||
"No past maintenances": "Geen onderhoudsbeurten uit het verleden",
|
||||
"No Status Available": "Geen status beschikbaar",
|
||||
"No upcoming maintenances": "Geen aankomend onderhoud",
|
||||
"No Updates": "No Updates",
|
||||
"No Updates": "Geen actualisaties",
|
||||
"No updates yet": "Nog geen updates",
|
||||
"Notifications": "Notifications",
|
||||
"Notifications": "Meldingen",
|
||||
"One-time": "Eenmalig",
|
||||
"Ongoing": "Lopend",
|
||||
"Operational": "Operationeel",
|
||||
"Partial Degraded Performance": "Gedeeltelijk verminderde prestaties",
|
||||
"Partial System Outage": "Gedeeltelijke systeemstoring",
|
||||
"Past": "Verleden",
|
||||
@@ -82,10 +97,14 @@
|
||||
"Please enter the 6-digit verification code": "Voer de 6-cijferige verificatiecode in",
|
||||
"Read less": "Lees minder",
|
||||
"Read more": "Lees meer",
|
||||
"Recurring": "Recurring",
|
||||
"READY": "GEREED",
|
||||
"Recurring": "Terugkerend",
|
||||
"RESOLVED": "RESOLVED",
|
||||
"SCHEDULED": "GEPLAND",
|
||||
"Scheduled Events (%count)": "Geplande evenementen (%count)",
|
||||
"Script": "Script",
|
||||
"Select Language": "Selecteer Taal",
|
||||
"Select latency metric to display": "Select latency metric to display",
|
||||
"Select latency metric to display": "Selecteer de latentie-metriek die moet worden weergegeven",
|
||||
"Select Range": "Selecteer Bereik",
|
||||
"Sending...": "Verzenden...",
|
||||
"Standard": "Standaard",
|
||||
@@ -97,13 +116,16 @@
|
||||
"Subscribe": "Abonneren",
|
||||
"Subscribe to Updates": "Abonneer u op updates",
|
||||
"There are no incidents or maintenances scheduled for this month.": "Er staan deze maand geen incidenten of onderhoud gepland.",
|
||||
"There are no ongoing incidents or maintenance events.": "There are no ongoing incidents or maintenance events.",
|
||||
"There are no ongoing incidents or maintenance events.": "Er zijn geen lopende incidenten of onderhoudsgebeurtenissen.",
|
||||
"Total Incidents": "Totaal aantal incidenten",
|
||||
"Total Maintenances": "Totaal onderhoud",
|
||||
"Under Maintenance": "Onder Onderhoud",
|
||||
"Unknown impact": "Onbekende impact",
|
||||
"UP": "ACTIEF",
|
||||
"Upcoming": "Aankomend",
|
||||
"Updates": "Updates",
|
||||
"Update Incident": "Incident bijwerken",
|
||||
"Update Maintenance": "Onderhoud bijwerken",
|
||||
"Updates": "Actualisaties",
|
||||
"Updates (%count)": "Updates (%count)",
|
||||
"Uptime": "Uptime",
|
||||
"Uptime Badge": "Uptime-badge",
|
||||
|
||||
+31
-9
@@ -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,32 +57,37 @@
|
||||
"Light": "Światło",
|
||||
"Live Status": "Stan na żywo",
|
||||
"Loading your preferences...": "Ładowanie Twoich preferencji...",
|
||||
"maintenance": "Konserwacja",
|
||||
"MAINTENANCE": "KONSERWACJA",
|
||||
"Maintenance Updates": "Aktualizacje konserwacyjne",
|
||||
"Maintenances": "Konserwacje",
|
||||
"Major System Outage": "Poważna awaria systemu",
|
||||
"Manage Site": "Zarządzaj stroną",
|
||||
"Manage your notification preferences.": "Zarządzaj preferencjami powiadomień.",
|
||||
"Max Latency": "Maksymalne opóźnienie",
|
||||
"Maximum Latency": "Maksymalne opóźnienie",
|
||||
"Min Latency": "Minimalne opóźnienie",
|
||||
"Minimum Latency": "Minimalne opóźnienie",
|
||||
"Minute-by-minute status data for this day": "Dane o stanie minuta po minucie na ten dzień",
|
||||
"MONITORING": "MONITORING",
|
||||
"Network error. Please try again.": "Błąd sieci. ",
|
||||
"No Events in %currentMonth": "Brak wydarzeń w %currentMonth",
|
||||
"No events to show": "No events to show",
|
||||
"No events to show": "Brak zdarzeń do wyświetlenia",
|
||||
"No incidents for this day": "Żadnych incydentów na ten dzień",
|
||||
"No latency data available for this day": "Brak danych dotyczących opóźnień dla tego dnia",
|
||||
"No maintenances for this day": "Brak konserwacji na ten dzień",
|
||||
"No monitors affected": "Nie dotyczy to żadnych monitorów",
|
||||
"No monitors available.": "No monitors available.",
|
||||
"No monitors available.": "Brak dostępnych monitorów.",
|
||||
"No ongoing maintenances": "Brak bieżących konserwacji",
|
||||
"No past maintenances": "Brak wcześniejszych konserwacji",
|
||||
"No Status Available": "Brak dostępnego statusu",
|
||||
"No upcoming maintenances": "Brak nadchodzących prac konserwacyjnych",
|
||||
"No Updates": "No Updates",
|
||||
"No Updates": "Brak aktualizacji",
|
||||
"No updates yet": "Nie ma jeszcze żadnych aktualizacji",
|
||||
"Notifications": "Notifications",
|
||||
"Notifications": "Powiadomienia",
|
||||
"One-time": "Jednorazowy",
|
||||
"Ongoing": "Bieżący",
|
||||
"Operational": "Operacyjny",
|
||||
"Partial Degraded Performance": "Częściowo obniżona wydajność",
|
||||
"Partial System Outage": "Częściowa awaria systemu",
|
||||
"Past": "Przeszłość",
|
||||
@@ -82,10 +97,14 @@
|
||||
"Please enter the 6-digit verification code": "Wprowadź 6-cyfrowy kod weryfikacyjny",
|
||||
"Read less": "Czytaj mniej",
|
||||
"Read more": "Przeczytaj więcej",
|
||||
"Recurring": "Recurring",
|
||||
"READY": "GOTOWE",
|
||||
"Recurring": "Cykliczne",
|
||||
"RESOLVED": "RESOLVED",
|
||||
"SCHEDULED": "ZAPLANOWANE",
|
||||
"Scheduled Events (%count)": "Zaplanowane wydarzenia (%count)",
|
||||
"Script": "Scenariusz",
|
||||
"Select Language": "Wybierz Język",
|
||||
"Select latency metric to display": "Select latency metric to display",
|
||||
"Select latency metric to display": "Wybierz metrykę opóźnienia do wyświetlenia",
|
||||
"Select Range": "Wybierz Zakres",
|
||||
"Sending...": "Przesyłka...",
|
||||
"Standard": "Standard",
|
||||
@@ -97,13 +116,16 @@
|
||||
"Subscribe": "Subskrybować",
|
||||
"Subscribe to Updates": "Subskrybuj aktualizacje",
|
||||
"There are no incidents or maintenances scheduled for this month.": "Na ten miesiąc nie zaplanowano żadnych incydentów ani konserwacji.",
|
||||
"There are no ongoing incidents or maintenance events.": "There are no ongoing incidents or maintenance events.",
|
||||
"There are no ongoing incidents or maintenance events.": "Brak trwających incydentów ani prac konserwacyjnych.",
|
||||
"Total Incidents": "Całkowita liczba incydentów",
|
||||
"Total Maintenances": "Całkowita konserwacja",
|
||||
"Under Maintenance": "W ramach konserwacji",
|
||||
"Unknown impact": "Nieznany wpływ",
|
||||
"UP": "DZIAŁA",
|
||||
"Upcoming": "Nadchodzące",
|
||||
"Updates": "Updates",
|
||||
"Update Incident": "Aktualizuj incydent",
|
||||
"Update Maintenance": "Aktualizuj konserwację",
|
||||
"Updates": "Aktualizacje",
|
||||
"Updates (%count)": "Aktualizacje (%count)",
|
||||
"Uptime": "Czas pracy",
|
||||
"Uptime Badge": "Odznaka dostępności",
|
||||
|
||||
@@ -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,32 +57,37 @@
|
||||
"Light": "Luz",
|
||||
"Live Status": "Status ao vivo",
|
||||
"Loading your preferences...": "Carregando suas preferências...",
|
||||
"maintenance": "Manutenção",
|
||||
"MAINTENANCE": "MANUTENÇÃO",
|
||||
"Maintenance Updates": "Atualizações de manutenção",
|
||||
"Maintenances": "Manutenção",
|
||||
"Major System Outage": "Indisponibilidade grave do sistema",
|
||||
"Manage Site": "Gerenciar site",
|
||||
"Manage your notification preferences.": "Gerencie suas preferências de notificação.",
|
||||
"Max Latency": "Latência máxima",
|
||||
"Maximum Latency": "Latência máxima",
|
||||
"Min Latency": "Latência mínima",
|
||||
"Minimum Latency": "Latência mínima",
|
||||
"Minute-by-minute status data for this day": "Dados de status minuto a minuto para este dia",
|
||||
"MONITORING": "MONITORING",
|
||||
"Network error. Please try again.": "Erro de rede. ",
|
||||
"No Events in %currentMonth": "Nenhum evento em %currentMonth",
|
||||
"No events to show": "No events to show",
|
||||
"No events to show": "Não há eventos para exibir",
|
||||
"No incidents for this day": "Nenhum incidente neste dia",
|
||||
"No latency data available for this day": "Não há dados de latência disponíveis para este dia",
|
||||
"No maintenances for this day": "Sem manutenções para este dia",
|
||||
"No monitors affected": "Nenhum monitor afetado",
|
||||
"No monitors available.": "No monitors available.",
|
||||
"No monitors available.": "Não há monitores disponíveis.",
|
||||
"No ongoing maintenances": "Sem manutenções contínuas",
|
||||
"No past maintenances": "Sem manutenções anteriores",
|
||||
"No Status Available": "Nenhum status disponível",
|
||||
"No upcoming maintenances": "Sem manutenções futuras",
|
||||
"No Updates": "No Updates",
|
||||
"No Updates": "Sem atualizações",
|
||||
"No updates yet": "Ainda não há atualizações",
|
||||
"Notifications": "Notifications",
|
||||
"Notifications": "Notificações",
|
||||
"One-time": "Único",
|
||||
"Ongoing": "Em andamento",
|
||||
"Operational": "Operacional",
|
||||
"Partial Degraded Performance": "Desempenho parcialmente degradado",
|
||||
"Partial System Outage": "Indisponibilidade parcial do sistema",
|
||||
"Past": "Passado",
|
||||
@@ -82,10 +97,14 @@
|
||||
"Please enter the 6-digit verification code": "Insira o código de verificação de 6 dígitos",
|
||||
"Read less": "Leia menos",
|
||||
"Read more": "Leia mais",
|
||||
"Recurring": "Recurring",
|
||||
"READY": "PRONTO",
|
||||
"Recurring": "Recorrente",
|
||||
"RESOLVED": "RESOLVED",
|
||||
"SCHEDULED": "AGENDADO",
|
||||
"Scheduled Events (%count)": "Eventos agendados (%count)",
|
||||
"Script": "Roteiro",
|
||||
"Select Language": "Selecione o idioma",
|
||||
"Select latency metric to display": "Select latency metric to display",
|
||||
"Select latency metric to display": "Selecione a métrica de latência para exibir",
|
||||
"Select Range": "Selecione o intervalo",
|
||||
"Sending...": "Enviando...",
|
||||
"Standard": "Padrão",
|
||||
@@ -97,13 +116,16 @@
|
||||
"Subscribe": "Inscrever-se",
|
||||
"Subscribe to Updates": "Assine atualizações",
|
||||
"There are no incidents or maintenances scheduled for this month.": "Não há incidentes ou manutenções programadas para este mês.",
|
||||
"There are no ongoing incidents or maintenance events.": "There are no ongoing incidents or maintenance events.",
|
||||
"There are no ongoing incidents or maintenance events.": "Não há incidentes ou manutenções em andamento.",
|
||||
"Total Incidents": "Total de incidentes",
|
||||
"Total Maintenances": "Manutenção total",
|
||||
"Under Maintenance": "Em manutenção",
|
||||
"Unknown impact": "Impacto desconhecido",
|
||||
"UP": "ATIVO",
|
||||
"Upcoming": "Por vir",
|
||||
"Updates": "Updates",
|
||||
"Update Incident": "Atualizar incidente",
|
||||
"Update Maintenance": "Atualizar manutenção",
|
||||
"Updates": "Atualizações",
|
||||
"Updates (%count)": "Atualizações (%count)",
|
||||
"Uptime": "Tempo de atividade",
|
||||
"Uptime Badge": "Selo de tempo de atividade",
|
||||
|
||||
+31
-9
@@ -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,32 +58,37 @@
|
||||
"Light": "Светлая",
|
||||
"Live Status": "Статус в реальном времени",
|
||||
"Loading your preferences...": "Загрузка настроек...",
|
||||
"maintenance": "Обслуживание",
|
||||
"MAINTENANCE": "ОБСЛУЖИВАНИЕ",
|
||||
"Maintenance Updates": "Обновления обслуживания",
|
||||
"Maintenances": "Обслуживание",
|
||||
"Major System Outage": "Крупный сбой системы",
|
||||
"Manage Site": "Управление сайтом",
|
||||
"Manage your notification preferences.": "Управляйте настройками уведомлений.",
|
||||
"Max Latency": "Max Latency",
|
||||
"Maximum Latency": "Max Latency",
|
||||
"Min Latency": "Min Latency",
|
||||
"Minimum Latency": "Min Latency",
|
||||
"Minute-by-minute status data for this day": "Поминутные данные о статусе за этот день",
|
||||
"MONITORING": "MONITORING",
|
||||
"Network error. Please try again.": "Ошибка сети. Пожалуйста, попробуйте снова.",
|
||||
"No Events in %currentMonth": "Нет событий в %currentMonth",
|
||||
"No events to show": "No events to show",
|
||||
"No events to show": "Нет событий для отображения",
|
||||
"No incidents for this day": "Нет инцидентов за этот день",
|
||||
"No latency data available for this day": "Нет данных о задержке за этот день",
|
||||
"No maintenances for this day": "Нет обслуживания за этот день",
|
||||
"No monitors affected": "Мониторы не затронуты",
|
||||
"No monitors available.": "No monitors available.",
|
||||
"No monitors available.": "Нет доступных мониторов.",
|
||||
"No ongoing maintenances": "Нет текущих обслуживаний",
|
||||
"No past maintenances": "Нет прошедших обслуживаний",
|
||||
"No Status Available": "Статус недоступен",
|
||||
"No upcoming maintenances": "Нет предстоящих обслуживаний",
|
||||
"No Updates": "No Updates",
|
||||
"No Updates": "Нет обновлений",
|
||||
"No updates yet": "Обновлений пока нет",
|
||||
"Notifications": "Notifications",
|
||||
"Notifications": "Уведомления",
|
||||
"One-time": "Одноразовое",
|
||||
"Ongoing": "Текущие",
|
||||
"Operational": "Работает",
|
||||
"Partial Degraded Performance": "Частичное снижение производительности",
|
||||
"Partial System Outage": "Частичный сбой системы",
|
||||
"Past": "Прошедшие",
|
||||
@@ -83,10 +98,14 @@
|
||||
"Please enter the 6-digit verification code": "Пожалуйста, введите 6-значный код подтверждения",
|
||||
"Read less": "Читать меньше",
|
||||
"Read more": "Читать далее",
|
||||
"Recurring": "Recurring",
|
||||
"READY": "ГОТОВО",
|
||||
"Recurring": "Повторяющийся",
|
||||
"RESOLVED": "RESOLVED",
|
||||
"SCHEDULED": "ЗАПЛАНИРОВАНО",
|
||||
"Scheduled Events (%count)": "Запланированные события (%count)",
|
||||
"Script": "Скрипт",
|
||||
"Select Language": "Выберите язык",
|
||||
"Select latency metric to display": "Select latency metric to display",
|
||||
"Select latency metric to display": "Выберите метрику задержки для отображения",
|
||||
"Select Range": "Выберите диапазон",
|
||||
"Sending...": "Отправка...",
|
||||
"Standard": "Стандартный",
|
||||
@@ -98,13 +117,16 @@
|
||||
"Subscribe": "Подписаться",
|
||||
"Subscribe to Updates": "Подписаться на обновления",
|
||||
"There are no incidents or maintenances scheduled for this month.": "На этот месяц не запланировано инцидентов или обслуживания.",
|
||||
"There are no ongoing incidents or maintenance events.": "There are no ongoing incidents or maintenance events.",
|
||||
"There are no ongoing incidents or maintenance events.": "Нет текущих инцидентов или работ по обслуживанию.",
|
||||
"Total Incidents": "Total Incidents",
|
||||
"Total Maintenances": "Total Maintenances",
|
||||
"Under Maintenance": "На обслуживании",
|
||||
"Unknown impact": "Неизвестное влияние",
|
||||
"UP": "РАБОТАЕТ",
|
||||
"Upcoming": "Предстоящие",
|
||||
"Updates": "Updates",
|
||||
"Update Incident": "Обновить инцидент",
|
||||
"Update Maintenance": "Обновить обслуживание",
|
||||
"Updates": "Обновления",
|
||||
"Updates (%count)": "Обновления (%count)",
|
||||
"Uptime": "Время работы",
|
||||
"Uptime Badge": "Значок времени работы",
|
||||
|
||||
+113
-71
@@ -2,114 +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",
|
||||
"No events to show": "No events to show",
|
||||
"No incidents for this day": "Žiadne incidenty pre tento deň",
|
||||
"No latency data available for this day": "Pre tento deň nie sú k dispozícii žiadne údaje o latencii",
|
||||
"No maintenances for this day": "Na tento deň nie sú žiadne údržby",
|
||||
"No monitors affected": "Nie sú ovplyvnené žiadne monitory",
|
||||
"No monitors available.": "No monitors available.",
|
||||
"No ongoing maintenances": "Žiadna priebežná údržba",
|
||||
"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": "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": "No Updates",
|
||||
"No Updates": "Žiadne aktualizácie",
|
||||
"No updates yet": "Zatiaľ žiadne aktualizácie",
|
||||
"Notifications": "Notifications",
|
||||
"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",
|
||||
"Recurring": "Recurring",
|
||||
"Read less": "Zobraziť menej",
|
||||
"Read more": "Zobraziť viac",
|
||||
"READY": "PRIPRAVENÉ",
|
||||
"Recent Incidents": "Nedávne incidenty",
|
||||
"Recurring": "Opakujúce sa",
|
||||
"RESOLVED": "VYRIEŠENÉ",
|
||||
"SCHEDULED": "NAPLÁNOVANÉ",
|
||||
"Scheduled Events (%count)": "Plánované udalosti (%count)",
|
||||
"Scheduled Windows": "Naplánované úlohy",
|
||||
"Script": "Skript",
|
||||
"Select Language": "Vyberte Jazyk",
|
||||
"Select latency metric to display": "Select latency metric to display",
|
||||
"Select Range": "Vyberte položku Rozsah",
|
||||
"Sending...": "Odosiela sa...",
|
||||
"Standard": "Štandardné",
|
||||
"Start Time": "Čas začiatku",
|
||||
"Select Language": "Vyberte jazyk",
|
||||
"Select latency metric to display": "Vyberte metriku latencie",
|
||||
"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.": "There are no ongoing incidents or maintenance events.",
|
||||
"Total Incidents": "Celkový počet incidentov",
|
||||
"Total Maintenances": "Celková údržba",
|
||||
"Under Maintenance": "V časti Údržba",
|
||||
"Unknown impact": "Neznámy vplyv",
|
||||
"Subscribe 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",
|
||||
"Updates": "Updates",
|
||||
"Upcoming Maintenances": "Nadchádzajúce údržby",
|
||||
"Update Incident": "Aktualizovať incident",
|
||||
"Update Maintenance": "Aktualizovať údržbu",
|
||||
"Updates": "Aktualizácie",
|
||||
"Updates (%count)": "Aktualizácie (%count)",
|
||||
"Uptime": "Uptime",
|
||||
"Uptime": "Dostupnosť",
|
||||
"Uptime Badge": "Odznak dostupnosti",
|
||||
"Verification failed": "Overenie zlyhalo",
|
||||
"Verify": "Overiť",
|
||||
"Verifying": "Overuje sa",
|
||||
"We sent a 6-digit code to": "Poslali sme 6-miestny kód na"
|
||||
"Verifying": "Overovanie",
|
||||
"We sent a 6-digit code to": "Odoslali sme 6-miestny kód na"
|
||||
}
|
||||
}
|
||||
|
||||
+31
-9
@@ -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,32 +57,37 @@
|
||||
"Light": "Işık",
|
||||
"Live Status": "Canlı Durum",
|
||||
"Loading your preferences...": "Tercihleriniz yükleniyor...",
|
||||
"maintenance": "Bakım",
|
||||
"MAINTENANCE": "BAKIM",
|
||||
"Maintenance Updates": "Bakım Güncellemeleri",
|
||||
"Maintenances": "Bakımlar",
|
||||
"Major System Outage": "Büyük sistem kesintisi",
|
||||
"Manage Site": "Siteyi yönet",
|
||||
"Manage your notification preferences.": "Bildirim tercihlerinizi yönetin.",
|
||||
"Max Latency": "Maksimum Gecikme",
|
||||
"Maximum Latency": "Maksimum Gecikme",
|
||||
"Min Latency": "Minimum Gecikme",
|
||||
"Minimum Latency": "Minimum Gecikme",
|
||||
"Minute-by-minute status data for this day": "Bu günün dakika dakika durum verileri",
|
||||
"MONITORING": "MONITORING",
|
||||
"Network error. Please try again.": "Ağ hatası. ",
|
||||
"No Events in %currentMonth": "%currentMonth Bölgesinde Etkinlik Yok",
|
||||
"No events to show": "No events to show",
|
||||
"No events to show": "Gösterilecek olay yok",
|
||||
"No incidents for this day": "Bu gün için herhangi bir olay yok",
|
||||
"No latency data available for this day": "Bu güne ait gecikme verisi yok",
|
||||
"No maintenances for this day": "Bu gün için bakım yok",
|
||||
"No monitors affected": "Hiçbir monitör etkilenmedi",
|
||||
"No monitors available.": "No monitors available.",
|
||||
"No monitors available.": "Kullanılabilir monitör yok.",
|
||||
"No ongoing maintenances": "Devam eden bakım yok",
|
||||
"No past maintenances": "Geçmiş bakım yok",
|
||||
"No Status Available": "Durum bilgisi yok",
|
||||
"No upcoming maintenances": "Yaklaşan bakım yok",
|
||||
"No Updates": "No Updates",
|
||||
"No Updates": "Güncelleme yok",
|
||||
"No updates yet": "Henüz güncelleme yok",
|
||||
"Notifications": "Notifications",
|
||||
"Notifications": "Bildirimler",
|
||||
"One-time": "Bir kerelik",
|
||||
"Ongoing": "devam ediyor",
|
||||
"Operational": "Çalışıyor",
|
||||
"Partial Degraded Performance": "Kısmi düşük performans",
|
||||
"Partial System Outage": "Kısmi sistem kesintisi",
|
||||
"Past": "Geçmiş",
|
||||
@@ -82,10 +97,14 @@
|
||||
"Please enter the 6-digit verification code": "Lütfen 6 haneli doğrulama kodunu girin",
|
||||
"Read less": "Daha az oku",
|
||||
"Read more": "Devamını oku",
|
||||
"Recurring": "Recurring",
|
||||
"READY": "HAZIR",
|
||||
"Recurring": "Tekrarlayan",
|
||||
"RESOLVED": "RESOLVED",
|
||||
"SCHEDULED": "PLANLANMIŞ",
|
||||
"Scheduled Events (%count)": "Planlanan etkinlikler (%count)",
|
||||
"Script": "Senaryo",
|
||||
"Select Language": "Dil Seçiniz",
|
||||
"Select latency metric to display": "Select latency metric to display",
|
||||
"Select latency metric to display": "Gösterilecek gecikme metriğini seçin",
|
||||
"Select Range": "Aralığı Seçin",
|
||||
"Sending...": "Gönderiliyor...",
|
||||
"Standard": "Standart",
|
||||
@@ -97,13 +116,16 @@
|
||||
"Subscribe": "Abone",
|
||||
"Subscribe to Updates": "Güncellemelere abone olun",
|
||||
"There are no incidents or maintenances scheduled for this month.": "Bu ay için planlanmış herhangi bir olay veya bakım bulunmamaktadır.",
|
||||
"There are no ongoing incidents or maintenance events.": "There are no ongoing incidents or maintenance events.",
|
||||
"There are no ongoing incidents or maintenance events.": "Devam eden olay veya bakım etkinliği yok.",
|
||||
"Total Incidents": "Toplam Olaylar",
|
||||
"Total Maintenances": "Toplam Bakımlar",
|
||||
"Under Maintenance": "Bakımda",
|
||||
"Unknown impact": "Bilinmeyen etki",
|
||||
"UP": "ÇALIŞIYOR",
|
||||
"Upcoming": "Yaklaşan",
|
||||
"Updates": "Updates",
|
||||
"Update Incident": "Olayı güncelle",
|
||||
"Update Maintenance": "Bakımı güncelle",
|
||||
"Updates": "Güncellemeler",
|
||||
"Updates (%count)": "Güncellemeler (%count)",
|
||||
"Uptime": "Çalışma süresi",
|
||||
"Uptime Badge": "Çalışma Süresi Rozeti",
|
||||
|
||||
+31
-9
@@ -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,32 +57,37 @@
|
||||
"Light": "Ánh sáng",
|
||||
"Live Status": "Trạng thái trực tiếp",
|
||||
"Loading your preferences...": "Đang tải tùy chọn của bạn...",
|
||||
"maintenance": "Bảo trì",
|
||||
"MAINTENANCE": "BẢO TRÌ",
|
||||
"Maintenance Updates": "Cập nhật bảo trì",
|
||||
"Maintenances": "Bảo trì",
|
||||
"Major System Outage": "Sự cố hệ thống nghiêm trọng",
|
||||
"Manage Site": "Quản lý trang",
|
||||
"Manage your notification preferences.": "Quản lý tùy chọn thông báo của bạn.",
|
||||
"Max Latency": "Độ trễ tối đa",
|
||||
"Maximum Latency": "Độ trễ tối đa",
|
||||
"Min Latency": "Độ trễ tối thiểu",
|
||||
"Minimum Latency": "Độ trễ tối thiểu",
|
||||
"Minute-by-minute status data for this day": "Dữ liệu trạng thái từng phút cho ngày này",
|
||||
"MONITORING": "MONITORING",
|
||||
"Network error. Please try again.": "Lỗi mạng. ",
|
||||
"No Events in %currentMonth": "Không có sự kiện nào trong %currentMonth",
|
||||
"No events to show": "No events to show",
|
||||
"No events to show": "Không có sự kiện nào để hiển thị",
|
||||
"No incidents for this day": "Không có sự cố nào trong ngày này",
|
||||
"No latency data available for this day": "Không có dữ liệu về độ trễ cho ngày này",
|
||||
"No maintenances for this day": "Không bảo trì trong ngày này",
|
||||
"No monitors affected": "Không có màn hình nào bị ảnh hưởng",
|
||||
"No monitors available.": "No monitors available.",
|
||||
"No monitors available.": "Không có trình giám sát nào khả dụng.",
|
||||
"No ongoing maintenances": "Không có bảo trì liên tục",
|
||||
"No past maintenances": "Không có bảo trì trước đây",
|
||||
"No Status Available": "Không có trạng thái khả dụng",
|
||||
"No upcoming maintenances": "Không có bảo trì sắp tới",
|
||||
"No Updates": "No Updates",
|
||||
"No Updates": "Không có cập nhật",
|
||||
"No updates yet": "Chưa có cập nhật nào",
|
||||
"Notifications": "Notifications",
|
||||
"Notifications": "Thông báo",
|
||||
"One-time": "Một lần",
|
||||
"Ongoing": "Đang thực hiện",
|
||||
"Operational": "Hoạt động",
|
||||
"Partial Degraded Performance": "Hiệu suất suy giảm một phần",
|
||||
"Partial System Outage": "Sự cố hệ thống một phần",
|
||||
"Past": "Quá khứ",
|
||||
@@ -82,10 +97,14 @@
|
||||
"Please enter the 6-digit verification code": "Vui lòng nhập mã xác minh gồm 6 chữ số",
|
||||
"Read less": "Đọc ít hơn",
|
||||
"Read more": "Đọc thêm",
|
||||
"Recurring": "Recurring",
|
||||
"READY": "SẴN SÀNG",
|
||||
"Recurring": "Định kỳ",
|
||||
"RESOLVED": "RESOLVED",
|
||||
"SCHEDULED": "ĐÃ LÊN LỊCH",
|
||||
"Scheduled Events (%count)": "Sự kiện đã lên lịch (%count)",
|
||||
"Script": "Kịch bản",
|
||||
"Select Language": "Chọn ngôn ngữ",
|
||||
"Select latency metric to display": "Select latency metric to display",
|
||||
"Select latency metric to display": "Chọn chỉ số độ trễ để hiển thị",
|
||||
"Select Range": "Chọn phạm vi",
|
||||
"Sending...": "Đang gửi...",
|
||||
"Standard": "Tiêu chuẩn",
|
||||
@@ -97,13 +116,16 @@
|
||||
"Subscribe": "Đặt mua",
|
||||
"Subscribe to Updates": "Đăng ký cập nhật",
|
||||
"There are no incidents or maintenances scheduled for this month.": "Không có sự cố hoặc bảo trì nào được lên kế hoạch trong tháng này.",
|
||||
"There are no ongoing incidents or maintenance events.": "There are no ongoing incidents or maintenance events.",
|
||||
"There are no ongoing incidents or maintenance events.": "Không có sự cố hoặc sự kiện bảo trì nào đang diễn ra.",
|
||||
"Total Incidents": "Tổng số sự cố",
|
||||
"Total Maintenances": "Tổng số lần bảo trì",
|
||||
"Under Maintenance": "Đang bảo trì",
|
||||
"Unknown impact": "Tác động không xác định",
|
||||
"UP": "HOẠT ĐỘNG",
|
||||
"Upcoming": "Sắp tới",
|
||||
"Updates": "Updates",
|
||||
"Update Incident": "Cập nhật sự cố",
|
||||
"Update Maintenance": "Cập nhật bảo trì",
|
||||
"Updates": "Cập nhật",
|
||||
"Updates (%count)": "Cập nhật (%count)",
|
||||
"Uptime": "Thời gian hoạt động",
|
||||
"Uptime Badge": "Huy hiệu thời gian hoạt động",
|
||||
|
||||
@@ -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,32 +57,37 @@
|
||||
"Light": "光",
|
||||
"Live Status": "实时状态",
|
||||
"Loading your preferences...": "正在加载您的偏好设置...",
|
||||
"maintenance": "维护",
|
||||
"MAINTENANCE": "维护中",
|
||||
"Maintenance Updates": "维护更新",
|
||||
"Maintenances": "维护保养",
|
||||
"Major System Outage": "重大系统故障",
|
||||
"Manage Site": "管理站点",
|
||||
"Manage your notification preferences.": "管理您的通知首选项。",
|
||||
"Max Latency": "最大延迟",
|
||||
"Maximum Latency": "最大延迟",
|
||||
"Min Latency": "最短延迟",
|
||||
"Minimum Latency": "最短延迟",
|
||||
"Minute-by-minute status data for this day": "当日每分钟的状态数据",
|
||||
"MONITORING": "MONITORING",
|
||||
"Network error. Please try again.": "网络错误。",
|
||||
"No Events in %currentMonth": "%currentMonth 没有活动",
|
||||
"No events to show": "No events to show",
|
||||
"No events to show": "没有可显示的事件",
|
||||
"No incidents for this day": "这一天没有发生任何事件",
|
||||
"No latency data available for this day": "当天没有可用的延迟数据",
|
||||
"No maintenances for this day": "今日无维护",
|
||||
"No monitors affected": "没有显示器受到影响",
|
||||
"No monitors available.": "No monitors available.",
|
||||
"No monitors available.": "没有可用的监控项。",
|
||||
"No ongoing maintenances": "无需持续维护",
|
||||
"No past maintenances": "过去没有维护过",
|
||||
"No Status Available": "无可用状态",
|
||||
"No upcoming maintenances": "没有即将进行的维护",
|
||||
"No Updates": "No Updates",
|
||||
"No Updates": "没有更新",
|
||||
"No updates yet": "还没有更新",
|
||||
"Notifications": "Notifications",
|
||||
"Notifications": "通知",
|
||||
"One-time": "一度",
|
||||
"Ongoing": "进行中",
|
||||
"Operational": "正常运行",
|
||||
"Partial Degraded Performance": "部分性能下降",
|
||||
"Partial System Outage": "部分系统故障.",
|
||||
"Past": "过去的",
|
||||
@@ -82,10 +97,14 @@
|
||||
"Please enter the 6-digit verification code": "请输入6位验证码",
|
||||
"Read less": "少读书",
|
||||
"Read more": "阅读更多",
|
||||
"Recurring": "Recurring",
|
||||
"READY": "就绪",
|
||||
"Recurring": "周期性",
|
||||
"RESOLVED": "RESOLVED",
|
||||
"SCHEDULED": "已安排",
|
||||
"Scheduled Events (%count)": "计划事件 (%count)",
|
||||
"Script": "脚本",
|
||||
"Select Language": "选择语言",
|
||||
"Select latency metric to display": "Select latency metric to display",
|
||||
"Select latency metric to display": "选择要显示的延迟指标",
|
||||
"Select Range": "选择范围",
|
||||
"Sending...": "正在发送...",
|
||||
"Standard": "标准",
|
||||
@@ -97,13 +116,16 @@
|
||||
"Subscribe": "订阅",
|
||||
"Subscribe to Updates": "订阅更新",
|
||||
"There are no incidents or maintenances scheduled for this month.": "本月没有安排任何事故或维护。",
|
||||
"There are no ongoing incidents or maintenance events.": "There are no ongoing incidents or maintenance events.",
|
||||
"There are no ongoing incidents or maintenance events.": "当前没有正在进行的事件或维护活动。",
|
||||
"Total Incidents": "事故总数",
|
||||
"Total Maintenances": "全面维护",
|
||||
"Under Maintenance": "维护中",
|
||||
"Unknown impact": "未知影响",
|
||||
"UP": "正常",
|
||||
"Upcoming": "即将推出",
|
||||
"Updates": "Updates",
|
||||
"Update Incident": "更新事件",
|
||||
"Update Maintenance": "更新维护",
|
||||
"Updates": "更新",
|
||||
"Updates (%count)": "更新 (%count)",
|
||||
"Uptime": "正常运行时间",
|
||||
"Uptime Badge": "正常运行时间徽章",
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user