Skip to content

cockpit v3: the real node API end to end (Home, the feature, the project and the task on a live server) - #211

Merged
LittleChimera merged 2 commits into
stagingfrom
factory/v3-integrate
Nov 4, 2026
Merged

LittleChimera merged 2 commits into
stagingfrom
factory/v3-integrate

Conversation

@LittleChimera

Copy link
Copy Markdown

Related issue

Closes # (cockpit v3, milestone 1 "Read-only v3": notes/factory-v3/final/plan.md §3, the integration of S1–S7 and W1–W5 on the real API, plus the first bullet of S8 and a first slice of W11)

Summary

Milestone 1 is done when Luka opens his live factory with ?cockpit=v3 and sees real data. Until now ?cockpit=v3 showed a built-in sample unless an operator set OMNIGENT_FACTORY_NODE_API=1. With the gate on, the real pages still had gaps: Home had no features, Apply returned 404, and the status lines skipped the ask a person really owed.

This PR:

  • makes v3 read the server's node API by default; the sample factory now lives only in Storybook and the web tests;
  • fixes every seam that showed up against a server seeded like Luka's factory at 13:40;
  • adds a browser test that runs that check in CI.

v1 and v2 are unchanged. v2 stays the default cockpit.

ELI5: the new cockpit used to show a pretend factory. It now asks the server for the real one. Running it against a realistic factory showed five places where the page and the server did not fit. Each is fixed, and a robot browser now clicks through the pages on every PR.

browser ?cockpit=v3 ──▶ liveNodeSource (default; the sample only via NodeSourceProvider in stories/tests)
   Home ──▶ GET …/n/product  +  GET …/n/product/level/feature   (new route: features grouped under needs)
   Apply ─▶ POST /v1/factory/decisions/{row_id}/answer          (DecisionRef.row_id; was "D-45" → 404)
   chat ──▶ POST …/chat {thread: factory | project:<slug>, "About T-16 · …" on a task}
server: blocks(decision)  ← proposal decisions are mirrored blocking=False; count them when they hold work

Plan bullet → what this PR does

Plan item Done here
Milestone 1: "Luka switches to v3 on his live factory with ?cockpit=v3; v2 stays the default" useNodeSource returns the provider's source, else liveNodeSource. There is no preflight gate and no sample fallback in the live path, and the sample chunk is no longer in the production bundle. OMNIGENT_FACTORY_NODE_API and preflight node_api are removed. The page shows the "Sample data" notice only for a sample source a story or test passes in.
S4 / W1: useNode / useLevel read the server useLevel had no server route behind it, so Home (home: "feature") rendered no features. S8, first bullet: GET …/n/{ref}/level/{level}. It groups the nodes on that level under their parent (a section title, never a node joined by lines) and folds groups whose work is all done (done_groups when more than one). Each group pages at 60, with a next cursor per group (<parent ref>@<offset>). It returns 404 unknown_level when the level is not below the node and 400 bad_cursor for a bad cursor. It ports the sample engine's level() and is validated by node_views.LevelViewView.
S5: status line ends at the person; answer buttons The answer route takes a decision's row id, but DecisionRef.id is the ref (D-45), so every Apply returned 404 unknown_decision. DecisionRef now carries row_id, and the live source posts it.
S5: "Decisions that block nothing never colour a node" Production stores every proposal decision with blocking=False (proposals._mirror), while the 13:40 fixture had marked them blocking. On real data, D-45 therefore neither coloured assistant-core nor ranked as a person-ending path, and the root's top blocker went to the merges. Tree.blocks(decision) fixes this. A proposal's decision holds work while the proposal adds a project (create or split), adds a rule (tuning, rules or code), or changes a project that is still active. C-14 and C-15, which change finished projects, still block nothing. The fixture now stores decisions the way production does.
S5: "every node says in one line why it is or isn't moving, ending at the person who must act" After Apply, T-16 read a bare "Not started". A task that nothing holds now ends at its project's lead: "Not started: waits for Juno to start it". The base env's setup project, an item in the factory tree, gets the same start line as a project: "Not started for 7 h: waits for Atlas to start it".
Spec §8 "nothing renders empty" A factory with no base env read "7 merged · not yet in base env". Results.in_base_env is now absent when the server knows no deployed commit, and the strip then reads "7 merged".
W5: chat column posts to the node's thread Verified on the real server: the product posts to factory, a project to project:<slug>, and a task to its project's thread with About T-16 · Debit each turn: … (per-node threads are S11).
W4: keyboard walk Verified on the real server: j/k to a tile, l/↵ to step in, from Home to I-1, assistant-core and T-16. h steps back out through the need to Home.
W11 (first slice): Playwright against the fixture server tests/e2e_ui/factory_v3 (below) runs in the existing e2e-ui workflow (uv run pytest tests/e2e_ui).

