Skip to content

Web GUI Design Preflight Guardrails

Scope

This note is the architecture preflight for UI-007 and the binding guardrail set for UI-008 implementation slices. It is not the UI-007 design specification and does not define the final information architecture, route map, or page wireframes. The UI-007 design specification must derive those details from the approved UI-006 product scope and must leave a direct handoff target for UI-008 implementation.

The design work must treat the current Svelte pages as an MVP surface, not as binding product authority. Existing architecture decisions and code boundaries remain binding.

Canonical Incumbents

Use the existing owners below before adding a new schema, helper, exception type, route contract, or workflow concept.

Concern Canonical owner
Web product paradigm docs/adrs/adr-011-web-ui.md
API assembly, CORS, route mounting, and shipped asset mounting src/aptl/api/main.py
Web auth, auth env binding, project-dir binding src/aptl/api/deps.py
API response DTOs src/aptl/api/schemas.py
Svelte API fetch boundary and SSE subscription web/src/lib/api.ts
Browser session second factor web/src/lib/session.ts, src/aptl/api/session.py
Browser terminal carrier helpers web/src/lib/bff.ts, src/aptl/api/routers/terminal.py
Svelte wire-type mirror web/src/lib/types.ts
aptl web serve bind/runtime contract src/aptl/cli/web.py
Split web Compose profile docker-compose.yml services aptl-web-api and aptl-web-ui
Web build artifact contract web/package.json, web/svelte.config.js, web/vite.config.ts
Lab lifecycle and startup diagnostics src/aptl/core/lab.py, src/aptl/core/lab_types.py, ADR-030
Endpoint display and terminal target metadata src/aptl/core/endpoints.py, ADR-036, ADR-040
Docker and Docker Compose access src/aptl/core/deployment/, ADR-023, ADR-037
Durable first-party config src/aptl/core/config.py, ADR-025
Runtime environment and placeholder checks src/aptl/core/env.py, src/aptl/utils/placeholders.py, WebAuthSettings.from_env()
Secret redaction and serialization safety src/aptl/utils/redaction.py, mcp/aptl-mcp-common/src/redaction.ts, ADR-029
Scenario catalog and SDL validation src/aptl/core/scenario_catalog.py, RAES parser authority, ADR-035
Logging src/aptl/utils/logging.py and module-local get_logger(...)
Run artifact persistence src/aptl/core/runstore.py
Web component inventory web/src/lib/components/ and web/src/lib/components/workbench/
Web tests web/tests/** with npm test / vitest run
Python API and core tests tests/test_api_*.py, tests/test_endpoints.py, pytest
Completion gate .ground-control.yaml, .gc/plan-rules.md, pre-commit run --all-files

Security Layers

The design specification must name how each in-scope page passes these layers.

Layer Required fit
Auth surface All /api/* HTTP and SSE traffic stays behind the canonical FastAPI auth boundary in src/aptl/api/main.py / src/aptl/api/deps.py before route logic. In the shipped aptl web serve model, the FastAPI BFF owns server-side control-plane authority. In the split dev/preview profile, the SvelteKit hook may remain the compatibility proxy. In both modes, browser REST/SSE calls must not place the API token in fetch headers or URLs.
CSRF/origin gate Mutating browser requests (POST, PUT, PATCH, DELETE) must pass a same-origin gate before any server path adds or accepts control-plane authority. Preserve the existing Origin plus Sec-Fetch-Site semantics from web/src/hooks.server.ts, but move the shipped boundary into FastAPI instead of duplicating it in each router.
WebSocket terminal auth Terminal access uses either ADR-039's non-URL Sec-WebSocket-Protocol token convention plus verify_ws_token(...), or an equivalent same-origin FastAPI terminal carrier that keeps the token server-side. Origin remains an extra browser defense, not a credential, and the token must not be rendered into static page code in the shipped model.
Secret handling API tokens, cookies, private keys, generated config secrets, and replayable session identifiers remain ADR-029 control-plane secrets. Designs must not show secret values in examples, error states, logs, screenshots, run artifacts, OpenAPI examples, or route URLs.
Env binding Web control-plane settings stay runtime-env owned. APTL_API_TOKEN, APTL_API_URL, APTL_API_HOST, and APTL_ALLOWED_HOSTS are not aptl.json fields. APTL_API_URL and APTL_API_HOST are split-profile compatibility knobs, not the shipped aptl web serve API contract. (UI-008a removed the APTL_ALLOWED_ORIGINS allow-list: cross-origin is now a strict same-origin check, and APTL_ALLOWED_HOSTS extends the loopback Host allow-list for DNS-rebinding defence.) Durable non-secret config goes through AptlConfig.
Config validation Any first-party config surfaced in the UI must come from load_config() / AptlConfig projections. Unknown first-party fields are errors under ADR-025; do not add pass-through config dictionaries for UI convenience.
OS/process exposure Tokens, passwords, cookies, private keys, and generated secret content must not appear in process argv, query strings, shell strings, access-log-visible paths, generated static bundles, rendered page data, or terminal prefill text. Docker actions stay typed backend calls, not raw command submission.
Error envelopes HTTP auth failures use the generic FastAPI 401 envelope with WWW-Authenticate: Bearer; proxy errors stay narrow; terminal errors use the existing WebSocket error message shape; lab actions use LabActionResponse.
Terminal trust Interactive terminals must satisfy ADR-040: container allow-list, lab-running check, endpoint projection from ENDPOINT_REGISTRY, and pinned known_hosts before asyncssh.connect.
Redaction and observability Logs may name route, component, step, and validation layer, but not secret values or raw secret-bearing payloads. New persistence or export paths must use the existing redaction/runstore boundaries.
Network exposure The default operator web surface remains loopback-bound per ADR-039. Any non-loopback design must be explicit about the operator risk decision and must not rely on CORS as authorization.

Design Guardrails

  • For UI-008a, aptl web serve is the shipped delivery contract: one FastAPI process, default loopback bind, built UI assets, and /api/* on one origin. The split aptl-web-api plus aptl-web-ui Compose profile remains a dev/preview path and must not become the only path that preserves auth or CSRF behavior.
  • Mount API routes before any static-asset or SPA fallback route. Unknown /api/* paths must still pass the API auth boundary before returning a route-specific result, not fall through to static asset handling.
  • Keep the BFF authority server-side. Page code should use relative same-origin API paths and a same-origin terminal connection contract; it must not receive APTL_API_TOKEN, synthesize Authorization headers, or move the bearer token into URLs, local storage, stores, route data, or TypeScript types.
  • Put the shipped CSRF/origin gate in one FastAPI-owned cross-cutting layer before router logic and before any control-plane authority is added or accepted. Do not copy the gate into each mutating route.
  • Define the web build artifact root once at the FastAPI app/serve boundary so packaging, editable installs, tests, and future relocation do not grow separate path guesses. A missing build artifact should fail with a narrow operator/developer diagnostic, not change API auth behavior.
  • The current SvelteKit build uses the Node adapter. UI-008a must make the FastAPI-mountable asset contract explicit before mounting files; do not assume a server bundle is a static asset directory.
  • Distinguish human-investigation surfaces from read-only status surfaces in the spec. Terminals, SIEM exploration, command execution, lab start/stop, and kill flows are control or investigation surfaces; lab status badges, container summaries, startup diagnostics, and config summaries are read-only status surfaces unless the page explicitly defines a mutation.
  • Keep the ADR-011 notebook/workbench paradigm. Do not drift into an enterprise SOC dashboard: no mission-control tile wall, icon-only sidebar, status-dot matrix, or dense product-console chrome.
  • Keep the visual language specific to a local purple-team lab workbench. Do not use marketing heroes, purple gradients, decorative glow, glassmorphism, bento grids, nested card walls, fake analytics charts, or oversized rounded icon tiles.
  • Reuse the current component families before inventing new ones: NavBar, LabStatusBadge, ContainerGrid, LabStartNotice, Terminal, and workbench blocks. If a new recurring UI primitive is needed, document which existing component pattern it extends.
  • Use Tailwind v4 tokens and a small APTL component kit as the design-system base. External component primitives may be used for accessible behavior such as dialogs, menus, popovers, tabs, and tooltips, but they must not replace APTL's route structure, palette, density, or security copy.
  • Keep API DTOs server-owned. Add or change Python response models in src/aptl/api/schemas.py first, then mirror the stable wire shape in web/src/lib/types.ts; do not let the Svelte layer infer core state from English messages.
  • Treat startup state as ADR-030 structured data. Do not collapse ready, degraded_usable, degraded_unusable, and failed into a single boolean or a color-only badge.
  • Treat endpoint metadata as display and reachability projection only. ENDPOINT_REGISTRY does not authorize terminal access and does not own host-published ports or credentials.
  • Treat RAES SDL and scenario_catalog.py as scenario authority. The legacy /api/scenarios endpoints are intentionally absent today; a design that includes scenario browsing must define a new canonical API projection instead of resurrecting the removed in-tree scenario schema by accident.
  • Keep page-local logic thin. Shared fetch behavior belongs in web/src/lib/api.ts, shared lab status in web/src/lib/stores/lab.ts, and scenario workbench shaping in web/src/lib/workbench.ts.
  • Target WCAG 2.2 AA for shipped routes. Keyboard access, visible focus, target sizing, contrast, non-color status cues, accessible dialogs, and screen-reader-safe live updates are implementation requirements, not polish.
  • Keep UI copy localization-ready even while v1 ships English-only. Centralize user-facing strings, use shared date/time/count formatters, avoid sentence concatenation, and avoid layouts that depend on English-length labels.
  • Treat persistent settings as browser-local non-secret preferences only. Do not persist tokens, terminal input/output, copied commands, raw SIEM custom queries, scenario answers, or investigation notes in v1.
  • Include a local-use and privacy notice before controlled actions. Do not add cookie-consent or terms-wall patterns unless non-essential analytics, third-party embeds, or legally approved terms text are introduced.

UI-008b Component Kit Guardrails

UI-008b should create a small APTL component kit from the current Svelte and Tailwind surface, not a parallel design system.

  • Treat web/src/app.css as the canonical token source. Promote the existing Tailwind v4 @theme variables into documented color, type, spacing, radius, status, focus, and density conventions there or in adjacent design docs. Do not introduce a second Tailwind config, JavaScript token mirror, CSS-in-TS theme object, or copied admin-template token set.
  • The component kit is a presentation and interaction layer. It must not own API fetching, auth carrier construction, DTO parsing, lifecycle semantics, scenario shaping, terminal ticket flow, SIEM query validation, config validation, logging, persistence, or redaction.
  • Keep the app shell anchored on web/src/lib/components/NavBar.svelte and web/src/routes/+layout.svelte: top navigation, text labels, compact lab status, and constrained content regions. Do not add an icon-only sidebar, mission-control nav rail, or wholesale Tailwind UI shell.
  • Keep primitive props semantic and content-model-oriented. A status badge should accept a known status/severity/label shape, not arbitrary color class strings. Tables should accept rows, columns, captions, loading/empty/error state, and bounded row actions; they should not infer backend states from English messages.
  • Use existing component families as incumbents: LabStatusBadge, LabStartNotice, ContainerGrid, ContainerCard, ScenarioCard, Terminal, TerminalBlock, and the workbench blocks. New primitives should factor repeated recipes out of those components without changing their data authority.
  • Centralize repeated status styling currently duplicated across LabStatusBadge, ContainerCard, ScenarioCard, WorkbenchStatusBar, ObjectiveBlock, and ContainerStatusBlock. Reuse web/src/lib/container-state.ts or evolve it into a narrow UI-state helper instead of adding one-off switch statements per component.
  • Dialogs, drawers, menus, popovers, tabs, and tooltips need accessible behavior. A small headless Svelte primitive or dependency is acceptable only for focus management, ARIA semantics, keyboard handling, and portal/overlay mechanics; the APTL kit still owns tokens, density, copy, and content structure.
  • Treat NarrativeBlock plus renderMarkdown() / DOMPurify as the only existing sanctioned raw-HTML path. Generic primitives should render text or Svelte children, not add new {@html} surfaces.
  • Preserve SvelteKit's static SPA and strict CSP posture from web/src/routes/+layout.ts and web/svelte.config.js: no remote assets, no inline-script-dependent component behavior, no API token in route data, generated bundles, local storage, examples, screenshots, or style tokens.
  • Kit tests belong under web/tests/components/ and should assert behavior, semantics, and variants: roles, accessible names, focus/keyboard behavior, disabled/loading/empty/error states, non-color status labels, and density class selection. Do not rely on snapshots as the only evidence.
  • If the kit adds local preferences such as density, color mode, motion, locale, time display, or terminal font size, put the schema-versioned browser-local preferences seam behind one helper/store. Persist only the non-secret keys allowed by web-gui-design.md; never persist auth material, terminal I/O, raw SIEM custom queries, copied command history, hints viewed, or notes.
  • A user-visible kit change needs a Conventional Commit PR title (release-please generates the changelog). Docs-only preflight changes use docs:.

UI-008c Lab Home Guardrails

UI-008c (/) is a composition of existing control-plane and component-kit boundaries. It must answer whether the lab is usable and what the operator can do next; it must not become a second lifecycle, scenario, terminal, or Docker authority.

  • Keep all HTTP and SSE traffic behind web/src/lib/api.ts and web/src/lib/stores/lab.ts. Do not fetch authenticated /api/* routes directly from +page.ts / route components unless the shared session-header boundary is still used.
  • Keep /api/lab/events on fetch streaming with X-APTL-Session; do not switch back to native EventSource, which cannot send the port-scoped session factor.
  • GET /api/lab/status currently owns running/container/error state only. POST /api/lab/start owns ADR-030 startup outcome and diagnostics. If Lab Home needs startup diagnostics after a reload or from SSE, add that structured field at the core/API DTO boundary first; do not infer readiness from container health, colors, stale browser state, or English messages.
  • Reuse LabActionResponse for start/stop and add/use a mirrored KillActionResponse for kill. Do not overload one action envelope with the other's fields, and do not parse backend messages to decide success.
  • Lifecycle controls are single-flight from the UI perspective. Disable or otherwise serialize start, stop, and kill while one action is pending, refresh status from the shared store/API afterward, and avoid optimistic state changes that contradict the next SSE event.
  • POST /api/lab/kill is the existing emergency path with the existing containers scope parameter. The UI confirmation state belongs in a small KillConfirmDialog built from the kit Dialog and Button; do not add a generic command-confirmation system or a second modal primitive.
  • Scenario entry points must come from a new narrow RAES/catalog API projection when this slice needs them. The current Python tests deliberately assert the removed legacy /api/scenarios routes are absent, so UI-008c must replace that absence with explicit DTOs in src/aptl/api/schemas.py backed by src/aptl/core/scenario_catalog.py / the RAES parser, not by reviving the old in-tree scenario model from src/aptl/core/scenarios.py.
  • Scenario summary DTOs should expose only card/list facts: id, name, description, mode, difficulty, estimated time, tags, required containers, and validation/status summary. Workbench detail, scoring, SIEM query execution, and terminal session state stay out of the Lab Home summary contract.
  • Terminal links on container cards must remain a projection of ENDPOINT_REGISTRY / terminal allow-list semantics. Do not hardcode a new SSH container set in the route when the endpoint registry already owns terminal target identity.
  • Use component-kit semantics for controls and status: Button variants for primary/destructive actions, Dialog for confirmation, shared status/badge tones, LabStartNotice for diagnostics, ContainerGrid for containers, and ScenarioCard for scenario summaries. Do not add page-local color switch statements or an alternate card/button system.
  • Catalog, status, and action error states need stable user-facing categories and redacted details. API routes may name the route, validation layer, scenario id, or component, but must not return raw stack traces, .env values, bearer/session credentials, private keys, or secret-bearing command output.
  • Tests for this slice should cover the route/component behavior, not only leaf helpers: kill confirmation focus/escape/cancel/confirm, single-flight action disabling, start diagnostics rendering for every ADR-030 outcome, catalog loading/error/empty states, SSE-driven status updates, and the API client carrying the session header on every /api/* call.

UI-008d Scenario Catalog and Workbench Guardrails

UI-008d (/scenarios/[id]) is a backend-owned RAES/catalog projection rendered as a notebook-style workbench. It must not turn the browser into a scenario parser, terminal authority, SIEM authority, scoring store, or replacement SDL.

  • Keep src/aptl/api/routers/scenarios.py and src/aptl/api/schemas.py as the scenario HTTP/DTO owners. The detail route should return a stable projection such as scenario header facts plus an ordered WorkbenchBlock discriminated union. Do not let the current web-only legacy ScenarioDefinition / buildBlockSequence() shape become the contract.
  • Keep scenario list/detail fetches behind web/src/lib/api.ts, with load-provided event.fetch threaded through where needed, so the shared X-APTL-Session carrier is applied. Do not fetch authenticated /api/* routes directly from web/src/routes/scenarios/[id]/+page.ts.
  • Resolve catalog ids through src/aptl/core/scenario_catalog.py. The catalog path stays internal and project-contained, with RAES SDL validation handled by the RAES parser authority; API responses must not expose local filesystem paths or archived legacy YAML locators.
  • Required containers, objective/step content, workflows, and authored narrative come from the RAES document or compiled RAES/runtime artifacts. If a card/detail header needs user-facing metadata RAES does not own uniformly today (mode, difficulty, duration, tags), add a narrow validated catalog metadata extension instead of inferring it in Svelte or reviving the deleted in-tree scenario schema.
  • Keep scenario validation state distinct from lab readiness, objective completion, and scoring. A "valid scenario" indicator means the catalog entry and RAES projection loaded/validated; it must not imply the lab is running, containers are healthy, detections fired, or objectives are complete.
  • Workbench blocks are display/action descriptors, not authorities. A terminal block may name a requested container target, but the terminal WebSocket still enforces ADR-039/ADR-040 gates: auth/ticket, same-origin upgrade, allow-list, lab-running check, runtime endpoint inventory, and pinned known_hosts.
  • SIEM query blocks should carry curated query ids/parameters and display copy. Execution belongs to the SIEM API owner with backend validation, time-range and row caps, stable error kinds, and redacted details. Do not ship raw OpenSearch passthrough from a scenario block unless that backend validation surface exists.
  • Narrative blocks may render Markdown only through the existing renderMarkdown() / DOMPurify path. Do not add another {@html} renderer, allow raw HTML from scenarios, or weaken the strict CSP/static-SPA posture.
  • Hints stay browser-local in v1. Do not persist revealed hints, points, transcripts, notes, answers, or notebook state until a separate scoring or persistence contract owns storage, redaction, and replay semantics.
  • Lazy tools stay lazy. Inline terminal and SIEM blocks must not open sockets or execute queries merely because the document rendered; visible user action or an explicit refresh control starts the side effect.
  • Errors should use route-appropriate standard envelopes: unknown scenario as a short 404, invalid/unreadable catalog or RAES projection as a redacted validation/unavailable state, terminal errors through the existing WebSocket message shape, and query errors through the SIEM result envelope. Do not add a scenario-specific exception hierarchy.
  • Tests for this slice should prove the projection boundary: detail DTOs do not leak catalog paths or raw parser exceptions, unknown ids fail narrowly, block unions render exhaustively, terminal/SIEM blocks stay lazy, markdown is sanitized, and every /api/* fetch path carries the shared session header.

UI-008e Focused Terminal Guardrails

UI-008e (/terminal/[container] and inline TerminalBlock) is a terminal surface over the existing ADR-039/ADR-040 relay. It must not become a generic command-execution API, a browser-owned container allow-list, or a second endpoint/trust registry.

  • Keep the terminal backend authority in src/aptl/api/routers/terminal.py. The route must continue to pass the gates in this order before dialing SSH: WebSocket auth ticket or direct token, strict same-origin upgrade, terminal allow-list from TERMINAL_CONTAINER_NAMES, lab-running state, runtime endpoint projection from lab_terminal_ssh_endpoints(), and pinned known_hosts from known_hosts_path().
  • Keep the browser carrier in web/src/lib/bff.ts and web/src/lib/session.ts. The component fetches a short-lived ticket from the relative /api/terminal/ticket endpoint with sessionHeaders(), then presents aptl-token.<ticket> as the WebSocket subprotocol. Do not put API tokens, tickets, or session factors in URL query strings, route data, local storage, stores, examples, logs, or generated bundles.
  • Treat TerminalBlock and focused route params as requested targets only. Inline blocks may name container, and container cards may show terminal affordances, but the server-side allow-list and runtime inventory remain the only authorization and reachability authority. Avoid a second hardcoded SSH container set as anything more than a removable display hint.
  • Surface terminal rejection as narrow user-facing categories without leaking secrets or validation internals: authentication/session unavailable, same-origin policy rejected, unknown terminal target, lab not running, container unavailable, host keys not pinned/verified, SSH connection failed, and WebSocket disconnected. Prefer the existing WebSocket {type: "error", message} envelope when the server can send one; use close reason/code only as a fallback for pre-accept failures.
  • Preserve terminal side effects as explicit and bounded. Inline terminal blocks stay lazy until user action, focused terminal opens one requested session, resize/stdin are the only client-to-server message types, and malformed messages are ignored rather than promoted to an execution surface.
  • Keep terminal output ephemeral in v1. Do not persist transcripts, command history, terminal input/output, copied commands, error payloads, or SSH stream data in sessionStorage, localStorage, run archives, scenario state, or logs.
  • Use existing observability and redaction conventions: module-local get_logger(...), sanitized route parameters, validation-layer labels, and no operator keystrokes, ticket values, bearer/session credentials, private key material, raw exception payloads, or terminal bytes in logs.
  • Tests should extend the existing seams instead of adding a parallel harness: tests/test_api_terminal.py for auth/origin/allow-list/lab-state/runtime endpoint/known-hosts gates, tests/test_host_keys.py and tests/test_endpoints.py for trust and endpoint projections, web/tests/lib/bff.test.ts for carrier construction, and web/tests/components/Terminal.test.ts plus workbench block tests for the component states and lazy mounting.

UI-008f Config, Settings, and Privacy Guardrails

UI-008f (/config, the Settings dialog, and the local-use/privacy notice) is a read-only configuration and browser-local preference slice. It must not become a second runtime-settings authority, consent platform, server-side preference store, secret viewer, or terminal/lab lifecycle gate.

  • Keep backend configuration facts in src/aptl/api/routers/config.py and src/aptl/api/schemas.py, projected from get_config() / aptl.core.config.AptlConfig. The route may add non-secret first-party fields only when they already exist in the strict AptlConfig schema or another named non-secret owner. Do not read .env, generated .aptl/config/*, private keys, token settings, cookies, session factors, service passwords, or raw Compose environment blocks for the config summary.
  • Keep the web DTO mirror in web/src/lib/types.ts and fetch through web/src/lib/api.ts with the shared sessionHeaders() path. Route components should not call authenticated /api/* endpoints directly unless they preserve that carrier, and /config remains a read-only route with no mutation capability.
  • Treat Settings as browser-local, non-secret UI preference state. A versioned key such as aptl.web.preferences.v1 should be owned by one helper/store that validates stored shape, falls back to defaults on corrupt or unknown data, and exposes reset. Do not put preferences in aptl.json, .env, server state, run archives, cookies, route params, or API DTOs.
  • Keep the preference schema narrow and explicit. Acceptable v1 values are appearance/density/motion/locale/time-display/terminal-rendering defaults plus the acknowledged local-use notice version and timestamp. Never persist API tokens, terminal tickets, session factors, terminal input/output, command history, copied commands, SIEM custom query bodies, investigation notes, raw API errors, hints viewed, or scenario progress.
  • Gate the first mutating lab action and terminal launch through one reusable local-use acknowledgement helper instead of scattering modal checks across buttons. The acknowledgement is a local UI precondition only: it does not authorize an action, weaken the FastAPI BFF auth/CSRF gates, replace terminal ticket/origin/allow-list/known-host checks, or change lab lifecycle semantics.
  • Use the existing accessible Dialog primitive for Settings and the local-use notice. Settings should be a dialog or side drawer opened from the app shell; the privacy disclosure should remain persistently reachable from a Help/footer affordance after acknowledgement.
  • Privacy copy must describe the actual v1 behavior: browser-local preference storage, no token storage by the SPA, ephemeral terminal I/O/no transcript persistence, local API/server logs that may record route names, component names, sanitized target identifiers, statuses, and validation/error categories, and no non-essential analytics, tracking, or third-party scripts. If analytics or synchronized preferences are added later, that is a new product and security decision.
  • Use existing observability and redaction conventions. Config and preference code may log route/component/state transitions and validation-layer labels, but not stored preference payloads wholesale, notice timestamps as identifiers, auth/session material, raw .env values, generated config, terminal bytes, or exception payloads that might carry secrets.
  • Tests should extend existing seams: tests/test_api_config.py for the backend projection and secret-exclusion cases, web/tests/lib/api.test.ts for /api/config carrier behavior, preference-helper tests for versioning, validation, corrupt storage fallback, and reset, route/component tests for /config, SettingsDialog, LocalUseNoticeDialog, and the persistent privacy entry point, plus existing terminal/lab action tests to prove the acknowledgement wraps but does not replace the server gates.

Extensibility Seams

The design specification should include a compact page contract table for each route. Each row should state:

  • route path and page purpose;
  • surface class: read-only status, human investigation, control action, or terminal;
  • API data source and DTO owner;
  • mutation capability, if any;
  • auth carrier: FastAPI BFF HTTP/SSE, split-profile proxy HTTP/SSE, or terminal WebSocket/same-origin carrier;
  • primary component family; and
  • future variation parameter.

Use existing extension points for obvious follow-up changes:

  • new lifecycle or startup fields extend LabResult / LabActionResponse;
  • new endpoint display data extends ENDPOINT_REGISTRY by target port and protocol, not host port;
  • new workbench content extends the WorkbenchBlock discriminated union and block renderer;
  • new scenario browsing data uses a narrow RAES/catalog projection rather than a local scenario parser;
  • new runtime web settings extend the typed env settings boundary, not aptl.json;
  • new browser-local UI preferences extend the versioned preference helper/store, not API DTOs or server-side config; and
  • the web asset root, bind host, and allowed origins are parameterized at the app/serve/runtime-settings boundary; future remote/shared modes must change that boundary deliberately instead of scattering path, host, or origin literals through routers, Svelte clients, Compose, and tests.

Anti-Patterns

  • Treating CORS, loopback binding, allowed origins, or "local lab" as authentication.
  • Putting bearer tokens in URLs, WebSocket URLs, browser-visible examples, shell snippets, process argv, logs, screenshots, or stored design fixtures.
  • Adding per-router auth snippets, duplicate bearer parsing, a new auth error schema, or a second web-auth settings object.
  • Adding per-router CSRF snippets, frontend-only CSRF checks, or a second origin parser when the FastAPI BFF can own the shipped gate centrally.
  • Rendering APTL_API_TOKEN or a derived bearer credential into static HTML, Svelte route data, browser stores, local storage, WebSocket URLs, or generated client bundles.
  • Mounting a SPA catch-all before /api/*, or letting /api/* misses return static assets without first satisfying the API auth boundary.
  • Running a hidden Node/SvelteKit server as the real shipped aptl web serve delivery path instead of making FastAPI the BFF and asset owner.
  • Creating a duplicate route map in Svelte that disagrees with mounted FastAPI routers or tests.
  • Reintroducing removed /api/scenarios behavior without an explicit RAES projection contract.
  • Parsing docker-compose.yml from a UI route or Svelte page to discover containers, ports, or terminal targets.
  • Mixing target fixture credentials with control-plane/operator secrets.
  • Adding UI-only booleans that reclassify startup readiness, terminal availability, or degraded SOC telemetry.
  • Building a generic "run Docker command" or "run shell command" web endpoint.
  • Persisting user-entered commands, terminal output, API errors, or investigation notes without a redaction and artifact-boundary design.

Non-Goals

  • Do not implement UI-007 or UI-008 in this preflight.
  • Do not choose a remote/shared deployment auth model for UI-008a.
  • Do not choose the final UI-007 route map, wireframes, or per-page copy here.
  • Do not add multi-user accounts, RBAC, OAuth/OIDC, or password login. (Amended for UI-008a: a single-user, server-issued two-factor session credential (an HttpOnly cookie plus a port-scoped sessionStorage header token) bootstrapped from a one-time launch token IS in scope and required, because loopback binding and forgeable Fetch-Metadata/Origin headers are not authentication, and a host-scoped cookie alone leaks across loopback ports. See the shipped-implementation note in web-gui-design.md.)
  • Do not redesign the deployment backend, Docker socket model, RAES SDL, scenario startup, run archive layout, or endpoint registry.
  • Do not make the web GUI the source of truth for lab topology, scenario semantics, credentials, or startup readiness.
  • Do not widen the current web control plane beyond the approved UI-006 scope.