Skip to content

serve: a web console that browses the URI data tree#4

Merged
tamnd merged 6 commits into
mainfrom
feat/serve-console
Jun 14, 2026
Merged

serve: a web console that browses the URI data tree#4
tamnd merged 6 commits into
mainfrom
feat/serve-console

Conversation

@tamnd

@tamnd tamnd commented Jun 14, 2026

Copy link
Copy Markdown
Owner

What

ant serve now answers a browser as well as a curl. The same routes a script reads as JSON render as a shadcn-styled console when the request asks for HTML (Accept: text/html, or ?format=html), so there is one URL surface, not two. JSON stays the default for tools; /api/... forces it.

Cache-first

A resource view reads the record already materialized under the data tree ($HOME/data, ANT_DATA-overridable) and only fetches over the network on a miss or when refresh is set. A live fetch is written back (JSON always, Markdown when the record has a body) so the next read is offline. A cached or live badge shows which path served, and a refresh control pulls a fresh copy.

Browse like directories

The console walks the on-disk tree as folders: the root lists every registered domain, a scheme lists its record types, a type lists its records, with breadcrumbs back up.

Search

Domains that expose a search op get a search box (driven by Host.Searchable/Host.Search, new in any-cli v0.3.3). A hit that is a preview shape is resolved back to its canonical URI, so a result is one click from its record. Verified live across goodreads, wikipedia, x and youtube.

How

  • web/ package serves from go:embed (templates + assets, no Node), SSR through html/template, Markdown through goldmark.
  • Per-response CSP nonce; no secrets in the page.
  • Content negotiation in web/negotiate.go; a hand-rolled first-segment router preserves // in raw-URI paths.

Tests

  • web/console_test.go renders every page and checks JSON negotiation against a network-free fake (status + shell + marker + content-type + CSP nonce).
  • cli/serve_test.go covers the wiring (named endpoints, HTML negotiation, raw-URI path not redirected).
  • go build/go test/go vet/gofmt -l all clean; binary built with CGO_ENABLED=0.

Depends on any-cli v0.3.3 (#12 there); the path replace used during development has been removed so go install and goreleaser stay clean.

tamnd added 6 commits June 14, 2026 17:58
ant serve now answers a browser as well as a curl. The same routes a
script reads as JSON render as a shadcn-styled console when the request
asks for HTML, so there is one URL surface, not two.

The console is cache-first. A resource view reads the record already
materialized under the data tree and only fetches over the network on a
miss or when refresh is set; a live fetch is written back (JSON always,
Markdown when the record has a body) so the next read is offline. A
"cached" or "live" badge says which path served, and a refresh control
pulls a fresh copy.

Browsing follows the on-disk tree like directories: the root lists every
registered domain, a scheme lists its record types, and a type lists its
records, with breadcrumbs back up. Domains that expose a search op get a
search box; a hit that is a preview shape is resolved back to its
canonical URI so it is one click from its record.

The web package serves from go:embed (templates and assets, no Node), SSR
through html/template, Markdown through goldmark, and sets a per-response
CSP nonce with no secrets in the page. Every page and the JSON
negotiation are covered by web/console_test.go against a network-free
fake, and serve_test.go covers the wiring.

Requires any-cli v0.3.3 for Host.Search/Host.Searchable.
The render path reads the request nonce inline; the helper was dead and
the linter flagged it (and its lone context import).
The dashboard and browse-root were listing the entire data root to count
and group records. $HOME/data is shared with many other tools, so that
walk crossed hundreds of thousands of unrelated files: the dashboard took
~7s and browse-root ~15s on a real tree, and it was wrong too, surfacing
other tools' files as ant records.

Scope both pages to ant's own domains: the dashboard sums per-domain
counts and browse-root lists the registered domains, never LL("").

Back LL with an in-memory index. The first call for a prefix walks the
domain's subtree; the result is held in memory, and every cache-write
folds the new URI into the matching listings, so a record fetched
mid-session shows up in browse with no re-walk. serve warms the index for
each domain in the background at startup, off the request path.

Measured on the real tree: dashboard 7438ms -> ~1ms, browse 14993ms ->
~1ms; every page well under 25ms. With a synthetic 50k-record domain the
dashboard and browse-root stay ~0.5ms (cached counts) and the deepest
grouping is ~11ms.

Tests: the engine index is cached and write-through (a file written
behind the Engine is not seen; one written through it appears at once);
the dashboard and browse-root never list the whole root.
The resource page called Dereference synchronously with a 30s request
timeout, so a slow upstream (x://status/... was the reported case) blocked
the request until the deadline tripped and the page died with a raw
"context deadline exceeded". Move the network off the request's critical
path so a page renders the data, a loading screen, or a clean error, but
never hangs and never times out.

Add a small job manager (web/jobs.go): background fetches deduplicated by
key so N viewers of one slow URI cost a single upstream call, results
retained for a few minutes so a reload reads memory, workers on a
background context so navigating away does not abort a fetch others want.

Reads resolve cache-first: a materialized record renders from disk with
no goroutine and no network (Engine.Lookup, the read-only half of
Dereference). On a miss, a browser waits a short grace window and either
renders inline (fast) or hands off to a self-reloading loading screen
(slow) that polls /status and reloads when ready; JS-off falls back to a
meta refresh. Scripts still wait for the data and get a structured
202 pending instead of a hang.

Warm the next click: viewing a record fire-and-forgets a bounded prefetch
of the records it links to, so the likely next navigation is a cache hit.
Prefetch is capped per page and globally and never queues ahead of an
interactive click.

The only synchronous, timeout-bounded path left is the deliberate export
button. Tests cover dedup, error capture, deadline-leaves-it-running,
prefetch bounding, and the cold loading-then-status path; race-clean.
@tamnd tamnd merged commit 6059fbd into main Jun 14, 2026
7 checks passed
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