The e2e test and its seed

  • tests/e2e_ui/factory_v3/seed.py writes a factory shaped like the 13:40 one into a running server's database, using the server's own SQL stores. It uses the same story as node_fixtures.factory_1340, with neutral names and every time relative to now:
    • the ask, I-1 with four wave-1 projects plus two held waves, T-15/[Bug] A message cancelled while queued blocks every later message in its session omnigent-ai/omnigent#9876 merged, T-16 held for C-16;
    • the shortlist, all merged; build-tier-drafts-3 with two PRs waiting for a person to merge into main;
    • C-14/C-15 blocking nothing, C-19/D-49, the rules C-17/C-18, and D-46…D-48 closed by the server.
    • C-16 and its D-45 are filed through POST …/proposals, so the answer route really applies them. It can also seed a local server by hand (python -m tests.e2e_ui.factory_v3.seed --db … --base-url … --owner …).
  • test_v3_live_api.py runs against one server per module: single-user, factory cockpit offered, supervisor off, a fresh factory per test.
    • Home, I-1, assistant-core and T-16 at 1440 and 390, light and dark. Each page must have no console error and no svg line/path/polyline/polygon in the page body. It must have no leaf reading 0, none or —, and no sideways overflow. The status line must have data-kind="needs", read "needs you: Apply C-16 (D-45)", and carry its Apply button. Home must show the level view's groups.
    • Apply on assistant-core's line: the browser sends POST /v1/factory/decisions/{row_id}/answer (200) and shows the toast "D-45: Apply". The line stops ending at D-45, the API says needs 0, and T-16 now reads "Not started: waits for … to start it".
    • Chat: three posts land in factory, project:assistant-core, and project:assistant-core with the "About T-16 · Debit each turn:" prefix.
    • Keyboard walk from Home down to T-16 and back up.

Deviations

  • The e2e server runs single-user. On a header-auth (multi-user) server, Playwright's extra_http_headers do not reach the /v1/sessions/updates WebSocket, and its 403 fails the console check. That is a quirk of the test harness; production uses a proxy, which sets the header on every request. The node API, the answer route and the chat are the same code in both modes.
  • The empty-cell and line checks skip the chat column. The Room's own cards (for example the proposal card's "— → value" diff) are v2 components, and this PR does not change v2.
  • The level view serves cursor only. group, lens, state and q (api.md §3.6) are not read yet; nothing in v3 sends them.
  • Not pixel-compared with shots/. The full screen set is W11 (milestone 3).
  • No migration, so the PostgreSQL upgrade/downgrade round trip was not needed.
  • The notes changed too. notes/factory-v3/final/api.md documents DecisionRef.row_id, the optional Results.in_base_env, the served level route and the proposal-blocking rule.

Test Plan

  • Python, after the last change:
    • uv run --frozen pytest -q -n 8 tests/server/factory tests/examples/test_factory_sample_tokens.py tests/stores/test_factory_store.py: 4383 passed.
    • Re-run of the touched suites (node status, nodes, nodes API, cockpit read, sample tokens): 112 passed.
    • New tests:
      • Level view: Home groups (the folded implicit need keeps its crumb), all-done groups fold, a group pages at 60, and 404/400 errors.
      • Level views at scale (13/45/360) under 300 ms.
      • Blocking: the proposal rule on the fixture, plus a person's proposal over the real routes that is blocking, carries row_id, and clears once answered through the route.
      • Start lines: a task ends at its lead, and the base env item at who starts it.
      • Results without a base env have no count.
  • Web:
    • npm run type-check, npm run lint and npm run format:check are clean.
    • vitest run src/components/factory src/pages/factory src/lib: 4094 passed.
    • The server contract test now also reads the level fixture written by the server test (nodeLevels.server.json). It checks that Home lists the features from it, that Apply posts the row id, and the results words.
    • CockpitV3.test.tsx serves the server's own pages: ?cockpit=v3 reads /n/product and /n/product/level/feature with no sample notice. Apply posts /decisions/<row_id>/answer and the page reads again.
  • E2E: CI=1 uv run pytest tests/e2e_ui/factory_v3 --ui-skip-build --browser-channel chrome: 7 passed (20 s), run twice on the final head.
  • pre-commit run --files <changed>: everything passes except pyrefly's 28 errors. All 28 were already on staging (runtime/telemetry.py, onboarding/sandboxes/gensee.py, background/reconcile.py), and none is in a touched file. The FORBIDDEN_SAMPLE_TOKENS ratchet is green and now also scans tests/e2e_ui/factory_v3.
  • Manual check, on a local single-user server seeded with the script:
    • all four pages, plus the need, build-tier-drafts-3, the panel, T-15, PR-9876, its run, the shortlist feature, fx, fx:health, fx:rules and base-env, at 1440 light and 390 dark;
    • no console error and no overflow;
    • Apply on T-16 cleared the hold, and the page and the chat's charter note updated.

