diff --git a/src/app/watchtower/_data/watchtower-data.ts b/src/app/watchtower/_data/watchtower-data.ts new file mode 100644 index 0000000..6419d76 --- /dev/null +++ b/src/app/watchtower/_data/watchtower-data.ts @@ -0,0 +1,104 @@ +export const watchtowerOverview = { + eyebrow: "Watchtower route / Alert digest", + title: "Scan alert drift, service health, and operator notes before the next rotation.", + description: + "The watchtower route compresses high-signal alerts, short health summaries, and operator context into a single handoff view for the next duty lead.", + digestWindow: "Digest window · 18:00 to 22:00 UTC", + focusNote: + "Two critical digests still need explicit operator approval before automation can widen traffic again.", + reviewWindow: "22:20 UTC", + shiftLead: "Shift lead · N. Alvarez", +}; + +export const watchtowerAlerts = [ + { + severity: "critical" as const, + service: "Orbital relay mesh", + title: "Fallback relays are saturating the north relay spine.", + scope: "21 relays / 3 regions", + owner: "M. Chen", + digest: + "Queue pressure crossed the auto-shed threshold twice after the west mesh reroute, so traffic is holding on manual trims until the next clean sample.", + nextStep: "Approve a 12-minute route trim before 22:20 UTC.", + affectedSystems: ["mesh-east-2", "north-spine cache", "fallback batch 4"], + }, + { + severity: "elevated" as const, + service: "Payload scanner lane", + title: "Scanner calibration drift is slowing parcel verification.", + scope: "4 scanner cells", + owner: "R. Singh", + digest: + "Verification retries climbed to 8.3% after the last firmware pull, which is still within failover range but now exceeding the shift target.", + nextStep: "Hold inbound surges until calibration pass C finishes.", + affectedSystems: ["scan-west-4", "handoff queue", "firmware mirror"], + }, + { + severity: "critical" as const, + service: "Manifest handoff", + title: "Manifest receipts are arriving behind the operator review window.", + scope: "2 late cutover lanes", + owner: "L. Ortega", + digest: + "Late receipts are not dropping traffic, but they are now compressing the audit review window enough to threaten the next automated closeout.", + nextStep: "Keep closeout manual for the next two digest cycles.", + affectedSystems: ["receipt stream", "lane-c7", "audit closeout bot"], + }, +]; + +export const watchtowerHealthSummaries = [ + { + tone: "risk" as const, + title: "Relay mesh health", + signal: "Capacity margin 68%", + owner: "Transit network", + summary: + "Manual trims are keeping the mesh stable, but the reserve margin is now thin enough that another burst would force wider shedding.", + recommendation: "Keep bulk traffic capped until relay drift clears.", + }, + { + tone: "watch" as const, + title: "Verification lane health", + signal: "Retry rate 8.3%", + owner: "Dock systems", + summary: + "Scanner throughput is still acceptable for current volume, although the current retry profile will create queue drag if inbound demand spikes.", + recommendation: "Finish calibration pass C before lifting intake caps.", + }, + { + tone: "stable" as const, + title: "Operator coverage health", + signal: "Coverage confidence 94%", + owner: "Shift command", + summary: + "Crew overlap and specialist coverage remain strong enough to absorb manual interventions without extending the next handoff.", + recommendation: "Use the reserve reviewer to shadow critical receipts only.", + }, +]; + +export const watchtowerOperatorNotes = [ + { + status: "tracking" as const, + channel: "Ops chat", + author: "N. Alvarez", + time: "21:42 UTC", + note: "Keep the north relay trim manual until queue depth stays below 68% for two consecutive samples.", + followUp: "Review after the 22:10 UTC digest snapshot.", + }, + { + status: "queued" as const, + channel: "Dock review", + author: "R. Singh", + time: "21:37 UTC", + note: "Bundle scanner drift updates into one operator post so the incoming shift does not chase duplicate recalibration notes.", + followUp: "Publish a single rollup before handoff.", + }, + { + status: "closed" as const, + channel: "Audit lane", + author: "L. Ortega", + time: "21:31 UTC", + note: "Receipt lag is manageable as long as the closeout bot stays paused and lane C7 remains on a human review path.", + followUp: "Confirm pause state in the 22:20 UTC review.", + }, +]; diff --git a/src/app/watchtower/page.test.tsx b/src/app/watchtower/page.test.tsx new file mode 100644 index 0000000..6982e3f --- /dev/null +++ b/src/app/watchtower/page.test.tsx @@ -0,0 +1,111 @@ +import { render, screen, within } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; + +import { + watchtowerAlerts, + watchtowerHealthSummaries, + watchtowerOperatorNotes, + watchtowerOverview, +} from "./_data/watchtower-data"; +import WatchtowerPage from "./page"; + +describe("WatchtowerPage", () => { + it("renders the route shell headings, pulse copy, and primary actions", () => { + render(); + + expect(screen.getByRole("heading", { name: watchtowerOverview.title })).toBeInTheDocument(); + expect(screen.getByText(watchtowerOverview.digestWindow)).toBeInTheDocument(); + expect(screen.getByText(watchtowerOverview.shiftLead)).toBeInTheDocument(); + expect( + screen.getByRole("heading", { + name: /alert digest cards built for the next operator handoff/i, + }), + ).toBeInTheDocument(); + expect( + screen.getByRole("heading", { + name: /short health summaries for the systems that shape the handoff/i, + }), + ).toBeInTheDocument(); + expect( + screen.getByRole("link", { name: /jump to alert digest/i }), + ).toHaveAttribute("href", "#watchtower-digest"); + expect( + screen.getByRole("link", { name: /review health summaries/i }), + ).toHaveAttribute("href", "#watchtower-health"); + expect( + screen.getByRole("link", { name: /back to route index/i }), + ).toHaveAttribute("href", "/"); + }); + + it("renders every alert digest card with scope, ownership, systems, and next steps", () => { + render(); + + const alertList = screen.getByRole("list", { name: /watchtower alert digest cards/i }); + + expect(alertList.querySelectorAll(':scope > [role="listitem"]')).toHaveLength( + watchtowerAlerts.length, + ); + + for (const alert of watchtowerAlerts) { + const card = within(alertList) + .getByRole("heading", { name: alert.title }) + .closest('[role="listitem"]'); + + expect(card).toBeTruthy(); + expect(card?.textContent).toContain(alert.service); + expect(card?.textContent).toContain(alert.scope); + expect(card?.textContent).toContain(alert.owner); + expect(card?.textContent).toContain(alert.nextStep); + expect(card?.textContent).toContain(alert.affectedSystems[0]); + } + }); + + it("renders health summaries with signals, owners, and recommendations", () => { + render(); + + const healthSection = screen.getByLabelText(/watchtower health summaries/i); + const healthList = within(healthSection).getByRole("list", { name: /watchtower health summary cards/i }); + + expect(within(healthList).getAllByRole("listitem")).toHaveLength( + watchtowerHealthSummaries.length, + ); + + for (const summary of watchtowerHealthSummaries) { + expect(within(healthSection).getByText(summary.title)).toBeInTheDocument(); + expect(within(healthSection).getByText(summary.signal)).toBeInTheDocument(); + expect(within(healthSection).getByText(summary.owner)).toBeInTheDocument(); + expect(within(healthSection).getByText(summary.recommendation)).toBeInTheDocument(); + } + + expect(within(healthSection).getByText("Stable")).toBeInTheDocument(); + expect(within(healthSection).getByText("Watch")).toBeInTheDocument(); + expect(within(healthSection).getByText("Risk")).toBeInTheDocument(); + }); + + it("renders the operator notes rail with the handoff anchor and every note entry", () => { + render(); + + const notesRail = screen.getByLabelText(/operator notes rail/i); + const noteList = within(notesRail).getByRole("list", { name: /watchtower operator note entries/i }); + + expect(within(notesRail).getByText(watchtowerOverview.focusNote)).toBeInTheDocument(); + expect( + within(notesRail).getByText(`Review window · ${watchtowerOverview.reviewWindow}`), + ).toBeInTheDocument(); + expect(noteList.querySelectorAll(':scope > [role="listitem"]')).toHaveLength( + watchtowerOperatorNotes.length, + ); + + for (const note of watchtowerOperatorNotes) { + const card = within(noteList) + .getByText(note.author) + .closest('[role="listitem"]'); + + expect(card).toBeTruthy(); + expect(card?.textContent).toContain(note.channel); + expect(card?.textContent).toContain(note.time); + expect(card?.textContent).toContain(note.note); + expect(card?.textContent).toContain(note.followUp); + } + }); +}); diff --git a/src/app/watchtower/page.tsx b/src/app/watchtower/page.tsx new file mode 100644 index 0000000..420b1ac --- /dev/null +++ b/src/app/watchtower/page.tsx @@ -0,0 +1,307 @@ +import type { Metadata } from "next"; +import Link from "next/link"; + +import { + watchtowerAlerts, + watchtowerHealthSummaries, + watchtowerOperatorNotes, + watchtowerOverview, +} from "./_data/watchtower-data"; + +export const metadata: Metadata = { + title: "Watchtower", + description: + "Alert digest cards, service health summaries, and a compact operator notes rail for the next shift handoff.", +}; + +const alertToneClasses = { critical: "border-rose-400/35 bg-rose-500/10", elevated: "border-amber-300/35 bg-amber-300/10", watch: "border-cyan-300/35 bg-cyan-400/10" }; +const alertToneLabels = { critical: "Critical", elevated: "Elevated", watch: "Watch" }; +const healthToneClasses = { stable: "border-emerald-300/25 bg-emerald-300/10", watch: "border-amber-300/25 bg-amber-300/10", risk: "border-rose-300/25 bg-rose-400/10" }; +const healthToneLabels = { stable: "Stable", watch: "Watch", risk: "Risk" }; +const noteStatusClasses = { queued: "border-amber-300/25 bg-amber-300/10", tracking: "border-cyan-300/25 bg-cyan-300/10", closed: "border-slate-400/25 bg-slate-400/10" }; +const noteStatusLabels = { queued: "Queued", tracking: "Tracking", closed: "Closed" }; + +export default function WatchtowerPage() { + const criticalAlerts = watchtowerAlerts.filter( + (alert) => alert.severity === "critical", + ).length; + const attentionSummaries = watchtowerHealthSummaries.filter( + (summary) => summary.tone !== "stable", + ).length; + const openNotes = watchtowerOperatorNotes.filter( + (note) => note.status !== "closed", + ).length; + + const handoffStats = [ + { label: "Critical digests", value: String(criticalAlerts), detail: "Need explicit approval before automated widening resumes." }, + { label: "Attention summaries", value: String(attentionSummaries), detail: "Systems showing watch or risk posture during this digest window." }, + { label: "Open notes", value: String(openNotes), detail: "Operator follow-ups still active for the incoming rotation." }, + ]; + + return ( +
+
+
+
+
+

+ {watchtowerOverview.eyebrow} +

+

+ {watchtowerOverview.title} +

+

+ {watchtowerOverview.description} +

+ + +
+ +
+
+ +
+
+
+
+
+

+ Alert digest cards +

+

+ Alert digest cards built for the next operator handoff +

+
+

+ Each card compresses service scope, digest context, affected + systems, and the next operator decision into one scan-ready + block. +

+
+
+ {watchtowerAlerts.map((alert) => ( +
+
+ + {alertToneLabels[alert.severity]} + + + {alert.service} + +
+

+ {alert.title} +

+

+ {alert.scope} · {alert.owner} +

+

+ {alert.digest} +

+
+ {alert.affectedSystems.map((system) => ( + + {system} + + ))} +
+

+ {alert.nextStep} +

+
+ ))} +
+
+ +
+
+
+

+ Health summaries +

+

+ Short health summaries for the systems that shape the handoff +

+
+

+ These summaries stay short on purpose: one signal, one owner, + one recommendation, and no hidden secondary state. +

+
+ +
+ {watchtowerHealthSummaries.map((summary) => ( +
+

+ {healthToneLabels[summary.tone]} +

+

+ {summary.title} +

+

+ {summary.signal} +

+

+ {summary.summary} +

+
+

+ Owner +

+

{summary.owner}

+

+ {summary.recommendation} +

+
+
+ ))} +
+
+
+ + +
+
+
+ ); +}