From b895202ef62bf8d30b8d6278783e49c7db6a0773 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Mon, 9 Nov 2026 17:33:43 -0500 Subject: [PATCH] docs(central-ui): decide against a unified notifications + site-calls outbox page (ScadaBridge#44) Evaluated against the current pages and their data sources. The notification outbox is central-owned, offset-paged and has its own status lifecycle. Site calls are site-owned and keyset-paged, with a different lifecycle and a Retry/Discard that relays to the owning site. A union grid would need a two-cursor merge, a status filter that does not line up, and buttons that hide different semantics. The revisit trigger (operator confusion reports) has not fired. Recommends against building it now. If confusion is reported, start with cross-links and a combined "stuck items" Health card. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV --- ...2026-11-09-unified-outbox-page-decision.md | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 docs/plans/2026-11-09-unified-outbox-page-decision.md diff --git a/docs/plans/2026-11-09-unified-outbox-page-decision.md b/docs/plans/2026-11-09-unified-outbox-page-decision.md new file mode 100644 index 00000000..f80a1288 --- /dev/null +++ b/docs/plans/2026-11-09-unified-outbox-page-decision.md @@ -0,0 +1,75 @@ +# Decision: unified notifications + site-calls outbox page (ScadaBridge#44) + +**Date:** 2026-11-09 · **Decision:** **recommend against** building a unified page now. Keep +the two report pages. +**Origin:** deferred-work register row 17 → Gitea #44. That row deferred M9 decision D3 +(`2026-06-18-m9-templates-authoring-design.md`), whose revisit trigger is "operator confusion +reports". + +## What exists today (verified against `main`) + +| | Notification Report `/notifications/report` | Site Calls `/site-calls/report` | +|---|---|---| +| Owner of the queue | **Central**: the notification outbox is a central table, and central delivers. | **Site**: site store-and-forward delivers. Central's `SiteCalls` table is an audit mirror. | +| Query message | `NotificationOutboxQueryRequest` | `SiteCallQueryRequest` | +| Paging | Offset (`PageNumber`/`PageSize`, `TotalCount`) | Keyset (`AfterCreatedAtUtc` + `AfterId`); no total count | +| Id | `string` NotificationId (N-form GUID text) | `Guid` TrackedOperationId | +| Status lifecycle | Pending → Retrying → Delivered / Parked / Discarded | Submitted → Forwarded → Attempted → Delivered / Parked / Failed / Discarded | +| Row provenance | list name, subject, source site **and instance** | channel (`ApiOutbound`/`DbOutbound`), target, HTTP status, source site; **no instance** column exists | +| Retry / Discard on Parked rows | Central acts on its own row. | Central **relays** the action to the owning site, which has its own `SiteUnreachable` outcome. | +| Deep link `?correlationId=` | yes (Flow Debugger, Audit Log) | yes (Flow Debugger, Audit Log) | +| Role gate | `RequireDeployment` | `RequireDeployment` | +| KPIs | Notification KPIs page plus Health-dashboard tiles | Site-call trends on the page plus Health-dashboard tiles | + +The Health dashboard already shows the Notification Outbox and Site Call KPI groups next to +each other. That is the existing "one place to see what is stuck". + +## Evaluation + +A unified page would need one row model and one pager over two sources that disagree on every +axis that matters to the grid: + +1. **Paging cannot be merged cheaply.** Interleaving an offset-paged source with a keyset-paged + source by `CreatedAt` needs a two-cursor merge with no total count. Alternatively, one of + the two repository queries would have to be reworked. Either change touches the central query + actors, not just the UI. +2. **Status filters do not line up.** The two lifecycles share only Delivered, Parked and + Discarded. A unified status dropdown either hides real states (Retrying, Forwarded, + Attempted, Failed) or shows options that match only one source. +3. **The actions have different semantics.** Retrying a notification is a local central write. + Retrying a site call is a relay that can fail because the site is unreachable. Putting both + behind one button makes the more fragile action look the same as the safe one. A confirm + dialog that has to explain this per row type is exactly the confusion the issue wants to + avoid. +4. **The columns barely overlap.** The only columns both sources share are site, status, + retry count, last error and created-at. Everything an operator needs to act on is + type-specific: list and subject for a notification, target and HTTP status for a site call. +5. **No trigger has fired.** The revisit trigger is operator confusion reports. None is + recorded: #44 carries no report, and no other ScadaBridge issue mentions the outbox or + site-call pages (searched 2026-11-09). Both pages are + discoverable from the nav rail, from the Health tiles and from Flow Debugger / Audit Log + deep links. +6. **Cost.** The existing pages are about 860 and 420 lines plus their bUnit suites + (`NotificationReportPageTests`, `SiteCallsReportPageTests`, detail-modal and drill-in + tests). A third page would duplicate filters, a detail modal and the action flows for both + types. Or it would replace the two pages and require reworking their tests and deep links. + That is not "small and clearly beneficial", the bar #44 set for implementing it. + +## Recommendation + +**Do not build the unified page.** M9's reasoning still holds on today's code, and the trigger +for revisiting it has not fired. + +If operators do report confusion, start with these cheaper options before any union view: + +- **Cross-links:** a "Site Calls →" link on the Notification Report header and the reverse. + This is a one-line change per page. +- **One "Stuck items" summary card** on the Health dashboard. It would combine the two + existing KPI snapshots, which are already loaded there, into a single "N stuck: X + notifications, Y site calls" card that deep-links to each page with the stuck filter + pre-applied. Site Calls already accepts `?stuck=true`. The Notification Report reads only + `?correlationId=` today, so it would need the same small `?stuck=` seed. The card reuses + the existing queries and needs no new paging model. + +Revisit a true union page only if both of these prove insufficient. At that point the +precondition is a shared keyset cursor on both repositories (point 1), designed first.