diff --git a/README.md b/README.md index c1ef00d..5330a80 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,63 @@ API-first management plane and horizontally scalable, isolated scanner engines. > [`ARCHITECTURE.md`](./ARCHITECTURE.md) and [`docs/`](./docs/) for the spec, and the GitHub > milestones/issues for what is left. +## The console + +![The IcebergSST overview screen: a dark navigation rail, four KPI tiles for critical and open +findings, a table of the newest open findings with severity chips, and panels for running and +recent scans.](./docs/img/overview-light.png) + +Server-rendered HTML driven by HTMX and Alpine, under a strict Content-Security-Policy. It is a +client of the same API routes documented in [`docs/api.md`](./docs/api.md) — not a second +implementation of them. Dark mode follows the operating system. + +
+More screens — findings and triage, live scan status, sources, the engine fleet, dark mode + +
+ +**Findings.** Every filter is part of the URL and is sent to the API as written, so the rows always +reconcile with the query above them. + +![The findings queue: a filter bar for source, state, severity, rule, assignee and suppression, +above a table of findings with severity and state chips.](./docs/img/findings-light.png) + +**A finding, and why it is in the state it is in.** The snippet was masked inside the engine before +it ever crossed the wire; the database has never held the secret. The history below the triage panel +is the `FindingEvent` trail. + +![A finding detail view showing the redacted JDBC connection string in a dark evidence well, its +location in Confluence, the triage panel, and a history of the assignment and the accepted-risk +decision with the analyst's reasoning.](./docs/img/finding-detail-light.png) + +**A scan in flight.** The status region polls itself every three seconds and stops when the scan +reaches a terminal state — the browser never has to know which statuses those are. + +![A running scan in dark mode: a progress bar at two of four tasks finished, tallies for units +scanned and findings, and a table of per-task state including one running and one queued.](./docs/img/scan-live-dark.png) + +**A source.** The credential is write-only at the API, so the form has nothing to echo back — it +reports that one is stored and offers to replace it. + +![A source detail view showing scan history, the Confluence connection form with space chips, and a +credential field marked as stored with a Replace button.](./docs/img/source-detail-light.png) + +**The fleet.** Engines hold no database credentials. When a rolling deploy leaves two rule-pack +versions in force, the page says so rather than picking one. + +![The engine dashboard: three engines with status and heartbeat ages, one offline, and a panel +reporting that two rule-pack versions are in force.](./docs/img/engines-light.png) + +**Dark mode** is the same token set with dark surfaces — no toggle, no cookie, and no inline +bootstrap script (which the CSP would forbid anyway). + +![The overview screen in dark mode, with the same layout on dark surfaces.](./docs/img/overview-dark.png) + +
+ +> Screenshots are of fabricated demo data on a local instance. Every snippet shown is already +> masked, because that is the only form the database can hold. + ## What it does - **Discover & scan** content in Confluence (MVP), with Jira and SMB/NFS file shares to follow. diff --git a/apps/api/src/iceberg_api/web/static/css/iceberg.css b/apps/api/src/iceberg_api/web/static/css/iceberg.css index a77c71b..db43504 100644 --- a/apps/api/src/iceberg_api/web/static/css/iceberg.css +++ b/apps/api/src/iceberg_api/web/static/css/iceberg.css @@ -438,7 +438,16 @@ select.field { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 10px center; padding-right: 30px; } -.field-hint { display: block; margin-top: 5px; font-size: 12px; color: var(--muted); line-height: 1.4; } +/* Hints live *inside* `.stack > label`, which is mono/uppercase/tracked because it + is a field label. A hint is prose, so it resets all three rather than inheriting + them — otherwise a sentence of guidance renders as shouted monospace. */ +.field-hint { + display: block; margin-top: 5px; + font-family: var(--fs-sans); font-size: 12px; font-weight: 400; + letter-spacing: normal; text-transform: none; + color: var(--muted); line-height: 1.45; +} +.field-hint code { font-size: 0.95em; } /* Stacked labelled form */ .stack { display: flex; flex-direction: column; gap: 0.95rem; } diff --git a/apps/api/src/iceberg_api/web/templates/partials/triage.html b/apps/api/src/iceberg_api/web/templates/partials/triage.html index 7b6e8a3..4bb92ca 100644 --- a/apps/api/src/iceberg_api/web/templates/partials/triage.html +++ b/apps/api/src/iceberg_api/web/templates/partials/triage.html @@ -106,9 +106,16 @@ {{ event.created_at | dt }} - {% if event.from_value or event.to_value %} + {% if event.kind.value == 'assign' %} + {#- An assign event records the assignee's *id*, which is what stays + correct when somebody is renamed. Show the name to the reader. -#} +
+ {{ event.from_value | person(user_names) }} → {{ event.to_value | person(user_names) }} +
+ {% elif event.from_value or event.to_value %}
- {{ event.from_value or '—' }} → {{ event.to_value or '—' }} + {{ event.from_value | humanize if event.from_value else '—' }} + → {{ event.to_value | humanize if event.to_value else '—' }}
{% endif %} {% if event.comment %} diff --git a/apps/api/src/iceberg_api/web/templating.py b/apps/api/src/iceberg_api/web/templating.py index 9896568..f130d92 100644 --- a/apps/api/src/iceberg_api/web/templating.py +++ b/apps/api/src/iceberg_api/web/templating.py @@ -80,6 +80,24 @@ def _humanize(value: object) -> str: return str(value).replace("_", " ").capitalize() +def _person(value: object, names: dict[uuid.UUID, str]) -> str: + """A user id recorded on an audit event, rendered as the person's name. + + An assignment event stores the assignee's id in ``to_value``, because that is + what stays correct when somebody is renamed. A trail that reads + "assign — → 966d7342-…" is technically complete and practically useless, so + the name is substituted wherever the reader is allowed to know it, and the id + is shortened rather than dropped when they are not. + """ + if value in (None, ""): + return "unassigned" + try: + key = uuid.UUID(str(value)) + except ValueError: + return str(value) + return names.get(key, f"{str(value)[:12]}…") + + def build_environment() -> Environment: """The template environment. @@ -99,6 +117,7 @@ def build_environment() -> Environment: env.filters["ago"] = _format_ago env.filters["short"] = _shorten env.filters["humanize"] = _humanize + env.filters["person"] = _person env.globals["app_name"] = APP_NAME return env diff --git a/docs/img/engines-light.png b/docs/img/engines-light.png new file mode 100644 index 0000000..068b5a1 Binary files /dev/null and b/docs/img/engines-light.png differ diff --git a/docs/img/finding-detail-light.png b/docs/img/finding-detail-light.png new file mode 100644 index 0000000..76a2a12 Binary files /dev/null and b/docs/img/finding-detail-light.png differ diff --git a/docs/img/findings-light.png b/docs/img/findings-light.png new file mode 100644 index 0000000..71e905e Binary files /dev/null and b/docs/img/findings-light.png differ diff --git a/docs/img/overview-dark.png b/docs/img/overview-dark.png new file mode 100644 index 0000000..616202f Binary files /dev/null and b/docs/img/overview-dark.png differ diff --git a/docs/img/overview-light.png b/docs/img/overview-light.png new file mode 100644 index 0000000..3d22c78 Binary files /dev/null and b/docs/img/overview-light.png differ diff --git a/docs/img/scan-live-dark.png b/docs/img/scan-live-dark.png new file mode 100644 index 0000000..f688a29 Binary files /dev/null and b/docs/img/scan-live-dark.png differ diff --git a/docs/img/source-detail-light.png b/docs/img/source-detail-light.png new file mode 100644 index 0000000..f46219d Binary files /dev/null and b/docs/img/source-detail-light.png differ diff --git a/docs/web.md b/docs/web.md index 22006f2..5df436b 100644 --- a/docs/web.md +++ b/docs/web.md @@ -227,6 +227,21 @@ breadcrumb + `.canvas`/`.canvas-inner`). Unauthenticated pages use rail-scoped styling from the `--rail*` tokens — a `.btn` there paints a white box on dark chrome. +## Screenshots + +`docs/img/*.png` are captured by hand and embedded in the README. They are not +generated by CI and will drift as screens change — treat a stale one as a doc +bug, not a broken build. + +Reproducing them needs two things this repository deliberately does not contain: +a database of fabricated findings, and a way to sign in. Authentication is OIDC +only (ADR 0005), so a local instance cannot be signed into without a provider, +and a development bypass — a login route that trusts a query string — is exactly +the kind of thing that survives into a production image. Both therefore live +outside the tree: a seeding script and a wrapper that imports the real +`create_app()` and adds a single `/dev-login` route. Nothing under `apps/` knows +either exists. + ## Security properties this surface adds - **Strict CSP** on every response except FastAPI's interactive docs, which load