Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,14 +131,16 @@ The portal is a **"command-center" design** — a light, print-like, authoritati

**Alpine under the CSP build — no inline JS (`static/js/tags.js`).** The CSP build cannot `eval`, so two rules hold: (1) **every component is registered** via `Alpine.data()` in `static/js/tags.js` (one registry, loaded *before* the Alpine `<script>` so its `alpine:init` listener is in place; `x-data="name(arg)"` names a registered component) — there are **no inline `x-data` object literals with methods**; (2) the build **prohibits `x-html`** (use `x-ref` + `x-effect="renderX()"` writing `innerHTML` in native JS) and **all inline `on*=` handlers / inline `<script>` blocks** (use Alpine `@`-handlers bound to component methods). Server state reaches a component through a **`<script type="application/json">` island** read with `JSON.parse` in the factory (the CSP interpreter does **not** decode `tojson`'s `\uXXXX` escapes, so HTML/SVG/prose passed via an attribute expression would corrupt) — small scalar args (ids, enum values) may still be passed inline. `tests/test_frontend_assets.py` guards these invariants (no inline handlers, no inline `<script>`, no `x-html`).

**App shell (`base.html`).** A fixed-height flex shell: a **persistent role-aware left rail** (240px, deep cool charcoal — `--rail*` tokens) with grouped nav + identity card + logout, and a **workspace** column = a sticky 56px **topbar** (breadcrumb · ⌘K trigger · stakeholder feed bell) over a scrolling **`.canvas`** that wraps the unchanged `{% block content %}` in a `.canvas-inner` (max ~1340px; `page-narrow`/`page-wide`/`canvas--flush` modifiers). The rail nav is server-rendered from `user.role` (groups differ per role) with **active state still derived from `request.url.path`**; a hidden inline SVG **icon sprite** (`#ic-*` symbols) feeds `<use>` references. Collapses to an icon strip under 960px. The iceberg mark is inline SVG in `templates/_glyph.html` (included by `base.html` and `login.html`; login renders with no rail/topbar since `user` is falsy).
**App shell (`base.html`).** The rail is ordered by the **intelligence cycle** — analysts get Workspace · Collect (notebooks, feed reader, tasking) · Produce (reports) · Discover; admins get Workspace · Discover · Administration, whose six integration/governance consoles collapse behind the `/admin` hub (eleven links → four). Nothing becomes unreachable: every collapsed console stays a ⌘K jump target, and a test asserts each palette destination still resolves. A fixed-height flex shell: a **persistent role-aware left rail** (240px, deep cool charcoal — `--rail*` tokens) with grouped nav + identity card + logout, and a **workspace** column = a sticky 56px **topbar** (breadcrumb · ⌘K trigger · stakeholder feed bell) over a scrolling **`.canvas`** that wraps the unchanged `{% block content %}` in a `.canvas-inner` (max ~1340px; `page-narrow`/`page-wide`/`canvas--flush` modifiers). The rail nav is server-rendered from `user.role` (groups differ per role) with **active state still derived from `request.url.path`**; a hidden inline SVG **icon sprite** (`#ic-*` symbols) feeds `<use>` references. Collapses to an icon strip under 960px. The iceberg mark is inline SVG in `templates/_glyph.html` (included by `base.html` and `login.html`; login renders with no rail/topbar since `user` is falsy).

**⌘K command palette.** An additive Alpine overlay in `base.html` (jump-to over the role's nav items, built as a Jinja `jump_items` list); ⌘K/Ctrl-K or the topbar trigger opens it, arrow keys + Enter navigate, Esc closes. It follows the combobox/listbox dialog pattern: focus stays in the palette while open, the active result is exposed with `aria-activedescendant`, and focus returns to the invoking control on close. No backend dependency (live `/api/search` results are a noted future hook).

**Report editor (`report_edit.html`).** A full-height 3-pane workspace (`.editor-shell`): a command header (inline title + **marking chips** + lifecycle flow + save-state + Submit/Approve/Publish), then a `1fr 1fr 340px` grid of **markdown ∣ live preview ∣ docked tabbed panel** (Citations · Tags · AI review for editable drafts · Reqs · Audience for admins · Render). Intel level, TLP and analytic confidence are the header chips and *only* the header chips — one source of truth, always visible while writing. Each chip lays its real `<select form="reportform">` transparently over itself, so a click opens the native dropdown: no popover code, keyboard- and screen-reader-navigable, and correct before Alpine hydrates (the component only mirrors the chosen option's own text back into the chip, so there is no label map to keep in sync with the enums). The dock uses standard tab/tablist/tabpanel semantics with arrow/Home/End roving selection; the taxonomy picker is a combobox/listbox with keyboard add, removal and dismissal. The AI review tab requests advisory judgement, controlled-tag and analytic-challenge suggestions only: analysts edit them locally, explicitly apply them through the ordinary report/tag save flows, and accepted report fields receive AI provenance after a successful save. Disabled/TLP/provider messages remain inline and fail-soft. The markdown pane's **Insert ▾** toolbar drops `[[diamond|figure|ach:ID]]` tokens at the cursor. The single product `<form id="reportform">` is wired across header/pane/footer via the HTML `form=` attribute (no nested forms); it keeps the existing per-section POSTs and live-preview `POST /api/preview/product` binding verbatim, plus **debounced autosave** (1.2s) to the same `/reports/{id}` endpoint behind the header save-state indicator.
**Report editor (`report_edit.html`).** A full-height 3-pane workspace (`.editor-shell`): a command header (inline title + **marking chips** + lifecycle flow + save-state + Submit/Approve/Publish), then a `1fr 1fr 340px` grid of **markdown ∣ live preview ∣ docked tabbed panel**, verb-labelled and kept to four — **Cite · Classify · Link · Publish** (plus **Assist** only when the AI backend is available to an editor). *Link* merges the requirement and audience forms; *Publish* holds the lifecycle stepper, the rendered-PDF list, and — pinned in the dock footer under every tab — the one lifecycle transition available to this user, replacing the separate subhead stepper + submit row. There is **one save model**: the editor autosaves and the header `save-state` is the only report of it (the old "Save draft" button is gone; a `<noscript>` submit keeps the form usable if Alpine never loads). Intel level, TLP and analytic confidence are the header chips and *only* the header chips — one source of truth, always visible while writing. Each chip lays its real `<select form="reportform">` transparently over itself, so a click opens the native dropdown: no popover code, keyboard- and screen-reader-navigable, and correct before Alpine hydrates (the component only mirrors the chosen option's own text back into the chip, so there is no label map to keep in sync with the enums). The dock uses standard tab/tablist/tabpanel semantics with arrow/Home/End roving selection; the taxonomy picker is a combobox/listbox with keyboard add, removal and dismissal. The AI review tab requests advisory judgement, controlled-tag and analytic-challenge suggestions only: analysts edit them locally, explicitly apply them through the ordinary report/tag save flows, and accepted report fields receive AI provenance after a successful save. Disabled/TLP/provider messages remain inline and fail-soft. The markdown pane's **Insert ▾** toolbar drops `[[diamond|figure|ach:ID]]` tokens at the cursor. The single product `<form id="reportform">` is wired across header/pane/footer via the HTML `form=` attribute (no nested forms); it keeps the existing per-section POSTs and live-preview `POST /api/preview/product` binding verbatim, plus **debounced autosave** (1.2s) to the same `/reports/{id}` endpoint behind the header save-state indicator.

**Dashboard (`dashboard.html`, `web/notebooks.py`).** Every KPI card is a link to the work behind it, and a **"Needs you now"** queue sits above the fold: the products waiting on a reviewer (for anyone who can review) ahead of the viewer's own drafts, capped at five. Both halves are existing lifecycle state (`Report.status` / `author_id`) surfaced as actions — no new model, no new query pattern. Notebook creation is demoted to a collapsed `<details>` so content leads.

**Stakeholder feed (`feed.html`, `services/feed.delivery_context`, `feedFilter` in `tags.js`).** A row states **two independent things**, both deliberately present-tense: whether the product answers a requirement the reader raised, and how it matches their routing preferences (tag subscription → audience group → level → all). They are separate because requirements are **not** a predicate in `dissemination.matched_stakeholders` — an RFI link is never *why* a product was routed, however useful it is to surface. Neither claims to reconstruct the publish-time routing decision: `DisseminationEvent` records no routing metadata, so a subscription added after delivery would make any "this is why it was sent" claim false. If historical fidelity is ever needed, the honest fix is to persist the match on the event at publish time, not to infer harder. Rows bucket server-side into Today / This week / Earlier; an All ⇄ Unread pill filters them with `x-show` and **no `x-cloak`** — cloaking content whose default state is *shown* would make a fully-read feed look empty whenever Alpine fails to load. Closing the loop: a row answering the reader's own requirement offers **Mark satisfied →**, deep-linking to the product's `#feedback` card with that requirement preselected. It deliberately does *not* POST — the feedback endpoint requires a usefulness rating, and inventing one would put a verdict in the reader's mouth. The `?requirement=` parameter only ever preselects a requirement the reader owns *and* the report answers.

**Notebook phases (`notebook_detail.html`, `notebookTabs` in `tags.js`).** The notebook is worked in phases rather than scrolled end to end: the nine sections are grouped into **Collect** (sources, notes, files, figures, indicators) · **Analyze** (Diamond, ACH) · **Produce** (products) · **Trace** (requirements), and one phase's sections render at a time. The old anchor-link collection strip becomes the phase tab bar, carrying the same per-section counts. Nothing is removed or gated: the tabs are real `#section` anchors, the server renders Collect as the default (the other phases are `x-cloak`ed), and a `<noscript>` rule cancels `x-cloak` so a browser without Alpine still gets every section on one page — exactly the previous behaviour.

This is a skin + layout reorganisation over the same routes/forms/Alpine bindings (`tagPicker`/`taxonomyFilter`, the preview/ach/diamond `x-data` blocks, citation autosave) — no behavioural coupling. **Two additive read-only index routes** back the rail: `GET /notebooks` (`notebooks_list.html`, writer-only) and `GET /tags` (`entities_list.html`, named-threat entity browse).
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ src/iceberg/
# feeds [analyst RSS reader], discovery [search/taxonomy/audience], admin_audit, admin_feeds [RSS config],
# admin_proxy [outbound proxy config], admin_oidc [multi-provider SSO config], admin_ai [AI provider config], admin_misp [MISP push config], admin_webhook [publication webhook config], admin_config [read-only effective-config viewer], admin_home [/admin Settings & integrations hub: status-pill tiles over the config pages])
auth/oidc/ # multi-provider OIDC adapters (base StandardOIDCAdapter + registry + entra/authentik/auth0/okta claim extraction)
services/ # users (OIDC provisioning keyed on (auth_provider,issuer,sub)), oidc_settings (multi-provider config row + enabled_providers resolver, env-only secrets), notebooks, lifecycle, citations/rendering (reports), publication (immutable publish snapshots + atomic publish), requirements, attachments, figures, upload_validation (magic-byte content sniffing for uploads), source_grading (offline Admiralty heuristic), diamond, ach, iocs (light-touch IOC staging CRUD + AI-candidate normalisation: refang + IOCType constraint), product_html (shared report-HTML assembler + inline-embed registry), dissemination (feed delivery + email/webhook job enqueue), jobs (durable OutboxJob outbox: enqueue/lease/retry + worker pass), singleton (conflict-safe settings-row seeding), webhook_settings (publication-webhook config row, env-only token), email, feed (stakeholder feed helpers), feedback (intel-cycle feedback loop / RFI-satisfaction), feeds (inbound RSS ingestion: admin CRUD + bounded SSRF-guarded fetch/parse/sanitise + send-to-notebook), inbound (writer-triggered TAXII/MISP pull → notebook sources/IOCs), misp + misp_settings (outbound MISP push: report indicators → one MISP event, env-only API key), proxy + proxy_settings (global outbound-proxy resolution for httpx), tags, search (FTS dispatch + paginated search_page), attack (ATT&CK Navigator export + coverage matrix), attack_import (Enterprise ATT&CK bundle → TECHNIQUE tags), stix, taxii (read-only TAXII 2.1-shaped serving), related, audience (fail-closed audience-group mutations), ai + ai_settings (governed AI assist + admin-editable provider config row, env-only key, resolver overlays row onto Settings), tradecraft (advisory estimative-language lint), maturity (CTI program maturity & effectiveness dashboard), audit + audit_settings + siem (security audit logging → SIEM), effective_config (read-only resolved-config snapshot for /admin/config: provenance + secret-redaction + prod-guard validation + feature tiles; also `admin_hub_tiles` — the per-subsystem status pills behind /admin)
services/ # users (OIDC provisioning keyed on (auth_provider,issuer,sub)), oidc_settings (multi-provider config row + enabled_providers resolver, env-only secrets), notebooks, lifecycle, citations/rendering (reports), publication (immutable publish snapshots + atomic publish), requirements, attachments, figures, upload_validation (magic-byte content sniffing for uploads), source_grading (offline Admiralty heuristic), diamond, ach, iocs (light-touch IOC staging CRUD + AI-candidate normalisation: refang + IOCType constraint), product_html (shared report-HTML assembler + inline-embed registry), dissemination (feed delivery + email/webhook job enqueue), jobs (durable OutboxJob outbox: enqueue/lease/retry + worker pass), singleton (conflict-safe settings-row seeding), webhook_settings (publication-webhook config row, env-only token), email, feed (stakeholder feed helpers + delivery_context: the RFI link and the current preference match, reported separately — the event stores no publish-time routing metadata), feedback (intel-cycle feedback loop / RFI-satisfaction), feeds (inbound RSS ingestion: admin CRUD + bounded SSRF-guarded fetch/parse/sanitise + send-to-notebook), inbound (writer-triggered TAXII/MISP pull → notebook sources/IOCs), misp + misp_settings (outbound MISP push: report indicators → one MISP event, env-only API key), proxy + proxy_settings (global outbound-proxy resolution for httpx), tags, search (FTS dispatch + paginated search_page), attack (ATT&CK Navigator export + coverage matrix), attack_import (Enterprise ATT&CK bundle → TECHNIQUE tags), stix, taxii (read-only TAXII 2.1-shaped serving), related, audience (fail-closed audience-group mutations), ai + ai_settings (governed AI assist + admin-editable provider config row, env-only key, resolver overlays row onto Settings), tradecraft (advisory estimative-language lint), maturity (CTI program maturity & effectiveness dashboard), audit + audit_settings + siem (security audit logging → SIEM), effective_config (read-only resolved-config snapshot for /admin/config: provenance + secret-redaction + prod-guard validation + feature tiles; also `admin_hub_tiles` — the per-subsystem status pills behind /admin)
rendering/ # markdown->HTML, report->PDF (typst), svg (shared diagram helpers: escape/fonts/wrap/placard)
data/ # starter_tags.json (importable starter taxonomy)
templates/ # Jinja2 + Alpine: base (command-center shell: rail+topbar+canvas+⌘K palette), _glyph, _macros,
Expand Down
Loading