mirror of
https://github.com/rajnandan1/kener.git
synced 2026-08-07 15:27:13 +00:00
Compare commits
47 Commits
chore/docs-4
...
v4.0.10
| Author | SHA1 | Date | |
|---|---|---|---|
| 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,195 @@
|
||||
---
|
||||
name: code-context
|
||||
description: Persistent code architecture documentation via a `.codecontext/` folder.
|
||||
user-invokable: false
|
||||
metadata:
|
||||
category: architecture
|
||||
---
|
||||
|
||||
# Code Architecture Documentation Skill
|
||||
|
||||
Use this skill to **read architecture docs before work** and **document architecture after work** using the `.codecontext/` folder.
|
||||
|
||||
`.codecontext/` is a living architecture reference — it helps new developers onboard and coding agents pick up where previous sessions left off. It is **NOT** a session log, changelog, or task diary.
|
||||
|
||||
---
|
||||
|
||||
## What Belongs in `.codecontext/`
|
||||
|
||||
Only document **architecture-level knowledge** that would take significant effort to rediscover by reading code alone.
|
||||
|
||||
### Include
|
||||
|
||||
- **Code architecture** — how modules/components are structured, layered, and why
|
||||
- **Code flow** — request lifecycle, data flow between layers, event/cron pipelines
|
||||
- **Component relationships** — which modules depend on each other, call chains, shared state
|
||||
- **Edge cases and gotchas** — non-obvious behaviors, race conditions, ordering constraints
|
||||
- **Design decisions and rationale** — why a pattern was chosen over alternatives
|
||||
- **Integration points** — how external services, databases, queues connect
|
||||
- **Invariants and constraints** — rules that must hold (e.g., "timestamps are always UTC seconds", "all DB access goes through db singleton")
|
||||
- **Error handling patterns** — how errors propagate, retry logic, fallback behavior
|
||||
- **Key file map** — which files own which responsibilities (only when non-obvious)
|
||||
|
||||
### Exclude
|
||||
|
||||
- Session logs, changelogs, or diary-style entries
|
||||
- What files were changed in a specific task
|
||||
- Raw terminal output or build logs
|
||||
- Code snippets (reference file paths + line ranges instead)
|
||||
- Obvious facts that can be inferred from reading one file
|
||||
- Task status, TODO lists, or progress tracking
|
||||
- Anything already covered in README, AGENTS.md, or inline comments
|
||||
|
||||
---
|
||||
|
||||
## Trigger Conditions
|
||||
|
||||
Run this skill at the **start and end** of any coding task that touches architecture:
|
||||
|
||||
- Feature implementations spanning multiple files/modules
|
||||
- Refactors that change module boundaries or data flow
|
||||
- Bug fixes that reveal non-obvious system behavior
|
||||
- New integrations or service connections
|
||||
- Discovery of undocumented edge cases or invariants
|
||||
|
||||
**Skip** for trivial changes (typo fixes, single-line edits, style-only changes).
|
||||
|
||||
---
|
||||
|
||||
## Phase A — Read Architecture Docs (Before Acting)
|
||||
|
||||
### A1) Discover docs
|
||||
|
||||
```bash
|
||||
ls .codecontext/
|
||||
```
|
||||
|
||||
If `.codecontext/` does not exist, continue the task and create it in Phase B.
|
||||
|
||||
### A2) Find relevant docs
|
||||
|
||||
```bash
|
||||
grep -ril "<domain keyword>" .codecontext/
|
||||
```
|
||||
|
||||
Use keywords from the feature area you are working on (e.g., "alerting", "auth", "monitors", "cron").
|
||||
|
||||
### A3) Read and apply
|
||||
|
||||
Read only relevant files. Extract:
|
||||
|
||||
- Architecture constraints that affect your implementation
|
||||
- Code flow you need to hook into or extend
|
||||
- Edge cases to preserve or handle
|
||||
- Integration points to respect
|
||||
|
||||
If existing docs conflict with current code, trust the code — update docs in Phase B.
|
||||
|
||||
---
|
||||
|
||||
## Phase B — Document Architecture (Before Ending)
|
||||
|
||||
Only write/update docs if the task revealed architecture knowledge worth preserving.
|
||||
|
||||
### B1) Decide what to document
|
||||
|
||||
Ask: _"Would a new developer or future agent need to re-discover this to work in this area?"_
|
||||
|
||||
If yes, proceed. If no, skip Phase B entirely.
|
||||
|
||||
Then apply this filter to **every sentence** before writing:
|
||||
|
||||
> "Does this sentence describe how the code is structured, a design decision, or a constraint that would change how someone writes future code in this area?"
|
||||
|
||||
If no → cut it. This is the line between architecture documentation and a session diary.
|
||||
|
||||
### B2) Write architecture documentation
|
||||
|
||||
Structure each doc as a **reference document**, not a session diary.
|
||||
|
||||
Template (use only the sections that apply):
|
||||
|
||||
```markdown
|
||||
# <Domain/Feature Area>
|
||||
|
||||
## Overview
|
||||
|
||||
Brief description of what this area does and its role in the system.
|
||||
|
||||
## Architecture
|
||||
|
||||
How the components are structured, key abstractions, layers.
|
||||
|
||||
## Code Flow
|
||||
|
||||
Step-by-step flow for the primary operations (e.g., "How a monitor check executes").
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Responsibility |
|
||||
| -------------------- | -------------- |
|
||||
| `src/lib/server/...` | Does X |
|
||||
|
||||
## Edge Cases and Gotchas
|
||||
|
||||
- Non-obvious behavior 1
|
||||
- Constraint that must be preserved
|
||||
|
||||
## Design Decisions
|
||||
|
||||
- Why X was chosen over Y (if non-obvious)
|
||||
```
|
||||
|
||||
Not all sections are required — include only what is relevant. Keep each doc under **300 lines**.
|
||||
|
||||
### B3) Pick target file
|
||||
|
||||
```bash
|
||||
ls .codecontext/
|
||||
grep -ril "<topic keyword>" .codecontext/
|
||||
```
|
||||
|
||||
| Condition | Action |
|
||||
| ------------------------------- | -------------------------------- |
|
||||
| Existing doc covers this domain | Update/rewrite relevant sections |
|
||||
| Different domain | Create new file |
|
||||
| No match | Create new file |
|
||||
|
||||
When updating, **replace outdated sections** rather than appending session entries. The doc should always read as a clean, current architecture reference.
|
||||
|
||||
### B4) Persist
|
||||
|
||||
```bash
|
||||
mkdir -p .codecontext
|
||||
```
|
||||
|
||||
Create or overwrite the file so it reads as a standalone reference:
|
||||
|
||||
```bash
|
||||
cat > .codecontext/<domain>.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Naming Rules
|
||||
|
||||
- Name by domain/feature area: `alerting.md`, `auth.md`, `monitor-execution.md`, `incident-lifecycle.md`
|
||||
- Use kebab-case for multi-word topics
|
||||
- Never use generic names: `notes.md`, `misc.md`, `context.md`, `session-1.md`
|
||||
- One file per bounded domain — split if a file exceeds ~300 lines
|
||||
|
||||
---
|
||||
|
||||
## Fast Checklist
|
||||
|
||||
Before coding:
|
||||
|
||||
- [ ] Checked `.codecontext/` for relevant architecture docs
|
||||
- [ ] Applied constraints and patterns from existing docs
|
||||
|
||||
Before finishing:
|
||||
|
||||
- [ ] Every sentence passed the B1 architecture filter
|
||||
- [ ] Documented any new architecture knowledge discovered
|
||||
- [ ] Updated outdated docs if current code contradicts them
|
||||
- [ ] Doc reads as a clean architecture reference, not a session log
|
||||
@@ -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)
|
||||
|
||||
@@ -119,4 +119,22 @@ Always use `import type { ... }` when importing types to avoid accidental runtim
|
||||
|
||||
# Other skills
|
||||
|
||||
Read files in .claude/skills for more instructions on specific tasks or file types.
|
||||
Read files in .claude/skills for more instructions on specific tasks or file types.
|
||||
|
||||
## Code Architecture Documentation (MUST)
|
||||
|
||||
For every coding task that touches architecture (multi-file features, refactors, new integrations):
|
||||
|
||||
1. **Before edits**
|
||||
- Read and apply `.claude/skills/code-context/SKILL.md`.
|
||||
- Load relevant architecture docs from `.codecontext/` when present.
|
||||
|
||||
2. **Before finishing the response**
|
||||
- If the task revealed new architecture knowledge (code flow, edge cases, component relationships, design decisions), write/update a `.codecontext/*.md` entry as a clean reference doc.
|
||||
- Skip if the task was trivial (typo fixes, single-line edits).
|
||||
|
||||
3. **Final response contract**
|
||||
- Include a short line: `Context loaded: ...`
|
||||
- Include a short line: `Context updated: ...`
|
||||
|
||||
`.codecontext/` documents **code architecture only** — not session logs, changelogs, or task summaries.
|
||||
@@ -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
|
||||
|
||||
@@ -29,3 +29,23 @@ When the user asks to write or edit documentation, follow the skill file:
|
||||
- `.claude/skills/documentation-writer/SKILL.md`
|
||||
|
||||
This is mandatory for docs-related tasks. Prioritize short, clear, action-oriented docs and avoid bloat.
|
||||
|
||||
## Code architecture docs skill - Important for all tasks
|
||||
|
||||
Always try to use the code-context skill at the start and end of coding sessions:
|
||||
|
||||
- `.claude/skills/code-context/SKILL.md`
|
||||
|
||||
## Code architecture enforcement (mandatory)
|
||||
|
||||
The code-context skill is not optional. Agents MUST do both:
|
||||
|
||||
1. **Before coding**: load relevant architecture docs from `.codecontext/`.
|
||||
2. **Before final response**: if the task revealed new architecture knowledge (code flow, edge cases, component relationships), update or create a `.codecontext/*.md` entry. Skip if the task was trivial.
|
||||
|
||||
Required output evidence in the final response:
|
||||
|
||||
- `Context loaded:` list of `.codecontext` files read (or `none found`).
|
||||
- `Context updated:` exact `.codecontext` file path written (or `skipped — no architecture changes`).
|
||||
|
||||
The `.codecontext/` folder documents **code architecture only** — not session logs, changelogs, or task summaries.
|
||||
|
||||
@@ -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>
|
||||
@@ -173,11 +168,11 @@ PORT=3000
|
||||
|
||||
For the full quick start (including local Docker builds and dev mode), see the docs:
|
||||
|
||||
- https://kener.ing/docs/quick-start
|
||||
- https://kener.ing/docs/v4/getting-started/quick-start
|
||||
|
||||
## One Click Deployment
|
||||
|
||||
[](https://railway.com/template/spSvic?referralCode=1Pn7vs)
|
||||
[](https://railway.com/deploy/spSvic?referralCode=1Pn7vs&utm_medium=integration&utm_source=template&utm_campaign=generic)
|
||||
|
||||
## Features
|
||||
|
||||
@@ -207,10 +202,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 +211,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,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");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "kener",
|
||||
"version": "4.0.0",
|
||||
"version": "4.0.10",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "kener",
|
||||
"version": "4.0.0",
|
||||
"version": "4.0.10",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4",
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "kener",
|
||||
"version": "4.0.0",
|
||||
"version": "4.0.10",
|
||||
"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",
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -24,6 +24,18 @@ app.use(handler);
|
||||
//migrations
|
||||
async function runMigrations() {
|
||||
try {
|
||||
// Rename old .js migration entries to .ts in the knex_migrations table
|
||||
// so Knex can find the renamed files on disk
|
||||
const hasTable = await db.schema.hasTable("knex_migrations");
|
||||
if (hasTable) {
|
||||
const oldJsMigrations = await db("knex_migrations").where("name", "like", "%.js");
|
||||
for (const row of oldJsMigrations) {
|
||||
const newName = row.name.replace(/\.js$/, ".ts");
|
||||
await db("knex_migrations").where("id", row.id).update({ name: newName });
|
||||
console.log(`Renamed migration record: ${row.name} -> ${newName}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Running migrations...");
|
||||
await db.migrate.latest(); // Runs migrations to the latest state
|
||||
console.log("Migrations completed successfully!");
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<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";
|
||||
@@ -9,6 +10,7 @@
|
||||
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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
@@ -79,7 +78,9 @@
|
||||
</script>
|
||||
|
||||
<div class="theme-plus-bar scrollbar-hidden sticky top-18 z-20 flex w-full items-center gap-2 rounded py-2">
|
||||
<PageSelector />
|
||||
{#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">
|
||||
|
||||
+13
-9
@@ -58,19 +58,19 @@
|
||||
"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 events to show": "Žádné události k zobrazení",
|
||||
"No incidents for this day": "Pro tento den nejsou žádné incidenty",
|
||||
"No latency data available for this day": "Pro tento den nejsou k dispozici data latence",
|
||||
"No maintenances for this day": "Pro tento den nejsou naplánované žádné údržby",
|
||||
"No monitors affected": "Žádné zasažené monitory",
|
||||
"No monitors available.": "No monitors available.",
|
||||
"No monitors available.": "Žádné dostupné monitory.",
|
||||
"No ongoing maintenances": "Žádné probíhající údržby",
|
||||
"No past maintenances": "Žádné minulé údržby",
|
||||
"No Status Available": "Stav není k dispozici",
|
||||
"No upcoming maintenances": "Žádné nadcházející údržby",
|
||||
"No Updates": "No Updates",
|
||||
"No Updates": "Žádné aktualizace",
|
||||
"No updates yet": "Zatím bez aktualizací",
|
||||
"Notifications": "Notifications",
|
||||
"Notifications": "Oznámení",
|
||||
"One-time": "Jednorázově",
|
||||
"Ongoing": "Probíhající",
|
||||
"Partial Degraded Performance": "Částečně zhoršený výkon",
|
||||
@@ -82,10 +82,10 @@
|
||||
"Please enter the 6-digit verification code": "Zadejte prosím 6místný ověřovací kód",
|
||||
"Read less": "Zobrazit méně",
|
||||
"Read more": "Zobrazit více",
|
||||
"Recurring": "Recurring",
|
||||
"Recurring": "Opakující se",
|
||||
"Script": "Skript",
|
||||
"Select Language": "Vyberte jazyk",
|
||||
"Select latency metric to display": "Select latency metric to display",
|
||||
"Select latency metric to display": "Vyberte metriku latence k zobrazení",
|
||||
"Select Range": "Vyberte rozsah",
|
||||
"Sending...": "Odesílání...",
|
||||
"Standard": "Standardní",
|
||||
@@ -97,19 +97,23 @@
|
||||
"Subscribe": "Odebírat",
|
||||
"Subscribe to Updates": "Odebírat aktualizace",
|
||||
"There are no incidents or maintenances scheduled for this month.": "Na tento měsíc nejsou naplánované žádné incidenty ani údržby.",
|
||||
"There are no ongoing incidents or maintenance events.": "There are no ongoing incidents or maintenance events.",
|
||||
"There are no ongoing incidents or maintenance events.": "Na tento okamžik nejsou žádné probíhající incidenty ani údržby.",
|
||||
"Total Incidents": "Celkem incidentů",
|
||||
"Total Maintenances": "Celkem údržeb",
|
||||
"Under Maintenance": "Probíhá údržba",
|
||||
"Unknown impact": "Neznámý dopad",
|
||||
"Upcoming": "Nadcházející",
|
||||
"Updates": "Updates",
|
||||
"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": "Poslali jsme 6místný kód na",
|
||||
"INVESTIGATING": "VYŠETŘOVÁNÍ",
|
||||
"IDENTIFIED": "IDENTIFIKOVÁNO",
|
||||
"MONITORING": "MONITOROVÁNÍ",
|
||||
"RESOLVED": "ŘEŠENO"
|
||||
}
|
||||
}
|
||||
|
||||
+13
-9
@@ -58,19 +58,19 @@
|
||||
"Minute-by-minute status data for this day": "Minutenweise Statusdaten für diesen Tag",
|
||||
"Network error. Please try again.": "Netzwerkfehler. ",
|
||||
"No Events in %currentMonth": "Keine Ereignisse in %currentMonth",
|
||||
"No events to show": "No events to show",
|
||||
"No events to show": "Keine Ereignisse zum Anzeigen",
|
||||
"No incidents for this day": "Keine Vorfälle für diesen Tag",
|
||||
"No latency data available for this day": "Für diesen Tag sind keine Latenzdaten verfügbar",
|
||||
"No maintenances for this day": "An diesem Tag finden keine Wartungsarbeiten statt",
|
||||
"No monitors affected": "Keine Monitore betroffen",
|
||||
"No monitors available.": "No monitors available.",
|
||||
"No monitors available.": "Keine Monitore verfügbar.",
|
||||
"No ongoing maintenances": "Keine laufenden Wartungsarbeiten",
|
||||
"No past maintenances": "Keine früheren Wartungsarbeiten",
|
||||
"No Status Available": "Kein Status verfügbar",
|
||||
"No upcoming maintenances": "Keine bevorstehenden Wartungsarbeiten",
|
||||
"No Updates": "No Updates",
|
||||
"No Updates": "Keine Aktualisierungen",
|
||||
"No updates yet": "Noch keine Updates",
|
||||
"Notifications": "Notifications",
|
||||
"Notifications": "Benachrichtigungen",
|
||||
"One-time": "Einmalig",
|
||||
"Ongoing": "Laufend",
|
||||
"Partial Degraded Performance": "Teilweise beeinträchtigte Leistung",
|
||||
@@ -82,10 +82,10 @@
|
||||
"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",
|
||||
"Recurring": "Wiederkehrend",
|
||||
"Script": "Skript",
|
||||
"Select Language": "Wählen Sie Sprache aus",
|
||||
"Select latency metric to display": "Select latency metric to display",
|
||||
"Select latency metric to display": "Latenzmetrik zur Anzeige auswählen",
|
||||
"Select Range": "Wählen Sie Bereich aus",
|
||||
"Sending...": "Senden...",
|
||||
"Standard": "Standard",
|
||||
@@ -97,19 +97,23 @@
|
||||
"Subscribe": "Abonnieren",
|
||||
"Subscribe to Updates": "Abonnieren Sie Updates",
|
||||
"There are no incidents or maintenances scheduled for this month.": "Für diesen Monat sind keine Vorfälle oder Wartungsarbeiten geplant.",
|
||||
"There are no ongoing incidents or maintenance events.": "There are no ongoing incidents or maintenance events.",
|
||||
"There are no ongoing incidents or maintenance events.": "Es gibt keine laufenden Vorfälle oder Wartungsereignisse.",
|
||||
"Total Incidents": "Gesamtzahl der Vorfälle",
|
||||
"Total Maintenances": "Gesamtwartungen",
|
||||
"Under Maintenance": "Unter Wartung",
|
||||
"Unknown impact": "Unbekannte Auswirkung",
|
||||
"Upcoming": "Demnächst",
|
||||
"Updates": "Updates",
|
||||
"Updates": "Aktualisierungen",
|
||||
"Updates (%count)": "Aktualisierungen (%count)",
|
||||
"Uptime": "Betriebszeit",
|
||||
"Uptime Badge": "Verfügbarkeitsabzeichen",
|
||||
"Verification failed": "Die Überprüfung ist fehlgeschlagen",
|
||||
"Verify": "Verifizieren",
|
||||
"Verifying": "Verifizieren",
|
||||
"We sent a 6-digit code to": "Wir haben einen 6-stelligen Code an gesendet"
|
||||
"We sent a 6-digit code to": "Wir haben einen 6-stelligen Code an gesendet",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"MONITORING": "MONITORING",
|
||||
"RESOLVED": "RESOLVED"
|
||||
}
|
||||
}
|
||||
|
||||
+13
-9
@@ -58,19 +58,19 @@
|
||||
"Minute-by-minute status data for this day": "Minut for minut statusdata for denne dag",
|
||||
"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",
|
||||
"Partial Degraded Performance": "Delvist nedsat ydeevne",
|
||||
@@ -82,10 +82,10 @@
|
||||
"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",
|
||||
"Recurring": "Tilbagevendende",
|
||||
"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,19 +97,23 @@
|
||||
"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",
|
||||
"Upcoming": "Kommende",
|
||||
"Updates": "Updates",
|
||||
"Updates": "Opdateringer",
|
||||
"Updates (%count)": "Opdateringer (%count)",
|
||||
"Uptime": "Oppetid",
|
||||
"Uptime Badge": "Oppetidsmærke",
|
||||
"Verification failed": "Bekræftelse mislykkedes",
|
||||
"Verify": "Verificere",
|
||||
"Verifying": "Bekræfter",
|
||||
"We sent a 6-digit code to": "Vi sendte en 6-cifret kode til"
|
||||
"We sent a 6-digit code to": "Vi sendte en 6-cifret kode til",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"MONITORING": "MONITORING",
|
||||
"RESOLVED": "RESOLVED"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +110,10 @@
|
||||
"Verification failed": "Verification failed",
|
||||
"Verify": "Verify",
|
||||
"Verifying": "Verifying",
|
||||
"We sent a 6-digit code to": "We sent a 6-digit code to"
|
||||
"We sent a 6-digit code to": "We sent a 6-digit code to",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"MONITORING": "MONITORING",
|
||||
"RESOLVED": "RESOLVED"
|
||||
}
|
||||
}
|
||||
|
||||
+13
-9
@@ -58,19 +58,19 @@
|
||||
"Minute-by-minute status data for this day": "Datos del estado minuto a minuto de este día",
|
||||
"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",
|
||||
"Partial Degraded Performance": "Rendimiento parcialmente degradado",
|
||||
@@ -82,10 +82,10 @@
|
||||
"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",
|
||||
"Recurring": "Recurrente",
|
||||
"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,19 +97,23 @@
|
||||
"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",
|
||||
"Upcoming": "Próximo",
|
||||
"Updates": "Updates",
|
||||
"Updates": "Actualizaciones",
|
||||
"Updates (%count)": "Actualizaciones (%count)",
|
||||
"Uptime": "tiempo de actividad",
|
||||
"Uptime Badge": "Insignia de tiempo de actividad",
|
||||
"Verification failed": "La verificación falló",
|
||||
"Verify": "Verificar",
|
||||
"Verifying": "Verificando",
|
||||
"We sent a 6-digit code to": "Enviamos un código de 6 dígitos a"
|
||||
"We sent a 6-digit code to": "Enviamos un código de 6 dígitos a",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"MONITORING": "MONITORING",
|
||||
"RESOLVED": "RESOLVED"
|
||||
}
|
||||
}
|
||||
|
||||
+13
-9
@@ -58,19 +58,19 @@
|
||||
"Minute-by-minute status data for this day": "داده های وضعیت دقیقه به دقیقه برای این روز",
|
||||
"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": "در حال انجام است",
|
||||
"Partial Degraded Performance": "کاهش عملکرد جزئی",
|
||||
@@ -82,10 +82,10 @@
|
||||
"Please enter the 6-digit verification code": "لطفا کد تایید 6 رقمی را وارد کنید",
|
||||
"Read less": "کمتر بخوانید",
|
||||
"Read more": "ادامه مطلب",
|
||||
"Recurring": "Recurring",
|
||||
"Recurring": "دورهای",
|
||||
"Script": "اسکریپت",
|
||||
"Select Language": "زبان را انتخاب کنید",
|
||||
"Select latency metric to display": "Select latency metric to display",
|
||||
"Select latency metric to display": "معیار تأخیر برای نمایش را انتخاب کنید",
|
||||
"Select Range": "Range را انتخاب کنید",
|
||||
"Sending...": "ارسال...",
|
||||
"Standard": "استاندارد",
|
||||
@@ -97,19 +97,23 @@
|
||||
"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": "تاثیر نامعلوم",
|
||||
"Upcoming": "آینده",
|
||||
"Updates": "Updates",
|
||||
"Updates": "بهروزرسانیها",
|
||||
"Updates (%count)": "بهروزرسانیها (%count)",
|
||||
"Uptime": "آپتایم",
|
||||
"Uptime Badge": "نشان Uptime",
|
||||
"Verification failed": "تأیید ناموفق بود",
|
||||
"Verify": "تأیید کنید",
|
||||
"Verifying": "در حال تأیید",
|
||||
"We sent a 6-digit code to": "ما یک کد 6 رقمی ارسال کردیم"
|
||||
"We sent a 6-digit code to": "ما یک کد 6 رقمی ارسال کردیم",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"MONITORING": "MONITORING",
|
||||
"RESOLVED": "RESOLVED"
|
||||
}
|
||||
}
|
||||
|
||||
+13
-9
@@ -58,19 +58,19 @@
|
||||
"Minute-by-minute status data for this day": "Données d'état minute par minute pour cette journée",
|
||||
"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",
|
||||
"Partial Degraded Performance": "Performance partiellement dégradée",
|
||||
@@ -82,10 +82,10 @@
|
||||
"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",
|
||||
"Recurring": "Récurrent",
|
||||
"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,19 +97,23 @@
|
||||
"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",
|
||||
"Upcoming": "Prochain",
|
||||
"Updates": "Updates",
|
||||
"Updates": "Mises à jour",
|
||||
"Updates (%count)": "Mises à jour (%count)",
|
||||
"Uptime": "Temps de disponibilité",
|
||||
"Uptime Badge": "Badge de disponibilité",
|
||||
"Verification failed": "La vérification a échoué",
|
||||
"Verify": "Vérifier",
|
||||
"Verifying": "Vérification",
|
||||
"We sent a 6-digit code to": "Nous avons envoyé un code à 6 chiffres à"
|
||||
"We sent a 6-digit code to": "Nous avons envoyé un code à 6 chiffres à",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"MONITORING": "MONITORING",
|
||||
"RESOLVED": "RESOLVED"
|
||||
}
|
||||
}
|
||||
|
||||
+13
-9
@@ -58,19 +58,19 @@
|
||||
"Minute-by-minute status data for this day": "इस दिन के लिए मिनट-दर-मिनट स्थिति डेटा",
|
||||
"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": "चल रहा है",
|
||||
"Partial Degraded Performance": "आंशिक प्रदर्शन गिरावट",
|
||||
@@ -82,10 +82,10 @@
|
||||
"Please enter the 6-digit verification code": "कृपया 6 अंकों का सत्यापन कोड दर्ज करें",
|
||||
"Read less": "कम पढ़ें",
|
||||
"Read more": "और पढ़ें",
|
||||
"Recurring": "Recurring",
|
||||
"Recurring": "आवर्ती",
|
||||
"Script": "स्क्रिप्ट",
|
||||
"Select Language": "भाषा चुनें",
|
||||
"Select latency metric to display": "Select latency metric to display",
|
||||
"Select latency metric to display": "दिखाने के लिए विलंबता मेट्रिक चुनें",
|
||||
"Select Range": "रेंज चुनें",
|
||||
"Sending...": "भेजा जा रहा है...",
|
||||
"Standard": "मानक",
|
||||
@@ -97,19 +97,23 @@
|
||||
"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": "अज्ञात प्रभाव",
|
||||
"Upcoming": "आने वाला",
|
||||
"Updates": "Updates",
|
||||
"Updates": "अपडेट्स",
|
||||
"Updates (%count)": "अपडेट (%count)",
|
||||
"Uptime": "अपटाइम",
|
||||
"Uptime Badge": "अपटाइम बैज",
|
||||
"Verification failed": "सत्यापन विफल रहा",
|
||||
"Verify": "Verify",
|
||||
"Verifying": "Verifying",
|
||||
"We sent a 6-digit code to": "हमने 6 अंकों का कोड भेजा है"
|
||||
"We sent a 6-digit code to": "हमने 6 अंकों का कोड भेजा है",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"MONITORING": "MONITORING",
|
||||
"RESOLVED": "RESOLVED"
|
||||
}
|
||||
}
|
||||
|
||||
+13
-9
@@ -58,19 +58,19 @@
|
||||
"Minute-by-minute status data for this day": "Dati sullo stato minuto per minuto per questo giorno",
|
||||
"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",
|
||||
"Partial Degraded Performance": "Prestazioni parzialmente degradate",
|
||||
@@ -82,10 +82,10 @@
|
||||
"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",
|
||||
"Recurring": "Ricorrente",
|
||||
"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,19 +97,23 @@
|
||||
"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",
|
||||
"Upcoming": "Prossimamente",
|
||||
"Updates": "Updates",
|
||||
"Updates": "Aggiornamenti",
|
||||
"Updates (%count)": "Aggiornamenti (%count)",
|
||||
"Uptime": "Tempo di attività",
|
||||
"Uptime Badge": "Badge di operatività",
|
||||
"Verification failed": "Verifica non riuscita",
|
||||
"Verify": "Verificare",
|
||||
"Verifying": "Verifica",
|
||||
"We sent a 6-digit code to": "Abbiamo inviato un codice di 6 cifre a"
|
||||
"We sent a 6-digit code to": "Abbiamo inviato un codice di 6 cifre a",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"MONITORING": "MONITORING",
|
||||
"RESOLVED": "RESOLVED"
|
||||
}
|
||||
}
|
||||
|
||||
+13
-9
@@ -58,19 +58,19 @@
|
||||
"Minute-by-minute status data for this day": "この日の分単位のステータスデータ",
|
||||
"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": "進行中",
|
||||
"Partial Degraded Performance": "部分的なパフォーマンス低下",
|
||||
@@ -82,10 +82,10 @@
|
||||
"Please enter the 6-digit verification code": "6桁の認証コードを入力してください",
|
||||
"Read less": "読む量を減らす",
|
||||
"Read more": "続きを読む",
|
||||
"Recurring": "Recurring",
|
||||
"Recurring": "繰り返し",
|
||||
"Script": "スクリプト",
|
||||
"Select Language": "言語の選択",
|
||||
"Select latency metric to display": "Select latency metric to display",
|
||||
"Select latency metric to display": "表示するレイテンシ指標を選択",
|
||||
"Select Range": "範囲の選択",
|
||||
"Sending...": "送信中...",
|
||||
"Standard": "標準",
|
||||
@@ -97,19 +97,23 @@
|
||||
"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": "未知の影響",
|
||||
"Upcoming": "今後の予定",
|
||||
"Updates": "Updates",
|
||||
"Updates": "更新",
|
||||
"Updates (%count)": "アップデート (%count)",
|
||||
"Uptime": "稼働時間",
|
||||
"Uptime Badge": "稼働時間バッジ",
|
||||
"Verification failed": "検証に失敗しました",
|
||||
"Verify": "確認する",
|
||||
"Verifying": "検証中",
|
||||
"We sent a 6-digit code to": "6桁のコードを送信しました"
|
||||
"We sent a 6-digit code to": "6桁のコードを送信しました",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"MONITORING": "MONITORING",
|
||||
"RESOLVED": "RESOLVED"
|
||||
}
|
||||
}
|
||||
|
||||
+13
-9
@@ -58,19 +58,19 @@
|
||||
"Minute-by-minute status data for this day": "오늘의 분별 상태 데이터",
|
||||
"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": "전진",
|
||||
"Partial Degraded Performance": "부분적 성능 저하",
|
||||
@@ -82,10 +82,10 @@
|
||||
"Please enter the 6-digit verification code": "인증번호 6자리를 입력해주세요",
|
||||
"Read less": "덜 읽으세요",
|
||||
"Read more": "더 읽어보세요",
|
||||
"Recurring": "Recurring",
|
||||
"Recurring": "반복",
|
||||
"Script": "스크립트",
|
||||
"Select Language": "언어 선택",
|
||||
"Select latency metric to display": "Select latency metric to display",
|
||||
"Select latency metric to display": "표시할 지연 시간 지표를 선택하세요",
|
||||
"Select Range": "범위 선택",
|
||||
"Sending...": "배상...",
|
||||
"Standard": "기준",
|
||||
@@ -97,19 +97,23 @@
|
||||
"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": "알 수 없는 영향",
|
||||
"Upcoming": "예정",
|
||||
"Updates": "Updates",
|
||||
"Updates": "업데이트",
|
||||
"Updates (%count)": "업데이트(%count)",
|
||||
"Uptime": "가동 시간",
|
||||
"Uptime Badge": "가동 시간 배지",
|
||||
"Verification failed": "확인 실패",
|
||||
"Verify": "확인하다",
|
||||
"Verifying": "확인 중",
|
||||
"We sent a 6-digit code to": "6자리 코드를 다음 주소로 보냈습니다."
|
||||
"We sent a 6-digit code to": "6자리 코드를 다음 주소로 보냈습니다.",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"MONITORING": "MONITORING",
|
||||
"RESOLVED": "RESOLVED"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,19 +58,19 @@
|
||||
"Minute-by-minute status data for this day": "Statusdata fra minutt for minutt for denne dagen",
|
||||
"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",
|
||||
"Partial Degraded Performance": "Delvis redusert ytelse",
|
||||
@@ -82,10 +82,10 @@
|
||||
"Please enter the 6-digit verification code": "Vennligst skriv inn den 6-sifrede bekreftelseskoden",
|
||||
"Read less": "Les mindre",
|
||||
"Read more": "Les mer",
|
||||
"Recurring": "Recurring",
|
||||
"Recurring": "Gjentakende",
|
||||
"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,19 +97,23 @@
|
||||
"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",
|
||||
"Upcoming": "Kommende",
|
||||
"Updates": "Updates",
|
||||
"Updates": "Oppdateringer",
|
||||
"Updates (%count)": "Oppdateringer (%count)",
|
||||
"Uptime": "Oppetid",
|
||||
"Uptime Badge": "Oppetidsmerke",
|
||||
"Verification failed": "Bekreftelsen mislyktes",
|
||||
"Verify": "Verifisere",
|
||||
"Verifying": "Bekrefter",
|
||||
"We sent a 6-digit code to": "Vi sendte en 6-sifret kode til"
|
||||
"We sent a 6-digit code to": "Vi sendte en 6-sifret kode til",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"MONITORING": "MONITORING",
|
||||
"RESOLVED": "RESOLVED"
|
||||
}
|
||||
}
|
||||
|
||||
+13
-9
@@ -58,19 +58,19 @@
|
||||
"Minute-by-minute status data for this day": "Statusgegevens van minuut tot minuut voor deze dag",
|
||||
"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",
|
||||
"Partial Degraded Performance": "Gedeeltelijk verminderde prestaties",
|
||||
@@ -82,10 +82,10 @@
|
||||
"Please enter the 6-digit verification code": "Voer de 6-cijferige verificatiecode in",
|
||||
"Read less": "Lees minder",
|
||||
"Read more": "Lees meer",
|
||||
"Recurring": "Recurring",
|
||||
"Recurring": "Terugkerend",
|
||||
"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,19 +97,23 @@
|
||||
"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",
|
||||
"Upcoming": "Aankomend",
|
||||
"Updates": "Updates",
|
||||
"Updates": "Actualisaties",
|
||||
"Updates (%count)": "Updates (%count)",
|
||||
"Uptime": "Uptime",
|
||||
"Uptime Badge": "Uptime-badge",
|
||||
"Verification failed": "Verificatie mislukt",
|
||||
"Verify": "Verifiëren",
|
||||
"Verifying": "Verifiëren",
|
||||
"We sent a 6-digit code to": "We hebben een 6-cijferige code gestuurd naar"
|
||||
"We sent a 6-digit code to": "We hebben een 6-cijferige code gestuurd naar",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"MONITORING": "MONITORING",
|
||||
"RESOLVED": "RESOLVED"
|
||||
}
|
||||
}
|
||||
|
||||
+13
-9
@@ -58,19 +58,19 @@
|
||||
"Minute-by-minute status data for this day": "Dane o stanie minuta po minucie na ten dzień",
|
||||
"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",
|
||||
"Partial Degraded Performance": "Częściowo obniżona wydajność",
|
||||
@@ -82,10 +82,10 @@
|
||||
"Please enter the 6-digit verification code": "Wprowadź 6-cyfrowy kod weryfikacyjny",
|
||||
"Read less": "Czytaj mniej",
|
||||
"Read more": "Przeczytaj więcej",
|
||||
"Recurring": "Recurring",
|
||||
"Recurring": "Cykliczne",
|
||||
"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,19 +97,23 @@
|
||||
"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",
|
||||
"Upcoming": "Nadchodzące",
|
||||
"Updates": "Updates",
|
||||
"Updates": "Aktualizacje",
|
||||
"Updates (%count)": "Aktualizacje (%count)",
|
||||
"Uptime": "Czas pracy",
|
||||
"Uptime Badge": "Odznaka dostępności",
|
||||
"Verification failed": "Weryfikacja nie powiodła się",
|
||||
"Verify": "Zweryfikować",
|
||||
"Verifying": "Weryfikacja",
|
||||
"We sent a 6-digit code to": "Wysłaliśmy 6-cyfrowy kod na adres"
|
||||
"We sent a 6-digit code to": "Wysłaliśmy 6-cyfrowy kod na adres",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"MONITORING": "MONITORING",
|
||||
"RESOLVED": "RESOLVED"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,19 +58,19 @@
|
||||
"Minute-by-minute status data for this day": "Dados de status minuto a minuto para este dia",
|
||||
"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",
|
||||
"Partial Degraded Performance": "Desempenho parcialmente degradado",
|
||||
@@ -82,10 +82,10 @@
|
||||
"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",
|
||||
"Recurring": "Recorrente",
|
||||
"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,19 +97,23 @@
|
||||
"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",
|
||||
"Upcoming": "Por vir",
|
||||
"Updates": "Updates",
|
||||
"Updates": "Atualizações",
|
||||
"Updates (%count)": "Atualizações (%count)",
|
||||
"Uptime": "Tempo de atividade",
|
||||
"Uptime Badge": "Selo de tempo de atividade",
|
||||
"Verification failed": "Falha na verificação",
|
||||
"Verify": "Verificar",
|
||||
"Verifying": "Verificando",
|
||||
"We sent a 6-digit code to": "Enviamos um código de 6 dígitos para"
|
||||
"We sent a 6-digit code to": "Enviamos um código de 6 dígitos para",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"MONITORING": "MONITORING",
|
||||
"RESOLVED": "RESOLVED"
|
||||
}
|
||||
}
|
||||
|
||||
+13
-9
@@ -59,19 +59,19 @@
|
||||
"Minute-by-minute status data for this day": "Поминутные данные о статусе за этот день",
|
||||
"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": "Текущие",
|
||||
"Partial Degraded Performance": "Частичное снижение производительности",
|
||||
@@ -83,10 +83,10 @@
|
||||
"Please enter the 6-digit verification code": "Пожалуйста, введите 6-значный код подтверждения",
|
||||
"Read less": "Читать меньше",
|
||||
"Read more": "Читать далее",
|
||||
"Recurring": "Recurring",
|
||||
"Recurring": "Повторяющийся",
|
||||
"Script": "Скрипт",
|
||||
"Select Language": "Выберите язык",
|
||||
"Select latency metric to display": "Select latency metric to display",
|
||||
"Select latency metric to display": "Выберите метрику задержки для отображения",
|
||||
"Select Range": "Выберите диапазон",
|
||||
"Sending...": "Отправка...",
|
||||
"Standard": "Стандартный",
|
||||
@@ -98,19 +98,23 @@
|
||||
"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": "Неизвестное влияние",
|
||||
"Upcoming": "Предстоящие",
|
||||
"Updates": "Updates",
|
||||
"Updates": "Обновления",
|
||||
"Updates (%count)": "Обновления (%count)",
|
||||
"Uptime": "Время работы",
|
||||
"Uptime Badge": "Значок времени работы",
|
||||
"Verification failed": "Проверка не удалась",
|
||||
"Verify": "Verify",
|
||||
"Verifying": "Verifying",
|
||||
"We sent a 6-digit code to": "Мы отправили 6-значный код на"
|
||||
"We sent a 6-digit code to": "Мы отправили 6-значный код на",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"MONITORING": "MONITORING",
|
||||
"RESOLVED": "RESOLVED"
|
||||
}
|
||||
}
|
||||
|
||||
+13
-9
@@ -58,19 +58,19 @@
|
||||
"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 events to show": "Žiadne udalosti na zobrazenie",
|
||||
"No incidents for this day": "Žiadne incidenty pre tento deň",
|
||||
"No latency data available for this day": "Pre tento deň nie sú k dispozícii žiadne údaje o latencii",
|
||||
"No maintenances for this day": "Na tento deň nie sú žiadne údržby",
|
||||
"No monitors affected": "Nie sú ovplyvnené žiadne monitory",
|
||||
"No monitors available.": "No monitors available.",
|
||||
"No monitors available.": "Nie sú k dispozícii žiadne monitory.",
|
||||
"No ongoing maintenances": "Žiadna priebežná ú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",
|
||||
"Notifications": "Upozornenia",
|
||||
"One-time": "Jednorazovo",
|
||||
"Ongoing": "Prebieha",
|
||||
"Partial Degraded Performance": "Čiastočne znížený výkon",
|
||||
@@ -82,10 +82,10 @@
|
||||
"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",
|
||||
"Recurring": "Opakujúce sa",
|
||||
"Script": "Skript",
|
||||
"Select Language": "Vyberte Jazyk",
|
||||
"Select latency metric to display": "Select latency metric to display",
|
||||
"Select latency metric to display": "Vyberte metriku latencie na zobrazenie",
|
||||
"Select Range": "Vyberte položku Rozsah",
|
||||
"Sending...": "Odosiela sa...",
|
||||
"Standard": "Štandardné",
|
||||
@@ -97,19 +97,23 @@
|
||||
"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.",
|
||||
"There are no ongoing incidents or maintenance events.": "Neexistujú žiadne prebiehajúce incidenty ani udalosti údržby.",
|
||||
"Total Incidents": "Celkový počet incidentov",
|
||||
"Total Maintenances": "Celková údržba",
|
||||
"Under Maintenance": "V časti Údržba",
|
||||
"Unknown impact": "Neznámy vplyv",
|
||||
"Upcoming": "Nadchádzajúce",
|
||||
"Updates": "Updates",
|
||||
"Updates": "Aktualizácie",
|
||||
"Updates (%count)": "Aktualizácie (%count)",
|
||||
"Uptime": "Uptime",
|
||||
"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"
|
||||
"We sent a 6-digit code to": "Poslali sme 6-miestny kód na",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"MONITORING": "MONITORING",
|
||||
"RESOLVED": "RESOLVED"
|
||||
}
|
||||
}
|
||||
|
||||
+13
-9
@@ -58,19 +58,19 @@
|
||||
"Minute-by-minute status data for this day": "Bu günün dakika dakika durum verileri",
|
||||
"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",
|
||||
"Partial Degraded Performance": "Kısmi düşük performans",
|
||||
@@ -82,10 +82,10 @@
|
||||
"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",
|
||||
"Recurring": "Tekrarlayan",
|
||||
"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,19 +97,23 @@
|
||||
"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",
|
||||
"Upcoming": "Yaklaşan",
|
||||
"Updates": "Updates",
|
||||
"Updates": "Güncellemeler",
|
||||
"Updates (%count)": "Güncellemeler (%count)",
|
||||
"Uptime": "Çalışma süresi",
|
||||
"Uptime Badge": "Çalışma Süresi Rozeti",
|
||||
"Verification failed": "Doğrulama başarısız oldu",
|
||||
"Verify": "Doğrulamak",
|
||||
"Verifying": "Doğrulanıyor",
|
||||
"We sent a 6-digit code to": "6 haneli kodu gönderdik"
|
||||
"We sent a 6-digit code to": "6 haneli kodu gönderdik",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"MONITORING": "MONITORING",
|
||||
"RESOLVED": "RESOLVED"
|
||||
}
|
||||
}
|
||||
|
||||
+13
-9
@@ -58,19 +58,19 @@
|
||||
"Minute-by-minute status data for this day": "Dữ liệu trạng thái từng phút cho ngày này",
|
||||
"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",
|
||||
"Partial Degraded Performance": "Hiệu suất suy giảm một phần",
|
||||
@@ -82,10 +82,10 @@
|
||||
"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",
|
||||
"Recurring": "Định kỳ",
|
||||
"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,19 +97,23 @@
|
||||
"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",
|
||||
"Upcoming": "Sắp tới",
|
||||
"Updates": "Updates",
|
||||
"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",
|
||||
"Verification failed": "Xác minh không thành công",
|
||||
"Verify": "Xác minh",
|
||||
"Verifying": "Đang xác minh",
|
||||
"We sent a 6-digit code to": "Chúng tôi đã gửi mã gồm 6 chữ số tới"
|
||||
"We sent a 6-digit code to": "Chúng tôi đã gửi mã gồm 6 chữ số tới",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"MONITORING": "MONITORING",
|
||||
"RESOLVED": "RESOLVED"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,19 +58,19 @@
|
||||
"Minute-by-minute status data for this day": "当日每分钟的状态数据",
|
||||
"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": "进行中",
|
||||
"Partial Degraded Performance": "部分性能下降",
|
||||
@@ -82,10 +82,10 @@
|
||||
"Please enter the 6-digit verification code": "请输入6位验证码",
|
||||
"Read less": "少读书",
|
||||
"Read more": "阅读更多",
|
||||
"Recurring": "Recurring",
|
||||
"Recurring": "周期性",
|
||||
"Script": "脚本",
|
||||
"Select Language": "选择语言",
|
||||
"Select latency metric to display": "Select latency metric to display",
|
||||
"Select latency metric to display": "选择要显示的延迟指标",
|
||||
"Select Range": "选择范围",
|
||||
"Sending...": "正在发送...",
|
||||
"Standard": "标准",
|
||||
@@ -97,19 +97,23 @@
|
||||
"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": "未知影响",
|
||||
"Upcoming": "即将推出",
|
||||
"Updates": "Updates",
|
||||
"Updates": "更新",
|
||||
"Updates (%count)": "更新 (%count)",
|
||||
"Uptime": "正常运行时间",
|
||||
"Uptime Badge": "正常运行时间徽章",
|
||||
"Verification failed": "验证失败",
|
||||
"Verify": "核实",
|
||||
"Verifying": "正在验证",
|
||||
"We sent a 6-digit code to": "我们发送了一个 6 位代码至"
|
||||
"We sent a 6-digit code to": "我们发送了一个 6 位代码至",
|
||||
"INVESTIGATING": "INVESTIGATING",
|
||||
"IDENTIFIED": "IDENTIFIED",
|
||||
"MONITORING": "MONITORING",
|
||||
"RESOLVED": "RESOLVED"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,15 +1,36 @@
|
||||
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,
|
||||
}));
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -2,8 +2,15 @@ import MobileDetect from "mobile-detect";
|
||||
import type { Cookies } from "@sveltejs/kit";
|
||||
import type { UserRecordPublic } from "$lib/server/types/db";
|
||||
import seedSiteData from "$lib/server/db/seedSiteData";
|
||||
import { GetAllSiteData, GetLoggedInSession, GetLocaleFromCookie, GetUsersCount, IsEmailSetup } from "./controller.js";
|
||||
import type { EventDisplaySettings } from "$lib/types/site.js";
|
||||
import {
|
||||
GetAllSiteData,
|
||||
GetLoggedInSession,
|
||||
GetLocaleFromCookie,
|
||||
GetUsersCount,
|
||||
IsEmailSetup,
|
||||
IsSetupComplete,
|
||||
} from "./controller.js";
|
||||
import type { EventDisplaySettings, GlobalPageVisibilitySettings } from "$lib/types/site.js";
|
||||
|
||||
export interface LayoutServerData {
|
||||
isMobile: boolean;
|
||||
@@ -62,6 +69,7 @@ export interface LayoutServerData {
|
||||
eventDisplaySettings: EventDisplaySettings;
|
||||
socialPreviewImage?: string;
|
||||
customCSS?: string;
|
||||
globalPageVisibilitySettings: GlobalPageVisibilitySettings;
|
||||
}
|
||||
|
||||
export async function GetLayoutServerData(cookies: Cookies, request: Request): Promise<LayoutServerData> {
|
||||
@@ -75,7 +83,7 @@ export async function GetLayoutServerData(cookies: Cookies, request: Request): P
|
||||
GetUsersCount(),
|
||||
]);
|
||||
|
||||
const isSetupComplete = process.env.KENER_SECRET_KEY !== undefined && Object.keys(siteData).length > 0;
|
||||
const isSetupComplete = await IsSetupComplete();
|
||||
|
||||
const selectedLang = GetLocaleFromCookie(siteData, cookies);
|
||||
const siteStatusColors = siteData.colors;
|
||||
@@ -128,5 +136,6 @@ export async function GetLayoutServerData(cookies: Cookies, request: Request): P
|
||||
eventDisplaySettings: siteData.eventDisplaySettings || seedSiteData.eventDisplaySettings,
|
||||
socialPreviewImage: siteData.socialPreviewImage,
|
||||
customCSS: siteData.customCSS,
|
||||
globalPageVisibilitySettings: siteData.globalPageVisibilitySettings || seedSiteData.globalPageVisibilitySettings,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@ import type {
|
||||
MaintenanceEventFilter,
|
||||
} from "../types/db.js";
|
||||
import { GetMinuteStartNowTimestampUTC } from "../tool.js";
|
||||
import { maintenanceToVariables } from "../notification/notification_utils.js";
|
||||
import { maintenanceToVariables, siteDataToVariables } from "../notification/notification_utils.js";
|
||||
import { GetAllSiteData } from "./controller.js";
|
||||
import subscriberQueue from "../queues/subscriberQueue.js";
|
||||
import GC from "../../global-constants";
|
||||
|
||||
@@ -540,6 +541,9 @@ export const formatDurationSeconds = (seconds: number): string => {
|
||||
export const UpdateMaintenanceEventStatuses = async (): Promise<void> => {
|
||||
const currentTimestamp = GetMinuteStartNowTimestampUTC();
|
||||
const sixtyMinutesInSeconds = 60 * 60;
|
||||
const siteData = await GetAllSiteData();
|
||||
const siteVars = siteDataToVariables(siteData);
|
||||
const siteUrl = siteVars.site_url;
|
||||
|
||||
try {
|
||||
// 1. Mark SCHEDULED events starting within 60 minutes as READY
|
||||
@@ -559,6 +563,7 @@ export const UpdateMaintenanceEventStatuses = async (): Promise<void> => {
|
||||
`**is starting in ${timeUntilStart}**`,
|
||||
"starting_soon",
|
||||
"Maintenance Starting Soon",
|
||||
siteUrl,
|
||||
);
|
||||
await subscriberQueue.push(update);
|
||||
}
|
||||
@@ -576,6 +581,7 @@ export const UpdateMaintenanceEventStatuses = async (): Promise<void> => {
|
||||
"**is now in progress**",
|
||||
"ongoing",
|
||||
"Maintenance In Progress",
|
||||
siteUrl,
|
||||
);
|
||||
await subscriberQueue.push(update);
|
||||
}
|
||||
@@ -593,6 +599,7 @@ export const UpdateMaintenanceEventStatuses = async (): Promise<void> => {
|
||||
"**is now in progress**",
|
||||
"ongoing",
|
||||
"Maintenance In Progress",
|
||||
siteUrl,
|
||||
);
|
||||
await subscriberQueue.push(update);
|
||||
}
|
||||
@@ -610,6 +617,7 @@ export const UpdateMaintenanceEventStatuses = async (): Promise<void> => {
|
||||
"**has been completed**",
|
||||
"completed",
|
||||
"Maintenance Completed",
|
||||
siteUrl,
|
||||
);
|
||||
await subscriberQueue.push(update);
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ export async function CreateMonitorAlertConfig(
|
||||
};
|
||||
|
||||
// Insert alert config
|
||||
const [id] = await db.insertMonitorAlertConfig(insertData);
|
||||
const id = await db.insertMonitorAlertConfig(insertData);
|
||||
|
||||
// Add triggers if provided
|
||||
if (data.trigger_ids && data.trigger_ids.length > 0) {
|
||||
|
||||
@@ -39,6 +39,31 @@ interface MonitorInput extends MonitorRecordInsert {
|
||||
id?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that a monitor tag is URL-friendly: lowercase alphanumeric, hyphens, and underscores only.
|
||||
* Must start and end with an alphanumeric character.
|
||||
*/
|
||||
const VALID_TAG_REGEX = /^[a-z0-9][a-z0-9_-]*[a-z0-9]$/;
|
||||
const VALID_TAG_SINGLE_CHAR_REGEX = /^[a-z0-9]$/;
|
||||
|
||||
function isValidMonitorTag(tag: string): boolean {
|
||||
if (!tag || tag.length === 0) return false;
|
||||
if (tag.length === 1) return VALID_TAG_SINGLE_CHAR_REGEX.test(tag);
|
||||
return VALID_TAG_REGEX.test(tag);
|
||||
}
|
||||
|
||||
function validateMonitorTag(tag: string): void {
|
||||
const trimmed = tag?.trim();
|
||||
if (!trimmed) {
|
||||
throw new Error("Monitor tag is required");
|
||||
}
|
||||
if (!isValidMonitorTag(trimmed)) {
|
||||
throw new Error(
|
||||
"Monitor tag must be URL-friendly: only lowercase letters, numbers, hyphens, and underscores. Must start and end with a letter or number.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface DayGroupData {
|
||||
timestamp: number;
|
||||
total: number;
|
||||
@@ -169,6 +194,7 @@ export const CreateUpdateMonitor = async (monitor: MonitorInput): Promise<number
|
||||
if (monitorData.id) {
|
||||
return await db.updateMonitor(monitorData as MonitorRecord);
|
||||
} else {
|
||||
validateMonitorTag(monitorData.tag);
|
||||
return await db.insertMonitor(monitorData);
|
||||
}
|
||||
};
|
||||
@@ -178,6 +204,7 @@ export const CreateMonitor = async (monitor: MonitorInput): Promise<number[]> =>
|
||||
if (monitorData.id) {
|
||||
throw new Error("monitor id must be empty or 0");
|
||||
}
|
||||
validateMonitorTag(monitorData.tag);
|
||||
return await db.insertMonitor(monitorData);
|
||||
};
|
||||
|
||||
@@ -198,6 +225,7 @@ export const CloneMonitor = async ({ sourceTag, newTag, newName }: CloneMonitorI
|
||||
if (!newTagTrimmed) {
|
||||
throw new Error("Tag is required");
|
||||
}
|
||||
validateMonitorTag(newTagTrimmed);
|
||||
if (!newNameTrimmed) {
|
||||
throw new Error("Name is required");
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import type { Cookies } from "@sveltejs/kit";
|
||||
import type {
|
||||
DataRetentionPolicy,
|
||||
EventDisplaySettings,
|
||||
GlobalPageVisibilitySettings,
|
||||
PageOrderingSettings,
|
||||
SiteAnalyticsItem,
|
||||
SiteAnnouncement,
|
||||
SiteCategory,
|
||||
@@ -56,6 +58,8 @@ export interface SiteDataTransformed {
|
||||
eventDisplaySettings?: EventDisplaySettings;
|
||||
socialPreviewImage?: string;
|
||||
customCSS?: string;
|
||||
globalPageVisibilitySettings?: GlobalPageVisibilitySettings;
|
||||
pageOrderingSettings?: PageOrderingSettings;
|
||||
}
|
||||
|
||||
export function InsertKeyValue(key: string, value: string): Promise<number[]> {
|
||||
@@ -130,6 +134,12 @@ export const IsSetupComplete = async (): Promise<boolean> => {
|
||||
if (process.env.KENER_SECRET_KEY === undefined) {
|
||||
return false;
|
||||
}
|
||||
if (process.env.ORIGIN === undefined) {
|
||||
return false;
|
||||
}
|
||||
if (process.env.REDIS_URL === undefined) {
|
||||
return false;
|
||||
}
|
||||
let data = await db.getAllSiteData();
|
||||
|
||||
if (!data) {
|
||||
|
||||
@@ -261,4 +261,14 @@ export const siteDataKeys: SiteDataKey[] = [
|
||||
isValid: (value) => typeof value === "string",
|
||||
data_type: "string",
|
||||
},
|
||||
{
|
||||
key: "globalPageVisibilitySettings",
|
||||
isValid: IsValidJSONString,
|
||||
data_type: "object",
|
||||
},
|
||||
{
|
||||
key: "pageOrderingSettings",
|
||||
isValid: IsValidJSONString,
|
||||
data_type: "object",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -251,6 +251,9 @@ export const GetLoggedInSession = async (cookies: Cookies): Promise<UserRecordPu
|
||||
if (!userDB) {
|
||||
return null;
|
||||
}
|
||||
if (!userDB.is_active) {
|
||||
return null;
|
||||
}
|
||||
return userDB;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
import crypto from "crypto";
|
||||
import db from "$lib/server/db/db";
|
||||
import type { VaultRecord } from "$lib/server/db/repositories/vault";
|
||||
|
||||
const DUMMY_SECRET = "DUMMY_SECRET_KEY_32_BYTES_LONG!";
|
||||
const ALGORITHM = "aes-256-cbc";
|
||||
const IV_LENGTH = 16;
|
||||
|
||||
/**
|
||||
* Get the encryption key from environment variable
|
||||
* Pads or truncates to 32 bytes for AES-256
|
||||
*/
|
||||
function getEncryptionKey(): Buffer {
|
||||
const key = process.env.KENER_SECRET_KEY || DUMMY_SECRET;
|
||||
// AES-256 requires a 32-byte key
|
||||
const keyBuffer = Buffer.alloc(32);
|
||||
Buffer.from(key).copy(keyBuffer);
|
||||
return keyBuffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt a value using AES-256-CBC
|
||||
*/
|
||||
export function encryptValue(plainText: string): string {
|
||||
const key = getEncryptionKey();
|
||||
const iv = crypto.randomBytes(IV_LENGTH);
|
||||
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
||||
let encrypted = cipher.update(plainText, "utf8", "hex");
|
||||
encrypted += cipher.final("hex");
|
||||
// Prepend IV to encrypted data (IV:encrypted)
|
||||
return iv.toString("hex") + ":" + encrypted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt a value using AES-256-CBC
|
||||
*/
|
||||
export function decryptValue(encryptedText: string): string {
|
||||
const key = getEncryptionKey();
|
||||
const parts = encryptedText.split(":");
|
||||
if (parts.length !== 2) {
|
||||
throw new Error("Invalid encrypted value format");
|
||||
}
|
||||
const iv = Buffer.from(parts[0], "hex");
|
||||
const encrypted = parts[1];
|
||||
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
||||
let decrypted = decipher.update(encrypted, "hex", "utf8");
|
||||
decrypted += decipher.final("utf8");
|
||||
return decrypted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for decrypted vault secret
|
||||
*/
|
||||
export interface VaultSecretDecrypted {
|
||||
id: number;
|
||||
secret_name: string;
|
||||
secret_value: string; // Decrypted value
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all secrets with decrypted values
|
||||
*/
|
||||
export async function GetAllSecrets(): Promise<VaultSecretDecrypted[]> {
|
||||
const secrets = await db.getAllSecrets();
|
||||
return secrets.map((secret) => ({
|
||||
...secret,
|
||||
secret_value: decryptValue(secret.secret_value),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a secret by ID with decrypted value
|
||||
*/
|
||||
export async function GetSecretById(id: number): Promise<VaultSecretDecrypted | undefined> {
|
||||
const secret = await db.getSecretById(id);
|
||||
if (!secret) return undefined;
|
||||
return {
|
||||
...secret,
|
||||
secret_value: decryptValue(secret.secret_value),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a secret by name with decrypted value
|
||||
*/
|
||||
export async function GetSecretByName(secretName: string): Promise<VaultSecretDecrypted | undefined> {
|
||||
const secret = await db.getSecretByName(secretName);
|
||||
if (!secret) return undefined;
|
||||
return {
|
||||
...secret,
|
||||
secret_value: decryptValue(secret.secret_value),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new secret (encrypts the value before storing)
|
||||
*/
|
||||
export async function CreateSecret(
|
||||
secretName: string,
|
||||
secretValue: string,
|
||||
): Promise<{ success: boolean; error?: string; id?: number }> {
|
||||
// Validate input
|
||||
if (!secretName || !secretName.trim()) {
|
||||
return { success: false, error: "Secret name is required" };
|
||||
}
|
||||
if (!secretValue) {
|
||||
return { success: false, error: "Secret value is required" };
|
||||
}
|
||||
|
||||
// Check if name already exists
|
||||
const exists = await db.secretNameExists(secretName.trim());
|
||||
if (exists) {
|
||||
return { success: false, error: `Secret with name "${secretName}" already exists` };
|
||||
}
|
||||
|
||||
// Encrypt and store
|
||||
const encryptedValue = encryptValue(secretValue);
|
||||
const ids = await db.insertSecret({
|
||||
secret_name: secretName.trim(),
|
||||
secret_value: encryptedValue,
|
||||
});
|
||||
|
||||
return { success: true, id: ids[0] };
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a secret by ID (encrypts the new value before storing)
|
||||
*/
|
||||
export async function UpdateSecret(
|
||||
id: number,
|
||||
data: { secret_name?: string; secret_value?: string },
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
// Check if secret exists
|
||||
const existing = await db.getSecretById(id);
|
||||
if (!existing) {
|
||||
return { success: false, error: `Secret with ID ${id} not found` };
|
||||
}
|
||||
|
||||
// If updating name, check for duplicates
|
||||
if (data.secret_name && data.secret_name !== existing.secret_name) {
|
||||
const nameExists = await db.secretNameExists(data.secret_name.trim(), id);
|
||||
if (nameExists) {
|
||||
return { success: false, error: `Secret with name "${data.secret_name}" already exists` };
|
||||
}
|
||||
}
|
||||
|
||||
// Build update data
|
||||
const updateData: { secret_name?: string; secret_value?: string } = {};
|
||||
if (data.secret_name !== undefined) {
|
||||
updateData.secret_name = data.secret_name.trim();
|
||||
}
|
||||
if (data.secret_value !== undefined) {
|
||||
updateData.secret_value = encryptValue(data.secret_value);
|
||||
}
|
||||
|
||||
await db.updateSecretById(id, updateData);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a secret by ID
|
||||
*/
|
||||
export async function DeleteSecret(id: number): Promise<{ success: boolean; error?: string }> {
|
||||
const existing = await db.getSecretById(id);
|
||||
if (!existing) {
|
||||
return { success: false, error: `Secret with ID ${id} not found` };
|
||||
}
|
||||
|
||||
await db.deleteSecretById(id);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get secrets count
|
||||
*/
|
||||
export async function GetSecretsCount(): Promise<number> {
|
||||
return await db.getSecretsCount();
|
||||
}
|
||||
@@ -14,7 +14,6 @@ import { MaintenancesRepository } from "./repositories/maintenances.js";
|
||||
import { MonitorAlertConfigRepository } from "./repositories/monitorAlertConfig.js";
|
||||
import { SubscriptionSystemRepository } from "./repositories/subscriptionSystem.js";
|
||||
import { EmailTemplateConfigRepository } from "./repositories/emailTemplateConfig.js";
|
||||
import { VaultRepository } from "./repositories/vault.js";
|
||||
|
||||
// Re-export types from base
|
||||
export type { MonitorFilter, TriggerFilter, IncidentFilter, CountResult } from "./repositories/base.js";
|
||||
@@ -44,7 +43,6 @@ class DbImpl {
|
||||
private monitorAlertConfig!: MonitorAlertConfigRepository;
|
||||
private subscriptionSystem!: SubscriptionSystemRepository;
|
||||
private emailTemplateConfig!: EmailTemplateConfigRepository;
|
||||
private vault!: VaultRepository;
|
||||
|
||||
// Method bindings - declared with definite assignment assertion
|
||||
// ============ Monitoring Data ============
|
||||
@@ -347,18 +345,6 @@ class DbImpl {
|
||||
deleteEmailTemplate!: EmailTemplateConfigRepository["deleteEmailTemplate"];
|
||||
upsertEmailTemplate!: EmailTemplateConfigRepository["upsertEmailTemplate"];
|
||||
|
||||
// ============ Vault ============
|
||||
getAllSecrets!: VaultRepository["getAllSecrets"];
|
||||
getSecretById!: VaultRepository["getSecretById"];
|
||||
getSecretByName!: VaultRepository["getSecretByName"];
|
||||
insertSecret!: VaultRepository["insertSecret"];
|
||||
updateSecretById!: VaultRepository["updateSecretById"];
|
||||
updateSecretByName!: VaultRepository["updateSecretByName"];
|
||||
deleteSecretById!: VaultRepository["deleteSecretById"];
|
||||
deleteSecretByName!: VaultRepository["deleteSecretByName"];
|
||||
secretNameExists!: VaultRepository["secretNameExists"];
|
||||
getSecretsCount!: VaultRepository["getSecretsCount"];
|
||||
|
||||
constructor(opts: KnexType.Config) {
|
||||
this.knex = Knex(opts);
|
||||
|
||||
@@ -375,7 +361,6 @@ class DbImpl {
|
||||
this.monitorAlertConfig = new MonitorAlertConfigRepository(this.knex);
|
||||
this.subscriptionSystem = new SubscriptionSystemRepository(this.knex);
|
||||
this.emailTemplateConfig = new EmailTemplateConfigRepository(this.knex);
|
||||
this.vault = new VaultRepository(this.knex);
|
||||
|
||||
// Bind methods after repositories are initialized
|
||||
this.bindMonitoringMethods();
|
||||
@@ -390,7 +375,6 @@ class DbImpl {
|
||||
this.bindMonitorAlertConfigMethods();
|
||||
this.bindSubscriptionSystemMethods();
|
||||
this.bindEmailTemplateConfigMethods();
|
||||
this.bindVaultMethods();
|
||||
|
||||
this.init();
|
||||
}
|
||||
@@ -794,19 +778,6 @@ class DbImpl {
|
||||
this.upsertEmailTemplate = this.emailTemplateConfig.upsertEmailTemplate.bind(this.emailTemplateConfig);
|
||||
}
|
||||
|
||||
private bindVaultMethods(): void {
|
||||
this.getAllSecrets = this.vault.getAllSecrets.bind(this.vault);
|
||||
this.getSecretById = this.vault.getSecretById.bind(this.vault);
|
||||
this.getSecretByName = this.vault.getSecretByName.bind(this.vault);
|
||||
this.insertSecret = this.vault.insertSecret.bind(this.vault);
|
||||
this.updateSecretById = this.vault.updateSecretById.bind(this.vault);
|
||||
this.updateSecretByName = this.vault.updateSecretByName.bind(this.vault);
|
||||
this.deleteSecretById = this.vault.deleteSecretById.bind(this.vault);
|
||||
this.deleteSecretByName = this.vault.deleteSecretByName.bind(this.vault);
|
||||
this.secretNameExists = this.vault.secretNameExists.bind(this.vault);
|
||||
this.getSecretsCount = this.vault.getSecretsCount.bind(this.vault);
|
||||
}
|
||||
|
||||
async init(): Promise<void> {}
|
||||
|
||||
async close(): Promise<void> {
|
||||
|
||||
@@ -826,8 +826,9 @@ export class IncidentsRepository extends BaseRepository {
|
||||
async getIncidentsForEventsByDateRange(
|
||||
startTs: number,
|
||||
endTs: number,
|
||||
monitorTags?: string[],
|
||||
): Promise<IncidentForMonitorListWithComments[]> {
|
||||
const rows = await this.knex("incidents")
|
||||
const query = this.knex("incidents")
|
||||
.select(
|
||||
"incidents.id",
|
||||
"incidents.title",
|
||||
@@ -848,8 +849,15 @@ export class IncidentsRepository extends BaseRepository {
|
||||
.where("incidents.incident_type", GC.INCIDENT)
|
||||
.andWhere("incidents.status", "OPEN")
|
||||
.andWhere("incidents.start_date_time", ">=", startTs)
|
||||
.andWhere("incidents.start_date_time", "<=", endTs)
|
||||
.orderBy("incidents.start_date_time", "desc");
|
||||
.andWhere("incidents.start_date_time", "<=", endTs);
|
||||
|
||||
if (monitorTags) {
|
||||
query.andWhere(function () {
|
||||
this.whereIn("incident_monitors.monitor_tag", monitorTags).orWhere("incidents.is_global", "YES");
|
||||
});
|
||||
}
|
||||
|
||||
const rows = await query.orderBy("incidents.start_date_time", "desc");
|
||||
|
||||
const incidents = this.groupIncidentsByIdForMonitorListFilterHidden(rows);
|
||||
|
||||
|
||||
@@ -631,8 +631,9 @@ export class MaintenancesRepository extends BaseRepository {
|
||||
async getMaintenanceEventsForEventsByDateRange(
|
||||
startTs: number,
|
||||
endTs: number,
|
||||
monitorTags?: string[],
|
||||
): Promise<MaintenanceEventsMonitorList[]> {
|
||||
const rows = await this.knex("maintenances_events")
|
||||
const query = this.knex("maintenances_events")
|
||||
.select(
|
||||
"maintenances_events.id",
|
||||
"maintenances.title",
|
||||
@@ -652,8 +653,15 @@ export class MaintenancesRepository extends BaseRepository {
|
||||
.leftJoin("maintenance_monitors", "maintenances_events.maintenance_id", "maintenance_monitors.maintenance_id")
|
||||
.leftJoin("monitors", "maintenance_monitors.monitor_tag", "monitors.tag")
|
||||
.andWhere("maintenances_events.start_date_time", ">=", startTs)
|
||||
.andWhere("maintenances_events.start_date_time", "<=", endTs)
|
||||
.orderBy("maintenances_events.start_date_time", "desc");
|
||||
.andWhere("maintenances_events.start_date_time", "<=", endTs);
|
||||
|
||||
if (monitorTags) {
|
||||
query.andWhere(function () {
|
||||
this.whereIn("maintenance_monitors.monitor_tag", monitorTags).orWhere("maintenances.is_global", "YES");
|
||||
});
|
||||
}
|
||||
|
||||
const rows = await query.orderBy("maintenances_events.start_date_time", "desc");
|
||||
|
||||
return this.groupMaintenancesByIdForMonitorList(rows);
|
||||
}
|
||||
|
||||
@@ -26,8 +26,9 @@ export class MonitorAlertConfigRepository extends BaseRepository {
|
||||
/**
|
||||
* Insert a new monitor alert config
|
||||
*/
|
||||
async insertMonitorAlertConfig(data: MonitorAlertConfigInsert): Promise<number[]> {
|
||||
return await this.knex("monitor_alerts_config").insert({
|
||||
async insertMonitorAlertConfig(data: MonitorAlertConfigInsert): Promise<number> {
|
||||
const dbType = GetDbType();
|
||||
const insertData = {
|
||||
monitor_tag: data.monitor_tag,
|
||||
alert_for: data.alert_for,
|
||||
alert_value: data.alert_value,
|
||||
@@ -39,7 +40,21 @@ export class MonitorAlertConfigRepository extends BaseRepository {
|
||||
severity: data.severity || "WARNING",
|
||||
created_at: this.knex.fn.now(),
|
||||
updated_at: this.knex.fn.now(),
|
||||
});
|
||||
};
|
||||
|
||||
if (dbType === "postgresql") {
|
||||
const result = await this.knex("monitor_alerts_config").insert(insertData).returning("id");
|
||||
const inserted = Array.isArray(result) ? result[0] : result;
|
||||
return typeof inserted === "object" && inserted !== null
|
||||
? Number((inserted as { id: number }).id)
|
||||
: Number(inserted);
|
||||
}
|
||||
|
||||
const result = await this.knex("monitor_alerts_config").insert(insertData);
|
||||
const inserted = Array.isArray(result) ? result[0] : result;
|
||||
return typeof inserted === "object" && inserted !== null
|
||||
? Number((inserted as { id: number }).id)
|
||||
: Number(inserted);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,7 +22,8 @@ export class PagesRepository extends BaseRepository {
|
||||
};
|
||||
|
||||
if (dbType === "postgresql") {
|
||||
const [page] = await this.knex("pages").insert(insertData).returning("*");
|
||||
const result = await this.knex("pages").insert(insertData).returning("*");
|
||||
const page = Array.isArray(result) ? result[0] : result;
|
||||
return page;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
import { BaseRepository } from "./base.js";
|
||||
|
||||
export interface VaultRecord {
|
||||
id: number;
|
||||
secret_name: string;
|
||||
secret_value: string; // Encrypted value in DB
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
}
|
||||
|
||||
export interface VaultInsert {
|
||||
secret_name: string;
|
||||
secret_value: string; // Should be encrypted before insert
|
||||
}
|
||||
|
||||
export interface VaultUpdate {
|
||||
secret_name?: string;
|
||||
secret_value?: string; // Should be encrypted before update
|
||||
}
|
||||
|
||||
/**
|
||||
* Repository for vault (secrets) operations
|
||||
* Note: Encryption/decryption should be handled at the controller level
|
||||
*/
|
||||
export class VaultRepository extends BaseRepository {
|
||||
/**
|
||||
* Get all vault secrets
|
||||
*/
|
||||
async getAllSecrets(): Promise<VaultRecord[]> {
|
||||
return await this.knex("vault").orderBy("id", "asc");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a secret by ID
|
||||
*/
|
||||
async getSecretById(id: number): Promise<VaultRecord | undefined> {
|
||||
return await this.knex("vault").where({ id }).first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a secret by name
|
||||
*/
|
||||
async getSecretByName(secretName: string): Promise<VaultRecord | undefined> {
|
||||
return await this.knex("vault").where({ secret_name: secretName }).first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a new secret
|
||||
*/
|
||||
async insertSecret(data: VaultInsert): Promise<number[]> {
|
||||
return await this.knex("vault").insert({
|
||||
secret_name: data.secret_name,
|
||||
secret_value: data.secret_value,
|
||||
created_at: this.knex.fn.now(),
|
||||
updated_at: this.knex.fn.now(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a secret by ID
|
||||
*/
|
||||
async updateSecretById(id: number, data: VaultUpdate): Promise<number> {
|
||||
const updateData: Record<string, unknown> = {
|
||||
updated_at: this.knex.fn.now(),
|
||||
};
|
||||
|
||||
if (data.secret_name !== undefined) {
|
||||
updateData.secret_name = data.secret_name;
|
||||
}
|
||||
if (data.secret_value !== undefined) {
|
||||
updateData.secret_value = data.secret_value;
|
||||
}
|
||||
|
||||
return await this.knex("vault").where({ id }).update(updateData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a secret by name
|
||||
*/
|
||||
async updateSecretByName(secretName: string, data: VaultUpdate): Promise<number> {
|
||||
const updateData: Record<string, unknown> = {
|
||||
updated_at: this.knex.fn.now(),
|
||||
};
|
||||
|
||||
if (data.secret_name !== undefined) {
|
||||
updateData.secret_name = data.secret_name;
|
||||
}
|
||||
if (data.secret_value !== undefined) {
|
||||
updateData.secret_value = data.secret_value;
|
||||
}
|
||||
|
||||
return await this.knex("vault").where({ secret_name: secretName }).update(updateData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a secret by ID
|
||||
*/
|
||||
async deleteSecretById(id: number): Promise<number> {
|
||||
return await this.knex("vault").where({ id }).del();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a secret by name
|
||||
*/
|
||||
async deleteSecretByName(secretName: string): Promise<number> {
|
||||
return await this.knex("vault").where({ secret_name: secretName }).del();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a secret name already exists
|
||||
*/
|
||||
async secretNameExists(secretName: string, excludeId?: number): Promise<boolean> {
|
||||
let query = this.knex("vault").where({ secret_name: secretName });
|
||||
if (excludeId !== undefined) {
|
||||
query = query.andWhereNot({ id: excludeId });
|
||||
}
|
||||
const result = await query.first();
|
||||
return !!result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get secrets count
|
||||
*/
|
||||
async getSecretsCount(): Promise<number> {
|
||||
const result = await this.knex("vault").count("id as count").first();
|
||||
return Number(result?.count || 0);
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ const seedSiteData = {
|
||||
],
|
||||
nav: [
|
||||
{ name: "Documentation", url: "https://kener.ing/docs/home", iconURL: "" },
|
||||
{ name: "Github", iconURL: "", url: "https://github.com/rajnandan1/kener" },
|
||||
{ name: "GitHub", iconURL: "", url: "https://github.com/rajnandan1/kener" },
|
||||
{ name: "Login", iconURL: "", url: "/account/signin" },
|
||||
],
|
||||
hero: {
|
||||
@@ -155,6 +155,10 @@ const seedSiteData = {
|
||||
upcoming: { show: true, maxCount: 5, daysInFuture: 7 },
|
||||
},
|
||||
},
|
||||
globalPageVisibilitySettings: {
|
||||
showSwitcher: true,
|
||||
forceExclusivity: false,
|
||||
},
|
||||
};
|
||||
|
||||
export default seedSiteData;
|
||||
|
||||
@@ -72,12 +72,13 @@ export function maintenanceToVariables(
|
||||
statusMessage: string,
|
||||
updateIdSuffix: string,
|
||||
subjectPrefix: string,
|
||||
siteUrl: string = "",
|
||||
): SubscriptionVariableMap {
|
||||
const template = formatMaintenanceMarkdown(monitorNames, event, statusMessage);
|
||||
return {
|
||||
title: `${subjectPrefix}: ${event.title}`,
|
||||
event_type: "maintenances",
|
||||
cta_url: "/maintenances/" + event.maintenance_id,
|
||||
cta_url: siteUrl + "maintenances/" + event.maintenance_id,
|
||||
cta_text: "View Maintenance Details",
|
||||
update_id: `maintenance_${event.id}_${updateIdSuffix}`,
|
||||
update_subject: `${subjectPrefix}: ${event.title}`,
|
||||
|
||||
@@ -65,6 +65,7 @@ async function createNewIncident(
|
||||
config: MonitorAlertConfigRecord,
|
||||
monitorName: string,
|
||||
monitorTag: string,
|
||||
siteUrl: string = "",
|
||||
): Promise<{ incident_id: number }> {
|
||||
let startDateTime = getUnixTime(new Date(alert.created_at));
|
||||
let incidentInput: IncidentInput = {
|
||||
@@ -83,7 +84,7 @@ async function createNewIncident(
|
||||
|
||||
const updateVariables: SubscriptionVariableMap = {
|
||||
title: incidentInput.title,
|
||||
cta_url: "/incidents/" + incidentCreated.incident_id,
|
||||
cta_url: siteUrl + "incidents/" + incidentCreated.incident_id,
|
||||
cta_text: "View Incident",
|
||||
update_text: mdToHTML(update),
|
||||
update_subject: `[#${incidentCreated.incident_id}:${GC.TRIGGERED}] ${incidentInput.title}`,
|
||||
@@ -105,6 +106,7 @@ async function closeIncident(
|
||||
config: MonitorAlertConfigRecord,
|
||||
monitorName: string,
|
||||
monitorTag: string,
|
||||
siteUrl: string = "",
|
||||
): Promise<void> {
|
||||
//check if incident is already resolved
|
||||
if (!alert.incident_id) {
|
||||
@@ -124,7 +126,7 @@ async function closeIncident(
|
||||
const updatedAt = getUnixTime(new Date(alert.updated_at));
|
||||
const updateMessage: SubscriptionVariableMap = {
|
||||
title: incident.title,
|
||||
cta_url: `/incidents/${incident_id}`,
|
||||
cta_url: `${siteUrl}incidents/${incident_id}`,
|
||||
cta_text: "View Incident",
|
||||
update_text: mdToHTML(comment),
|
||||
update_subject: `[#${incident.id}:${GC.RESOLVED}] ${incident.title}`,
|
||||
@@ -251,6 +253,7 @@ const addWorker = () => {
|
||||
monitor_alerts_configured,
|
||||
monitor_name,
|
||||
monitor_tag,
|
||||
templateSiteVars.site_url,
|
||||
);
|
||||
//update alert with incident number
|
||||
if (newIncidentNumber && newIncidentNumber.incident_id > 0) {
|
||||
@@ -289,7 +292,13 @@ const addWorker = () => {
|
||||
|
||||
// If alert has an incident, add closure comment
|
||||
if (activeAlert.incident_id) {
|
||||
await closeIncident(activeAlert, monitor_alerts_configured, monitor_name, monitor_tag);
|
||||
await closeIncident(
|
||||
activeAlert,
|
||||
monitor_alerts_configured,
|
||||
monitor_name,
|
||||
monitor_tag,
|
||||
templateSiteVars.site_url,
|
||||
);
|
||||
}
|
||||
|
||||
// Send resolution notifications
|
||||
|
||||
@@ -11,13 +11,15 @@ async function Startup(): Promise<void> {
|
||||
await maintenanceScheduler.start();
|
||||
await dailyCleanupScheduler.start();
|
||||
|
||||
figlet("Kener v" + version(), function (err, data) {
|
||||
const runtimeVersion = version();
|
||||
|
||||
figlet("Kener v" + runtimeVersion, function (err, data) {
|
||||
if (err) {
|
||||
console.log("Something went wrong...");
|
||||
return;
|
||||
}
|
||||
console.log(data);
|
||||
console.log(`Kener version ${version()} is running!`);
|
||||
console.log(`Kener version ${runtimeVersion} is running!`);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -125,3 +125,13 @@ export interface EventDisplaySettings {
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface GlobalPageVisibilitySettings {
|
||||
showSwitcher: boolean;
|
||||
forceExclusivity: boolean;
|
||||
}
|
||||
|
||||
export interface PageOrderingSettings {
|
||||
enabled: boolean;
|
||||
order: number[]; // Array of page IDs in the desired order
|
||||
}
|
||||
|
||||
@@ -51,6 +51,13 @@ export const actions: Actions = {
|
||||
return fail(401, { error: "Invalid password or Email", values: { email } });
|
||||
}
|
||||
|
||||
if (!userDB.is_active) {
|
||||
return fail(403, {
|
||||
error: "Your account has been deactivated. Please contact an administrator.",
|
||||
values: { email },
|
||||
});
|
||||
}
|
||||
|
||||
const token = await GenerateToken(userDB);
|
||||
const cookieConfig = CookieConfig();
|
||||
cookies.set(cookieConfig.name, token, {
|
||||
|
||||
@@ -46,7 +46,17 @@
|
||||
<p>Please make sure to set the below environment variables:</p>
|
||||
<ul class="list-inside list-disc text-sm">
|
||||
<li>KENER_SECRET_KEY</li>
|
||||
<li>ORIGIN</li>
|
||||
<li>REDIS_URL</li>
|
||||
</ul>
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
class="text-destructive w-full justify-start underline"
|
||||
href="https://kener.ing/docs/v4/setup/environment-variables"
|
||||
>
|
||||
Go to docs
|
||||
</Button>
|
||||
</Alert.Description>
|
||||
</Alert.Root>
|
||||
{:else}
|
||||
|
||||
@@ -65,6 +65,20 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||
return json(errorResponse, { status: 400 });
|
||||
}
|
||||
|
||||
// Validate tag is URL-friendly: lowercase alphanumeric, hyphens, underscores
|
||||
const tagTrimmed = body.tag.trim();
|
||||
const validTagRegex = tagTrimmed.length === 1 ? /^[a-z0-9]$/ : /^[a-z0-9][a-z0-9_-]*[a-z0-9]$/;
|
||||
if (!validTagRegex.test(tagTrimmed)) {
|
||||
const errorResponse: BadRequestResponse = {
|
||||
error: {
|
||||
code: "BAD_REQUEST",
|
||||
message:
|
||||
"Tag must be URL-friendly: only lowercase letters, numbers, hyphens, and underscores. Must start and end with a letter or number.",
|
||||
},
|
||||
};
|
||||
return json(errorResponse, { status: 400 });
|
||||
}
|
||||
|
||||
if (!body.name || typeof body.name !== "string" || body.name.trim().length === 0) {
|
||||
const errorResponse: BadRequestResponse = {
|
||||
error: {
|
||||
|
||||
@@ -39,6 +39,10 @@
|
||||
"group": "Setup Guide",
|
||||
"collapsible": false,
|
||||
"pages": [
|
||||
{
|
||||
"title": "Deployment",
|
||||
"content": "v4/setup/deployment"
|
||||
},
|
||||
{
|
||||
"title": "Site Configuration",
|
||||
"content": "v4/setup/site-configuration"
|
||||
@@ -237,6 +241,10 @@
|
||||
"title": "Alerting Trigger Examples",
|
||||
"content": "v4/guides/alerting-trigger-examples"
|
||||
},
|
||||
{
|
||||
"title": "Mattermost Webhook Trigger",
|
||||
"content": "v4/guides/mattermost-webhook-trigger"
|
||||
},
|
||||
{
|
||||
"title": "Reverse Proxy Setup",
|
||||
"content": "v4/guides/reverse-proxy"
|
||||
@@ -244,6 +252,14 @@
|
||||
{
|
||||
"title": "Base Path Deployment",
|
||||
"content": "v4/guides/base-path"
|
||||
},
|
||||
{
|
||||
"title": "Custom Fonts",
|
||||
"content": "v4/guides/custom-fonts"
|
||||
},
|
||||
{
|
||||
"title": "Custom JS & CSS",
|
||||
"content": "v4/guides/custom-js-css-guide"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -154,8 +154,8 @@
|
||||
<meta property="og:type" content="article" />
|
||||
</svelte:head>
|
||||
|
||||
<div class="mx-auto flex max-w-[1100px] gap-8">
|
||||
<article class="max-w-4xl min-w-0 flex-1">
|
||||
<div class="mx-auto flex justify-between gap-8">
|
||||
<article class=" min-w-0 flex-1">
|
||||
<div class="relative mb-8">
|
||||
<span class="text-accent-foreground mb-2 inline-block text-xs font-semibold tracking-wide uppercase">
|
||||
{data.group}
|
||||
|
||||
@@ -303,7 +303,12 @@
|
||||
|
||||
<div class="mb-8 flex flex-wrap justify-center gap-3 md:gap-4 lg:justify-start">
|
||||
{#each getCtaButtons() as button (button.title)}
|
||||
<Button href={getHref(button.href)} variant={button.primary ? "default" : "outline"} size="lg">
|
||||
<Button
|
||||
href={getHref(button.href)}
|
||||
variant={button.primary ? "default" : "outline"}
|
||||
rel="external"
|
||||
size="lg"
|
||||
>
|
||||
{button.title}
|
||||
{#if button.primary}
|
||||
<ArrowRight class="h-4 w-4" />
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { DocsConfig } from "$lib/types/docs";
|
||||
|
||||
import { browser } from "$app/environment";
|
||||
import { goto } from "$app/navigation";
|
||||
import Sun from "@lucide/svelte/icons/sun";
|
||||
import Moon from "@lucide/svelte/icons/moon";
|
||||
@@ -102,12 +101,23 @@
|
||||
function openSearch() {
|
||||
searchOpen = true;
|
||||
}
|
||||
|
||||
function getLlmsHref(): string {
|
||||
const fallbackVersion = config.versions?.find((version) => version.latest)?.slug ?? config.versions?.[0]?.slug;
|
||||
const versionSlug = config.activeVersion ?? fallbackVersion;
|
||||
|
||||
if (!versionSlug) {
|
||||
return "/docs";
|
||||
}
|
||||
|
||||
return `/docs/${versionSlug}/llms.txt`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Main Navbar -->
|
||||
<header class="bg-background border-border/50 fixed top-0 right-0 left-0 z-50">
|
||||
<!-- Primary Nav Row -->
|
||||
<div class="mx-auto flex h-14 max-w-[1400px] items-center justify-between px-6">
|
||||
<div class="mx-auto flex h-14 items-center justify-between px-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<button
|
||||
class="text-foreground flex h-9 w-9 cursor-pointer items-center justify-center rounded border-none bg-transparent lg:hidden"
|
||||
@@ -207,20 +217,30 @@
|
||||
<!-- Sub Navbar with Tabs -->
|
||||
{#if config.navigation?.tabs && config.navigation.tabs.length > 1}
|
||||
<div class="border-border/50 px-0">
|
||||
<nav class="mx-auto flex h-10 items-center gap-2 px-4">
|
||||
{#each config.navigation.tabs as tab, index (`${tab.name}-${index}`)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="rounded-none border-0 {tab.key === getActiveTabKey()
|
||||
? 'border-b-accent-foreground! border-b!'
|
||||
: ''}"
|
||||
onclick={() => selectTab(tab)}
|
||||
>
|
||||
{tab.name}
|
||||
</Button>
|
||||
{/each}
|
||||
</nav>
|
||||
<div class="mx-auto flex h-10 items-center justify-between px-4">
|
||||
<nav class="flex min-w-0 items-center gap-2 overflow-x-auto">
|
||||
{#each config.navigation.tabs as tab, index (`${tab.name}-${index}`)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="rounded-none border-0 {tab.key === getActiveTabKey()
|
||||
? 'border-b-accent-foreground! border-b!'
|
||||
: ''}"
|
||||
onclick={() => selectTab(tab)}
|
||||
>
|
||||
{tab.name}
|
||||
</Button>
|
||||
{/each}
|
||||
</nav>
|
||||
|
||||
<a
|
||||
href={getLlmsHref()}
|
||||
rel="external"
|
||||
class="text-muted-foreground hover:text-foreground ml-4 shrink-0 text-xs no-underline transition-colors duration-200"
|
||||
>
|
||||
llms.txt
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { error, type RequestHandler } from "@sveltejs/kit";
|
||||
import { getDocsVersionBySlug, getVersionDocsLlmEntries } from "../../docs-utils.server";
|
||||
|
||||
const LLM_BASE_DOMAIN = "https://kener.ing";
|
||||
|
||||
export const GET: RequestHandler = ({ params }) => {
|
||||
const versionSlug = params.version;
|
||||
|
||||
if (!versionSlug) {
|
||||
throw error(404, "Docs version not found");
|
||||
}
|
||||
|
||||
const version = getDocsVersionBySlug(versionSlug);
|
||||
|
||||
if (!version) {
|
||||
throw error(404, "Docs version not found");
|
||||
}
|
||||
|
||||
const entries = getVersionDocsLlmEntries(versionSlug, LLM_BASE_DOMAIN);
|
||||
|
||||
const lines = [
|
||||
"# Kener",
|
||||
"",
|
||||
"## Docs",
|
||||
"",
|
||||
...entries.map((entry) => {
|
||||
if (!entry.description) {
|
||||
return `- [${entry.title}](${entry.url})`;
|
||||
}
|
||||
|
||||
return `- [${entry.title}](${entry.url}): ${entry.description}`;
|
||||
}),
|
||||
];
|
||||
|
||||
const body = lines.join("\n");
|
||||
|
||||
return new Response(body, {
|
||||
headers: {
|
||||
"content-type": "text/plain; charset=utf-8",
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -58,7 +58,7 @@ Here are the changelogs for Kener. Changelogs are only published when there are
|
||||
|
||||
- Improved build time by migrating lucid-svelte to individual components
|
||||
|
||||
### Github Issues Resolved in this release {#v3-2-5-github-issues}
|
||||
### GitHub Issues Resolved in this release {#v3-2-5-github-issues}
|
||||
|
||||
- Feature: Enhancement of Webhook Custom Body Functionality [#238](https://github.com/rajnandan1/kener/issues/238)
|
||||
- Feature: Add LDAP for authentication [#210](https://github.com/rajnandan1/kener/issues/210)
|
||||
|
||||
@@ -25,7 +25,7 @@ metaTags:
|
||||
nav:
|
||||
- name: "Documentation"
|
||||
url: "/docs"
|
||||
- name: "Github"
|
||||
- name: "GitHub"
|
||||
iconURL: "/github.svg"
|
||||
url: "https://github.com/rajnandan1/kener"
|
||||
siteURL: https://kener.ing
|
||||
|
||||
@@ -39,7 +39,7 @@ pm2 start main.js
|
||||
docker.io/rajnandan1/kener:latest
|
||||
```
|
||||
|
||||
[Github Packages](https://github.com/rajnandan1/kener/pkgs/container/kener)
|
||||
[GitHub Packages](https://github.com/rajnandan1/kener/pkgs/container/kener)
|
||||
|
||||
```bash
|
||||
ghcr.io/rajnandan1/kener:latest
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Incident Management | Kener
|
||||
description: Kener uses Github to power incident management using labels
|
||||
description: Kener uses GitHub to power incident management using labels
|
||||
---
|
||||
|
||||
Kener lets you manage incidents using its dashboard or APIs.
|
||||
|
||||
@@ -97,8 +97,8 @@ export GH_TOKEN=some.token.for.github
|
||||
> **_NOTE:_** DO NOT forget the `$` sign in your monitor secret, otherwise it will not be picked up.
|
||||
|
||||
```yaml
|
||||
- name: Github Issues
|
||||
description: Github Issues Fetch
|
||||
- name: GitHub Issues
|
||||
description: GitHub Issues Fetch
|
||||
tag: "gh-search-issue"
|
||||
api:
|
||||
method: GET
|
||||
@@ -112,8 +112,8 @@ export GH_TOKEN=some.token.for.github
|
||||
Assuming `ORDER_ID` is present in env
|
||||
|
||||
```yaml
|
||||
- name: Github Issues
|
||||
description: Github Issues Fetch
|
||||
- name: GitHub Issues
|
||||
description: GitHub Issues Fetch
|
||||
tag: "gh-search-issue"
|
||||
api:
|
||||
method: POST
|
||||
@@ -130,8 +130,8 @@ Read more about [eval](https://kener.ing/docs/monitors#eval)
|
||||
Below example will call https://api.github.com/repos/rajnandan1/kener/issues. If the status code is 200 then it will be UP else DOWN. It will also check if the response time is greater than 2000ms then it will be DEGRADED.
|
||||
|
||||
```yaml
|
||||
- name: Github Issues
|
||||
description: Github Issues Fetch
|
||||
- name: GitHub Issues
|
||||
description: GitHub Issues Fetch
|
||||
tag: "gh-search-issue"
|
||||
api:
|
||||
method: GET
|
||||
@@ -208,7 +208,7 @@ The below monitor will show DEGRADED if 3 or more degraded status in a day and D
|
||||
|
||||
Make sure you have set up triggers in `server.yaml`. Read more about [alerts](/docs/alerting).
|
||||
|
||||
The below example will trigger an alert if the monitor is DOWN for 10 consecutive times. It will also create an incident in Github and send alerts to Webhook, Discord and Slack. It will also trigger an alert if the monitor is DEGRADED for 5 consecutive times. It will not create an incident in Github and send alerts to Webhook, Discord and Slack.
|
||||
The below example will trigger an alert if the monitor is DOWN for 10 consecutive times. It will also create an incident in GitHub and send alerts to Webhook, Discord and Slack. It will also trigger an alert if the monitor is DEGRADED for 5 consecutive times. It will not create an incident in GitHub and send alerts to Webhook, Discord and Slack.
|
||||
|
||||
```yaml
|
||||
- name: Earth
|
||||
|
||||
@@ -72,7 +72,7 @@ This is an anonymous JS function, it should return a **Promise**, that resolves
|
||||
The following example shows how to use the eval function to evaluate the response. The function checks if the status code is 2XX then the status is UP, if the status code is 5XX then the status is DOWN. If the response contains the word `Unknown Error` then the status is DOWN. If the response time is greater than 2000 then the status is DEGRADED.
|
||||
|
||||
```javascript
|
||||
;(async function (statusCode, responseTime, responseRaw, modules) {
|
||||
async function (statusCode, responseTime, responseRaw, modules) {
|
||||
let status = "DOWN"
|
||||
|
||||
//if the status code is 2XX then the status is UP
|
||||
@@ -94,7 +94,7 @@ The following example shows how to use the eval function to evaluate the respons
|
||||
status: status,
|
||||
latency: responseTime
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Example 2 {#example-2}
|
||||
@@ -102,7 +102,7 @@ The following example shows how to use the eval function to evaluate the respons
|
||||
This next example shows how to call another API withing eval. It is scrapping the second last script tag from the response and checking if the heading is "No recent issues" then the status is UP else it is DOWN.
|
||||
|
||||
```js
|
||||
;(async function (statusCode, responseTime, responseRaw, modules) {
|
||||
async function (statusCode, responseTime, responseRaw, modules) {
|
||||
let htmlString = responseRaw
|
||||
const scriptTags = htmlString.match(/<script[^>]*src="([^"]+)"[^>]*>/g)
|
||||
if (scriptTags && scriptTags.length >= 2) {
|
||||
@@ -126,7 +126,7 @@ This next example shows how to call another API withing eval. It is scrapping th
|
||||
status: "DOWN",
|
||||
latency: responseTime
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Example 3 {#example-3}
|
||||
@@ -134,7 +134,7 @@ This next example shows how to call another API withing eval. It is scrapping th
|
||||
The next example shows how to use cheerio to parse bitbucket status page and check if all the components are operational. If all the components are operational then the status is UP else it is DOWN.
|
||||
|
||||
```js
|
||||
;(async function (statusCode, responseTime, responseDataBase64, modules) {
|
||||
async function (statusCode, responseTime, responseDataBase64, modules) {
|
||||
let html = atob(responseDataBase64)
|
||||
const $ = modules.cheerio.load(html)
|
||||
const components = $(".components-section .components-container .component-container")
|
||||
@@ -150,7 +150,7 @@ The next example shows how to use cheerio to parse bitbucket status page and che
|
||||
status: status ? "UP" : "DOWN",
|
||||
latency: responseTime
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Examples {#examples}
|
||||
|
||||
@@ -50,12 +50,12 @@ This is an anonymous JS function, it should return a **Promise**, that resolves
|
||||
> `{status: "DEGRADED", latency: 200}`.
|
||||
|
||||
```javascript
|
||||
;(async function (responseTime, responseRaw) {
|
||||
async function (responseTime, responseRaw) {
|
||||
return {
|
||||
status: "UP",
|
||||
latency: responseTime
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
- `responseTime` **REQUIRED** is a number. It is the latency in milliseconds
|
||||
|
||||
@@ -30,7 +30,7 @@ This is an anonymous JS function, it should return a **Promise**, that resolves
|
||||
> `{status:"DEGRADED", latency: 200}`.
|
||||
|
||||
```javascript
|
||||
;(async function (arrayOfPings) {
|
||||
async function (arrayOfPings) {
|
||||
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
|
||||
return acc + ping.latency
|
||||
}, 0)
|
||||
@@ -43,7 +43,7 @@ This is an anonymous JS function, it should return a **Promise**, that resolves
|
||||
status: alive ? "UP" : "DOWN",
|
||||
latency: latencyTotal / arrayOfPings.length
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
- `arrayOfPings` **REQUIRED** is an array of Ping Response Objects as shown below.
|
||||
@@ -103,7 +103,7 @@ Each object in the array represents the ping response of a host.
|
||||
The following example shows how to use the eval function to evaluate the response. The function checks if the combined latency is more 10ms then returns `DEGRADED`.
|
||||
|
||||
```javascript
|
||||
;(async function (arrayOfPings) {
|
||||
async function (arrayOfPings) {
|
||||
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
|
||||
return acc + ping.latency
|
||||
}, 0)
|
||||
@@ -125,5 +125,5 @@ The following example shows how to use the eval function to evaluate the respons
|
||||
status: areAllOpen ? "UP" : "DOWN",
|
||||
latency: avgLatency
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
@@ -30,7 +30,7 @@ This is an anonymous JS function, it should return a **Promise**, that resolves
|
||||
> `{status:"DEGRADED", latency: 200}`.
|
||||
|
||||
```javascript
|
||||
;(async function (arrayOfPings) {
|
||||
async function (arrayOfPings) {
|
||||
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
|
||||
return acc + ping.latency
|
||||
}, 0)
|
||||
@@ -47,7 +47,7 @@ This is an anonymous JS function, it should return a **Promise**, that resolves
|
||||
status: alive ? "UP" : "DOWN",
|
||||
latency: latencyTotal / arrayOfPings.length
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
- `arrayOfPings` **REQUIRED** is an array of TCP Response Objects as shown below.
|
||||
@@ -98,7 +98,7 @@ Each object in the array represents the tcp response of a host.
|
||||
The following example shows how to use the eval function to evaluate the response. The function checks if the combined latency is more 10ms then returns `DEGRADED`.
|
||||
|
||||
```javascript
|
||||
;(async function (arrayOfPings) {
|
||||
async function (arrayOfPings) {
|
||||
let latencyTotal = arrayOfPings.reduce((acc, ping) => {
|
||||
return acc + ping.latency
|
||||
}, 0)
|
||||
@@ -124,5 +124,5 @@ The following example shows how to use the eval function to evaluate the respons
|
||||
status: areAllOpen ? "UP" : "DOWN",
|
||||
latency: avgLatency
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
@@ -115,7 +115,7 @@ To add a trigger to a monitor make sure you have created a trigger. You can read
|
||||
- Add details for either DOWN or DEGRADED.
|
||||
- Failure Threshold(Required): The number of consecutive failures before the trigger is activated.
|
||||
- Success Threshold(Required): The number of consecutive successes before the trigger is deactivated.
|
||||
- Create Incident: Chose whether to create an incident or not when the trigger is activated. The incident will be created in Github. So make sure you have set up the Github token in the environment variables. The incident will be closed when the monitor is back to UP.
|
||||
- Create Incident: Chose whether to create an incident or not when the trigger is activated. The incident will be created in GitHub. So make sure you have set up the GitHub token in the environment variables. The incident will be closed when the monitor is back to UP.
|
||||
- Severity (Required): The severity of the incident. It can be `Critcal` or `Warning`.
|
||||
- Custom Message (Required): Add your owner alert message.
|
||||
- Choose Triggers: Choose the triggers you want to activate the trigger. You can choose multiple triggers.
|
||||
|
||||
@@ -63,6 +63,13 @@ v4 introduces page-scoped status views and monitor mapping:
|
||||
|
||||
You can now publish multiple status pages/subpaths from one instance and control monitor visibility per page.
|
||||
|
||||
### Categories and monitor ordering moved to pages {#categories-and-monitor-ordering}
|
||||
|
||||
In v4, monitor grouping/display should be managed through **Pages**.
|
||||
|
||||
- Add monitors to a page and they will be shown in that page order.
|
||||
- The monitor order on a page is the order users see on the status page.
|
||||
|
||||
### Recurring maintenance scheduling {#recurring-maintenance-scheduling}
|
||||
|
||||
v4 adds RRULE-based maintenance scheduling with dedicated tables:
|
||||
@@ -112,16 +119,63 @@ Uptime behavior is more configurable in v4, including formula-level flexibility
|
||||
|
||||
v4 includes broader frontend/runtime improvements, including more client-driven data interactions for monitor and operational views, with improved responsiveness and control.
|
||||
|
||||
## Known migration issues (v3 → v4) {#known-migration-issues}
|
||||
|
||||
Community-reported issues when upgrading from v3 to v4. Review these before migrating.
|
||||
|
||||
### Uploaded files and custom fonts return 404 {#uploads-return-404}
|
||||
|
||||
v3 served static files (images, fonts) from the `/uploads/` directory. v4 stores images in the database and serves them from `/assets/images/[id]`.
|
||||
|
||||
After upgrading, any URL referencing `/uploads/...` will return 404. This affects:
|
||||
|
||||
- Custom font files (`.woff2`, `.woff`, `.ttf`) referenced in Custom CSS
|
||||
- Any image previously uploaded via the v3 dashboard
|
||||
|
||||
**Fix:** Re-upload site images (logo, favicon, etc.) through the v4 dashboard (**Manage → Site**). For custom fonts, host them externally (CDN or self-hosted URL) and update your Custom CSS `@font-face` `src` URLs accordingly.
|
||||
|
||||
### Subscriptions and subscribers are not migrated {#subscriptions-not-migrated}
|
||||
|
||||
The subscription system was fully redesigned in v4 with new tables (`subscriber_users`, `subscriber_methods`, `user_subscriptions_v2`). Data from v3 tables (`subscribers`, `subscriptions`, `subscription_triggers`) is **not** automatically migrated.
|
||||
|
||||
**Fix:** After upgrading, re-configure subscription settings in the v4 dashboard. Existing subscribers will need to re-subscribe.
|
||||
|
||||
### Monitor timeout type error {#monitor-timeout-type-error}
|
||||
|
||||
Monitors migrated from v3 may store the `timeout` value as a string (e.g. `"30000"`) instead of a number. This causes a runtime error:
|
||||
|
||||
```
|
||||
"msecs" argument must be of type number. Received type string ('30000')
|
||||
```
|
||||
|
||||
**Fix:** Open each affected monitor in the v4 dashboard, verify the timeout value, and save. Re-saving converts the value to the correct numeric type.
|
||||
|
||||
### Monitors not visible on status pages {#monitors-not-visible}
|
||||
|
||||
v4 introduced a [Pages](/docs/v4/pages) model where monitors must be explicitly assigned to a page before they appear on any status page. After migration, existing monitors will not be visible until assigned.
|
||||
|
||||
**Fix:** Go to **Manage → Pages**, create or edit a page, and assign your monitors to it.
|
||||
|
||||
### Site images (logo, favicon) need re-upload {#site-images-reupload}
|
||||
|
||||
Site branding images (logo, favicon) uploaded in v3 are stored as file paths pointing to `/uploads/`. v4 expects these as database-stored images.
|
||||
|
||||
**Fix:** Go to **Manage → Site** and re-upload your logo and favicon.
|
||||
|
||||
## Upgrade checklist from v3 {#upgrade-checklist-from-v3}
|
||||
|
||||
Before promoting v4 to production:
|
||||
|
||||
1. Configure Redis (`REDIS_URL`).
|
||||
2. Update all heartbeat callers to `/ext/heartbeat/{tag}:{secret}`.
|
||||
3. Revalidate subscription data/flows against the new v4 system.
|
||||
4. Audit integrations/scripts for removed v3 API routes.
|
||||
5. Regenerate or re-import API clients from `static/api-references/v4.json`.
|
||||
6. Validate custom domain + `ORIGIN` and auth form behavior in production.
|
||||
3. Re-upload site images (logo, favicon) via **Manage → Site** — v3 `/uploads/` paths no longer work.
|
||||
4. Assign monitors to pages via **Manage → Pages** — monitors are not visible until assigned.
|
||||
5. Open and re-save any monitors that used a custom timeout to fix string-to-number type issues.
|
||||
6. Re-configure subscriptions — v3 subscriber data is not migrated to the new v4 system.
|
||||
7. If you used custom fonts via `/uploads/`, host them externally and update Custom CSS `@font-face` URLs.
|
||||
8. Audit integrations/scripts for removed v3 API routes.
|
||||
9. Regenerate or re-import API clients from `static/api-references/v4.json`.
|
||||
10. Validate custom domain + `ORIGIN` and auth form behavior in production.
|
||||
|
||||
## See also {#see-also}
|
||||
|
||||
|
||||
@@ -13,10 +13,6 @@ Most of the default settings are designed to work out of the box, but you can cu
|
||||
|
||||
- **Site Name**: The name of your status page. This will be displayed in the header and title of the page.
|
||||
- **Site URL**: The URL of your status page. This is used for sharing and linking to your status page.
|
||||
- **Home Path**: The path to the home page of your status page. This is used for routing and navigation within your status page. By default, it is set to `/`, but you can change it to something else if you want.
|
||||
|
||||
> [!NOTE]
|
||||
> If you are hosting site on a subpath, make sure to set the Home Path to the correct value. For example, if your site is hosted at `https://example.com/status`, then you should set the Home Path to `/status`.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Setting up the Site URL correctly is very important for Kener to function properly.
|
||||
|
||||
@@ -6,7 +6,7 @@ description: Welcome to Kener - the modern, open-source status page system
|
||||
Kener is an open-source status page system designed to help you monitor and communicate the status of your services effectively.
|
||||
|
||||
> [!NOTE]
|
||||
> This documentation is for kener version 4x. There are few changes from version 3.x. Please refer to the [migration guide](/docs/migration/v3-to-v4) if you are upgrading from version 3.x. If you are looking for version 3.x documentation, you can find it [here](/docs/v3/home).
|
||||
> This documentation is for kener version 4x. There are few changes from version 3.x. Please refer to the [migration guide](/docs/v4/changelogs/v4.0.0) if you are upgrading from version 3.x. If you are looking for version 3.x documentation, you can find it [here](/docs/v3/home).
|
||||
|
||||
Kener is a simple status monitoring app that helps you keep your users informed about the status of your services. It is built with Node.js and Svelte, and uses Redis for caching.
|
||||
|
||||
@@ -29,4 +29,4 @@ Ready to get started? Head over to the [Quick Start](/docs/v4/getting-started/qu
|
||||
1. [GitHub Sponsors](https://github.com/sponsors/rajnandan1)
|
||||
2. [PayPal](https://www.paypal.com/paypalme/rajnandan1)
|
||||
3. [Buy Me a Coffee](https://www.buymeacoffee.com/rajnandan1)
|
||||
4. [Github Star](https://github.com/rajnandan1/kener)
|
||||
4. [GitHub Star](https://github.com/rajnandan1/kener)
|
||||
|
||||
@@ -5,6 +5,8 @@ description: Get Kener up and running in under 5 minutes
|
||||
|
||||
Get Kener up and running in under 5 minutes with this quick start guide.
|
||||
|
||||
For production-focused deployment details (Docker image options, Node.js server setup, and healthcheck URL), use [Deployment](/docs/v4/setup/deployment).
|
||||
|
||||
## Docker Quick Start {#docker-quick-start}
|
||||
|
||||
The fastest way to get started is with Docker Compose.
|
||||
@@ -22,76 +24,6 @@ Kener will be available at `http://localhost:3000`.
|
||||
> [!IMPORTANT]
|
||||
> Set a strong value for `KENER_SECRET_KEY` and set `ORIGIN` to your public URL in `docker-compose.yml` before starting.
|
||||
|
||||
### Run pre-built image {#run-pre-built-image-docker-hub-or-ghcr}
|
||||
|
||||
You can pull Kener from either registry:
|
||||
|
||||
- Docker Hub: `docker.io/rajnandan1/kener:latest`
|
||||
- GHCR: `ghcr.io/rajnandan1/kener:latest`
|
||||
|
||||
Example with Docker Hub:
|
||||
|
||||
```bash
|
||||
mkdir -p database
|
||||
docker run -d \
|
||||
--name kener \
|
||||
-p 3000:3000 \
|
||||
-v "$(pwd)/database:/app/database" \
|
||||
--env-file .env \
|
||||
docker.io/rajnandan1/kener:latest
|
||||
```
|
||||
|
||||
Same command with GHCR:
|
||||
|
||||
```bash
|
||||
mkdir -p database
|
||||
docker run -d \
|
||||
--name kener \
|
||||
-p 3000:3000 \
|
||||
-v "$(pwd)/database:/app/database" \
|
||||
--env-file .env \
|
||||
ghcr.io/rajnandan1/kener:latest
|
||||
```
|
||||
|
||||
Minimum `.env` for Docker:
|
||||
|
||||
```dotenv
|
||||
KENER_SECRET_KEY=replace_with_a_random_string
|
||||
ORIGIN=http://localhost:3000
|
||||
REDIS_URL=redis://host.docker.internal:6379
|
||||
PORT=3000
|
||||
```
|
||||
|
||||
Or pass required variables directly with `-e`:
|
||||
|
||||
```bash
|
||||
mkdir -p database
|
||||
docker run -d \
|
||||
--name kener \
|
||||
-p 3000:3000 \
|
||||
-v "$(pwd)/database:/app/database" \
|
||||
-e "KENER_SECRET_KEY=replace_with_a_random_string" \
|
||||
-e "ORIGIN=http://localhost:3000" \
|
||||
-e "REDIS_URL=redis://host.docker.internal:6379" \
|
||||
docker.io/rajnandan1/kener:latest
|
||||
```
|
||||
|
||||
If you want to build locally from source:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/rajnandan1/kener.git
|
||||
cd kener
|
||||
docker build -t kener:local .
|
||||
|
||||
mkdir -p database
|
||||
docker run -d \
|
||||
--name kener-local \
|
||||
-p 3000:3000 \
|
||||
-v "$(pwd)/database:/app/database" \
|
||||
--env-file .env \
|
||||
kener:local
|
||||
```
|
||||
|
||||
### Build from local source (optional) {#docker-build-from-local-source-optional}
|
||||
|
||||
Use this if you want to test your local changes in Docker:
|
||||
@@ -100,43 +32,7 @@ Use this if you want to test your local changes in Docker:
|
||||
docker compose -f docker-compose.dev.yml up -d --build
|
||||
```
|
||||
|
||||
## Non-Docker Quick Start {#non-docker-quick-start}
|
||||
|
||||
Use this path if you want to run Kener directly with Node.js.
|
||||
|
||||
### Requirements {#non-docker-requirements}
|
||||
|
||||
- Node.js `>= 20`
|
||||
- Redis
|
||||
|
||||
### Steps {#non-docker-steps}
|
||||
|
||||
```bash
|
||||
git clone https://github.com/rajnandan1/kener.git
|
||||
cd kener
|
||||
npm install
|
||||
|
||||
# Start Redis (example)
|
||||
docker run -d --name kener-redis -p 6379:6379 redis:7-alpine
|
||||
```
|
||||
|
||||
Create or update your `.env`:
|
||||
|
||||
```dotenv
|
||||
KENER_SECRET_KEY=replace_with_a_random_string
|
||||
ORIGIN=http://localhost:3000
|
||||
REDIS_URL=redis://localhost:6379
|
||||
PORT=3000
|
||||
# Optional (defaults to SQLite):
|
||||
# DATABASE_URL=sqlite://./database/kener.sqlite.db
|
||||
```
|
||||
|
||||
Then build and start:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
npm run start
|
||||
```
|
||||
Need full deployment options? See [Deployment](/docs/v4/setup/deployment).
|
||||
|
||||
## Development Setup {#development-setup}
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
title: How to Add Custom Fonts
|
||||
description: Use self-hosted or external font files with Kener via Custom CSS
|
||||
---
|
||||
|
||||
Kener supports two ways to use custom fonts on your status page:
|
||||
|
||||
1. **Font CSS URL** — point to a hosted stylesheet (Google Fonts, Bunny Fonts, etc.)
|
||||
2. **Custom CSS** — define `@font-face` rules for self-hosted font files
|
||||
|
||||
## Use a hosted font {#use-a-hosted-font}
|
||||
|
||||
If your font is available from a CDN (Google Fonts, Bunny Fonts, etc.):
|
||||
|
||||
1. Go to **Manage → Customizations**.
|
||||
2. In the **Font** card, set **Font CSS URL** to the stylesheet URL — e.g. `https://fonts.bunny.net/css?family=lato:400,700&display=swap`.
|
||||
3. Set **Font Family Name** to the font name — e.g. `Lato`.
|
||||
4. Click **Save Font**.
|
||||
|
||||
## Use a self-hosted font {#use-a-self-hosted-font}
|
||||
|
||||
If you want to use your own font files (`.woff2`, `.woff`, `.ttf`), host them on a CDN or static file server and reference them in Custom CSS.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Kener v4 does not serve files from a local `/uploads/` directory. Font files must be hosted at a publicly accessible URL.
|
||||
|
||||
1. Upload your font files to a CDN or static host and note the URLs.
|
||||
2. Go to **Manage → Customizations**.
|
||||
3. Clear the **Font CSS URL** and **Font Family Name** fields in the **Font** card and save.
|
||||
4. In the **Custom CSS** card, add your `@font-face` rules:
|
||||
|
||||
```css
|
||||
@font-face {
|
||||
font-family: "CustomFont";
|
||||
src:
|
||||
url("https://cdn.example.com/fonts/CustomFont.woff2") format("woff2"),
|
||||
url("https://cdn.example.com/fonts/CustomFont.woff") format("woff"),
|
||||
url("https://cdn.example.com/fonts/CustomFont.ttf") format("truetype");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
* {
|
||||
font-family: "CustomFont", sans-serif;
|
||||
}
|
||||
```
|
||||
|
||||
5. Click **Save Custom CSS**.
|
||||
|
||||
## Verify {#verify}
|
||||
|
||||
Open your public status page and confirm the font renders correctly. Use browser DevTools → **Computed** tab on any text element to confirm the applied `font-family`.
|
||||
|
||||
## Migrating from v3 {#migrating-from-v3}
|
||||
|
||||
In v3, font files could be placed in the `/uploads/` directory and referenced as `/uploads/CustomFont.woff2`. This path no longer works in v4.
|
||||
|
||||
**Fix:** Move your font files to an external host and update the `@font-face` `src` URLs accordingly.
|
||||
|
||||
## See also {#see-also}
|
||||
|
||||
- [Site Customizations](/docs/v4/setup/customizations)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user