docs(design): the web UI rewrite — zero-dependency, view-source, on-device - #531
Merged
Conversation
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:8765is 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 — wherehttp://127.0.0.1is 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.jsonpinsCodeGateSoftware/keel@mainandfetch-engine-docs.mjscalls itself "the only writer ofsrc/content/engine-docs/", exiting non-zero if a pinned document disappears. Strippingdocs/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 nodocs/, so/glossaryrenders an empty state in every installed deployment — ashelp_console.py:138-146says in its own docstring. The app code drops doc prose and deep-links tokeeltrading.com/{lang}/docs/{slug}, which yields en/fr/ar for free.--goodand--badare the same brightness. Light mode:#1f5f4fL=0.0904,#96322aL=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,--lineat 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 --noEmitin 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
.pycwhile this philosophy prizes view-source — which makes thecurl | bashwheel installer (#479) the more aligned option.🤖 Generated with Claude Code
https://claude.ai/code/session_01NyeggYtojNXCTHeD3JHxb6