Skip to content

Gotham console mockup - #65

Merged
AndrewCTF merged 7 commits into
masterfrom
gotham-console-mockup
Aug 2, 2026
Merged

Gotham console mockup#65
AndrewCTF merged 7 commits into
masterfrom
gotham-console-mockup

Conversation

@AndrewCTF

Copy link
Copy Markdown
Owner

No description provided.

AndrewCTF and others added 7 commits August 1, 2026 18:57
…essed

The earlier mockups matched Blueprint's palette exactly and still did not read
as Gotham, because the gap was never colour. It was structure, type and density,
and none of that had been measured.

Calibration. Gotham is Blueprint, and Blueprint publishes $pt-button-height:
30px. Both accent-filled buttons in the Object Explorer screenshot measure 19px,
so that capture sits at 19/30 = 0.633 of true scale and its 1161px width is an
1834px viewport. Nine further measurements then land on published Blueprint
control sizes, which is what confirms the factor rather than assuming it.

What that corrected:

- Gotham's dense row is 20px, and Blueprint pairs $pt-button-height-smaller:
  20px with $pt-font-size-small: 12px. The 12px floor and Gotham density were
  never in tension; the earlier 26px rows recorded that gap as "deliberate".
- The font stack asked for Inter and IBM Plex. Neither is installed, so pages
  rendered in an accidental Noto Sans fallback. Blueprint names no webfont; it
  asks for the platform stack, which resolves to Cantarell here (verified via
  CDP getPlatformFontsForNode, not computed style).
- Panels were on dark-gray1. Sampling shows real Gotham is 63.6% dark-gray2,
  15.2% black, 3.4% dark-gray1. The surface the operator looks at was one step
  too dark; every token was individually valid, which is why a palette audit
  passed it.
- The basemap's land fill was 25.4% of all pixels in saturated blue, giving the
  page a second colour cast fighting the neutral chrome.
- Layout used a three-column grid with the map as a middle track, so opening a
  panel narrowed it. The spec forbids exactly that: panels dock over an
  edge-to-edge map, nothing owns a grid track.
- List rows showed one absolute count and a 15% single-tone bar. Gotham shows a
  monospace matched/total fraction and a two-tone bar at ~26% of panel width.
- The map toolbar was a bare icon strip. Gotham labels its toolbar groups.
- Chrome was 22.3% of window height against Gotham's measured 13.3%. Removing
  the status strip (a Gaia device), folding the event lanes into the transport,
  and deleting the floating time dock (which duplicated the transport's own
  scrub track) brought it to 16.7%.

Regressions this introduced and fixed: dark-gray3 hover puts --txt-3 at 4.35:1,
under the 4.5 floor, so hover is a half-step at 4.67:1; toolbar group containers
were unlabelled divs the verifier flagged as unreachable controls.

_verify.mjs measures the claims in a real browser rather than asserting them,
because two of the same claims were false last time: 0 text below 12px, 0
focusables without an accessible name, 0 keyboard-unreachable controls, every
symbol-referencing svg has a viewBox, 0 console errors, and all five structural
metrics against the measured reference.

Palantir-derived reference crops and the scraped documentation corpus are
deliberately excluded from this public repo; README-gotham.md records why and
how to restore them locally.

Nothing under apps/ has changed. This is a mockup.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9Ka6HJykXNE9ToA3fu1yi
…erial

Committed at the operator's explicit request, superseding the earlier decision
to keep tmp/ out of this public repo. What that decision protected against still
applies and is recorded here rather than silently dropped:

  tmp/palantir/  the two G-Cloud 14 service definition PDFs and every image
                 extracted from them. These are public procurement documents,
                 freely downloadable from the UK Digital Marketplace, but the
                 UI screenshots inside them are Palantir's copyrighted work.
  tmp/redesign/_ref/     Gotham UI crops taken from those PDFs.
  tmp/redesign/_corpus/  115 pages / 34,119 words of Palantir's published
                 Gotham documentation, scraped by _scrape.py.

.gitignore still lists tmp/ and Palantir_Gotham_*.pdf; these paths are tracked
by force-add, so the ignore rules no longer apply to them.

Scanned before committing: no API keys, private keys, .env files or
credential-shaped strings. The token/bearer matches are prose in Palantir's own
authentication documentation.

