feat: Last known status default for Manual monitors - #748
Conversation
ADR 0006, glossary terms, and implementation plan from the grilling session. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…arry source Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-data queries Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ixes #721 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… dropdown Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
📝 WalkthroughWalkthroughThis PR implements Last Known Status, a feature that allows NONE-type monitors to repeat their most recent alert-visible status across minutes without new data by writing CARRIED sample rows, addressing the gap-filling issue reported in ChangesLast Known Status: Carry-Forward Fill for NONE Monitors
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Pull request overview
Adds a new “Last known status” default for Manual (NONE-type) monitors so push-driven monitors keep their last status (and latency) between pushes by writing per-minute CARRIED samples sourced from the latest alert-visible sample. It also centralizes default_status validation/normalization (closed set + LAST_KNOWN scope rule), updates the manage UI/docs, and migrates legacy/invalid default_status values.
Changes:
- Implement
LAST_KNOWNdefault status andCARRIEDsample type, including carry-source query restricted to alert-visible samples. - Enforce a closed
default_statusvalue set via a sharedNormalizeDefaultStatusacross manage writes and v4 POST/PATCH. - Update manage UI + docs/ADRs/glossary; migrate legacy/invalid/NULL
default_statusvalues toNONE.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/routes/(manage)/manage/app/monitors/[tag]/components/GeneralSettingsCard.svelte | Adds LAST_KNOWN option for Manual monitors, plus explanatory warning callout and auto-reset behavior when monitor type changes. |
| src/routes/(docs)/docs/content/v4/monitors/overview.md | Documents “Default Status” and the new LAST_KNOWN behavior and warnings. |
| src/routes/(api)/api/v4/monitors/+server.ts | Applies NormalizeDefaultStatus on v4 monitor creation. |
| src/routes/(api)/api/v4/monitors/[monitor_tag]/+server.ts | Applies NormalizeDefaultStatus on v4 monitor updates (including type-change auto-reset). |
| src/lib/server/queues/monitorExecuteQueue.ts | Implements scheduler fill logic that writes CARRIED samples for LAST_KNOWN and preserves fill types through NO_DATA merge. |
| src/lib/server/db/repositories/monitoring.ts | Adds CARRIED to alert-visible types and introduces getLatestAlertVisibleData() for carry-source selection. |
| src/lib/server/db/dbimpl.ts | Exposes getLatestAlertVisibleData() via the db singleton. |
| src/lib/server/controllers/monitorsController.ts | Introduces NormalizeDefaultStatus() and enforces it in the manage create/update path. |
| src/lib/global-constants.ts | Adds CARRIED (sample type) and LAST_KNOWN (default_status value). |
| migrations/20260607150000_normalize_default_status.ts | Normalizes NULL/unknown legacy default_status values to NONE. |
| docs/superpowers/plans/2026-06-07-last-known-status.md | Adds an implementation plan reference for the feature work. |
| docs/adr/0006-last-known-status-fill.md | Records the architectural decision and invariants for last-known-status fill. |
| docs/adr/0005-alerts-evaluate-alert-visible-samples.md | Updates ADR 0005 to note CARRIED joining the alert-visible set. |
| CONTEXT.md | Updates glossary definitions to include CARRIED and last-known status semantics. |
| let defaultStatus: string; | ||
| try { | ||
| defaultStatus = NormalizeDefaultStatus(body.monitor_type ?? "API", body.default_status ?? "UP"); | ||
| } catch (e) { |
| # Alerts evaluate alert-visible samples, not just REALTIME ones | ||
|
|
||
| The consecutive-sample checks behind alert evaluation (`consecutivelyStatusFor`, `consecutivelyLatencyGreaterThan`, `consecutivelyLatencyLessThan` in `src/lib/server/db/repositories/monitoring.ts`) consider samples whose type is `REALTIME`, `ERROR`, `TIMEOUT`, `MANUAL`, or `DEFAULT_STATUS` — the "alert-visible" set — instead of `REALTIME` only. Both data-API PATCH endpoints (single timestamp and range) enqueue one alert evaluation after writing `MANUAL` rows. `SIGNAL` rows and `INCIDENT`/`MAINTENANCE` overlay rows remain invisible to alerting. | ||
| The consecutive-sample checks behind alert evaluation (`consecutivelyStatusFor`, `consecutivelyLatencyGreaterThan`, `consecutivelyLatencyLessThan` in `src/lib/server/db/repositories/monitoring.ts`) consider samples whose type is `REALTIME`, `ERROR`, `TIMEOUT`, `MANUAL`, or `DEFAULT_STATUS` — the "alert-visible" set — instead of `REALTIME` only. Both data-API PATCH endpoints (single timestamp and range) enqueue one alert evaluation after writing `MANUAL` rows. `SIGNAL` rows and `INCIDENT`/`MAINTENANCE` overlay rows remain invisible to alerting. Amended by ADR 0006: last-known-status fill (`CARRIED`) later joined the alert-visible set under the same invariant. |
| **Synthetic Sample**: | ||
| A Monitoring Sample written by the system or an admin rather than by a check: a raw heartbeat receipt (`SIGNAL`), a status pushed through the data API (`MANUAL`), a default-status fill (`DEFAULT_STATUS`), or an incident/maintenance overlay (`INCIDENT`, `MAINTENANCE`). | ||
| A Monitoring Sample written by the system or an admin rather than by a check: a raw heartbeat receipt (`SIGNAL`), a status pushed through the data API (`MANUAL`), a default-status fill (`DEFAULT_STATUS`), a last-known-status fill (`CARRIED`), or an incident/maintenance overlay (`INCIDENT`, `MAINTENANCE`). | ||
|
|
| **Alert-Visible Sample**: | ||
| A Monitoring Sample that alert evaluation can see: every Observed Sample, plus data-API pushes (`MANUAL`) and default-status fill (`DEFAULT_STATUS`). Raw heartbeat receipts (`SIGNAL`) and incident/maintenance overlays are never alert-visible — while an overlay is active the alert window freezes (alerts neither trigger nor resolve). All alert conditions (status and latency alike) evaluate the same alert-visible timeline. | ||
| A Monitoring Sample that alert evaluation can see: every Observed Sample, plus data-API pushes (`MANUAL`), default-status fill (`DEFAULT_STATUS`), and last-known-status fill (`CARRIED`). Raw heartbeat receipts (`SIGNAL`) and incident/maintenance overlays are never alert-visible — while an overlay is active the alert window freezes (alerts neither trigger nor resolve). All alert conditions (status and latency alike) evaluate the same alert-visible timeline. | ||
|
|
Greptile SummaryThis PR introduces a
Confidence Score: 4/5The carry fill logic is correct for NONE-type monitors; NoneCall.execute() returns null so realtimeData is always empty and the CARRIED row wins the merge cleanly. The main rough edge is that POST creates silently accept LAST_KNOWN for non-NONE types and store UP instead, which could surprise API consumers. The core carry mechanism is well-reasoned and the latency preservation works correctly because NoneCall returns null. The normalization chokepoint covers all three write paths. The outstanding issue is a UX/contract gap in the create endpoint: passing LAST_KNOWN for a non-NONE type returns 200 with a silently different stored value, which contrasts with the 400 returned for other invalid values. src/routes/(api)/api/v4/monitors/+server.ts — the POST create path silently coerces LAST_KNOWN to UP for non-NONE types; src/lib/server/queues/monitorExecuteQueue.ts — the LAST_KNOWN carry branch should guard against carrying a NO_DATA status from a REALTIME row. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Scheduler tick for NONE-type monitor] --> B{default_status?}
B -- UP/DOWN/DEGRADED --> C[Write DEFAULT_STATUS row]
B -- LAST_KNOWN --> D[getLatestAlertVisibleData]
B -- NONE --> E[No fill — gap stays as no data]
D -- found row --> F{status is NO_DATA?}
F -- no --> G[Write CARRIED row\nstatus + latency from last known]
F -- yes --> E
D -- not found --> E
G --> H[mergedData wins in spread:\ndefaultData beats empty realtimeData]
C --> H
H --> I[incidentData / maintenanceData\ncan still override]
I --> J[monitorResponseQueue.push]
Reviews (1): Last reviewed commit: "style: prettify files touched by Last Kn..." | Re-trigger Greptile |
|
|
||
| let defaultStatus: string; | ||
| try { | ||
| defaultStatus = NormalizeDefaultStatus(body.monitor_type ?? "API", body.default_status ?? "UP"); |
There was a problem hiding this comment.
Silent LAST_KNOWN coercion masks invalid requests on creation
NormalizeDefaultStatus silently returns "UP" when default_status: "LAST_KNOWN" is sent with a non-NONE monitor_type. For a PATCH where monitor_type is changing away from NONE, this auto-reset is intentional and desirable. For a POST (create), a caller that explicitly sets default_status: "LAST_KNOWN" with e.g. monitor_type: "API" gets a 201 response but the stored default_status is "UP" — a silent data change that is inconsistent with the 400 returned for every other invalid value (e.g. "MAINTENANCE"). Consider throwing inside NormalizeDefaultStatus when the combination is invalid on creation, or having the POST endpoint validate the combination before calling the normalizer.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| } 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); | ||
| if (lastKnown && lastKnown.status) { |
There was a problem hiding this comment.
Defensive guard: avoid carrying a NO_DATA status
getLatestAlertVisibleData includes REALTIME rows in its result set. It is theoretically possible for a REALTIME row to carry status: "NO_DATA" (e.g. if a monitor type is changed after data is already written). If such a row is the most recent alert-visible sample, defaultData[ts].status would be "NO_DATA" and that value would be written into a CARRIED row and persist indefinitely. Adding an explicit lastKnown.status !== GC.NO_DATA guard closes this edge case.
| } 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); | |
| if (lastKnown && lastKnown.status) { | |
| } 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); | |
| if (lastKnown && lastKnown.status && lastKnown.status !== GC.NO_DATA) { |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/superpowers/plans/2026-06-07-last-known-status.md`:
- Around line 689-691: The fenced code block containing "Amended by ADR 0006:
last-known-status fill (`CARRIED`) later joined the alert-visible set under the
same invariant." should include a language identifier (e.g., ```text) or be
converted to normal paragraph text to satisfy MD040; update the existing
triple-backtick block around that sentence to ```text ... ``` or replace the
fenced block with a plain paragraph containing that sentence.
- Line 17: Several task section headings use H3 (e.g., "### Task 1: Constants +
alert-visible whitelist") which causes markdown-lint MD001; update those
headings to H2 by changing the leading "###" to "##". Search for the pattern
"### Task" across the document (including the instances around the provided
comment) and promote each to "## Task ..." so all task sections are H2 and lint
compliant.
In `@migrations/20260607150000_normalize_default_status.ts`:
- Around line 8-11: The migration's up function should guard against missing
table/column to be idempotent: inside up(), first call await
knex.schema.hasTable("monitors") and if false return early; then call await
knex.schema.hasColumn("monitors", "default_status") and return early if false;
only after both exist run the two update statements (the existing await
knex("monitors").whereNull... and await
knex("monitors").whereNotIn("default_status", VALID)...). Keep the VALID symbol
as-is and ensure the early returns prevent running updates on partial schemas.
In `@src/routes/`(docs)/docs/content/v4/monitors/overview.md:
- Around line 57-62: Update the curl example to clarify the timestamp
placeholder: replace {current_unix_minute} with a clearer placeholder like
{unix_timestamp} and add a short inline comment above the command stating
"Replace {unix_timestamp} with the Unix timestamp of the current minute (e.g.,
1686139200)". Ensure the example PATCH request line in the docs content (the
curl invocation) uses the new placeholder {unix_timestamp} so readers know to
substitute a numeric Unix timestamp representing the current minute.
- Around line 32-68: The example curl uses the unclear placeholder
{current_unix_minute}; update the explanatory text (near the "Example push flow"
curl block) to state succinctly that {current_unix_minute} must be the Unix
timestamp in seconds representing the start of the target minute (the API will
round to the UTC minute boundary using its minute-start helpers), keep the
existing > [!WARNING] callout syntax as-is, and ensure the curl command and
surrounding prose mention this replacement so readers know to substitute the
timestamp seconds value.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: d50f96a6-ff10-4466-ba5e-f6010e8227d2
📒 Files selected for processing (14)
CONTEXT.mddocs/adr/0005-alerts-evaluate-alert-visible-samples.mddocs/adr/0006-last-known-status-fill.mddocs/superpowers/plans/2026-06-07-last-known-status.mdmigrations/20260607150000_normalize_default_status.tssrc/lib/global-constants.tssrc/lib/server/controllers/monitorsController.tssrc/lib/server/db/dbimpl.tssrc/lib/server/db/repositories/monitoring.tssrc/lib/server/queues/monitorExecuteQueue.tssrc/routes/(api)/api/v4/monitors/+server.tssrc/routes/(api)/api/v4/monitors/[monitor_tag]/+server.tssrc/routes/(docs)/docs/content/v4/monitors/overview.mdsrc/routes/(manage)/manage/app/monitors/[tag]/components/GeneralSettingsCard.svelte
|
|
||
| --- | ||
|
|
||
| ### Task 1: Constants + alert-visible whitelist |
There was a problem hiding this comment.
Fix heading-level jumps to satisfy markdown lint.
Task sections jump from H1 to H3. Promote these task headings to H2 to avoid MD001 violations.
Suggested diff
-### Task 1: Constants + alert-visible whitelist
+## Task 1: Constants + alert-visible whitelist
...
-### Task 2: Repository — latest alert-visible sample query
+## Task 2: Repository — latest alert-visible sample query
...
-### Task 8: End-to-end verification against the dev server
+## Task 8: End-to-end verification against the dev serverAlso applies to: 86-86, 187-187, 308-308, 490-490, 541-541, 676-676, 720-720
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 17-17: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3
(MD001, heading-increment)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/superpowers/plans/2026-06-07-last-known-status.md` at line 17, Several
task section headings use H3 (e.g., "### Task 1: Constants + alert-visible
whitelist") which causes markdown-lint MD001; update those headings to H2 by
changing the leading "###" to "##". Search for the pattern "### Task" across the
document (including the instances around the provided comment) and promote each
to "## Task ..." so all task sections are H2 and lint compliant.
Source: Linters/SAST tools
| ``` | ||
| Amended by ADR 0006: last-known-status fill (`CARRIED`) later joined the alert-visible set under the same invariant. | ||
| ``` |
There was a problem hiding this comment.
Add a language identifier to the fenced code block.
The fenced block triggers MD040; add a language tag (e.g., text) or convert it to plain paragraph text.
Suggested diff
-```
+```text
Amended by ADR 0006: last-known-status fill (`CARRIED`) later joined the alert-visible set under the same invariant.</details>
<details>
<summary>🧰 Tools</summary>
<details>
<summary>🪛 markdownlint-cli2 (0.22.1)</summary>
[warning] 689-689: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
</details>
</details>
<details>
<summary>🤖 Prompt for AI Agents</summary>
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @docs/superpowers/plans/2026-06-07-last-known-status.md around lines 689 -
691, The fenced code block containing "Amended by ADR 0006: last-known-status
fill (CARRIED) later joined the alert-visible set under the same invariant."
should include a language identifier (e.g., text) or be converted to normal paragraph text to satisfy MD040; update the existing triple-backtick block around that sentence to text ... ``` or replace the fenced block with a plain
paragraph containing that sentence.
</details>
<!-- fingerprinting:phantom:poseidon:hawk -->
<!-- cr-comment:v1:0b63b68c908a8f63fb1f33cc -->
_Source: Linters/SAST tools_
<!-- This is an auto-generated comment by CodeRabbit -->
| 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" }); | ||
| } |
There was a problem hiding this comment.
Add schema guards before data updates in this migration.
This migration should guard monitors/default_status existence before running updates, otherwise it can fail on partial/non-standard schema states.
Suggested fix
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" });
+ const hasMonitors = await knex.schema.hasTable("monitors");
+ if (!hasMonitors) return;
+
+ const hasDefaultStatus = await knex.schema.hasColumn("monitors", "default_status");
+ if (!hasDefaultStatus) return;
+
+ await knex("monitors").whereNull("default_status").update({ default_status: "NONE" });
+ await knex("monitors").whereNotIn("default_status", VALID).update({ default_status: "NONE" });
}Based on learnings: “Use knex.schema.hasColumn and knex.schema.hasTable guards for migration idempotency.”
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 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" }); | |
| } | |
| export async function up(knex: Knex): Promise<void> { | |
| const hasMonitors = await knex.schema.hasTable("monitors"); | |
| if (!hasMonitors) return; | |
| const hasDefaultStatus = await knex.schema.hasColumn("monitors", "default_status"); | |
| if (!hasDefaultStatus) return; | |
| await knex("monitors").whereNull("default_status").update({ default_status: "NONE" }); | |
| await knex("monitors").whereNotIn("default_status", VALID).update({ default_status: "NONE" }); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@migrations/20260607150000_normalize_default_status.ts` around lines 8 - 11,
The migration's up function should guard against missing table/column to be
idempotent: inside up(), first call await knex.schema.hasTable("monitors") and
if false return early; then call await knex.schema.hasColumn("monitors",
"default_status") and return early if false; only after both exist run the two
update statements (the existing await knex("monitors").whereNull... and await
knex("monitors").whereNotIn("default_status", VALID)...). Keep the VALID symbol
as-is and ensure the early returns prevent running updates on partial schemas.
Source: Learnings
| ## Default Status {#default-status} | ||
|
|
||
| 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 | | ||
| | `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} | ||
|
|
||
| `LAST_KNOWN` is only available on **Manual (`NONE`-type) monitors**. If you select it on any other monitor type, the API resets it to `UP`. Changing a monitor's type away from Manual also resets it to `UP`. | ||
|
|
||
| How it works: | ||
|
|
||
| - Every scheduler tick with no new data, Kener writes a `CARRIED` sample copying the status and latency of the most recent alert-visible sample. | ||
| - Carry is tick-forward only — it starts at the next scheduler tick after you save the setting, with no backfill of past gaps. | ||
| - Carried rows persist in history even if you later change the setting. | ||
|
|
||
| Example push flow: | ||
|
|
||
| ```bash | ||
| curl -X PATCH 'https://status.example.com/api/v4/monitors/my-service/data/{current_unix_minute}' \ | ||
| -H 'Authorization: Bearer <api-key>' \ | ||
| -H 'Content-Type: application/json' \ | ||
| --data '{"status": "DOWN", "latency": 100}' | ||
| # With Default Status = Last known status, the monitor stays DOWN until you push UP. | ||
| ``` | ||
|
|
||
| > [!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. | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Check if other docs use the [!WARNING] callout syntax
# Search for similar markdown callout usage in docs
rg -n '>\s*\[!(WARNING|NOTE|TIP|IMPORTANT)' --type=md --glob 'src/routes/(docs)/**'Repository: rajnandan1/kener
Length of output: 3796
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Read the documentation-writer guidelines skill file
if [ -f .claude/skills/documentation-writer/SKILL.md ]; then
echo "=== .claude/skills/documentation-writer/SKILL.md (head) ==="
sed -n '1,200p' .claude/skills/documentation-writer/SKILL.md
else
echo "SKILL.md not found at .claude/skills/documentation-writer/SKILL.md"
fi
# 2) Check whether {current_unix_minute} is used elsewhere in docs
echo
echo "=== Usage of {current_unix_minute} ==="
rg -n '\{current_unix_minute\}' --type=md --glob 'src/routes/(docs)/**' || true
# 3) Check whether other docs use a similar PATCH URL placeholder for "minute" endpoints
echo
echo "=== Usage of /data/{current_*_minute} patterns ==="
rg -n '/data/\{current_.*_minute\}' --type=md --glob 'src/routes/(docs)/**' || trueRepository: rajnandan1/kener
Length of output: 3819
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Inspect the relevant section in the target file
echo "=== monitors/overview.md lines 40-90 ==="
nl -ba src/routes/(docs)/docs/content/v4/monitors/overview.md | sed -n '40,95p'
# 2) Find any other mention of current_unix_minute / unix_minute placeholders
echo
echo "=== occurrences of current_unix_minute in repo ==="
rg -n 'current_unix_minute' --hidden --glob '!.git/*' || true
# 3) Find any mentions of the data endpoint minute placeholder style
echo
echo "=== occurrences of /data/{current_*_minute} ==="
rg -n '/data/\{current_.*_minute\}' --type=md --glob 'src/routes/(docs)/**' || true
# 4) Look for API/code route parameter naming for monitors data patch
echo
echo "=== server/routes code mentioning monitors data patch and unix minute ==="
rg -n 'monitors.*data.*(minute|unix)' --hidden --glob 'src/**' || trueRepository: rajnandan1/kener
Length of output: 181
🏁 Script executed:
#!/bin/bash
set -euo pipefail
TARGET_FILE="src/routes/(docs)/docs/content/v4/monitors/overview.md"
# 1) Inspect the relevant section in the target file
echo "=== monitors/overview.md lines 40-90 ==="
nl -ba "$TARGET_FILE" | sed -n '40,95p'
# 2) Find any other mention of current_unix_minute / unix_minute placeholders
echo
echo "=== occurrences of current_unix_minute in repo ==="
rg -n 'current_unix_minute' --hidden --glob '!.git/*' || true
echo
echo "=== occurrences of unix_minute placeholder ==="
rg -n 'unix_minute' --hidden --glob '!.git/*' || true
# 3) Find any mentions of the data endpoint minute placeholder style
echo
echo "=== occurrences of /data/{current_*_minute} in docs ==="
rg -n '/data/\{current_.*_minute\}' --type=md --glob 'src/routes/(docs)/**' || true
# 4) Look for server-side route param naming in code
echo
echo "=== server/code references to monitors data + unix minute ==="
rg -n 'monitors.*data.*(minute|unix)' --hidden --glob 'src/**' || trueRepository: rajnandan1/kener
Length of output: 162
🏁 Script executed:
#!/bin/bash
set -euo pipefail
TARGET_FILE="src/routes/(docs)/docs/content/v4/monitors/overview.md"
# 1) Inspect the relevant section in the target file
echo "=== monitors/overview.md lines 40-95 ==="
cat -n "$TARGET_FILE" | sed -n '40,100p'
# 2) Find any other mention of current_unix_minute / unix_minute placeholders
echo
echo "=== occurrences of current_unix_minute in repo ==="
rg -n 'current_unix_minute' --hidden --glob '!.git/*' || true
echo
echo "=== occurrences of unix_minute placeholder in repo ==="
rg -n 'unix_minute' --hidden --glob '!.git/*' || true
# 3) Find any mentions of the data endpoint minute placeholder style
echo
echo "=== occurrences of /data/{current_*_minute} in docs ==="
rg -n '/data/\{current_.*_minute\}' --type=md --glob 'src/routes/(docs)/**' || true
# 4) Look for server-side route param naming in code
echo
echo "=== server/code references to monitors data + (minute|unix) ==="
rg -n 'monitors.*data.*(minute|unix)' --hidden --glob 'src/**' || trueRepository: rajnandan1/kener
Length of output: 4640
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the other doc that uses {current_unix_minute}
FILE="docs/superpowers/plans/2026-06-07-last-known-status.md"
echo "=== $FILE (lines 670-730) ==="
cat -n "$FILE" | sed -n '670,730p'
echo
echo "=== $FILE occurrences of current_unix_minute ==="
rg -n 'current_unix_minute' --hidden --glob '!.git/*' "$FILE" || trueRepository: rajnandan1/kener
Length of output: 3670
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Locate the PATCH route implementation for monitors data
echo "=== Search for PATCH monitors data route in src/ ==="
rg -n "PATCH.*monitors.*data" --hidden --glob 'src/**' || true
echo
echo "=== Search for '/api/v4/monitors' and 'data' route handlers ==="
rg -n "/api/v4/monitors" --hidden --glob 'src/**' || true
echo
echo "=== Search for 'current_unix_minute' and 'unix minute' in server code ==="
rg -n "current_unix_minute|unix.?minute|unix_minute" --hidden --glob 'src/**' || true
echo
echo "=== Search for route param names like ':.*minute' near monitors data ==="
rg -n "monitors.*data.*:.*minute|monitors.*:.*data.*:.*minute" --hidden --glob 'src/**' || trueRepository: rajnandan1/kener
Length of output: 1964
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Repo-wide search for monitors data PATCH patterns ==="
rg -n "monitors/\{?tag\}?.*data.*\{?timestamp\}?|\/api\/v4\/monitors\/\{?\w+\}?\/data" --hidden --glob '!.git/*' . || true
echo
echo "=== Repo-wide search for 'monitors/{tag}/data' and 'data/{' ==="
rg -n "monitors\/\{tag\}\/data|\/data\/\{[a-zA-Z_]+\}" --hidden --glob '!.git/*' . || true
echo
echo "=== Repo-wide search for 'minute' route param naming ==="
rg -n "minute[s]?|unix.?minute|unix_minute|timestamp" --hidden --glob '!.git/*' src || trueRepository: rajnandan1/kener
Length of output: 50698
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Search: /api/v4/monitors + data + patch in src/lib/server ==="
rg -n "/api/v4/monitors" src/lib/server --hidden -g'*.ts' -S || true
echo
echo "=== Search: monitors.*data in api-server folder ==="
rg -n "monitors.*data|data.*monitors" src/lib/server/api-server --hidden -g'*.ts' -S || true
echo
echo "=== Search: PATCH handlers in api-server folder ==="
rg -n "patch\\(" src/lib/server/api-server --hidden -g'*.ts' -S || true
echo
echo "=== Inspect monitorsController.ts for data/timestamp handling ==="
FILE="src/lib/server/controllers/monitorsController.ts"
if [ -f "$FILE" ]; then
sed -n '1,220p' "$FILE" | nl -ba | sed -n '1,220p'
echo
echo "=== monitor controller occurrences of 'data' and 'timestamp' ==="
rg -n "data|timestamp" "$FILE" -g'*.ts' || true
else
echo "Missing: $FILE"
fiRepository: rajnandan1/kener
Length of output: 615
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Inspect monitorsController.ts (where timestamp likely validated)
FILE="src/lib/server/controllers/monitorsController.ts"
echo "=== $FILE (head 260 lines) ==="
if [ -f "$FILE" ]; then
cat -n "$FILE" | sed -n '1,260p'
else
echo "Missing: $FILE"
fi
# 2) Search for route paths / param names in server code
echo
echo "=== Search (fixed strings) for monitors data patch paths ==="
rg -n -F "/api/v4/monitors" src/lib/server --hidden -g'*.ts' -S || true
echo
echo "=== Search (fixed strings) for 'data/{' patterns ==="
rg -n -F "data/" src/lib/server --hidden -g'*.ts' -S || true
echo
echo "=== Search for 'timestamp' param parsing in monitors controller ==="
rg -n "timestamp|minuteStart|MinuteStart|minute" src/lib/server/controllers/monitorsController.ts --hidden -g'*.ts' -S || true
# 3) Search elsewhere for monitors data endpoint implementation
echo
echo "=== Search for 'monitorsController' usage ==="
rg -n "monitorsController" src/lib/server --hidden -g'*.ts' -S || true
echo
echo "=== Search for 'monitors' route registration (express/fastify) ==="
rg -n "router\\.|app\\.|route\\(" src/lib/server --hidden -g'*.ts' -S || trueRepository: rajnandan1/kener
Length of output: 12344
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect tool.ts functions
TOOL_FILE="src/lib/server/tool.ts"
echo "=== $TOOL_FILE (relevant UTC minute helpers) ==="
rg -n "GetMinuteStartTimestampUTC|GetMinuteStartNowTimestampUTC" "$TOOL_FILE" || true
cat -n "$TOOL_FILE" | sed -n '1,120p'
echo
# Find which API endpoints call these controller helpers
echo "=== Find usages of InsertMonitoringData / UpdateMonitoringData ==="
rg -n "InsertMonitoringData\\b|UpdateMonitoringData\\b" src/lib/server --hidden -g'*.ts' -S
echo
# Inspect the endpoint file(s) found above (show small surrounding windows)
FILES=$(rg -l "InsertMonitoringData\\b|UpdateMonitoringData\\b" src/lib/server --hidden -g'*.ts' -S || true)
echo "=== Files found ==="
echo "$FILES" | head -n 50
for f in $FILES; do
echo
echo "=== $f (mentions) ==="
rg -n "InsertMonitoringData\\b|UpdateMonitoringData\\b" "$f" -n -S
# show a small neighborhood around the first match
line=$(rg -n "InsertMonitoringData\\b|UpdateMonitoringData\\b" "$f" | head -n 1 | cut -d: -f1)
if [ -n "${line:-}" ]; then
start=$((line-40)); end=$((line+80))
cat -n "$f" | sed -n "${start},${end}p"
fi
doneRepository: rajnandan1/kener
Length of output: 8595
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Inspect monitorsController around where InsertMonitoringData is called
CTRL="src/lib/server/controllers/monitorsController.ts"
echo "=== monitorsController.ts: lines 340-430 ==="
awk 'NR>=340 && NR<=430 {printf "%6d\t%s\n", NR, $0}' "$CTRL"
echo
echo "=== monitorsController.ts: occurrences of InsertMonitoringData( / UpdateMonitoringData( ==="
rg -n "InsertMonitoringData\\(|UpdateMonitoringData\\(" "$CTRL" -S
# 2) Inspect tool.ts for minute start helper usage (already partially seen; grab remaining lines too)
TOOL="src/lib/server/tool.ts"
echo
echo "=== tool.ts: GetMinuteStartTimestampUTC / GetMinuteStartNowTimestampUTC definitions (lines 20-80) ==="
awk 'NR>=20 && NR<=90 {printf "%6d\t%s\n", NR, $0}' "$TOOL"
# 3) Find api-server endpoints for monitors "data" operations
echo
echo "=== api-server: files that mention monitorsController and/or 'monitor.*data' ==="
rg -l "monitorsController|monitor.*data|/api/v4/monitors|data/\\{|" src/lib/server/api-server -S --glob '*.ts' || true
echo
echo "=== api-server: any routes/controllers mentioning 'data' for monitors ==="
rg -n "monitors.*data|data.*monitors|monitor.*data" src/lib/server/api-server --glob '*.ts' -S || true
# 4) If there are dynamic route patterns like :timestamp or {timestamp}, search globally in src/lib/server
echo
echo "=== server: route param patterns for timestamp and monitors data ==="
rg -n "monitors.*data.*(:|\\{).*(timestamp|minute)|data.*(:|\\{).*(timestamp|minute)|:timestamp" src/lib/server --glob '*.ts' -S || trueRepository: rajnandan1/kener
Length of output: 11492
Adjust the {current_unix_minute} curl placeholder + keep the callout syntax suggestion as-is
- The docs use the same
> [!WARNING]callout syntax elsewhere insrc/routes/(docs)/docs/content/v4, so the callout format is consistent across the repo; no change needed there. {current_unix_minute}only appears in this page (and in a planning doc), and nothing in the docs explains what to replace it with; clarify that it must be the Unix timestamp (seconds) for the start of the target minute (the API rounds minute timestamps to the UTC minute boundary in its minute-start helpers).- Brevity: the two warning bullets are fine given they describe distinct failure/alerting implications; optional only.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/routes/`(docs)/docs/content/v4/monitors/overview.md around lines 32 - 68,
The example curl uses the unclear placeholder {current_unix_minute}; update the
explanatory text (near the "Example push flow" curl block) to state succinctly
that {current_unix_minute} must be the Unix timestamp in seconds representing
the start of the target minute (the API will round to the UTC minute boundary
using its minute-start helpers), keep the existing > [!WARNING] callout syntax
as-is, and ensure the curl command and surrounding prose mention this
replacement so readers know to substitute the timestamp seconds value.
| curl -X PATCH 'https://status.example.com/api/v4/monitors/my-service/data/{current_unix_minute}' \ | ||
| -H 'Authorization: Bearer <api-key>' \ | ||
| -H 'Content-Type: application/json' \ | ||
| --data '{"status": "DOWN", "latency": 100}' | ||
| # With Default Status = Last known status, the monitor stays DOWN until you push UP. | ||
| ``` |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | 💤 Low value
Clarify the timestamp placeholder in the curl example.
The example uses {current_unix_minute} as a placeholder. While developers familiar with the API will understand this, first-time users might benefit from a more explicit note.
📝 Proposed improvement
```bash
-curl -X PATCH 'https://status.example.com/api/v4/monitors/my-service/data/{current_unix_minute}' \
+# Replace {unix_timestamp} with the Unix timestamp of the current minute (e.g., 1686139200)
+curl -X PATCH 'https://status.example.com/api/v4/monitors/my-service/data/{unix_timestamp}' \
-H 'Authorization: Bearer <api-key>' \
-H 'Content-Type: application/json' \
--data '{"status": "DOWN", "latency": 100}'
# With Default Status = Last known status, the monitor stays DOWN until you push UP.
</details>
<details>
<summary>🤖 Prompt for AI Agents</summary>
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @src/routes/(docs)/docs/content/v4/monitors/overview.md around lines 57 - 62,
Update the curl example to clarify the timestamp placeholder: replace
{current_unix_minute} with a clearer placeholder like {unix_timestamp} and add a
short inline comment above the command stating "Replace {unix_timestamp} with
the Unix timestamp of the current minute (e.g., 1686139200)". Ensure the example
PATCH request line in the docs content (the curl invocation) uses the new
placeholder {unix_timestamp} so readers know to substitute a numeric Unix
timestamp representing the current minute.
</details>
<!-- fingerprinting:phantom:poseidon:puma -->
<!-- cr-comment:v1:93fb35bd8f15ff5ca59677ca -->
<!-- This is an auto-generated comment by CodeRabbit -->
Summary
Fixes #721 — push-driven (Manual/NONE-type) monitors lost their status between pushes in v4: one red minute, then gray forever.
LAST_KNOWN(Manual monitors only): every scheduler tick without new data writes aCARRIEDsample repeating the most recent alert-visible sample — status and latency alike — so a pushed status sticks until the next push.REALTIME/ERROR/TIMEOUT/MANUAL/DEFAULT/CARRIED). Incident/maintenance overlays and heartbeatSIGNALreceipts can never become sticky; backdated corrections don't change the present.CARRIEDjoins the alert-visible set, keeping ADR 0005's invariant (carried minutes count toward alert thresholds; alerts resolve only on a pushed recovery).default_statusvalue set (NONE|UP|DOWN|DEGRADED|LAST_KNOWN) enforced by a singleNormalizeDefaultStatuschokepoint across all three write paths (manage UI, v4 POST, v4 PATCH).LAST_KNOWNon a non-Manual type silently resets toUP— including when a monitor's type is changed away from Manual.MAINTENANCE/unknown/NULL→NONE) — behavior-preserving, since the fill engine never honored them. The deadMAINTENANCEdropdown option is removed.Test Plan
npm run check— 0 errorsNormalizeDefaultStatusverified: LAST_KNOWN passes on NONE type, resets to UP on others, null→NONE, MAINTENANCE rejectedCARRIED DOWNrows each tick (latency carried) → push UP →CARRIED UProws → type change to API auto-resets default to UP → PATCHMAINTENANCEreturns 400🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
Documentation