Review follow-up on #754: deleteMonitorAlertConfigsByMonitorTag removed
v2 alert rows only when the whole config died, so a shared config that
survived the detach kept monitor_alerts_v2 rows pointing at the deleted
monitor's tag. Verified red/green with an in-memory SQLite script:
the deleted tag's v2 rows now go with the detach while the surviving
monitors' rows are untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Monitor deletion is now available via the v4 API, reusing the same
DeleteMonitorCompletelyUsingTag path as the manage UI. While wiring it
in, monitor deletion was found to orphan alert configs on SQLite:
the code relied on FK cascades that SQLite never enforces (the
foreign_keys pragma is off). Delete paths now remove child rows
explicitly — v2 alerts, trigger junctions, monitor junctions — in both
the by-id and by-tag config deletes; see ADR 0008 for why explicit
deletes were chosen over enabling the pragma.
Also corrects the CONTEXT.md Stale Member entry (deletion strips group
membership and rebalances weights; only pausing produces a stale
member), documents the DELETE endpoint in the OpenAPI spec, points the
pages doc at the ~home token, and removes an orphaned fictional
api-reference markdown page superseded by the spec tab.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Maintenance descriptions rendered as plain text in MaintenanceItem, so
HTML tags and line breaks showed literally on the home page while the
maintenance detail page rendered them correctly. Use the same
SveltePurify + mdToHTML prose pattern as the detail page and incident
comments.
Fixes#713
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Introduce functionality to manually complete or cancel ongoing maintenance events.
- Update event status to COMPLETED or CANCELLED, adjusting end_date_time accordingly.
- Ensure terminal statuses prevent further modifications and notify subscribers of changes.
- Revise API to support status transitions alongside window edits, enforcing mutual exclusivity.
- Document behavior and consequences of manual transitions in ADR.
A maintenance with is_global=YES (the default) has no per-monitor rows, so
the feed builder dropped it via the monitors.length===0 guard intended to
hide events whose monitors are all hidden. Global maintenances therefore
never appeared in /rss.xml, even though they show on the public events page.
Carry is_global through getMaintenanceEventsForEventsByDateRange and the
shared monitor-list grouping, then keep global events in the feed while still
dropping non-global ones whose monitors are all hidden. Global items render
"Affected: All monitors".
Two fixes verified E2E with playwright before pushing:
1. The feed window bounded maintenances to (now - 90d, now), excluding
any SCHEDULED maintenance with a future start_date_time. Split the
range so incidents keep their past-only window but maintenances span
(now - 90d, now + 90d), so subscribers learn about upcoming windows
alongside historical ones. Incidents are still past-bounded.
2. Mirror the subMenuOptions.showRssFeed toggle on the Subscriptions
admin page. RSS is a notification channel alongside email subs, so
admins thinking about subscriber-facing surface area shouldn't have
to bounce to Site Configurations to manage it. Same backing
site_data key — toggling either place updates the other on reload.
The feed routes (/rss.xml etc) stay reachable when the toggle is
off; the toggle only controls the in-page icon button.
Review feedback on #747:
- Wrap alertingQueue.push in try/catch in both data PATCH endpoints — the
sample is already committed, so a Redis/BullMQ outage must not turn a
successful write into a 500 (which would invite client retries and
duplicate MANUAL rows).
- Compute lastWrittenTs with GetMinuteStartTimestampUTC(body.end_ts) —
UpdateMonitoringData floors both bounds to minute starts and writes
through the floored end inclusive, so the previous raw-offset formula
could name a timestamp no stored row has when start_ts was unaligned.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addresses coderabbit review on #746: the bounds checks allowed decimals.
Number.isInteger is now part of the validity deriveds in the monitor-level
status history card, and the pages editor gains the same guard (it previously
saved display settings with no validation at all) plus step=1 and error
styling on the inputs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addresses coderabbit's outside-diff comment on #746: isDesktopValid and
isMobileValid are derived once and reused by isValid and both input error
states, instead of repeating the bounds check four times.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address coderabbit review on #746:
- toApiPageSettings runs type-filtering sanitizers over stored incidents and
include_maintenances so wrong-typed leaves (enabled: "yes",
max_count: "five") never override defaults in responses
- validatePageSettings checks every known leaf: booleans for enabled/show,
non-negative integers for max_count/days fields
- DeepPartial recurses only into plain object maps; arrays pass through
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address review feedback on #746:
- applyPageSettingsPatch now deep-merges the mapped patch into the stored
JSON: nested unknown keys survive, untouched siblings survive, and extra
client keys are persisted as the schema's additionalProperties allows
- toApiPageSettings normalizes invalid stored values (unknown layout style,
out-of-range or non-integer history days) so responses always satisfy the
OpenAPI enum and bounds
- new PageSettingsPatch (DeepPartial) type lets TS clients send partial
nested updates like { monitor_status_history_days: { mobile: 14 } }
- incidents / include_maintenances and their known sub-objects are now
validated as objects (incidents: 123 returns 400)
- CONTEXT.md wording: the UI and API expose the same settings, surfaces may
name fields differently
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
DeletePage in pagesController already enforces this invariant for the manage
UI (the UI's delete confirm for home was always rejected server-side), and
the public site root assumes the home page exists. Before the ~home token
the v4 DELETE could never reach the home page; now that it can, it returns
400 like the rest of the app.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
What a consumer reads is now exactly what it can address: the list, single,
and write responses all show ~home for the home page instead of an empty
string, so list -> pick -> PATCH round-trips cleanly. Read-modify-write
bodies that send ~home back are treated as no path change. The token moves
to global-constants as HOME_PAGE_TOKEN.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The home page is stored with an empty page_path, which can not appear as a
URL segment, so /api/v4/pages/{page_path} could not address it at all. The
middleware now maps the special segment ~home to the empty-path lookup. The
token can never collide with a real page because the path sanitizer strips
tildes, and tilde is RFC 3986 unreserved so clients never need to encode it
(percent-encoded %7Ehome works too).
Semantics follow the manage UI: PATCH via ~home accepts every field except
page_path, which is fixed for the home page, and DELETE is allowed.
Requests to /api/ paths with no matching route (e.g. GET /api/pages/) now
return a json NOT_FOUND error instead of SvelteKit's html error page.
Documented in the OpenAPI spec (PagePath parameter + PATCH note), ADR 0004,
and the CONTEXT.md glossary.
Fixes#737
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address review feedback on #744:
- clamp DATABASE_POOL_MAX to >= 1 and DATABASE_POOL_MIN to <= max so bad
env values can not produce a pool that fails every acquire
- healthcheck redis probe checks client status before PING so commands are
not queued indefinitely while redis is down (maxRetriesPerRequest is null)
- probe() clears its timeout timer once the check settles
- error.html shows only the status code, not the error message
- docs: correct SQLite default to kener.sqlite.db to match knexfile
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address review feedback on #742:
- GetSiteURL now only returns absolute http(s) origins, falling back to the
ORIGIN env var when the stored siteURL is unset or scheme-less, so the v4
url fields honor the OpenAPI format: uri contract
- event status casts now use the response interface types
(MaintenanceEventResponse["status"], MaintenanceEventDetailResponse["event_status"])
so READY is included and the casts can not drift from the API contract
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The public /maintenances/<id> route is keyed by maintenance EVENT id by
default, while /api/v4/maintenances returns maintenance ids. Consumers
that concatenated API ids onto the public path landed on the wrong page
(#723) — an apparent off-by-one title mismatch with no actual data
corruption.
Instead of flipping the route default (which would break every internal
link, subscriber email, and bookmarked URL), v4 API responses now carry
an absolute `url` field built from the configured Site URL:
- Maintenance responses link via /maintenances/<id>?type=maintenance
- Maintenance event responses link via /maintenances/<event_id>
- Incident responses link via /incidents/<id> (parity)
Also updates the OpenAPI spec, records the decision in
docs/adr/0002, and pins Maintenance vs Maintenance Event in CONTEXT.md.
Fixes#723
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The sticky theme-plus bar has no background, and the gap between it and the
fixed nav stays transparent, so page content shows through both sections as
it scrolls. Paint one blurred layer behind both (above content, below nav
z-10 and bar z-20).
ThemePlus now renders a small Rss icon-button next to Subscribe / share
controls, scoped to the current view (monitor / page / default) and
opening the feed in a new tab. Gated on subMenuOptions.showRssFeed
(defaults true) so admins can hide it via Manage → Site Configurations →
Monitor Sub Menu Options without losing the underlying feed routes.
Adds RSS feed key to all 22 locale files (untranslated; RSS is a tech term).
New route GET /monitors/{monitor_tag}/rss.xml serves a feed scoped to a
single monitor (404s for hidden, inactive, or unknown monitors). Existing
page-scoped routes are unchanged in URL; renderRssFeedResponse now takes
a discriminated scope arg so all three routes share one handler.
The (kener) layout emits a <link rel="alternate" type="application/rss+xml">
in <head>, scoped to the current view: monitor page -> monitor feed,
named status page -> page feed, otherwise default. Lets feed readers and
browsers discover the feed automatically. Title attribute is hardcoded
(machine-facing) so no locale files are touched.
Exposes /rss.xml (default page) and /{page_path}/rss.xml (named pages).
Items inherit visibility from the existing events-by-month data path:
hidden monitors are stripped, KENER_BASE_PATH is honored in absolute
links, and forceExclusivity is respected on the default route.
Feed window: last 90 days, 50 most-recent items, sorted desc.
Response: application/rss+xml; charset=utf-8, Cache-Control max-age=300.
404 on unknown page_path or when siteURL is not configured.
Adds a Core Concepts doc page covering URLs, item shape, and verification.
No user-facing UI text; i18n locales untouched.