Commit Graph

7 Commits

Author SHA1 Message Date
Edward Langley cf086f8c04 Merge: one keymap table, shared by the TUI and the browser (improvise-bpq2 successor)
The browser's hand-written key classifier was a second spelling of
default_keymaps(), held honest by a lockstep test over a hand-written list --
doctrine(a_consolidation_that_keeps_its_compatibility_test_is_unfinished)'s
exact shape. So the TABLE moved to improvise-protocol and improvise-command
keeps only what needs a CmdRegistry. Evidence is binding_dump_diff, not the
suite: 4021 resolved (mode, key, modifier) lookups over sixteen modes,
BYTE-IDENTICAL, with the dump kept as an opt-in test and proved
discriminating by renaming one binding.

The boundary that did NOT move is the better one: key -> intent is shared,
intent -> Command stays client-side, and the named list of what still needs
the host is now a list of COMMAND NAMES rather than keys -- so a rebinding no
longer invalidates it.

AND A LOCKSTEP TEST FOUND A LIVE BUG, BY A COMMA. CellDisplay::text has been
through format_f64; App::display_value_at_cursor has not. Seeding an editor
from the drawn text and committing turns a stored 1200 into the string
'1,200' -- display rounding fed back into the model, which design-principles
4 forbids outright. CellDisplay::edit_text puts the unformatted value on the
wire, and it repairs web/app.js's click-to-edit, which had been seeding its
input from td.textContent since it was written. That bug is on the live page
today for any value with a thousands separator.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH
2026-09-19 21:36:47 -07:00
Edward Langley 59cc6720b5 feat(browser): the page draws the mode's hint instead of spelling keys
`?` now enters `Help` — the keymap says so and this client resolves
`enter-mode` — but the page has no help overlay: `src/ui/help.rs` is 600
lines of ratatui and nothing shared carries its content. Landing the key
without this would have swapped the mode label to HELP over an unchanged
grid, which looks broken rather than unfinished.

`AppMode::hint_text()` already exists in the protocol crate and is what the
TUI's footer draws, so `RenderModel::hint` relays it. Every mode now states
its own keys in the browser, the page spells none of them, and the modes
this client can enter but not yet draw say what they are. A real help
overlay needs the content moved into the shared layer first, which is the
same move design-principles §5 asks for and is filed rather than faked.

`web/*` is unrun; `node --check` only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH
2026-09-19 21:35:54 -07:00
Edward Langley 94d26d8705 fix(web): default the socket to TLS, and cache-bust the deployed bundles
?ws= now takes a host:port and the page supplies the scheme: wss:// when it
is itself served over https, ws:// when over http. An explicit ws:// from an
https page is UPGRADED rather than passed through, because an https origin
refuses it as mixed content -- so passing it through can only produce a
failure that reads like 'the server is not running'. The scheme is not the
caller's to get wrong.

And the deploy now stamps ?v=<sha> onto both bundles. Pages serves everything
max-age=600 and the wasm-pack filenames carry no content hash, so for ten
minutes after a deploy a returning visitor runs the PREVIOUS build -- which
made a shipped fix look like it had not shipped, twice, during the first real
browser debugging session.

BOTH halves are stamped, because a fresh glue file does not pull a fresh
.wasm: the glue locates it with new URL('..._bg.wasm', import.meta.url), and
a URL built against a base does not inherit the base's query. Stamping only
the import specifier would refresh the wrapper and keep the stale binary --
exactly the failure this is here to end.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH
2026-09-19 21:17:08 -07:00
Edward Langley 4e1a79fe5f docs(web): from https the address must be wss://, and the server does not speak it
The page and the README told the reader that a ws:// address is blocked from
an https origin, and then gave them a ws:// address. improvise-ws-server has
no TLS at all -- no rustls, no native-tls, nothing -- so wss:// is not
obtainable from it and the documented pairing could not work from the
deployed site.

Both now say the address must be wss:// from the deployed page, that the
server speaks plain ws:// only so TLS has to be terminated in front of it,
and that the ws:// form works when the page is served over http://localhost.
The missing capability is filed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH
2026-09-19 20:54:04 -07:00
Edward Langley a550201fae feat(web): the page spawns the worker, opens a demo, and reads a key
Three pieces of the standalone deployment, all in web/.

improvise-djm — app.js picks its peer instead of assuming a socket. The
wasm bundle is the same artifact either way (improvise-xks6), so the
difference is confined to one factory: `?ws=` selects the websocket, its
absence spawns `new Worker('./worker.js?doc=&seed=', {type:'module'})`.
The websocket path is KEPT, because improvise-ws-server is how a
TUI-hosted document is shared and nothing else can reach it; the presence
of the parameter is the whole selection mechanism, since a second knob
naming the same fact is a second thing to get wrong.

improvise-bck — web/demo.improv, built against an assertion rather than
by flipping an axis. examples/demo.improv leaves Customer/Date on None,
so every cell is an Aggregate and nothing is editable. The new demo puts
exactly one category on each visible axis (Product on row, _Measure on
column, Quarter on page) and carries Profit = Revenue - Cost, so a
visitor sees the formula move beside the cell they typed in, with no
keystroke and no scrolling. Two tests hold it: tests/web_demo_model.rs
projects the whole grid through grid_of and asserts every CellId is
Record (ADR 002 — that is what `editable` means), and post_message.rs
drives the shipped bytes through the real seed -> VFS -> open -> Snapshot
chain. The first was proved discriminating by pointing it at
examples/demo.improv, where it names the aggregate at (0,0).

