Skip to content

feat(positions): create terminal clients from the client side - #201

Merged
windischb merged 4 commits into
developfrom
feat/terminal-client-from-client-side
Aug 15, 2026
Merged

feat(positions): create terminal clients from the client side#201
windischb merged 4 commits into
developfrom
feat/terminal-client-from-client-side

Conversation

@windischb

@windischb windischb commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Was

„Ich will beides — wie in Service Accounts", präzisiert nach Design-Diskussion: Terminal-Clients entstehen in der UI ausschließlich im Positions-Modal; der Client-Seite-Anlegeweg existiert als API-Pfad (Automation), und im OAuth-Client-Grid bleiben Terminal-Clients als read-only Geräte-Inventar sichtbar.

Die tragende Regel (Terminal-Gegenstück zur client_credentials ⇔ ServiceAccount-Kopplung): ein Client mit dem Staffing-Grant braucht zwingend eine Position — referenziert (LinkedPositionPrincipalId) oder inline neu angelegt (NewPosition), nie beides.

Backend

  • CreateClientAsync erkennt Terminal-Absicht (Staffing-Grant oder Positions-/Terminal-Felder) und biegt in einen eigenen Zweig ab, der an StageCreateTerminalClient delegiert — den einzigen Produzenten des Fixprofils (public, secretless, DPoP-pflichtig, Reference-Tokens, exakt device_code + refresh_token + staffing). Das Profil kann nicht abdriften.
  • Position laden oder event-sourced inline anlegen (PositionPrincipalCreatedEvent), inkl. gestagter Grant-User; TerminalEnrollment-Stream startet im selben Save. Ein SaveChangesAsync — Position, Grants, Slot und Client existieren zusammen oder gar nicht.
  • ClientId-Konvention {position}.terminal.{8-Zeichen-Suffix} (auto-generiert, Audit liest die Position aus der Kennung).
  • Guards: DCR kann nie Terminal-Clients anlegen; Staffing-Grant per PUT nachrüsten ist verboten (symmetrisch zum cc-Guard); Endpoint verlangt position:write zusätzlich zu oauth-client:write und 404t bei ausgeschaltetem Feature-Flag.
  • OAuthClientDto trägt jetzt LinkedPositionPrincipalId; OAuthClientCreatedDto trägt CreatedPosition + CreatedTerminalId; der Endpoint dispatcht Position-/Terminal-SignalR-Events.

Frontend

  • Client-Grid: neue Terminal-Spalte (Name der besitzenden Position — die Geräteflotte ist auf einen Blick zählbar). Doppelklick auf einen Terminal-Client springt ins Positions-Modal (gleiche Regel wie SA-verwaltete Clients → SA-Editor).
  • Client-Modal: für Terminal-Clients ein reiner Viewer (Hinweis + „Zur Position", Footer nur Schließen). Der Staffing-Grant wird im Create nicht angeboten; ein bestehender Terminal-Client zeigt seine Grants aber weiterhin an.
  • Positions-Modal: RP-ID-Vorbelegung und -Sperre, sobald die Position Slots hat — Passkeys hängen an der RP-ID, jeder weitere Slot erbt sie.

Verifiziert

  • 13 Integrationstests (TerminalClientFromClientSideTests) für den API-Anlegeweg: beide Link-Modi, Ausschließlichkeit, alle Guards, Atomarität. Positions+OAuth-Suite 79/79, Unit 1519/1519 grün.
  • Lokal durchgeklickt (lokales Backend + Vite gegen Dev-DB): API-Create → 201 mit Fixprofil + Konvention-ClientId; Terminal-Spalte zeigt die Position; Viewer + „Zur Position"-Sprung landet im Positions-Modal; Staffing-Grant im Create-Picker abwesend; RP-ID-Sperre beim Folge-Slot; Delete-Kaskade (fix(positions): SignalR hub, delete cascade for terminal slots, common.remove #200) live mitverifiziert.
  • Doku: docs/admin/positions.md (Terminal-Clients in Grid/Modal + API-Pfad), docs/integrate/position-terminals.md.

🤖 Generated with Claude Code

windischb and others added 4 commits August 15, 2026 11:37
The staffing counterpart of the client_credentials <-> ServiceAccount rule:
a client carrying the staffing grant must reference an existing Position or
inline-create one (never both). CreateClientAsync diverts terminal-intent
requests to a dedicated branch that delegates the client build to
StageCreateTerminalClient (the single producer of the fixed terminal
profile), stages position / grants / enrollment stream, and commits
everything in one SaveChanges. client_id follows the
{position}.terminal.{suffix} convention; DCR and the generic PUT path are
guarded against staffing clients.

Frontend mirrors the ServiceAccount pattern in ClientDetails: staffing
grant option (feature-gated), position picker or PositionDetails draft
modal (new draftOnly mode), terminal slot fields, fixed-profile hints.
RP-IDs are now inherited and locked once a position has slots - passkeys
hang off the RP-ID, so every slot of a position shares it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d, born only in the position modal

Design decision (Bernhard): a terminal client is the technical footprint of
a slot, not a configurable OAuth client - so the client grid keeps it
visible as fleet inventory (new Terminal column naming the owning position)
but read-only: double-click deep-links into the position modal, mirroring
the SA-managed-client rule, and a direct fragment open degrades to a
viewer (close-only footer + "Zur Position").

The staffing-grant create path disappears from the client modal (grant no
longer offered; terminal block, position draft and draftOnly mode removed).
The API path with all invariants and its 13 integration tests stays -
automation can still create position + slot + client in one save.

OAuthClientDto now carries LinkedPositionPrincipalId to drive the column,
the redirect and the viewer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… switch

The position modal is tabbed but ran on the cap-to-content MODAL_MD size,
so switching between the short General tab and the list tabs resized the
panel - against the modal size contract. Same fix as .user-edit-frame in
UserDetails: the route stays cap-to-content, the component pins a 60vh
body (create included, it is tabbed too); long grant/slot/session lists
scroll inside the frame.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The model behind positions/terminals explained plainly: the four building
blocks, the three links (grant / slot assignment / enrollment), why a
position is not a group (distributor vs actor, same person = two actors on
two devices), what the audit attests (unlock, not actions), which principal
for which job - with inline SVG diagrams. Ends with the accepted design
direction (ADR 0003: proof/device-binding menus, position-owned tokens,
realm guard rails, multi-position terminals) clearly marked as roadmap.

Linked from the admin workflow page and the sidebar.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@windischb
windischb merged commit 0a5b21a into develop Aug 15, 2026
8 checks passed
@windischb
windischb deleted the feat/terminal-client-from-client-side branch August 15, 2026 16:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant