Files
Fuchs_Intranet/Fuchs/Docs/Decisions/0007-targeted-draft-signalr-groups.md
StefanandClaude Opus 4.8 af445c015e Add backend-authoritative invoice draft editing (ADR 0006/0007)
Move the invoice draft editor to a backend single source of truth: an
in-memory InvoiceDraftSession (per-token, cached) holds the editable payload,
server-computed sums/VAT and validation, plus an automatic change history. The
browser posts single edits; the server recomputes and signals the editing
session over a dedicated SignalR hub (DraftPreviewHub) to re-fetch.

This reverses the previously-documented stateless editor (EVAL_live_invoice_editing,
INVOICE_LIFECYCLE §10), by explicit product decision — captured in ADR 0006 and 0007
plus the live-draft-editing concept doc.

Backend (this milestone):
- InvoiceDraftSession + ChangeHistoryEntry data holders
- InvoiceDraftCalculator: pure port of quantChange/invSumUpdate (§13b, VAT-by-rate)
  and consistency checks — fully unit-tested
- IInvoiceDraftCache/InvoiceDraftCache: in-memory store with idle sliding TTL
- IInvoiceDraftService/InvoiceDraftEditService: open (payload or DB reload), patch,
  build state, flush via existing RegisterInvoiceAsync (no new persistence), preview
  from cache, discard (DB reload), history
- InvoiceDraftExpiryService: pre-expiry warning + eviction-with-reason
- DraftPreviewHub + IDraftNotifier/DraftNotifier: targeted draftReady/draftExpiring/
  draftClosed signals per draft token
- inv/dopen|dstate|dpatch|dpreview|dsave|dhistory|ddiscard|dclose endpoints; save
  reports success/failure via the existing EventService
- DI + hub mapping in Program.cs

Frontend (additive foundation): $fis.draft SignalR client for /draftpreview.
The editor DOM inversion (routing deltas, rendering from server state) is the
next, separately-verified step; existing endpoints are unaffected.

Tests: 30 new (calculator, cache, expiry, patch/history, flush); 306 total passing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 13:29:35 +02:00

60 lines
3.1 KiB
Markdown

---
status: Accepted
date: 2026-07-10
applyTo:
- "Fuchs/Notifications/DraftPreviewHub.cs"
- "Fuchs/Notifications/IDraftNotifier.cs"
- "Fuchs/Notifications/DraftNotifier.cs"
- "Fuchs/Program.cs"
- "Fuchs/js/intranet/**"
supersededBy: ""
---
# 0007 — Draft-editing signals are targeted via a dedicated hub with per-draft groups
## Context
Backend-authoritative draft editing (ADR 0006) needs to notify **exactly the one
browser** editing a given draft that its cached state changed, is about to expire, or
was closed. The existing `NotificationHub` (ADR 0002) deliberately **broadcasts** every
business toast to all logged-in sessions and explicitly deferred per-user/targeted
delivery as "a new decision". Draft coordination pings are high-frequency, per-editor,
and must not spray to every session.
## Decision
Draft signals use a **dedicated** SignalR hub, `DraftPreviewHub`, mapped at
`/draftpreview` (separate from `NotificationHub` at `/notifications`). Targeting is by
**SignalR group named after the draft's session token**:
- The client calls the hub methods `JoinDraft(token)` / `LeaveDraft(token)` to
subscribe/unsubscribe its connection to a draft's group. The hub carries **no
commands** — only group membership (edits are POSTs; see ADR 0006).
- The server sends via `IDraftNotifier` (`DraftNotifier`) to `Clients.Group(token)`:
`draftReady{token,version}` (re-fetch), `draftExpiring{token,secondsLeft}` (idle
warning), `draftClosed{token,reason}` (session evicted/discarded → close the editor).
- Like `EventService`, delivery failures are logged and swallowed — a missed
coordination ping must never fail the underlying operation; the client also re-syncs on
reconnect and on its next POST.
Business success/failure messages for draft operations (e.g. "Zwischenstand
gespeichert") continue to flow through `IEventService`/`NotificationHub`, **not** this
hub — the two channels stay separate.
## Consequences
- The session **token doubles as the group name**; it is an opaque GUID and must not
encode sensitive data. Any browser that knows a token can join its group, so tokens
must be treated as capabilities and only handed to the authenticated editor that opened
the draft.
- Adding a new draft signal means adding a method to `IDraftNotifier` + `DraftNotifier`
and a client handler in `$fis.draft` — not overloading the business notification path.
- ADR 0002 is unchanged: `NotificationHub` stays broadcast-only for toasts. This hub is
the answer to its "if per-user targeting becomes necessary, that is a new decision".
- Multi-instance scale-out needs a SignalR backplane for group delivery — same limitation
as ADR 0006.
## Alternatives considered
- **Reuse `NotificationHub` with groups**: rejected — it would entangle broadcast toasts
with targeted, high-frequency editing pings and force ADR 0002's broadcast contract to
change. A separate hub keeps the concerns and their decisions independent.
- **Per-user groups (by account id)**: rejected — a user may open two drafts/tabs;
per-draft-token groups target the precise editor and naturally support that.