Skip to content

docs(design): the web UI rewrite — zero-dependency, view-source, on-device - #531

Merged
eaitbrahim merged 6 commits into
mainfrom
docs-web-ui-spec
Aug 24, 2026
Merged

docs(design): the web UI rewrite — zero-dependency, view-source, on-device#531
eaitbrahim merged 6 commits into
mainfrom
docs-web-ui-spec

Conversation

@eaitbrahim

Copy link
Copy Markdown
Contributor

Design spec for the UI rewrite, following youperiod.app's technical philosophy.

docs/superpowers/specs/2026-08-23-web-ui-rewrite-design.md. No code changes.

What it proposes

Delete render.py's 872 lines of server-side HTML. The local process serves JSON plus static files; the client is plain ES modules with no framework, no bundler, no transpile, no minification, no source maps, and zero third-party JavaScript.

Zero dependencies is achievable rather than aspirational for two reasons: keel's cryptography lives in Python and the OS keychain, never in the browser — so youperiod's three dependencies have no analogue here — and money crosses the wire as pre-formatted strings, so the client never computes and never needs a decimal library.

Three findings that shaped it

Installing the app from keeltrading.com cannot work. An origin problem, not a preference. keel's data is a local SQLite file; an HTTPS page fetching http://127.0.0.1:8765 is exactly the path Chrome's Private Network Access is tightening and Safari blocks. The exits are a tunnel or a hosted service, both excluded by "no server reach." So the PWA is served by keel itself — where http://127.0.0.1 is already a secure context, and manifest, service worker and install all work today.

Consequence recorded plainly: iPhone and Android are out of scope, excluded by the constraint rather than deferred by choice.

docs/ is the source, not a duplicate. engine-docs.manifest.json pins CodeGateSoftware/keel@main and fetch-engine-docs.mjs calls itself "the only writer of src/content/engine-docs/", exiting non-zero if a pinned document disappears. Stripping docs/ from keel would fail the website build.

What ships instead fixes a bug that is already in every release: the wheel carries artifacts = ["keel/templates/*.yaml"] and no docs/, so /glossary renders an empty state in every installed deployment — as help_console.py:138-146 says in its own docstring. The app code drops doc prose and deep-links to keeltrading.com/{lang}/docs/{slug}, which yields en/fr/ar for free.

--good and --bad are the same brightness. Light mode: #1f5f4f L=0.0904, #96322a L=0.0893 — a 1.01:1 ratio. Profit and loss are separated by hue alone (render.py:82 — pure colour classes). WCAG 1.4.1 failure in an application whose central signal is gain-versus-loss; roughly one in twelve men cannot distinguish them, and greyscale or direct sunlight collapse them entirely. Form inputs also have no visible boundary (render.py:96-97, --line at 1.27:1 against the page background), failing 1.4.11 — which lands on D4's wizard, almost entirely forms.

Text contrast, for the record, is already good: every foreground passes AA on both surfaces in both themes, most pass AAA. Contrast becomes a CI assertion — twenty lines of dependency-free Python — rather than a design review.

Recorded decisions

The TUI is kept (only surface that works over SSH; deletion is a one-way door). Zero third-party JS. No build step. Money as strings. Mobile out of scope. docs/ stays.

Type safety without a build: // @ts-check + JSDoc + tsc --noEmit in CI. Nothing transpiled, nothing shipped altered.

Open questions in §12

