Skip to content

feat(console): the console on the website - #82

Open
ExposureGuard wants to merge 1 commit into
feat/discover-and-consolefrom
feat/web-console
Open

ExposureGuard wants to merge 1 commit into
feat/discover-and-consolefrom
feat/web-console

Conversation

@ExposureGuard

Copy link
Copy Markdown
Owner

Stacked on #81 (the desktop console + discovery). Merge #81 first, then retarget to main.

haldir console is a desktop window. This is the same thing at /console — where someone who has installed nothing can see it.

the web console

The honest split

A website cannot read your filesystem. The half that scans a machine stays local (haldir discover --json), so the page takes that report as a paste and renders it beside the register — same rows, same next steps, same on-ramp snippet, same summary line. Against a self-hosted instance your machine's inventory never leaves your network, and the page says so next to the box rather than implying it.

One definition of a row

Shaping happens server-side by calling haldir_console.build_rows — the function the CLI and the desktop window already render. The page's JavaScript only draws what comes back, and a test parses the served page for row.<field> reads and asserts each is a field /v1/console/rows returns. That's the same contract the dashboard pages are held to, for the same reason.

The paste is untrusted input, sanitized before it is rendered: lists coerced to the documented shape, non-dict entries skipped, strings capped, unknown keys dropped (a list where a dict belongs raised three frames down inside build_rows). Bodies over 256 KB get a 413 before they are parsed.

Two bugs found by rendering it rather than reading it

  1. The summary said "0 clients" beside a rendered list of clients. summarize() read a summary block that the sanitizer drops. It now computes from the lists it is describing — which is also why it can report the number that matters: "1 ungoverned".
  2. "1 sessions" — that detail line is customer-facing text, and it read like an unfinished template.

Both were invisible in the tests that existed; the screenshot found them. ?example=1 loads a sample so the empty state has something to show and the page can be linked from docs.

Also: the palette moved into one constant both cloud pages interpolate (they're one product from two angles), and the dashboard nav gained a Console ↗ link.

Verification

1164 tests, flake8 clean, mypy clean over 33 files, openapi.json regenerated (91 paths). The screenshot above is this page rendered in headless Chrome against a real local instance — not a mockup.

Still yours

The desktop half (#81) and this page both end at the same place: the snippet gets a server under governance. Nothing here publishes, enrolls, or starts anything.

🤖 Generated with Claude Code

`haldir console` is a desktop window; this is the same thing at `/console`,
where a person who has not installed anything can see it.

The honest split: **a website cannot read your filesystem.** The half that
scans a machine stays local (`haldir discover --json`), so the page takes that
report as a paste and renders it beside the register — with the same next
steps, the same on-ramp snippet, and the same one-line summary. Run against a
self-hosted instance and your machine's inventory never leaves your network;
the page says so next to the box rather than implying it.

**Shaping happens server-side, in the function that already owns it.** The
endpoint calls `haldir_console.build_rows` — so the CLI, the desktop window and
the web page are one definition of what a row is, not three that drift. The
page's JavaScript only renders what comes back, and a test parses the served
page for `row.<field>` reads and asserts each one is a field the endpoint
returns (the same contract the dashboard pages are held to).

**The paste is untrusted input**, so it is sanitized before it is rendered:
lists coerced to the documented shape, non-dict entries skipped, strings
capped, unknown keys dropped — a list where a dict belongs used to raise three
frames down inside `build_rows`. Bodies over 256 KB are refused with a 413
before they are parsed.

Two bugs found by rendering it rather than reading it:

  * the summary said "0 clients" beside a rendered list of them, because
    `summarize()` read a `summary` block the sanitizer drops. It now computes
    from the lists it is describing, which is also why it can show the number
    that matters: "1 ungoverned".
  * "1 sessions" — the register detail is customer-facing text, and it reads
    like a template that was not finished.

`?example=1` loads a sample report, so the empty state has something to show
and the page can be linked from docs. The palette moved into one constant that
both cloud pages interpolate — they are one product seen from two angles.

Screenshots: `docs/console-web.png` (and the desktop one in `docs/console.png`),
both rendered on this machine — headless Chrome for this page, a real window
for the other.

Verified: 1164 tests, flake8 clean, mypy clean over 33 files, `openapi.json`
regenerated for the new endpoint (91 paths).

Co-Authored-By: Claude Code <noreply@anthropic.com>

This branch has not been deployed

No deployments
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