To try it: deploy, then open https://<server>/factory/<id>?cockpit=v3 (no env var needed). Home lists your features under the ask they came from. The status line ends at the ask you owe, and Apply on it answers that ask for real. To try it locally, start omnigent server with OMNIGENT_FEATURES=factory OMNIGENT_FACTORY_COCKPIT_V2=1 and run uv run python -m tests.e2e_ui.factory_v3.seed --db sqlite:///<its db> --base-url http://127.0.0.1:<port> --owner local. The seed prints the URL to open.

Demo

The screenshots from the manual run (Home, I-1, assistant-core and T-16 at 1440 light and 390 dark) are local captures. Uploading GitHub attachments is not available from this CLI session, so they are not attached; per CLAUDE.md they are not committed either. I can post them if someone uploads them, or the e2e above shows the same pages.

Type of change

  • Bug fix
  • Feature
  • UI / frontend change
  • Refactor / chore
  • Docs
  • Test / CI
  • Breaking change

Test coverage

  • Unit tests added / updated
  • Integration tests added / updated
  • E2E tests added / updated
  • Manual verification completed
  • Existing tests cover this change
  • Not applicable

Coverage notes

Manual: a local server seeded with seed.py, checked in the browser at 1440 light and 390 dark across 17 node pages, and Apply clicked through to the real answer route. Not covered: the live factory itself (its data is not touched from here), and the pixel comparison with the prototype's screenshots (W11).

Changelog

Cockpit v3 (?cockpit=v3) shows your real factory: features grouped under the asks they came from, status lines that end at the ask you owe, and Apply that answers it.

🤖 Generated with Claude Code

Cockpit v3 reads the server's node API by default; the sample factory is
for Storybook and the web tests only (the OMNIGENT_FACTORY_NODE_API gate
and preflight node_api are gone).

Seams fixed against a server seeded like a real factory:
- Home lists the features under their needs: GET …/n/{ref}/level/{level}
  is served (grouped under parents, all-done groups folded, 60 a group).
- Apply answered with the ref (D-45), which the answer route 404s: a
  DecisionRef now carries the decision's row_id and the live source posts it.
- Proposal decisions are stored as blocking nothing, so real "needs you"
  lines and the root's top blocker ignored them: the tree counts a
  proposal's decision as holding work while it adds a project or a rule,
  or changes a project that is still active.
- A task nothing holds, and the base env's setup project, now end at who
  starts them instead of a bare "Not started".
- Results leave out the in-base-env count when there is no base env.

tests/e2e_ui/factory_v3 seeds a 13:40-shaped factory into a running
server and checks Home, the feature, the project and the task at 1440 and
390, light and dark (no console error, no line between nodes, no zero or
"none" cell, a status line ending at the person), Apply through the real
answer route, the chat column's thread, and the keyboard walk.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added the size/XL Pull request size: XL label Nov 4, 2026
After Apply on D-45 released T-16, assistant-core's line read a bare
"Not started" and assistant-panel-ux's ended at "waits on assistant-core":
nothing blocked under the project, so the path found no step. A node with
nothing blocked under it now steps down to the child that has not started
("Not started: T-16 → waits for Juno to start it"), and a task in a
project nobody has started hops to who starts that project instead of
"its lead". The e2e Apply test checks the project, the panel and Home
after the answer, and the render test covers the level views.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@LittleChimera
LittleChimera merged commit 9b97829 into staging Nov 4, 2026
30 of 36 checks passed
@LittleChimera
LittleChimera deleted the factory/v3-integrate branch November 4, 2026 00:56
@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

❌ UI Snapshot doesn't match the committed baseline.

If this UI change is intentional, update the baseline — each path renders in the same pinned image, so the result matches this gate:

  • Label the PR (recommended): add the update-ui-snapshot label — the bot regenerates the baseline in the pinned image, pushes it back here, and re-runs the checks.
  • Locally with Docker: run tests/e2e_ui/visual/regen_baseline_docker.sh, review the PNG, then commit + push.

Diff PNGs (expected_=baseline, actual_=your render, diff_) are in the run artifact. Full guide: tests/e2e_ui/visual/README.md.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/XL Pull request size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant