diff --git a/docs/superpowers/specs/2026-08-23-web-ui-rewrite-design.md b/docs/superpowers/specs/2026-08-23-web-ui-rewrite-design.md new file mode 100644 index 0000000..eb16f66 --- /dev/null +++ b/docs/superpowers/specs/2026-08-23-web-ui-rewrite-design.md @@ -0,0 +1,437 @@ +# Technical Specification: keel's web UI + +**Date:** 2026-08-23 · **Status:** specification for the target state + +This document describes the browser interface keel serves from the operator's own machine: what the +server does, what the client does, and why each decision was made the way it was. It follows the +shape of [youperiod.app's SPECS.md](https://github.com/getify/youperiod.app/blob/main/SPECS.md) and +the decision rules of +[its technical philosophy](https://github.com/getify/youperiod.app/discussions/36). + +**It describes a target, not the present.** Today `keel serve` renders HTML in Python +(`keel/web/render.py`, 872 lines) and ships no JavaScript. What follows replaces that layer. Where a +thing already exists it is marked as such, because the existing security model in particular must be +preserved rather than rebuilt. + +--- + +## Philosophy + +The rules used to decide everything below. They mirror the reference philosophy, adapted where keel +differs from an app with no engine. + +1. **Privacy, security and safety are the most important feature.** keel holds venue credentials and + places real orders. Every trade-off resolves in favour of a smaller, more auditable surface, even + at a cost in convenience or development speed. + +2. **Complexity is fought at the individual decision level.** A tool or dependency is admitted only + when omitting it would undermine the principles above — never because it is convenient or + familiar. §"Dependencies" records the outcome: there are none on the client. + +3. **The app runs entirely on the user's device.** No hosted service, no account, no telemetry, no + analytics. youperiod says "no stateful server"; keel's equivalent is **the server is on your + device.** Nothing crosses the network but the venue calls the engine already makes. + + Unlike youperiod, keel cannot be client-only: a Python process wakes on a schedule, enforces the + rails and writes SQLite **with no browser open.** The browser is a window onto that, never the app + itself, so IndexedDB is not and cannot be the store of record. + +4. **Nothing is transpiled, bundled, minified, or obfuscated** — and no source maps, for the reason + the reference gives: the code running must be byte-identical to the code the user reads in + devtools. gzip only. The value of view-source is worth more than the value of tooling. + + keel's own auditability claim must stay scoped honestly: the **UI** is fully readable in any + browser on any device; the **engine** is readable on GitHub and in the wheel. A PyInstaller + bundle ships `.pyc`, so it does not make the Python readable, and no wording should imply it does. + +5. **Offline-capable, performant, and accessible by default.** Semantic HTML, native form elements, + `aria-live` on regions that change underneath a reader, keyboard paths, and colour contrast that + is asserted in CI rather than judged by eye. For financial data, "offline" means *the shell loads + and says the engine is not running* — never a cached balance (§"Service worker"). + +--- + +## Server + +`keel serve` runs a local HTTP server, defaulting to `127.0.0.1` on port **8765**, and opens the URL +in the user's default browser. + +```cmd +keel serve +``` + +Port 8765 is deliberate: Freqtrade's FreqUI and Jesse's dashboard both sit on 8080, and an operator +running one of them alongside keel should not discover the clash through a bind error. `--host` and +`--port` override; `--no-open` suppresses the browser launch. + +The server does **no rendering**. As in the reference, its job is to serve static files with correct +security headers, plus a JSON API over the database the engine writes. + +``` +launchd ──► keel agent (daily) ──► SQLite ◄── keel serve ──► 127.0.0.1:8765 + │ + ┌──────────────────────┴───────────┐ + │ GET / static assets │ + │ GET /api/* JSON │ + │ POST /api/* gated actions │ + └──────────────────────────────────┘ +``` + +Static assets and the API share **one origin**. There is consequently no CORS configuration at all, +and they must not be split. + +### The app cannot be served from keeltrading.com + +Recorded because it is the first thing anyone proposes. keel's data is a SQLite file on the +operator's machine. A page served from `https://keeltrading.com` is a **different origin** and cannot +read it; it could only fetch from the local server, and an HTTPS page fetching `http://127.0.0.1:8765` +is exactly the path browsers are closing — Private Network Access preflights in Chrome, blocked in +Safari. The only exits are a TLS certificate for the local server (needs a tunnel) or moving the data +to a hosted service (a different product). + +`http://127.0.0.1` **is** a secure context by specification, so the service worker, the web app +manifest and browser install all work from the local server with no networking decision required. +keeltrading.com hosts the install instructions and the documentation. + +**Consequence: iPhone and Android are out of scope.** Excluded by the architecture, not deferred by +preference — there is no keel on iOS, and no tunnel is in scope. The interface is still built +responsive, because desktop windows get dragged narrow and touch devices running a full OS exist. + +### Security headers + +The server's principal job, as in the reference. + +| header | what it prevents | +|---|---| +| `Content-Security-Policy: default-src 'self'` | Any third-party resource. No CDN, no web fonts, no analytics — none are used, and the header makes that checkable rather than promised. | +| `connect-src 'self'` | **The interface is provably incapable of sending positions, equity or trade history anywhere but the local process.** Browser-enforced, one header, verifiable in seconds. Outbound documentation links are navigation, not connections, and are unaffected. | +| `Referrer-Policy: no-referrer` | Leaking the URL to keeltrading.com when a documentation link is followed. The session token rides in the URL until the cookie exchange; modern browsers already send origin-only cross-origin, so exposure is small, but one header closes it. | +| `X-Content-Type-Options: nosniff` | Content-type sniffing vulnerabilities. | +| `Strict-Transport-Security` | Not sent. The origin is loopback HTTP by design; HSTS would be meaningless here and is included in the reference only because that app is served over the public internet. | + +Subresource Integrity authorises any inline `