From 83465bdca70d5236984fe5044c260f40d3b6142f Mon Sep 17 00:00:00 2001
From: Anthony Baldwin <6219998+anthonybaldwin@users.noreply.github.com>
Date: Thu, 13 Aug 2026 08:32:46 -0700
Subject: [PATCH] fix(instatus): anchor the year heuristic on the entry's
earliest timestamp
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The Instatus Atom feed omits the year from each update's `` timestamp,
so the parser reconstructed it from `` and rolled forward a year for
anything landing more than 24h before it. That assumes `` is a lower
bound on the entry's updates, which holds for incidents but not for scheduled
maintenance: there `` is the scheduled START, while the announcement
update is posted days or weeks earlier (and `` carries that time).
An early announcement was therefore dated a year ahead, which made it sort last,
so the entry's status came from the announcement instead of the final
"Completed" update. `resolved_at` stayed null and the maintenance was counted
open forever — it kept a pinned card ungreyed and inflated the presence ticker's
active-incident count. Kagi's Jul 19 database maintenance (announced Jul 8) has
been stuck open since Jul 19.
Anchor on the earlier of `` and `` instead. Verified against
the live kagi and perplexity feeds: this changes exactly one update timestamp
(the stuck announcement) and drops the false open incident, with no other
timestamp shifting.
---
src/providers/instatus.test.ts | 44 +++++++++++++++++++++++++++++++
src/providers/instatus.ts | 47 ++++++++++++++++++++++++++--------
2 files changed, 80 insertions(+), 11 deletions(-)
diff --git a/src/providers/instatus.test.ts b/src/providers/instatus.test.ts
index 8cb7965..58678f3 100644
--- a/src/providers/instatus.test.ts
+++ b/src/providers/instatus.test.ts
@@ -154,6 +154,50 @@ describe("parseInstatusAtom", () => {
});
});
+// A scheduled maintenance announced well before it runs: is the
+// scheduled START (Jul 19) while is the announcement (Jul 8), so the
+// first update legitimately predates . Anchoring the year-rollover
+// heuristic on alone pushed that update into the NEXT year, which
+// made it sort last and left the entry looking permanently in_progress.
+const ATOM_MAINTENANCE_ANNOUNCED_EARLY = `
+
+
+ tag:status.kagi.com,2005:Maintenance/cmrbr5ynx05vc0kp9eoawdsbu
+ 2026-07-19T06:00:00.000+00:00
+ 2026-07-08T07:24:48.978+00:00
+
+ Database maintenance for Kagi Search
+ Type: Maintenance
+
Duration: 8 minutes
+
Jul 8, 07:24:48 GMT+0 Identified -
+ We plan to perform a minor upgrade at this time..
+
Jul 19, 06:00:01 GMT+0 Identified -
+ Maintenance is now in progress.
+ ]]>
+
+`;
+
+describe("parseInstatusAtom — maintenance announced before its scheduled start", () => {
+ const [maint] = parseInstatusAtom(ATOM_MAINTENANCE_ANNOUNCED_EARLY);
+
+ test("keeps the announcement in the published year instead of rolling it forward", () => {
+ expect(maint.incident_updates.map((u) => u.created_at)).toEqual([
+ "2026-07-08T07:24:48.000Z",
+ "2026-07-19T06:00:01.000Z",
+ "2026-07-19T06:07:55.000Z",
+ ]);
+ });
+
+ test("resolves on the Completed update rather than staying in_progress forever", () => {
+ expect(maint.status).toBe("resolved");
+ expect(maint.resolved_at).toBe("2026-07-19T06:07:55.000Z");
+ expect(maint.created_at).toBe("2026-07-08T07:24:48.000Z");
+ });
+});
+
import { mapInstatusSummary, instatusPageStatus, type InstatusSummaryJson } from "./instatus";
const SUMMARY_ACTIVE: InstatusSummaryJson = {
diff --git a/src/providers/instatus.ts b/src/providers/instatus.ts
index 826e292..a8fd34a 100644
--- a/src/providers/instatus.ts
+++ b/src/providers/instatus.ts
@@ -73,11 +73,16 @@ const MONTHS: Record = {
/**
* Parse an Instatus feed `` timestamp like "Jun 5, 01:40:38 GMT+0"
- * (no year) into an ISO-8601 UTC string. The year is taken from `publishedIso`;
- * if the resulting date lands before `publishedIso` (beyond a small grace
- * window), it belongs to the following year (incident spanning a year boundary).
+ * (no year) into an ISO-8601 UTC string. The year is taken from `anchorIso`;
+ * if the resulting date lands before `anchorIso` (beyond a small grace
+ * window), it belongs to the following year (entry spanning a year boundary).
+ *
+ * `anchorIso` MUST be a lower bound on the entry's updates — see
+ * `entryAnchor()`. Anchoring on `` alone is wrong for scheduled
+ * maintenance, where `` is the scheduled start and the announcement
+ * update legitimately precedes it.
*/
-export function parseUpdateTimestamp(small: string, publishedIso: string): string | null {
+export function parseUpdateTimestamp(small: string, anchorIso: string): string | null {
const m = small.match(/([A-Za-z]{3})\s+(\d{1,2})\s*,\s+(\d{1,2}):(\d{2}):(\d{2})/);
if (!m) return null;
const month = MONTHS[m[1].toLowerCase()];
@@ -87,13 +92,13 @@ export function parseUpdateTimestamp(small: string, publishedIso: string): strin
const min = Number(m[4]);
const sec = Number(m[5]);
- const published = new Date(publishedIso);
- const year = Number.isNaN(published.getTime()) ? new Date(0).getUTCFullYear() : published.getUTCFullYear();
+ const anchor = new Date(anchorIso);
+ const year = Number.isNaN(anchor.getTime()) ? new Date(0).getUTCFullYear() : anchor.getUTCFullYear();
let date = new Date(Date.UTC(year, month, day, hour, min, sec));
- // The feed omits the year, so an update that lands before `published` (with a
+ // The feed omits the year, so an update that lands before `anchor` (with a
// ~24h grace window for rounding) must belong to the following year (Dec→Jan).
- if (!Number.isNaN(published.getTime()) && date.getTime() < published.getTime() - 24 * 3600 * 1000) {
+ if (!Number.isNaN(anchor.getTime()) && date.getTime() < anchor.getTime() - 24 * 3600 * 1000) {
date = new Date(Date.UTC(year + 1, month, day, hour, min, sec));
}
if (Number.isNaN(date.getTime())) return null;
@@ -122,6 +127,24 @@ function firstMatch(source: string, re: RegExp): string | undefined {
return m ? m[1] : undefined;
}
+/**
+ * Lower bound for an entry's update timestamps, used to resolve the year the
+ * feed omits. `` is NOT that bound on its own: for a scheduled
+ * maintenance it is the scheduled start, while the announcement update can be
+ * days or weeks earlier (`` carries that announcement time). Taking
+ * the earlier of the two keeps early announcements in their real year — dating
+ * one a year ahead makes it sort last, so the entry's derived status comes from
+ * the announcement instead of the final "Completed" and the incident never
+ * looks resolved.
+ */
+function entryAnchor(published: string, updated: string | undefined): string {
+ const publishedMs = new Date(published).getTime();
+ const updatedMs = updated === undefined ? NaN : new Date(updated).getTime();
+ if (Number.isNaN(updatedMs)) return published;
+ if (Number.isNaN(publishedMs)) return updated as string;
+ return updatedMs < publishedMs ? (updated as string) : published;
+}
+
type ParsedUpdate = { status: string; body: string; created_at: string };
/**
@@ -130,7 +153,7 @@ type ParsedUpdate = { status: string; body: string; created_at: string };
* `STATUS -` marker. Header blocks (`Type: …`)
* are skipped because their `` text ends in a colon.
*/
-function parseUpdateBlocks(content: string, publishedIso: string, isMaintenance: boolean): ParsedUpdate[] {
+function parseUpdateBlocks(content: string, anchorIso: string, isMaintenance: boolean): ParsedUpdate[] {
const updates: ParsedUpdate[] = [];
const blockRe = /