Whether the PWA assets ship in the wheel or only the desktop bundle; whether to bundle a rendered docs snapshot for offline; and whether the signed PyInstaller bundle is still the primary channel, given that it ships .pyc while this philosophy prizes view-source — which makes the curl | bash wheel installer (#479) the more aligned option.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NyeggYtojNXCTHeD3JHxb6

eaitbrahim and others added 6 commits August 23, 2026 17:45
…evice

Records the design for replacing render.py's server-side HTML with a JSON API
plus plain ES modules, following youperiod.app's technical philosophy.

The three findings that shaped it:

- Installing the app FROM keeltrading.com cannot work. keel's data is a local
  SQLite file; an HTTPS page cannot reach 127.0.0.1 reliably, and the exits are
  a tunnel or a hosted service — both excluded. The PWA is therefore served by
  keel itself, where localhost is already a secure context.

- keel's docs/ is the SOURCE, not a duplicate: keeltrading.com fetches it at
  build time and fails loudly if a pinned document vanishes. What ships is the
  app code dropping doc prose and deep-linking instead — which also fixes an
  already-shipping bug, since the wheel carries no docs/ and /glossary renders
  an empty state in every installed deployment.

- --good and --bad have the same relative luminance in light mode (1.01:1), so
  profit and loss are separated by hue alone. Fails WCAG 1.4.1 in a trading app.
  Contrast becomes a CI assertion, not a design review.

Zero third-party JS is achievable because crypto lives in Python and money
crosses the wire as pre-formatted strings, so the client never computes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NyeggYtojNXCTHeD3JHxb6
Resolves §12's second open question. The app fetches, bundles and caches
nothing: it opens keeltrading.com/en/docs/{slug}/#{anchor} in a new tab.

Verified this needs no new infrastructure. docs/glossary.md already states its
own rule -- "Each entry is a `## term` heading" -- and the built site carries
the IDs: dist/en/docs/glossary/index.html has id="rail", id="attestation",
id="kill-switch", id="qabd". So the anchor contract is just kebab-casing the
heading, covered by a test that every anchor the app emits exists upstream.

Consequences: /glossary, render_glossary() and the web layer's glossary reader
are deleted rather than ported -- a link needs no renderer. No offline fallback,
deliberately; an operator running a trading engine has network by definition,
and the least technology that does the job is the correct amount.

Version skew is now the main residual risk and is recorded as such: the site
pins main while an operator runs a tagged release.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NyeggYtojNXCTHeD3JHxb6
…, SW, ?v=, bundle

Adopted, with two mechanisms corrected:

- TUI deleted, but in W7 not W0. tui.py imports from commands/{activity,
  admission,status} -- it is a front-end over the shared report builders, not
  their owner -- so deletion never touches what the web UI reads. But D2
  requires read-surface parity first, and the 4,369 test lines ARE the
  specification of what is being ported. A second gate: W7 must confirm the
  browser (with D3's gate) covers what the TUI covered, or the non-technical
  user is stranded at a step needing a terminal.

- Sorting is server-side, not scaled integers. Precision is per-product
  (base_increment), which is exactly what #514/#517 were; a fixed 100x scale
  truncates below a cent and 1e8 caps a USD notional near MAX_SAFE_INTEGER.
  If client-side is ever wanted the field is named `sort`, is ordering-only,
  and is never displayed or summed.

- Versioned doc paths would 404 today: the site builds dist/en/docs/glossary/
  and the manifest pins ref "main". Replaced with ?v= plus a banner, which
  makes skew visible for one small website change and no new routes.

Adopted as proposed: X-Keel-Client custom header forcing a CORS preflight (a
third CSRF layer -- security.py already has Host validation, SameSite=Strict
HttpOnly cookies and an HMAC CSRF token), Sec-Fetch-Site, explicit CacheFirst/
NetworkOnly service-worker routing with the cache keyed to the build version,
and static assets as a W2 acceptance criterion with the bundle as primary.

Also added Referrer-Policy: no-referrer, since the app now links out while the
session token rides in the URL before the cookie exchange. Rejected an embedded
native window: it costs a dependency, three platform backends, and the devtools
that make the view-source claim checkable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NyeggYtojNXCTHeD3JHxb6
…th the runbook

I had kept "works over SSH on a headless host" as the TUI's remaining reason to
exist. keel's own documentation refutes it three ways:

- No server deployment profile exists. The four profiles are trading profiles
  (live, paper, paper-hourly, paper-equities), not topologies.
- A headless live cycle already fails closed -- confirm mode "waits for a typed
  `y` at a terminal" (operator-runbook.md:365).
- "Notify-only, by design. There is no remote control surface -- no command,
  query or capability arrives through notifications, ever."
  (operator-runbook.md:864)

And granting a headless host anyway, `ssh -L 8765:127.0.0.1:8765 host` forwards
the WEB UI to a local browser: full interface, encrypted by SSH, still a secure
context because it is localhost at the reading end, no curses. Better than the
TUI on every axis.

The W7-not-W0 ordering is unaffected -- it rests on D2's parity requirement and
on test_tui.py being the specification of what is being ported, neither of which
involved SSH.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NyeggYtojNXCTHeD3JHxb6
youperiod's SPECS.md is technical documentation -- Server, Security Headers,
Client, file structure, present tense, concrete ports and filenames -- and Kyle
keeps the rationale somewhere else entirely (discussion #36). This document was
PRD-shaped: phases, risk tables, decision logs, competitive archaeology.

Reshaped to match. Philosophy first as numbered decision rules mirroring #36,
then Server (headers as a table of what each one prevents, plus the five layers
security.py already has and this must not rebuild), then Client (first run, the
data contract with a worked JSON payload, a bullet per JS file), then
Documentation, deletions, build order, distribution.

The rationale is kept but subordinated -- folded into the section it governs,
the way SPECS.md explains why each security header exists, rather than sitting
in separate PRD sections. Nothing verified was dropped: the origin argument, the
contrast measurements, the docs pipeline direction, the SSH refutation and the
per-product precision argument against scaled integers all survive where they
belong.

Two additions the new shape forced out:

- A worked JSON payload. Writing it made the `state` field necessary, so the
  client never infers an outcome from a sign, and it carries the glyph that
  makes gain-vs-loss survive without colour.
- "In js/external there is nothing" -- keel's inversion of the reference's
  three dependencies, and structural rather than lucky: crypto lives in Python
  and the keychain, and the client does no arithmetic.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NyeggYtojNXCTHeD3JHxb6
…uctural

The spec claimed the wheel omits docs/ because `artifacts` lists only the YAML
template. Wrong reason, right conclusion, and the real reason is stronger.

Measured: building with `artifacts = ["keel/templates/*.yaml"]` and with
`artifacts = []` produces wheels with IDENTICAL contents -- 140 entries each,
both carrying the two YAML templates. The key is inert on the pinned backend
(uv_build >=0.10.4,<0.13.0), which ships the whole module root regardless.

So docs/ is absent because it sits at the REPOSITORY root, outside `keel/`,
which is what uv_build packages. No glob can reach it.

That makes #539's case stronger rather than weaker: the empty glossary is not a
packaging oversight someone could fix by adding a pattern, it is structural, and
linking out is the only fix that reaches an installed deployment.

The adjacent source comment in pyproject.toml -- "the wheel otherwise contains
only .py files" -- is also wrong by the same measurement.

Found by the agent implementing #535, which I had briefed with my own incorrect
version; it tested the claim instead of inheriting it. Build-order guidance
updated to assert against a built wheel rather than a glob.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NyeggYtojNXCTHeD3JHxb6
@eaitbrahim
eaitbrahim merged commit 023c441 into main Aug 24, 2026
5 checks passed
@eaitbrahim
eaitbrahim deleted the docs-web-ui-spec branch August 24, 2026 05:48
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