Excluded as build artefacts, not as policy: tmp/redesign/node_modules (14 MB,
playwright-core) and tmp/redesign/__pycache__.

Also lands the three direction mockups, the panel and parity galleries, the
rehoming inventory and the states sheet, which the handoff document warned were
one `git clean` from being lost the same way tmp/mock.css and tmp/power.css were
while still cited at shell/instruments.tsx:4.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9Ka6HJykXNE9ToA3fu1yi
Several upstreams answer this backend with 403/451 rather than a key error,
and the only tool the repo had for that was an UPSTREAM_PROXIES pool that
ships empty because there is nothing free to point it at. Add two tiers, both
off by default, and — more usefully — the measurements that say which one a
given host actually needs.

Cloudflare WARP (app/warp.py, scripts/warp.sh): `warp-cli mode proxy` makes
the daemon publish a SOCKS5 proxy on loopback whose exit is a Cloudflare
consumer address. The consumer free tier registers anonymously, so there is no
account, no key and no login; host routing and DNS are untouched and nothing
needs root. Traffic through it resolves at the exit on Cloudflare's resolver.
app/upstream.py mounts it per host, or for everything via WARP_ALL, with a
kill switch that removes the direct fallback. The tier is supervised and
surfaced in /api/status, which also finally exposes proxy_stats().

WARP_HOSTS ships EMPTY on purpose. Measured from this host (tools/probe_warp.py):
WARP unblocked nothing, left every already-working feed unchanged, and made
OpenSky unreachable. The 451/403s this was built for were measured from the
datacenter deployment; re-run the probe there before listing a host.

Three different things look like a Cloudflare block and only one is:
- A WAF path rule. globe.airplanes.live/data/aircraft.json is 403 to httpx, to
  curl with a full Chrome header set, through WARP, and to real Chrome that
  had just loaded the site. Nothing opens it; read the page's own request via
  /fetch?url=<page>&capture=<regex>.
- Not blocked at all. The endpoint that page uses, /re-api/?binCraft, answers
  bare httpx with 200. The browser's value there is discovery, not bypass.
  This contradicts the standing "re-api is Cloudflare-403" note in config.py,
  which was taken from the droplet; flagged there rather than acted on.
- A real managed challenge. globe.adsb.fi serves "Just a moment"; httpx cannot
  pass it and neither can headless Chrome, which sat on it for 25s. Headful
  Chrome under xvfb-run got 200 and the real page from the same address.

So the browser tier (tools/browser-fetch, app/browser_fetch.py) runs real
Chrome with a per-host cookie profile, no UA override, one page load at a time
per host on a jittered floor, and browser_headful for the challenge case.
Headful never touches the operator's session: xvfb-run -a takes its own
display and --ozone-platform=x11 is pinned, because WAYLAND_DISPLAY is
inherited even inside xvfb-run and a Chrome that preferred Wayland would draw
on the real desktop. stop() takes the process group; measured no orphaned
Chrome or Xvfb.

Guards: tests/test_warp.py, tests/test_browser_fetch.py, and a node selftest
for the pace gate and the x11 pin, wired into scripts/verify.sh.
Baseline 2141 -> 2162.
app/profile.py documents setdefault semantics: "anything set explicitly in the
environment or .env still wins". The .env half was not true. pydantic-settings
ranks real environment variables ABOVE .env, so seeding a profile default into
os.environ does not lose to an .env entry, it silently outranks it.

Measured on a 32-core / 121 GB / RTX 5090 box: .env carried ADSB_SIDECAR_ONLY=1
and the operator added ADSB_SIDECAR_ENABLED=1, and the backend still booted
with :8090 never spawned. OSINT_PROFILE was unset, so the profile resolved to
lite, lite seeded ADSB_SIDECAR_ENABLED=0 into the environment, and main.py
gates both start() and supervise() on that flag. The tier left no trace at all
(zero sidecar lines in 3.6 MB of log) and the snapshot ran on its sub-8000
backfill while sidecar-only mode was still set.

apply() now treats a key present in either .env as operator-configured and
leaves it alone.

The existing "an explicit setting always beats the profile" test passed
throughout, because it only ever used monkeypatch.setenv — a real environment
variable, the half that already worked. Add the .env case, plus one for comment
and blank-line parsing. The autouse fixture now chdirs to a tmp directory: with
apply() reading .env, every assertion in that file would otherwise depend on
whatever the developer has in the repo root, green on one box and red on
another.

Baseline 2162 -> 2164.
The root CLAUDE.md had grown into one long list covering the globe, the
backend feeds, the sidecars, the venvs and the deploy scripts at once, so
anyone editing a single directory had to read all of it to find the two rules
that applied to them, and in practice skipped it.

Split it per directory: each of apps/api, apps/web, apps/ml, apps/desktop,
packages/shared, tools, scripts and infra now states the rules that govern it
and names the operator decision behind each one. The root file keeps the
operating method, the environment traps and a table pointing at the rest.

The guards are unchanged; scripts/verify.sh still runs all of them.
The two previous mockup sets this codebase was built from lived in tmp/ and
were deleted; shell/instruments.tsx still cites tmp/mock.css and tmp/power.css,
neither of which exists. That is the reason a rebuild has to re-derive the
grammar from screenshots every time.

This is the durable copy: the twenty-odd pages the current shell was measured
against, their generators, the reference shots, and WIRING.md, which maps every
one of the old rail surfaces to where it lives now and carries the status of
each. COVERAGE.md and FRAMES.md record what was compared and how.

The shots are 12 MB. That is the price of not doing this a fourth time.
The new shell landed with four named left panels, a right dock and one app
launcher, and a panels.ts recording where each of the 27 old rail surfaces
went. The record was accurate and the wiring was not: byHome() only keeps
entries of kind 'panel', so the eleven surfaces re-homed as map tools, as app
tabs or as window chrome were constructed on every render and thrown away.
The contract test passed either way, because it checked the record rather than
the code.

Each now lives at the address panels.ts gives it:

  all sources     Layers panel, curated / all switch
  annotate,       floating over the map, opened from GlobeToolbar
  watch, field
  imagery         Video app, Imagery tab
  extract         Investigate app, Extract tab
  countries       Country app, National sources tab (a source catalogue is
                  not the same thing as a country dossier)
  answers         AI app, above the Watch Officer
  inbox           title bar, beside the alerts bell

shell/rehoming.test.ts names the file that must render each one and fails if
an address goes empty. It also checks both halves of the old Ops panel survive:
AOI watch came across in the first pass and the standing-detection rollup did
not, which lost the level view of what is currently tripping the operator's
rules. Info carries it again, polling /api/alerts/standing.

Selection was the other half of the same problem. It had the new section
grammar and none of the dossier, so the assessment, the actions, the
connections graph, the imagery, the pattern of life, the raw property bag and
every place-specific card were gone. It now composes the existing cards rather
than reimplementing them, and routes situation: and domain:-style ids to their
own panels as the old inspector did. Three defects found against live contacts:
identity ignored the resolved enrichment and printed "Type -" beside an
enrichment card that knew the type; one aircraft-shaped identity block rendered
for every kind, so a quake showed six rows of dashes; and freshness read
seen_pos_s, which is ADS-B only, so every vessel reported no last fix.

The title bar's File / Edit / View / Collect / Window / Help set a menu state
that nothing read - no dropdown was ever rendered. They carry real commands
now, each running against the store that owns it, with disabled items stating
why in a tooltip. Settings had the same shape of bug from the other side: the
modal is hosted in AppRouter's TopBar, which returns null on the console route,
so the button had nothing to open. The palette moved from useState inside
Omnibar to its own store, since a command palette only its own keystroke can
reach is the problem it exists to solve.

The centre of the bar was a hardcoded amber square, a literal "Live map" and a
green check reading "Saved", on a console that saves nothing and is often not
showing the map. It reads the active app, the real clock, live or held, and the
alert socket, each from the store that owns it.

Find regains the object-type filter and the rolling time window it had before
the rebuild, and can save a search as an Inbox subscription. The four-region
compositor is not carried over; WIRING.md says so and why.

verify.sh green: 2164 backend passed + 2 skipped, 549 web tests.
@AndrewCTF
AndrewCTF merged commit bec2772 into master Aug 2, 2026
2 checks passed
@AndrewCTF
AndrewCTF deleted the gotham-console-mockup branch August 2, 2026 14:30
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