Commit graph

11 commits

Author SHA1 Message Date
da1d86aba7 web: reach-over-time chart with playhead, slice 5 of M3
Hand-rolled SVG as decided: dashed percent grid, one cumulative curve
per panel in its accent color, end labels via the shared formatPct, and
a playhead line with per-curve dots synced to the global round. The
cumulative-at-round counting moved into lib/reach.ts so the panel stats
and the chart cannot disagree.
2026-06-10 17:39:21 +02:00
e9d57b2082 web: node fade-in spans the whole round, playback never sits still
Per Justin: instead of a 250 ms pop followed by a still frame, each
newly reached node now fades and scales in across the full round
interval (scaled with playback speed, staggered per node), so the
animation reads as continuous motion like the prototype gif. Reduced
motion still collapses to discrete swaps.
2026-06-10 17:36:59 +02:00
85520ca504 web: regenerate package-lock so npm ci sees optional deps
The incrementally updated lock was missing @emnapi/core and
@emnapi/runtime (optional transitive deps skipped by the local
platform), which broke npm ci on CI. Regenerated from scratch.
2026-06-10 17:35:50 +02:00
0e3a14a0b3 web: playback, slice 4 of M3
usePlayback drives one global round on a rAF timer: 700 ms per round at
1x, speed pill cycling 0.5/1/2, play-once autoplay after the first load,
no looping; at the final round the play button becomes replay. Under
prefers-reduced-motion there is no autoplay, the page settles on the
final round, and the cadence slows to 1000 ms.

The PlayerBar is the mockup's centered pill with a native range
scrubber (one tick per round, aria-valuetext, seeking pauses) and the
global keyboard map: Space toggles, arrows step, Home/End jump; text
inputs and native control handling are left alone. A single polite live
region announces run updates, pauses, scrub releases, and the final
state.

Nodes reached in the current round pop in (scale 0.6 to 1, 250 ms
ease-out) with a small deterministic stagger so each round reads as a
wave. Per Justin's feedback the panel percentage and reached count now
follow the playhead instead of sitting on the final outcome; the hero
keeps the final numbers, since its sentence claims outcomes.
2026-06-10 17:15:38 +02:00
71baf10e73 web: the dashboard page, slice 3 of M3
The milestone 2 parity page becomes the spec's dashboard: app bar with
wordmark, disclaimer badge and About popover, theme toggle (persisted,
defaults to prefers-color-scheme, applied pre-mount to avoid a flash),
hero with the preset narrative and live toned percentages, scenario
toolbar, panel cards with skeleton loading and dim-while-refreshing,
legend, and footer. ComparisonTable evolves into the collapsed
ResultsTable (test moved along, plus a formatPct rounding case).

NetworkView renders the real topology with the spec's shape encodings.
The layout is seeded d3-force (mulberry32 random source, seeded initial
positions, 300 synchronous ticks, cached per graph hash); anisotropic
forceX/forceY pulls settle the cloud into the wide card shape so the
fit stays uniform-scale and the spacing organic. Justin asked for a
taller network area (380x230) and a reading caption under the legend;
the caption is study copy, so StudyPreset gains a readingCaption field.

Slices still to come keep their controls visibly parked: Export and the
panel kebab render disabled until slices 11 and 7.
2026-06-10 16:55:14 +02:00
2f21248c20 web: sim store, URL state scheme, debounced parallel runs (M3 slice 2)
useSimStore is the one reactive module singleton from the spec: panels
over a shared base config, runs debounced 400 ms, one POST /api/scenario
per panel in parallel, results swapped in atomically only when every
request succeeds. Failures keep the last good results; engine 400s are
routed to an inline validationError, everything else to the banner
message. Stale in-flight responses lose to newer runs per panel.

URL state follows spec section 7 (readable params, repeated panel=,
focus index, full fallback to the preset on any malformed part) and is
covered by round-trip and rejection tests. The study copy and initial
panels live in the deepfake-school preset module; formatPct is the
single percent-rounding rule.

The vitest tsconfig now keeps lib at ES2022 like the app config, and
prettier skips the tygo-generated src/types (drift guard stays green).
2026-06-10 16:36:26 +02:00
9a392b1860 api: POST /api/scenario returns result plus graph topology (M3 slice 1)
One panel's whole world in one call: effective config in, echoed
config + cascade result + undirected edge list out. Edges are
[from, to] pairs with from < to in deterministic node order;
Graph.Edges() walks the adjacency once, GraphEdges(config) rebuilds
the seeded world (~25us) so Result stays lean and /api/comparison
stays untouched. This closes the topology gap the design brief
flagged; the frontend's seeded d3-force layout consumes these pairs.
Go bits: [][2]int is a slice of fixed-size arrays; [2]int is a value
type, comparable, and JSON-marshals to [a, b], exactly the wire
shape the spec asks for.
tygo regen includes a fix: engine.Strategy now maps to the generated
Strategy type instead of decaying to 'any' in ScenarioRequest.
Verified live through the dev stack: 7/120 reached, 351 edges.
2026-06-10 15:48:48 +02:00
c935071bd1 web: parity page renders the three-scenario comparison from live data
App.vue fetches the default config, posts it to /api/comparison, and
renders ComparisonTable; the Vite dev server proxies /api to the Go
server so the browser sees one origin. src/lib/api.ts is the only
fetch code and uses exclusively generated types: no shape is defined
on the frontend. Scaffold example components removed.
Engine fix surfaced by the smoke test: a nil Go slice marshals to
JSON null, violating the generated 'educated: number[]' contract;
RunScenario now returns an empty slice instead.
Verified end to end: curl through the Vite proxy returns the golden
99/70/7. Vitest covers the table rendering; type-check, oxlint,
eslint clean. This completes the milestone 2 parity check.
2026-06-10 13:47:01 +02:00
e9f36f509e api: JSON API over the engine; server mode in cmd/spreadlab
internal/api stays a translation layer: decode (with
DisallowUnknownFields so typos 400 instead of silently defaulting),
run the engine, encode; all validation stays in the engine. Routes use
Go 1.22 mux patterns ('POST /api/comparison'), so wrong methods get
405 from the stdlib for free. httptest runs handlers fully in memory;
the API test re-pins the 99/70/7 golden values end to end.
ComparisonResponse is added to tygo.yaml with a type mapping so the
generated web/src/types/api.ts reuses the engine's TS types.
cmd/spreadlab now serves on -addr (default localhost:8080); -table
keeps the CLI comparison as a sanity check.
2026-06-10 13:03:12 +02:00
9bae4a9ca0 types: generate TypeScript from engine structs with tygo
tygo is pinned via go.mod's tool directive (Go 1.24+): the tool's
version is locked like any dependency and runs as 'go tool tygo', no
global install. 'go generate ./...' regenerates web/src/types/engine.ts
from Config/Result and their json tags; the generated file is committed
so the frontend builds without Go installed. The go:generate directive
anchors in a root generate.go because generate runs commands from the
declaring file's directory and tygo reads tygo.yaml from the cwd.
go.mod's ignore directive (Go 1.25+) keeps ./... from crawling into
web/node_modules, where some npm packages ship stray .go files.
2026-06-10 12:58:13 +02:00
f58f33c3fd web: scaffold Vue 3 + Vite app (create-vue, lean options)
Verbatim output of 'npm create vue@latest web -- --ts --vitest
--eslint --prettier' so this commit shows exactly what the official
scaffold generates; our own changes come separately. Lean per the
agreed choices: no Router/Pinia until a second page or shared state
exists. node_modules is ignored via the scaffold's own web/.gitignore.
2026-06-10 12:55:43 +02:00