TypeScript 55.4%
Python 43.2%
SQL 1.2%
1import { sha256, simhash, type ConnectorMetadata, type NormalizedContent, type Observation, type SensorEndpoint } from "@websensor/core";2import { httpFetchWithRetry } from "./fetcher";3import { NormalizeError, type WebSensorConnector } from "./types";45/**6 * Status-page connector for the non-Atlassian providers, normalized to the same item shape as7 * `statuspage` (incident:<id> / maintenance:<id> / overall) so downstream heuristics are shared.8 *9 * instatus — https://<page>/summary.json { page:{name,url,status}, activeIncidents?:[…], activeMaintenances?:[…] }10 * incidentio — https://<page>/api/v1/summary { page_title, page_url, ongoing_incidents, in_progress_maintenances, scheduled_maintenances }11 * statusio — https://api.status.io/1.0/status/<pageId> { result:{ status_overall, status:[…components], incidents:[…], maintenance:{active,upcoming} } }12 * hund — https://<page>/issues.json? (not supported yet)13 *14 * Config: { flavor?: "instatus" | "incidentio" | "statusio" } — auto-detected from the JSON shape.15 */16type Flavor = "instatus" | "incidentio" | "statusio";1718interface Item {19 key: string;20 kind: string;21 title: string;22 status: string;23 impact?: string;24 url: string;25 summary: string;26 publishedAt: string | null;27 updatedAt: string | null;28 [k: string]: unknown;29}3031function iso(v: unknown): string | null {32 if (!v) return null;33 const d = new Date(String(v));34 return Number.isNaN(d.getTime()) ? null : d.toISOString();35}3637export function detectFlavor(j: Record<string, unknown>): Flavor | null {38 if (Array.isArray(j.subpages) && (j.subpages as Record<string, unknown>[]).some((p) => p && typeof p === "object" && "summary" in p)) return "incidentio";39 if (j.page && typeof j.page === "object" && ("activeIncidents" in j || "activeMaintenances" in j || typeof (j.page as { status?: unknown }).status === "string")) return "instatus";40 if ("ongoing_incidents" in j || "page_title" in j) return "incidentio";41 if (j.result && typeof j.result === "object" && "status_overall" in (j.result as object)) return "statusio";42 return null;43}4445export function normalizeStatusJson(j: Record<string, unknown>, flavor: Flavor): { items: Item[]; title: string; indicator: string } {46 const items: Item[] = [];47 let title = "";48 let indicator = "";49 if (flavor === "instatus") {50 const page = (j.page ?? {}) as Record<string, unknown>;51 title = String(page.name ?? "");52 indicator = String(page.status ?? "").toLowerCase();53 for (const i of (j.activeIncidents ?? []) as Record<string, unknown>[]) {54 items.push({ key: `incident:${String(i.id)}`, kind: "incident", title: `${String(i.name)} — ${String(i.status).toLowerCase().replace(/_/g, " ")}`, status: String(i.status).toLowerCase(), impact: String(i.impact ?? "").toLowerCase(), url: String(i.url ?? page.url ?? ""), summary: "", publishedAt: iso(i.started), updatedAt: iso(i.updated ?? i.started) });55 }56 for (const m of (j.activeMaintenances ?? []) as Record<string, unknown>[]) {57 items.push({ key: `maintenance:${String(m.id)}`, kind: "maintenance", title: `${String(m.name)} — ${String(m.status).toLowerCase().replace(/_/g, " ")}`, status: String(m.status).toLowerCase(), url: String(m.url ?? page.url ?? ""), summary: m.duration ? `Duration ${String(m.duration)} min` : "", publishedAt: iso(m.start), updatedAt: iso(m.start) });58 }59 items.push({ key: "overall", kind: "overall", title: `Overall: ${indicator || "unknown"}`, status: indicator, url: String(page.url ?? ""), summary: "", publishedAt: null, updatedAt: null });60 } else if (flavor === "incidentio" && Array.isArray(j.subpages)) {61 // Multi-region incident.io pages (e.g. status.miro.com → eu/us/au): merge the sub-summaries, prefix keys.62 let anyIncident = false;63 for (const sp of j.subpages as { subpage?: string; summary?: Record<string, unknown> }[]) {64 if (!sp?.summary) continue;65 const sub = normalizeStatusJson(sp.summary, "incidentio");66 const prefix = String(sp.subpage ?? sub.title ?? "").toLowerCase();67 for (const it of sub.items) {68 if (it.key === "overall") continue;69 items.push({ ...it, key: `${prefix}:${it.key}`, title: `[${prefix.toUpperCase()}] ${it.title}` });70 }71 if (sub.indicator !== "operational") anyIncident = true;72 title = title || String(sp.summary.page_title ?? "").replace(/\s*\((eu|us|au|[a-z]{2})\)$/i, "");73 }74 indicator = anyIncident ? "incident" : "operational";75 items.push({ key: "overall", kind: "overall", title: `Overall: ${indicator}`, status: indicator, url: String(((j.subpages as { summary?: { page_url?: string } }[])[0]?.summary?.page_url ?? "")).replace(/\/[a-z]{2}$/, ""), summary: "", publishedAt: null, updatedAt: null });76 } else if (flavor === "incidentio") {77 title = String(j.page_title ?? "");78 const url = String(j.page_url ?? "");79 const inc = (j.ongoing_incidents ?? []) as Record<string, unknown>[];80 for (const i of inc) {81 const updates = (i.updates ?? []) as Record<string, unknown>[];82 const latest = updates[0];83 items.push({ key: `incident:${String(i.id)}`, kind: "incident", title: `${String(i.name)} — ${String(i.status ?? i.current_status ?? "").toLowerCase().replace(/_/g, " ")}`, status: String(i.status ?? i.current_status ?? "").toLowerCase(), impact: String(i.impact ?? i.worst_impact ?? "").toLowerCase(), url: String(i.url ?? url), summary: latest ? String(latest.message ?? latest.message_string ?? "").slice(0, 800) : "", publishedAt: iso(i.created_at ?? i.started_at), updatedAt: iso(i.updated_at ?? i.last_update_at) });84 }85 for (const m of [...((j.in_progress_maintenances ?? []) as Record<string, unknown>[]), ...((j.scheduled_maintenances ?? []) as Record<string, unknown>[])]) {86 items.push({ key: `maintenance:${String(m.id)}`, kind: "maintenance", title: `${String(m.name)} — ${String(m.status ?? "scheduled").toLowerCase().replace(/_/g, " ")}`, status: String(m.status ?? "scheduled").toLowerCase(), url: String(m.url ?? url), summary: "", publishedAt: iso(m.starts_at ?? m.created_at), updatedAt: iso(m.updated_at ?? m.starts_at) });87 }88 indicator = inc.length ? "incident" : "operational";89 items.push({ key: "overall", kind: "overall", title: `Overall: ${indicator}`, status: indicator, url, summary: "", publishedAt: null, updatedAt: null });90 } else {91 const r = (j.result ?? {}) as Record<string, unknown>;92 const overall = (r.status_overall ?? {}) as Record<string, unknown>;93 indicator = String(overall.status ?? "").toLowerCase();94 title = "";95 for (const i of (r.incidents ?? []) as Record<string, unknown>[]) {96 const msgs = (i.messages ?? []) as Record<string, unknown>[];97 const latest = msgs[0];98 items.push({ key: `incident:${String(i._id)}`, kind: "incident", title: `${String(i.name)} — ${String(i.current_status ?? "").toLowerCase()}`, status: String(i.current_status ?? "").toLowerCase(), impact: String(i.current_impact ?? "").toLowerCase(), url: String(i.shortlink ?? ""), summary: latest ? String(latest.details ?? "").slice(0, 800) : "", publishedAt: iso(i.datetime_open), updatedAt: iso(latest?.datetime ?? i.datetime_open) });99 }100 const maint = (r.maintenance ?? {}) as Record<string, unknown[]>;101 for (const m of [...((maint.active ?? []) as Record<string, unknown>[]), ...((maint.upcoming ?? []) as Record<string, unknown>[])]) {102 items.push({ key: `maintenance:${String(m._id)}`, kind: "maintenance", title: `${String(m.name)} — ${String(m.current_status ?? "scheduled").toLowerCase()}`, status: String(m.current_status ?? "scheduled").toLowerCase(), url: String(m.shortlink ?? ""), summary: "", publishedAt: iso(m.datetime_planned_start), updatedAt: iso(m.datetime_planned_start) });103 }104 for (const c of (r.status ?? []) as Record<string, unknown>[]) {105 for (const cont of (c.containers ?? []) as Record<string, unknown>[]) {106 const st = String(cont.status ?? "").toLowerCase();107 if (st && st !== "operational") items.push({ key: `component:${String(cont.id)}`, kind: "component", title: `${String(c.name)} / ${String(cont.name)} — ${st}`, status: st, url: "", summary: "", publishedAt: iso(cont.updated), updatedAt: iso(cont.updated) });108 }109 }110 items.push({ key: "overall", kind: "overall", title: `Overall: ${indicator || "unknown"}`, status: indicator, url: "", summary: "", publishedAt: null, updatedAt: null });111 }112 return { items, title, indicator };113}114115export class StatusJsonConnector implements WebSensorConnector {116 mode = "list" as const;117 metadata(): ConnectorMetadata {118 return { key: "statusjson", name: "Status page (Instatus / incident.io / Status.io)", sensorTypes: ["STATUSPAGE", "JSON"], description: "Non-Atlassian status providers normalized to incidents, maintenances, components and overall indicator", version: "1.0.0" };119 }120 async fetch(endpoint: SensorEndpoint): Promise<Observation> {121 return httpFetchWithRetry(endpoint.id, endpoint.url, { etag: endpoint.etag, lastModified: endpoint.lastModified, accept: "application/json" });122 }123 async normalize(endpoint: SensorEndpoint, obs: Observation): Promise<NormalizedContent> {124 if (!obs.body) throw new NormalizeError("no_body", "Observation has no body");125 const text = obs.body.toString("utf8");126 let j: Record<string, unknown>;127 try {128 j = JSON.parse(text) as Record<string, unknown>;129 } catch {130 throw new NormalizeError("bad_json", "Status response is not JSON");131 }132 const flavor = ((endpoint.config as { flavor?: Flavor }).flavor ?? detectFlavor(j)) as Flavor | null;133 if (!flavor) throw new NormalizeError("bad_shape", "Unrecognized status JSON (expected Instatus summary.json, incident.io /api/v1/summary or Status.io /1.0/status)");134 const { items, title, indicator } = normalizeStatusJson(j, flavor);135 const canonical = items.map((i) => `${i.key}\t${i.status}\t${i.title}\t${i.updatedAt ?? ""}`).join("\n");136 const newest = items.map((i) => i.publishedAt).filter((x): x is string => Boolean(x)).sort().at(-1);137 const incidents = items.filter((i) => i.kind === "incident").length;138 return {139 mode: "list",140 items,141 compareFields: ["status", "title", "updatedAt"],142 title: title || new URL(endpoint.url).hostname,143 rawHash: sha256(text),144 canonicalHash: sha256(canonical),145 semanticHash: simhash(items.map((i) => i.title).join("\n")),146 publishedAt: newest ? new Date(newest) : null,147 extractionConfidence: 1,148 extra: { flavor, indicator, activeIncidents: incidents, maintenances: items.filter((i) => i.kind === "maintenance").length, degradedComponents: items.filter((i) => i.kind === "component").length },149 };150 }151}152