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
This commit is contained in:
Joseph Doherty
2026-11-09 17:33:43 -05:00
parent 20fbbe3521
commit b895202ef6
@@ -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.