style: prettify files touched by Last Known Status work

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Raj Nandan Sharma
2026-06-07 23:18:22 +05:30
parent c4c16d65a6
commit 74b6311e81
3 changed files with 242 additions and 245 deletions
@@ -17,6 +17,7 @@
### Task 1: Constants + alert-visible whitelist
**Files:**
- Modify: `src/lib/global-constants.ts:43`
- Modify: `src/lib/server/db/repositories/monitoring.ts:13-20`
@@ -50,7 +51,7 @@ In `src/lib/server/db/repositories/monitoring.ts`, replace lines 13-20:
* SIGNAL rows (raw heartbeat receipts) and INCIDENT/MAINTENANCE overlays stay invisible, so the
* alert window freezes during manual overlays instead of triggering or resolving on them.
*/
const ALERT_VISIBLE_TYPES = [GC.REALTIME, GC.ERROR, GC.TIMEOUT, GC.MANUAL, GC.DEFAULT_STATUS];
const ALERT_VISIBLE_TYPES = [GC.REALTIME, GC.ERROR, GC.TIMEOUT, GC.MANUAL, GC.DEFAULT_STATUS]
```
with:
@@ -65,7 +66,7 @@ with:
* SIGNAL rows (raw heartbeat receipts) and INCIDENT/MAINTENANCE overlays stay invisible, so the
* alert window freezes during manual overlays instead of triggering or resolving on them.
*/
const ALERT_VISIBLE_TYPES = [GC.REALTIME, GC.ERROR, GC.TIMEOUT, GC.MANUAL, GC.DEFAULT_STATUS, GC.CARRIED];
const ALERT_VISIBLE_TYPES = [GC.REALTIME, GC.ERROR, GC.TIMEOUT, GC.MANUAL, GC.DEFAULT_STATUS, GC.CARRIED]
```
- [ ] **Step 3: Type-check**
@@ -85,6 +86,7 @@ git commit -m "feat(constants): add CARRIED sample type and LAST_KNOWN default s
### Task 2: Repository — latest alert-visible sample query
**Files:**
- Modify: `src/lib/server/db/repositories/monitoring.ts` (after `getLatestMonitoringData`, line 79)
- Modify: `src/lib/server/db/dbimpl.ts:52-53` (declaration) and `:412` (binding)
@@ -120,7 +122,7 @@ In `src/lib/server/db/dbimpl.ts`, after line 52 (`getLatestMonitoringData!: ...`
and after line 412 (`this.getLatestMonitoringData = ...bind(this.monitoring);`), add the binding:
```typescript
this.getLatestAlertVisibleData = this.monitoring.getLatestAlertVisibleData.bind(this.monitoring);
this.getLatestAlertVisibleData = this.monitoring.getLatestAlertVisibleData.bind(this.monitoring)
```
- [ ] **Step 3: Write the throwaway verification script**
@@ -128,42 +130,42 @@ and after line 412 (`this.getLatestMonitoringData = ...bind(this.monitoring);`),
Create `scripts/tmp-verify-carry-source.ts` (will be deleted, never committed):
```typescript
import Knex from "knex";
import { MonitoringRepository } from "../src/lib/server/db/repositories/monitoring";
import Knex from "knex"
import { MonitoringRepository } from "../src/lib/server/db/repositories/monitoring"
const knex = Knex({ client: "better-sqlite3", connection: { filename: ":memory:" }, useNullAsDefault: true });
const knex = Knex({ client: "better-sqlite3", connection: { filename: ":memory:" }, useNullAsDefault: true })
await knex.schema.createTable("monitoring_data", (t) => {
t.string("monitor_tag");
t.integer("timestamp");
t.string("status");
t.float("latency");
t.string("type");
t.text("error_message");
t.primary(["monitor_tag", "timestamp"]);
});
t.string("monitor_tag")
t.integer("timestamp")
t.string("status")
t.float("latency")
t.string("type")
t.text("error_message")
t.primary(["monitor_tag", "timestamp"])
})
const repo = new MonitoringRepository(knex);
const repo = new MonitoringRepository(knex)
// Timeline: MANUAL DOWN, then a CARRIED copy, then an INCIDENT overlay, then a SIGNAL receipt.
await knex("monitoring_data").insert([
{ monitor_tag: "t", timestamp: 100, status: "DOWN", latency: 42, type: "MANUAL" },
{ monitor_tag: "t", timestamp: 160, status: "DOWN", latency: 42, type: "CARRIED" },
{ monitor_tag: "t", timestamp: 220, status: "UP", latency: 0, type: "INCIDENT" },
{ monitor_tag: "t", timestamp: 280, status: "UP", latency: 0, type: "SIGNAL" },
]);
{ monitor_tag: "t", timestamp: 100, status: "DOWN", latency: 42, type: "MANUAL" },
{ monitor_tag: "t", timestamp: 160, status: "DOWN", latency: 42, type: "CARRIED" },
{ monitor_tag: "t", timestamp: 220, status: "UP", latency: 0, type: "INCIDENT" },
{ monitor_tag: "t", timestamp: 280, status: "UP", latency: 0, type: "SIGNAL" }
])
const latest = await repo.getLatestAlertVisibleData("t");
console.log("latest:", latest);
const latest = await repo.getLatestAlertVisibleData("t")
console.log("latest:", latest)
if (!latest || latest.timestamp !== 160 || latest.type !== "CARRIED" || latest.status !== "DOWN") {
throw new Error("FAIL: expected the CARRIED row at ts=160 (INCIDENT/SIGNAL must be skipped)");
throw new Error("FAIL: expected the CARRIED row at ts=160 (INCIDENT/SIGNAL must be skipped)")
}
const none = await repo.getLatestAlertVisibleData("missing");
if (none !== undefined) throw new Error("FAIL: expected undefined for unknown tag");
const none = await repo.getLatestAlertVisibleData("missing")
if (none !== undefined) throw new Error("FAIL: expected undefined for unknown tag")
console.log("PASS");
await knex.destroy();
console.log("PASS")
await knex.destroy()
```
- [ ] **Step 4: Run it**
@@ -185,6 +187,7 @@ git commit -m "feat(db): add getLatestAlertVisibleData query for last-known-stat
### Task 3: Engine — carry fill in the execute worker
**Files:**
- Modify: `src/lib/server/queues/monitorExecuteQueue.ts:125-156`
- [ ] **Step 1: Extend the defaultData branch**
@@ -192,55 +195,55 @@ git commit -m "feat(db): add getLatestAlertVisibleData query for last-known-stat
In `src/lib/server/queues/monitorExecuteQueue.ts`, replace lines 125-139:
```typescript
let defaultData: MonitoringResultTS = {};
let mergedData: MonitoringResultTS = {};
let defaultData: MonitoringResultTS = {}
let mergedData: MonitoringResultTS = {}
if (monitor.default_status !== undefined && monitor.default_status !== null) {
if (([GC.UP, GC.DOWN, GC.DEGRADED] as string[]).indexOf(monitor.default_status) !== -1) {
if (monitor.default_status !== undefined && monitor.default_status !== null) {
if (([GC.UP, GC.DOWN, GC.DEGRADED] as string[]).indexOf(monitor.default_status) !== -1) {
defaultData[ts] = {
status: monitor.default_status,
latency: 0,
type: GC.DEFAULT_STATUS,
};
if (monitor.default_status !== GC.UP) {
defaultData[ts].error_message = "Default status applied";
status: monitor.default_status,
latency: 0,
type: GC.DEFAULT_STATUS
}
if (monitor.default_status !== GC.UP) {
defaultData[ts].error_message = "Default status applied"
}
}
}
}
```
with:
```typescript
let defaultData: MonitoringResultTS = {};
let mergedData: MonitoringResultTS = {};
let defaultData: MonitoringResultTS = {}
let mergedData: MonitoringResultTS = {}
if (monitor.default_status !== undefined && monitor.default_status !== null) {
if (([GC.UP, GC.DOWN, GC.DEGRADED] as string[]).indexOf(monitor.default_status) !== -1) {
if (monitor.default_status !== undefined && monitor.default_status !== null) {
if (([GC.UP, GC.DOWN, GC.DEGRADED] as string[]).indexOf(monitor.default_status) !== -1) {
defaultData[ts] = {
status: monitor.default_status,
latency: 0,
type: GC.DEFAULT_STATUS,
};
if (monitor.default_status !== GC.UP) {
defaultData[ts].error_message = "Default status applied";
status: monitor.default_status,
latency: 0,
type: GC.DEFAULT_STATUS
}
} else if (monitor.default_status === GC.LAST_KNOWN) {
if (monitor.default_status !== GC.UP) {
defaultData[ts].error_message = "Default status applied"
}
} else if (monitor.default_status === GC.LAST_KNOWN) {
// Last Known Status fill (docs/adr/0006): repeat the most recent alert-visible
// sample — status and latency alike. No sample yet → nothing to carry → no fill.
const lastKnown = await db.getLatestAlertVisibleData(monitor.tag);
const lastKnown = await db.getLatestAlertVisibleData(monitor.tag)
if (lastKnown && lastKnown.status) {
defaultData[ts] = {
status: lastKnown.status,
latency: lastKnown.latency ?? 0,
type: GC.CARRIED,
};
if (lastKnown.status !== GC.UP) {
defaultData[ts].error_message = "Last known status applied";
}
defaultData[ts] = {
status: lastKnown.status,
latency: lastKnown.latency ?? 0,
type: GC.CARRIED
}
if (lastKnown.status !== GC.UP) {
defaultData[ts].error_message = "Last known status applied"
}
}
}
}
}
```
- [ ] **Step 2: Fix the NO_DATA-preference block to preserve the fill's type**
@@ -248,40 +251,40 @@ with:
Still in the same file, the block at (previously) lines 141-156 hardcodes `type: GC.DEFAULT_STATUS` when realtime returns NO_DATA but a fill exists. A heartbeat monitor with `LAST_KNOWN` would mislabel its carried rows. Replace:
```typescript
const defaultStatus = defaultData[ts]?.status;
const realtimeStatus = realtimeData[ts]?.status;
let realtimeDataForMerge = realtimeData;
if (defaultStatus && realtimeStatus === GC.NO_DATA) {
// Apply the preference *before* merging so incident/maintenance can still override later.
// Also avoid carrying over realtime NO_DATA error_message.
realtimeDataForMerge = { ...realtimeData };
realtimeDataForMerge[ts] = {
const defaultStatus = defaultData[ts]?.status
const realtimeStatus = realtimeData[ts]?.status
let realtimeDataForMerge = realtimeData
if (defaultStatus && realtimeStatus === GC.NO_DATA) {
// Apply the preference *before* merging so incident/maintenance can still override later.
// Also avoid carrying over realtime NO_DATA error_message.
realtimeDataForMerge = { ...realtimeData }
realtimeDataForMerge[ts] = {
...realtimeDataForMerge[ts],
status: defaultStatus,
type: GC.DEFAULT_STATUS,
};
delete realtimeDataForMerge[ts].error_message;
type: GC.DEFAULT_STATUS
}
delete realtimeDataForMerge[ts].error_message
}
```
with:
```typescript
const defaultStatus = defaultData[ts]?.status;
const realtimeStatus = realtimeData[ts]?.status;
let realtimeDataForMerge = realtimeData;
if (defaultStatus && realtimeStatus === GC.NO_DATA) {
// Apply the preference *before* merging so incident/maintenance can still override later.
// Also avoid carrying over realtime NO_DATA error_message.
// Keep the fill's own type: DEFAULT for fixed fill, CARRIED for last-known fill.
realtimeDataForMerge = { ...realtimeData };
realtimeDataForMerge[ts] = {
const defaultStatus = defaultData[ts]?.status
const realtimeStatus = realtimeData[ts]?.status
let realtimeDataForMerge = realtimeData
if (defaultStatus && realtimeStatus === GC.NO_DATA) {
// Apply the preference *before* merging so incident/maintenance can still override later.
// Also avoid carrying over realtime NO_DATA error_message.
// Keep the fill's own type: DEFAULT for fixed fill, CARRIED for last-known fill.
realtimeDataForMerge = { ...realtimeData }
realtimeDataForMerge[ts] = {
...realtimeDataForMerge[ts],
status: defaultStatus,
type: defaultData[ts].type,
};
delete realtimeDataForMerge[ts].error_message;
type: defaultData[ts].type
}
delete realtimeDataForMerge[ts].error_message
}
```
Note: `latency` in this branch intentionally stays whatever realtime reported — unchanged from today for fixed fill; for LAST_KNOWN-on-heartbeat the carried latency was already placed in `defaultData[ts]` and `mergedData` spread order (`{ ...defaultData, ...realtimeDataForMerge, ... }`) means the realtime object wins the spread; this matches existing fixed-fill behavior, do not "improve" it here.
@@ -305,6 +308,7 @@ git commit -m "feat(scheduler): write CARRIED samples for LAST_KNOWN default sta
### Task 4: Normalization chokepoint for all monitor writes
**Files:**
- Modify: `src/lib/server/controllers/monitorsController.ts` (near `CreateUpdateMonitor`, line 193)
- Modify: `src/routes/(api)/api/v4/monitors/+server.ts:104-121`
- Modify: `src/routes/(api)/api/v4/monitors/[monitor_tag]/+server.ts:72-78`
@@ -314,7 +318,7 @@ git commit -m "feat(scheduler): write CARRIED samples for LAST_KNOWN default sta
Directly above `CreateUpdateMonitor` (line 193), add:
```typescript
const VALID_DEFAULT_STATUSES = ["NONE", GC.UP, GC.DOWN, GC.DEGRADED, GC.LAST_KNOWN] as const;
const VALID_DEFAULT_STATUSES = ["NONE", GC.UP, GC.DOWN, GC.DEGRADED, GC.LAST_KNOWN] as const
/**
* Enforce the closed default_status value set and the LAST_KNOWN scope rule
@@ -322,19 +326,16 @@ const VALID_DEFAULT_STATUSES = ["NONE", GC.UP, GC.DOWN, GC.DEGRADED, GC.LAST_KNO
* on any other type it silently resets to UP so the invalid combination never persists.
* Throws on values outside the closed set.
*/
export const NormalizeDefaultStatus = (
monitorType: string | null | undefined,
defaultStatus: string | null | undefined,
): string => {
const value = defaultStatus ?? "NONE";
if (!(VALID_DEFAULT_STATUSES as readonly string[]).includes(value)) {
throw new Error(`default_status must be one of: ${VALID_DEFAULT_STATUSES.join(", ")}`);
}
if (value === GC.LAST_KNOWN && monitorType !== "NONE") {
return GC.UP;
}
return value;
};
export const NormalizeDefaultStatus = (monitorType: string | null | undefined, defaultStatus: string | null | undefined): string => {
const value = defaultStatus ?? "NONE"
if (!(VALID_DEFAULT_STATUSES as readonly string[]).includes(value)) {
throw new Error(`default_status must be one of: ${VALID_DEFAULT_STATUSES.join(", ")}`)
}
if (value === GC.LAST_KNOWN && monitorType !== "NONE") {
return GC.UP
}
return value
}
```
(`monitorsController.ts` already imports `GC` at line 25 — no import change needed.)
@@ -345,29 +346,29 @@ Replace `CreateUpdateMonitor` (lines 193-201):
```typescript
export const CreateUpdateMonitor = async (monitor: MonitorInput): Promise<number | number[]> => {
let monitorData = { ...monitor };
if (monitorData.id) {
return await db.updateMonitor(monitorData as MonitorRecord);
} else {
validateMonitorTag(monitorData.tag);
return await db.insertMonitor(monitorData);
}
};
let monitorData = { ...monitor }
if (monitorData.id) {
return await db.updateMonitor(monitorData as MonitorRecord)
} else {
validateMonitorTag(monitorData.tag)
return await db.insertMonitor(monitorData)
}
}
```
with:
```typescript
export const CreateUpdateMonitor = async (monitor: MonitorInput): Promise<number | number[]> => {
let monitorData = { ...monitor };
monitorData.default_status = NormalizeDefaultStatus(monitorData.monitor_type, monitorData.default_status);
if (monitorData.id) {
return await db.updateMonitor(monitorData as MonitorRecord);
} else {
validateMonitorTag(monitorData.tag);
return await db.insertMonitor(monitorData);
}
};
let monitorData = { ...monitor }
monitorData.default_status = NormalizeDefaultStatus(monitorData.monitor_type, monitorData.default_status)
if (monitorData.id) {
return await db.updateMonitor(monitorData as MonitorRecord)
} else {
validateMonitorTag(monitorData.tag)
return await db.insertMonitor(monitorData)
}
}
```
(The manage API route at `src/routes/(manage)/manage/api/+server.ts` wraps the action switch in try/catch (line 660) and surfaces thrown `Error.message` — no route change needed.)
@@ -377,7 +378,7 @@ export const CreateUpdateMonitor = async (monitor: MonitorInput): Promise<number
In `src/routes/(api)/api/v4/monitors/+server.ts`, add to the imports from the monitors controller (`GetMonitorsParsed` is already imported — extend that import):
```typescript
import { GetMonitorsParsed, NormalizeDefaultStatus } from "$lib/server/controllers/monitorsController";
import { GetMonitorsParsed, NormalizeDefaultStatus } from "$lib/server/controllers/monitorsController"
```
(match the existing import line's exact shape — if `GetMonitorsParsed` is imported from a different specifier, add `NormalizeDefaultStatus` to that same line).
@@ -391,18 +392,18 @@ Then replace line 111:
with a pre-validated variable. Above the `const monitorData = {` block (line 104), insert:
```typescript
let defaultStatus: string;
try {
defaultStatus = NormalizeDefaultStatus(body.monitor_type ?? "API", body.default_status ?? "UP");
} catch (e) {
let defaultStatus: string
try {
defaultStatus = NormalizeDefaultStatus(body.monitor_type ?? "API", body.default_status ?? "UP")
} catch (e) {
const errorResponse: BadRequestResponse = {
error: {
code: "BAD_REQUEST",
message: e instanceof Error ? e.message : "Invalid default_status",
},
};
return json(errorResponse, { status: 400 });
}
error: {
code: "BAD_REQUEST",
message: e instanceof Error ? e.message : "Invalid default_status"
}
}
return json(errorResponse, { status: 400 })
}
```
and in `monitorData` use:
@@ -413,37 +414,34 @@ and in `monitorData` use:
- [ ] **Step 4: Apply it in v4 PATCH**
In `src/routes/(api)/api/v4/monitors/[monitor_tag]/+server.ts`, the handler resolves `updateData.monitor_type` at line 78 *after* `updateData.default_status` at line 75 — the normalization must run after BOTH are resolved. Replace line 75:
In `src/routes/(api)/api/v4/monitors/[monitor_tag]/+server.ts`, the handler resolves `updateData.monitor_type` at line 78 _after_ `updateData.default_status` at line 75 — the normalization must run after BOTH are resolved. Replace line 75:
```typescript
updateData.default_status = body.default_status !== undefined ? body.default_status : existingMonitor.default_status;
updateData.default_status = body.default_status !== undefined ? body.default_status : existingMonitor.default_status
```
with (keep it in place so field ordering stays readable, but move the value through the helper after line 78):
```typescript
updateData.default_status = body.default_status !== undefined ? body.default_status : existingMonitor.default_status;
updateData.default_status = body.default_status !== undefined ? body.default_status : existingMonitor.default_status
```
…and after line 78 (`updateData.monitor_type = ...`), insert:
```typescript
// Closed-set validation + LAST_KNOWN scope rule (docs/adr/0006). Runs after monitor_type
// is resolved so a type change away from NONE auto-resets LAST_KNOWN to UP.
try {
updateData.default_status = NormalizeDefaultStatus(
updateData.monitor_type as string,
updateData.default_status as string | null,
);
} catch (e) {
// Closed-set validation + LAST_KNOWN scope rule (docs/adr/0006). Runs after monitor_type
// is resolved so a type change away from NONE auto-resets LAST_KNOWN to UP.
try {
updateData.default_status = NormalizeDefaultStatus(updateData.monitor_type as string, updateData.default_status as string | null)
} catch (e) {
const errorResponse: BadRequestResponse = {
error: {
code: "BAD_REQUEST",
message: e instanceof Error ? e.message : "Invalid default_status",
},
};
return json(errorResponse, { status: 400 });
}
error: {
code: "BAD_REQUEST",
message: e instanceof Error ? e.message : "Invalid default_status"
}
}
return json(errorResponse, { status: 400 })
}
```
Add `NormalizeDefaultStatus` to this file's monitors-controller import the same way as in Step 3.
@@ -453,26 +451,26 @@ Add `NormalizeDefaultStatus` to this file's monitors-controller import the same
Create `scripts/tmp-verify-normalize.ts`:
```typescript
import { NormalizeDefaultStatus } from "../src/lib/server/controllers/monitorsController";
import { NormalizeDefaultStatus } from "../src/lib/server/controllers/monitorsController"
const cases: Array<[string, string | null, string]> = [
["NONE", "LAST_KNOWN", "LAST_KNOWN"], // allowed on Manual monitors
["API", "LAST_KNOWN", "UP"], // auto-reset on any other type
["NONE", null, "NONE"], // null → NONE
["API", "DOWN", "DOWN"], // fixed values pass through
];
["NONE", "LAST_KNOWN", "LAST_KNOWN"], // allowed on Manual monitors
["API", "LAST_KNOWN", "UP"], // auto-reset on any other type
["NONE", null, "NONE"], // null → NONE
["API", "DOWN", "DOWN"] // fixed values pass through
]
for (const [type, input, expected] of cases) {
const got = NormalizeDefaultStatus(type, input);
if (got !== expected) throw new Error(`FAIL: (${type}, ${input}) → ${got}, expected ${expected}`);
const got = NormalizeDefaultStatus(type, input)
if (got !== expected) throw new Error(`FAIL: (${type}, ${input}) → ${got}, expected ${expected}`)
}
let threw = false;
let threw = false
try {
NormalizeDefaultStatus("API", "MAINTENANCE");
NormalizeDefaultStatus("API", "MAINTENANCE")
} catch {
threw = true;
threw = true
}
if (!threw) throw new Error("FAIL: MAINTENANCE must be rejected");
console.log("PASS");
if (!threw) throw new Error("FAIL: MAINTENANCE must be rejected")
console.log("PASS")
```
Run: `npx vite-node scripts/tmp-verify-normalize.ts`
@@ -492,26 +490,27 @@ git commit -m "feat(api): enforce closed default_status set with LAST_KNOWN scop
### Task 5: Migration — normalize legacy default_status values
**Files:**
- Create: `migrations/20260607150000_normalize_default_status.ts`
- [ ] **Step 1: Write the migration**
```typescript
import type { Knex } from "knex";
import type { Knex } from "knex"
// Closed default_status set as of docs/adr/0006. MAINTENANCE was offered by the old
// UI but never honored by the fill engine — it behaved exactly like "no fill", so it
// (and any other unknown value, and NULL) normalizes to NONE, preserving behavior.
const VALID = ["NONE", "UP", "DOWN", "DEGRADED", "LAST_KNOWN"];
const VALID = ["NONE", "UP", "DOWN", "DEGRADED", "LAST_KNOWN"]
export async function up(knex: Knex): Promise<void> {
await knex("monitors").whereNull("default_status").update({ default_status: "NONE" });
await knex("monitors").whereNotIn("default_status", VALID).update({ default_status: "NONE" });
await knex("monitors").whereNull("default_status").update({ default_status: "NONE" })
await knex("monitors").whereNotIn("default_status", VALID).update({ default_status: "NONE" })
}
export async function down(): Promise<void> {
// Irreversible by design: the values rewritten to NONE were dead (never honored
// by the fill engine), so there is nothing meaningful to restore.
// Irreversible by design: the values rewritten to NONE were dead (never honored
// by the fill engine), so there is nothing meaningful to restore.
}
```
@@ -542,6 +541,7 @@ git commit -m "feat(db): migrate default_status to closed value set"
### Task 6: Manage UI — dropdown options + callout + auto-reset
**Files:**
- Modify: `src/routes/(manage)/manage/app/monitors/[tag]/components/GeneralSettingsCard.svelte:230-247`
**REQUIRED SUB-SKILL for this task: `svelte-code-writer` (per CLAUDE.md, mandatory for all .svelte edits).**
@@ -551,29 +551,29 @@ git commit -m "feat(db): migrate default_status to closed value set"
In the `<script lang="ts">` block (GC is already imported at line 20), add to the imports:
```typescript
import * as Alert from "$lib/components/ui/alert/index.js";
import TriangleAlertIcon from "@lucide/svelte/icons/triangle-alert";
import * as Alert from "$lib/components/ui/alert/index.js"
import TriangleAlertIcon from "@lucide/svelte/icons/triangle-alert"
```
and below the props destructuring add:
```typescript
const defaultStatusLabels: Record<string, string> = {
const defaultStatusLabels: Record<string, string> = {
NONE: "None (show gaps as no data)",
UP: "UP",
DOWN: "DOWN",
DEGRADED: "DEGRADED",
LAST_KNOWN: "Last known status",
};
LAST_KNOWN: "Last known status"
}
// LAST_KNOWN is only valid on Manual (NONE-type) monitors; the server enforces the
// same rule (NormalizeDefaultStatus), this effect just keeps the UI honest live.
$effect(() => {
// LAST_KNOWN is only valid on Manual (NONE-type) monitors; the server enforces the
// same rule (NormalizeDefaultStatus), this effect just keeps the UI honest live.
$effect(() => {
if (monitor.monitor_type !== "NONE" && monitor.default_status === GC.LAST_KNOWN) {
monitor.default_status = GC.UP;
toast.info("Default status was reset to UP — Last known status is only available for Manual monitors.");
monitor.default_status = GC.UP
toast.info("Default status was reset to UP — Last known status is only available for Manual monitors.")
}
});
})
```
(`toast` is already imported from `svelte-sonner` at line 16.)
@@ -583,73 +583,69 @@ and below the props destructuring add:
Replace lines 230-247:
```svelte
<Label for="monitor-default-status">Default Status</Label>
<Select.Root
type="single"
value={monitor.default_status}
onValueChange={(v) => {
if (v) monitor.default_status = v;
}}
>
<Select.Trigger id="monitor-default-status" class="w-full">
{monitor.default_status}
</Select.Trigger>
<Select.Content>
<Select.Item value="UP">UP</Select.Item>
<Select.Item value="DOWN">DOWN</Select.Item>
<Select.Item value="DEGRADED">DEGRADED</Select.Item>
<Select.Item value="MAINTENANCE">MAINTENANCE</Select.Item>
</Select.Content>
</Select.Root>
<Label for="monitor-default-status">Default Status</Label>
<Select.Root
type="single"
value={monitor.default_status}
onValueChange={(v) => {
if (v) monitor.default_status = v
}}
>
<Select.Trigger id="monitor-default-status" class="w-full">
{monitor.default_status}
</Select.Trigger>
<Select.Content>
<Select.Item value="UP">UP</Select.Item>
<Select.Item value="DOWN">DOWN</Select.Item>
<Select.Item value="DEGRADED">DEGRADED</Select.Item>
<Select.Item value="MAINTENANCE">MAINTENANCE</Select.Item>
</Select.Content>
</Select.Root>
```
with:
```svelte
<Label for="monitor-default-status">Default Status</Label>
<Select.Root
type="single"
value={monitor.default_status ?? "NONE"}
onValueChange={(v) => {
if (v) monitor.default_status = v;
}}
>
<Select.Trigger id="monitor-default-status" class="w-full">
{defaultStatusLabels[monitor.default_status ?? "NONE"] ?? monitor.default_status}
</Select.Trigger>
<Select.Content>
<Select.Item value="NONE">None (show gaps as no data)</Select.Item>
<Select.Item value="UP">UP</Select.Item>
<Select.Item value="DOWN">DOWN</Select.Item>
<Select.Item value="DEGRADED">DEGRADED</Select.Item>
{#if monitor.monitor_type === "NONE"}
<Select.Item value="LAST_KNOWN">Last known status</Select.Item>
{/if}
</Select.Content>
</Select.Root>
{#if monitor.default_status === GC.LAST_KNOWN}
<Alert.Root>
<TriangleAlertIcon />
<Alert.Title>Last known status</Alert.Title>
<Alert.Description>
<p>
Kener will repeat the most recent status and latency every minute until your integration sends new
data.
</p>
<ul class="list-disc pl-4">
<li>
If your integration stops sending, the page keeps showing the last status indefinitely — Kener
cannot tell "still up" from "stopped reporting". Use a Heartbeat monitor to catch a silent
integration.
</li>
<li>
Carried minutes count toward alert thresholds: a single DOWN push will trigger alerts after your
failure threshold, and they stay triggered until you push a recovery.
</li>
</ul>
</Alert.Description>
</Alert.Root>
<Label for="monitor-default-status">Default Status</Label>
<Select.Root
type="single"
value={monitor.default_status ?? "NONE"}
onValueChange={(v) => {
if (v) monitor.default_status = v
}}
>
<Select.Trigger id="monitor-default-status" class="w-full">
{defaultStatusLabels[monitor.default_status ?? "NONE"] ?? monitor.default_status}
</Select.Trigger>
<Select.Content>
<Select.Item value="NONE">None (show gaps as no data)</Select.Item>
<Select.Item value="UP">UP</Select.Item>
<Select.Item value="DOWN">DOWN</Select.Item>
<Select.Item value="DEGRADED">DEGRADED</Select.Item>
{#if monitor.monitor_type === "NONE"}
<Select.Item value="LAST_KNOWN">Last known status</Select.Item>
{/if}
</Select.Content>
</Select.Root>
{#if monitor.default_status === GC.LAST_KNOWN}
<Alert.Root>
<TriangleAlertIcon />
<Alert.Title>Last known status</Alert.Title>
<Alert.Description>
<p>Kener will repeat the most recent status and latency every minute until your integration sends new data.</p>
<ul class="list-disc pl-4">
<li>
If your integration stops sending, the page keeps showing the last status indefinitely — Kener cannot tell "still up" from "stopped reporting". Use a Heartbeat
monitor to catch a silent integration.
</li>
<li>
Carried minutes count toward alert thresholds: a single DOWN push will trigger alerts after your failure threshold, and they stay triggered until you push a
recovery.
</li>
</ul>
</Alert.Description>
</Alert.Root>
{/if}
```
- [ ] **Step 3: Run the svelte autofixer / check**
@@ -680,6 +676,7 @@ git commit -m "feat(manage): Last known status option with callout in Default St
### Task 7: Docs — ADR cross-link + user documentation
**Files:**
- Modify: `docs/adr/0005-alerts-evaluate-alert-visible-samples.md` (append one sentence)
- Modify: `src/routes/(docs)/docs/content/v4/monitors/overview.md` (Default Status section)
@@ -33,12 +33,12 @@ Monitors run from cron expressions (for example `* * * * *` for every minute). U
Default Status is the monitor's answer to the question: **what does a minute with no monitoring sample mean?**
| Value | Behavior |
|---|---|
| `NONE` | Gap minutes show as no data (gray) |
| `UP` | A `DEFAULT` sample is written each minute marking the service UP |
| `DOWN` | A `DEFAULT` sample is written each minute marking the service DOWN |
| `DEGRADED` | A `DEFAULT` sample is written each minute marking the service DEGRADED |
| Value | Behavior |
| ------------ | ------------------------------------------------------------------------------------------------------------------------- |
| `NONE` | Gap minutes show as no data (gray) |
| `UP` | A `DEFAULT` sample is written each minute marking the service UP |
| `DOWN` | A `DEFAULT` sample is written each minute marking the service DOWN |
| `DEGRADED` | A `DEFAULT` sample is written each minute marking the service DEGRADED |
| `LAST_KNOWN` | Each minute without a new sample, Kener writes a `CARRIED` row repeating the most recent alert-visible status and latency |
### Last known status {#last-known-status}
@@ -62,6 +62,7 @@ curl -X PATCH 'https://status.example.com/api/v4/monitors/my-service/data/{curre
```
> [!WARNING]
>
> - If your integration stops sending, the page keeps showing the last status indefinitely — Kener cannot tell "still up" from "stopped reporting". Use a [Heartbeat monitor](/docs/v4/monitors/heartbeat) to catch a silent integration.
> - Carried minutes count toward alert thresholds: a single DOWN push will trigger alerts after your failure threshold, and they stay triggered until you push a recovery.
@@ -33,7 +33,7 @@
UP: "UP",
DOWN: "DOWN",
DEGRADED: "DEGRADED",
LAST_KNOWN: "Last known status",
LAST_KNOWN: "Last known status"
};
// LAST_KNOWN is only valid on Manual (NONE-type) monitors; the server enforces the
@@ -273,14 +273,13 @@
<Alert.Title>Last known status</Alert.Title>
<Alert.Description>
<p>
Kener will repeat the most recent status and latency every minute until your integration sends new
data.
Kener will repeat the most recent status and latency every minute until your integration sends new data.
</p>
<ul class="list-disc pl-4">
<li>
If your integration stops sending, the page keeps showing the last status indefinitely &mdash; Kener
cannot tell &quot;still up&quot; from &quot;stopped reporting&quot;. Use a Heartbeat monitor to catch a silent
integration.
cannot tell &quot;still up&quot; from &quot;stopped reporting&quot;. Use a Heartbeat monitor to catch
a silent integration.
</li>
<li>
Carried minutes count toward alert thresholds: a single DOWN push will trigger alerts after your