One category per axis is asserted too: GridCache slash-joins a
multi-level tuple (gotcha 44), so a second category on an axis puts that
known gap on the front page.

improvise-hwpi — all four. A keydown listener on window feeds
Client::on_key_event, so the classifier is reachable at last; a frame
that will not fold now writes to #status instead of only the console; the
socket has an error listener, so "not running" no longer reads like
"still connecting"; and close() has a re-entry guard. Plus one the ticket
could not have predicted: a worker whose start() rejects is caught inside
worker.js and fires no `error` on the main thread, so the same silence
was about to be rebuilt in the new path — a first-frame watchdog covers
both deployments, because "the peer never answered" is the one condition
that means the same thing on each.

Verified: cargo test --workspace 1159 passing (1155 + 4), clippy clean,
fmt clean, actionlint clean, ./web/build.sh end to end (345 KB / 123 KB
gzipped client, 1.33 MB / 498 KB gzipped worker), every call present in
the generated .d.ts with the right arity, every relative reference in the
assembled site resolving. NOT verified: there is no browser here. No line
of JavaScript in this commit has ever run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH
2026-09-19 19:28:27 -07:00
Edward Langley 188f3e94ef ci(pages): build the wasm bundle and publish web/ to GitHub Pages
improvise-d31's finished artifact depends on improvise-9x6 (the worker-hosted
server) through improvise-djm. The *pipeline* does not, and building it now is
what stops the delivery half from being a mystery when the worker lands: the
swap from websocket transport to worker transport then changes what is
bundled, not how it is built and shipped.

Two jobs in ci.yml rather than a second workflow. `pages-build` runs on every
push and pull request, so a change that breaks `web/build.sh` or the bundle
fails on the PR that made it. `pages-deploy` runs only on a push to main and
`needs: [test, wasm, pages-build]`, so nothing that failed `cargo test` or the
existing wasm32 gate can reach the published site — the deploy depends on
those checks instead of repeating them.

Deliberately not path-filtered. The bundle is improvise-wasm-client plus
improvise-protocol, improvise-core and improvise-formula, so a filter meaning
"skip when the bundle cannot have changed" would have to name most of the
workspace, and getting it wrong leaves a stale site with nothing saying so.

wasm-pack is a pinned release tarball, v0.15.0 — the version `nix develop`
now supplies — so the local run that produced 346 KB of wasm (120 KB gzipped)
is evidence about the CI run. The devShell was missing wasm-pack entirely:
the `wasm` job only runs `cargo check`, which does not need it, so nothing had
ever required the one tool that turns a passing check into a loadable page.

What this cannot do yet, said on the page itself rather than routed around:
web/app.js is a thin client that holds no model, so a copy served from a
static host paints an empty grid until it is pointed at an improvise-ws-server
with ?ws=. The notice is static HTML, not something app.js decides, because it
is true on every load and a page that cannot reach a server cannot be trusted
to explain why. README says the same and names the deployed URL.

Verified: cargo test --workspace 1134 passed / 0 failed, clippy clean, fmt
clean, actionlint clean, ./web/build.sh succeeds and the assemble step
produces a 388 KB site whose every reference resolves inside it. NOT verified:
the workflow has never run — GitHub Pages is not enabled on the repository
(GET /repos/fiddlerwoaroof/improvise/pages is 404), which is improvise-3gy.
The deploy job asks configure-pages to enable it; if the token may not, that
step is where the run fails and the fix is the repository setting.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH
2026-09-19 18:50:15 -07:00
Edward Langley c72832f542 feat(wasm-client,web): the DOM renderer for the browser grid (improvise-cr3)
The last blocker of improvise-1ey. Mostly assembly: `Client` already had the
fold, the key classifier and `set_cell`, `CellDisplay::id` already said what a
cell is (ADR 002), and `flatten_cat_tree` already owned the category panel's
flattening. What was missing was the three joints between them.

`render.rs` is the one that matters. The DOM half is JavaScript and JavaScript
here is untested, so every question with a *silent* wrong answer is answered
in Rust under test and crosses as decided data: `editable` is a fact about the
`CellId` variant (Record only — an Aggregate is a drill target and `Drill` is
not on the wire yet, and `None` is "no projected cell", not "aggregate"), the
selection is placed by subtracting the scroll offset because `GridCache` is
viewport-indexed while the cursor is not, and the category panel is
`flatten_cat_tree` rather than a second walk of the cache.

`socket.rs` frames what `improvise-ws-server` actually speaks, which is not
what `wire.rs` speaks: it has `Viewport` (the handshake — a snapshot built at
extent zero is an empty grid), it batches `Commands`, and it has no `Key`
variant. The two enums are duplicated because a native crate cannot be a wasm
dependency; `improvise-ws-server` is a dev-dependency so the compatibility is
checked through its own types, and a companion test proves the check is
discriminating on the variant names that would drift.

`web/` is the shell, and it is thin on purpose: four string calls in and out,
no protocol knowledge, no framework.

Gates: 1047 passing (was 1031), clippy and fmt clean, and
`cargo check -p improvise-wasm-client --target wasm32-unknown-unknown` clean.
Verified end to end headlessly — the wasm-pack artifact driven from node over
a real websocket to a real `improvise-ws-server`: 1,200 read, "98765" typed,
"98,765" projected back. The DOM painting itself is unverified; there is no
headless browser here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH
2026-09-19 17:34:39 -07:00