Skip to content

Factory pages on a phone and over the tailnet - #99

Merged
tomkaczocha merged 27 commits into
mainfrom
t3/factory-issue-61
Oct 17, 2026
Merged

tomkaczocha merged 27 commits into
mainfrom
t3/factory-issue-61

Conversation

@rawphp

@rawphp rawphp commented Oct 17, 2026 •

Copy link
Copy Markdown

Closes #61

Contract

  • done: /factory, /factory/$runId and /settings/factory work at 375 px (no horizontal scroll, every control 44 px tall through a touch size on Button and Input, the runs list, the gate commands and the ledger stacked as cards and lists below 640 px, attention items first); every factory call, the settings page's included, goes through one factory/http.ts on the primary environment's prepared connection (its HTTP base, cookie session or bearer token; a relay/DPoP connection is reported unavailable, never sent unauthenticated), never the page's own origin; factory/README.md "Remote" gives the pairing steps.
  • out: a native mobile screen; app.t3.codes; a factory-specific token; DPoP-signed factory calls (the factory routes are outside the typed API groups T3 signs proofs for).
  • risk: none
  • test: vp test run apps/web/src/factory → pass (9 files, 137 tests); vp run --filter @t3tools/web typecheck → pass

Acceptance criteria

  • The three pages work at 375 px: no horizontal page scroll, controls at least 44 px, run list as stacked cards below 640 px; component tests at 375 and 1280 px — FactoryPage.test.tsx "FactoryPage at 375 px / 1280 px" (cards vs table, plus 639/640 px on the breakpoint), FactoryRunPage.test.tsx "FactoryRunPage at %i px" (lists vs tables, 639/640), settings/FactorySettingsPanel.test.tsx "at %i px > asks for touch-sized inputs and buttons"; every Button and Input the pages render carries size="touch" (button.tsx:38, input.tsx:29: h-11 below sm). Live at 375 px in Chromium: document.scrollWidth 375/375 on all three pages, no overflowing element, every button, input and link in the pages measured at 44 px or more (cards 58 px). The two 24 px chips above the settings form ("Applying settings for … across …") are upstream's settings chrome, not factory code.

  • Every factory API call goes through the connected environment's HTTP base and credential, never the page origin; a remote-environment test proves it — api.test.ts "sends to a remote environment's base with its bearer token and no cookies" (window origin 127.0.0.1:3773, request to https://personal-mac.tail1234.ts.net:3876/api/factory/status with authorization: Bearer tok-1, credentials: "omit"), "sends a cookie session's request with credentials and no authorization header", "fails without sending over a relay (DPoP) connection", "waits for a connection that appears after the call started", "fails with no_connection when none appears within the time"; settings/api.test.ts (12 tests, same proofs for /api/factory/config). environments/primary/httpLayer and the Effect runtime are gone from api.ts.

  • The fork server over the tailnet serves the pages to a newly paired browser; README "Remote" gives pair, open /factory, add to the home screen — factory/README.md "Remote" (~/.local/share/t3fac/current/t3 pair --base-dir ~/.config/t3fac --tailscale, then /factory and /settings/factory on that origin, Share → Add to Home Screen). Pairing itself is T3's (Factory route layer behind T3's authentication #57); verified here against a dev server with a one-time pairing token, not over Tailscale (see below).

  • Attention items at the top of /factory on narrow screens — FactoryPage.test.tsx "puts the attention items above the runs" (375 px), "puts the message on its own full-width line after the badge, repo and age"; live at 375 px the attention section precedes the runs list in the DOM.

  • One integrated pass on a real phone browser over Tailscale — not done: this was a dark run with no phone in reach. The substitute: a dev server of this branch (vp run dev, the live factory's run files and one attention item copied into its .t3/factory, no machine config so nothing claims), paired with a one-time token and driven in Chromium at a 375×812 and a 1280×900 viewport. That pass found two layout bugs the render-tree tests cannot see (an attention message squeezed into a letter-wide column beside the repo label; the config file path overflowing its box) and both are fixed with tests (cf705e60d, 6264d3775). Screenshots are on the branch evidence/issue-61 (not part of this branch):

    /factory 375 px a run 375 px /settings/factory 375 px
    factory 375 run 375 settings 375

    factory 1280

Seams

  • apps/web/src/components/ui/button.tsx and apps/web/src/components/ui/input.tsx: one touch size each (44 px below sm, the default height from sm up), the smallest change that gives every factory control a 44 px tap target without className restyling of components/ui exports. Entries 7 and 8 in the design doc.
  • apps/web/src/components/sidebar/mainAppLocation.ts and mainAppLocation.test.ts: not this issue's change. main's b6bde56 (the Factory Back button) edited both without listing them, so check-seams.sh fails on main itself and on anything merged from it, and its test imported from vitest, which fails the web typecheck. This branch lists them (5ebd048dc) and fixes the import (461c45d8b) so the gate is green; entry 9 in the design doc.

Gate

  • factory/scripts/check.sh at 443e4f842 (after merging origin/main at 672a8ab; factory/SEAMS conflicted with main's own listing of the mainAppLocation.* seams, resolved by keeping both lists) → exit 0 (.factory/gate-3.log): seams 0, server factory tests 0 (2210), both typechecks 0.
  • factory/scripts/check.sh at 6264d3775 → exit 0 (.factory/gate-2.log): check-seams.sh → 0; vp test run apps/server/src/factory → 0 (48 files, 2204 tests); vp run --filter t3 typecheck → 0; vp run --filter @t3tools/web typecheck → 0. (An earlier run at 271756e51, before the live-pass fixes, also exited 0.)

Models

  • maker: claude-sonnet-5-5 (Tom's roster; .dw/config.yml has no models block)
  • checker: claude-opus-5-5
  • orchestrator: claude-fable-5-1

Review

  • cycles: 2 (cycle 1: correctness, security, testing, maintainability, reliability, read-only; cycle 2: the one-time mutation pass, 12 named mutants, 11 killed, 1 equivalent), then the live browser pass.
  • fixed: main's vitest import that broke the web typecheck; a 2xx or 422 config answer that is not JSON crashed the settings page (now an error); the phone cards lost the issue and pull-request links the table has (now sibling anchors under the card, 44 px); breakpoint tests at 639/640 px; size="touch" asserted on every error-path button; settings/api.ts got its own test file against a stubbed fetch; the design doc's seam count; the test window stub is restored at the end of each test; the two live-pass layout bugs above.
  • leftovers: mutant 5 (connectionWithin resolving on a null connection) survived the tests but cannot be written without a type error and the atom never emits null-to-null, so it is equivalent; the touch size's 44 px is proven live, not by a unit test (class strings are not measured in react-test-renderer); a DPoP (T3 Connect relay) connection gets a visible "not available over a relay connection yet" message instead of the pages' data.

Lessons

  • Run the gate right after merging the base, before the first review. Symptom: reviewers spent a cycle on a typecheck failure and a seam-check failure that main itself had, inherited by git merge origin/main. Cause: the base's last commit had not been gated on its own, and the merge was verified with the branch's tests only. Default: after git merge origin/<base>, run every factory.gate command once; a failure that comes from the base is fixed on the branch when the file is within reach (and said so on the receipt), and the reviewers get a green tip.
  • Stop a dev runner's children by port and cwd, not by its pid alone. Symptom: the second vp run dev picked shifted ports, then died with "a server is already running for this base dir", and a third start found a stray Vite on the shifted port. Cause: killing the runner's pid left its server and Vite children listening. Default: before restarting, list the listeners on the run's ports, confirm each one's cwd is this worktree, stop those, then start again.

Commits

  • 8fc12b4a3 test(factory): factory calls go to the connected environment — Make slice

  • 928495339 feat(factory): send every factory call on the environment's connection — Make slice

  • c0b987ac4 feat(factory): add a touch size to Button and Input — Make slice (no red phase: a size variant)

  • 83c6d0965 test(factory): the Factory page at 375 px and 1280 px — Make slice

  • 760828bc2 feat(factory): lay the Factory page out for a phone — Make slice

  • 072258015 test(factory): the run page at 375 px and 1280 px — Make slice

  • 808bdd27f feat(factory): lay the run page out for a phone — Make slice

  • d201d7d85 test(factory): the settings page asks for touch-sized controls — Make slice

  • c10a01ac1 feat(factory): lay the settings page out for a phone — Make slice

  • 3ceb4ace3 docs(factory): explain remote and phone use — Make slice

  • ac50d16da fix(factory): keep the repo row's lint suppression on its element — Make slice

  • 7b23a1671 Merge remote-tracking branch 'origin/main' into t3/factory-issue-61 — base merge (b6bde56)

  • 5ebd048dc chore(factory): list mainAppLocation as a seam — base fix

  • 461c45d8b fix(factory): import the sidebar test from vite-plus/test — Check cycle 1

  • bb2c7b280 test(factory): cover the settings API against a stubbed fetch — Check cycle 1

  • 455328e1b fix(factory): report a non-JSON config answer as an error — Check cycle 1

  • 2bfc4f75c test(factory): expect run cards to link the issue and pull request — Check cycle 1

  • 43cdba3de fix(factory): link the issue and pull request on run cards — Check cycle 1

  • ddff2fd59 test(factory): pin the 640 px breakpoint on both pages — Check cycle 1

  • 4d85ded9f test(factory): assert touch size on the error-path buttons — Check cycle 1

  • 116f509da docs(factory): list mainAppLocation as the ninth seam — Check cycle 1

  • 271756e51 test(factory): restore the test window when each test ends — Check cycle 1

  • cf705e60d test(factory): pin the 375 px layout of attention items and the config path — live pass

  • 6264d3775 fix(factory): keep attention messages and the config path readable at 375 px — live pass

  • 443e4f842 Merge remote-tracking branch 'origin/main' into t3/factory-issue-61 — base merge (672a8ab)

Files

  • apps/web/src/factory/http.ts — the one fetch for every factory call: base, credential, timeout
  • apps/web/src/factory/api.ts — on http.ts and the primary environment's prepared connection; waits for a connection at startup
  • apps/web/src/factory/settings/api.ts — on http.ts; non-JSON answers are errors
  • apps/web/src/factory/RunsTable.tsx — cards below sm with issue and PR links; shared issueRef/prRef
  • apps/web/src/factory/FactoryPage.tsx — attention first, one Pause/Resume button in the header, notes in the body
  • apps/web/src/factory/FactoryRunPage.tsx — gate commands and ledger as lists below sm; 44 px Back, Thread, Stop
  • apps/web/src/factory/AttentionList.tsx — message on its own line; repo label breaks
  • apps/web/src/factory/FactoryNotices.tsx — touch-sized Retry
  • apps/web/src/factory/settings/FactorySettingsPanel.tsx, WatchedRepos.tsx — touch-sized inputs and buttons, repo rows stack, path breaks, 44 px GitHub link
  • apps/web/src/factory/testing.ts — renderAt(width) with a matchMedia stub, restored per test
  • apps/web/src/factory/*.test.ts(x), settings/*.test.ts(x) — the proofs above
  • apps/web/src/components/ui/button.tsx, input.tsx — the touch size (seams 7, 8)
  • apps/web/src/components/sidebar/mainAppLocation.test.ts — main's vitest import fixed (seam 9)
  • factory/SEAMS, factory/docs/design.md — seams 7–9; the Factory pages on a phone and over the tailnet #61 decision rewritten as built
  • factory/README.md — "Remote"

rawphp added 25 commits October 17, 2026 15:59
The api tests now stub the connection instead of the window origin. They
expect a remote environment's calls to reach its HTTP base with its bearer
token and no cookies, a cookie session to send credentials and no
authorization header, a relay (DPoP) connection to fail without sending,
and a connection that appears late to be awaited. All fail: api.ts has no
connection seam yet and still resolves the page's own origin.

Issue: #61
A new factory/http.ts sends a request on a prepared connection: its HTTP
base, the session cookie or the bearer token, a timeout, and no attempt at
a relay (DPoP) connection, which is reported as unavailable instead of sent
unauthenticated. The factory API and the settings API both use it. The
factory API reads the primary environment's connection when it calls and
waits for it if startup has not produced one yet, so the pages work when
opened from a phone paired to another origin. The Effect runtime on the
page's own origin is gone.

Issue: #61
Both get a `touch` size that is 44 px tall below the sm breakpoint and the
default control height from sm up, for the Factory pages on a phone. The
two files join factory/SEAMS and the design doc's seam list, now eight.
No behavior to test, so there is no red phase.

Issue: #61
testing.ts gains renderAt(width, element), which gives the tree a window
whose matchMedia answers for that width; render is renderAt(1280). The
page tests expect run cards (one link each, no table) at 375 and the
table at 1280, the attention items above the stale notice and the runs at
both widths, a control's answer in the body under the attention items, and
touch-sized buttons. All of these fail: the page still renders a table at
every width, puts the notice above the attention items, shows the answer
in the header and uses sm buttons.

Issue: #61
Below 640 px the runs are stacked cards, each one link to the run with its
repo, issue, state, step, age and pull request; the table stays from 640
up. The body now reads attention items, notices, claim status and the
answer to the last control, then the runs, so attention is at the top on a
narrow screen. The header holds only the title and the pause or resume
button. Buttons are touch-sized and long messages can break.

Issue: #61
At 375 the gate commands and the ledger are lists with one item per
command and per entry, not tables; at 1280 both stay tables. The Stop
button asks for the touch size at both widths. All of these fail: the
page renders tables at every width and Stop is an sm button.

Issue: #61
Below 640 px the gate commands and the ledger are stacked lists, one item
per command and per entry, instead of tables; the tables stay from 640 up.
The Stop button is touch-sized, and the Back and Thread links are at least
44 px tall. Step and notes can break instead of forcing the page wider.

Issue: #61
At 375 and 1280 px every input and every button on the populated settings
form must carry the touch size, with Save and the repo buttons present.
Both fail: the form uses sm inputs and sm and xs buttons.

Issue: #61
Every input and button on the machine config form is touch-sized, a repo
row stacks its two inputs and Remove below 640 px, and the GitHub link of
a watched repo is at least 44 px tall. The test's SettingsSection stand-in
now renders headerAction so the Add repo button is checked too.

Issue: #61
factory/README.md gets a Remote section: pair with the fork's binary and
base dir over Tailscale HTTPS, open /factory on the phone, add it to the
home screen, and that a relay connection does not reach the pages yet. The
design entry for #61 now says what was built.

Issue: #61
The longer row classes made the formatter split the div, which moved the
key off the line the no-array-index-key suppression covers and left it
unused. The classes are a constant, so the div is one line again.

Issue: #61
main's b6bde56 edits mainAppLocation.ts and adds its test so /factory
gets the sidebar Back button, but did not list either file in SEAMS, so
check-seams.sh fails on main itself and on every branch merged from it.

Issue: #61
mainAppLocation.test.ts came from main's b6bde56 and imported
"vitest", which does not resolve in the web package (TS2307), so the
web typecheck failed. Import from "vite-plus/test" like every other
web test.

Issue: #61
Runs the real settings/api.ts for every status it maps, the relay
refusal, the request shape per connection type and the connection's
own URL. Two cases fail today: a 200 whose body is not JSON returns
ok with a null report, and a 422 whose body is not JSON returns
invalid with no errors. Both should be an error.

Issue: #61
A 2xx answer whose body was not JSON (an HTML fallback or a proxy
page) came back as ok with a null report, and the settings panel
crashed reading report.config. A 422 with such a body came back as
invalid with no errors. Both are now an error, as they were before
the shared request helper.

Issue: #61
At 375 px the card shows the issue and pull request as plain text, so
nothing on the phone layout reaches them, while the wide table links
both. The new test fails: no anchor with the issue or PR href exists
on the card. A second test pins that a non-GitHub pr_url stays text.

Issue: #61
The phone layout showed the issue and pull request as plain text, so
a card reached the run but never GitHub. The card now keeps one link
to the run and adds a row beneath it, outside that link, with real
anchors sized as tap targets. The issue and PR label/href rules moved
into issueRef and prRef next to prLabel, and the table row uses them
too, so each rule lives once.

Issue: #61
375 and 1280 sit on either side of every Tailwind breakpoint, so
changing max-sm to max-md or max-lg would not fail a test. Add 639 px
(cards and lists) and 640 px (table and tables) for the Factory page
and the run page. These pass already; they guard the boundary.

Issue: #61
The size="touch" assertions only covered the populated pages. Assert
it on the Retry of the Factory page (full error and the stale-poll
notice), the run page's error Retry, Show recent runs after Show all,
and the settings panel's load-error Retry. These pass already.

Issue: #61
factory/SEAMS also names mainAppLocation.ts and its test, from the
#28 follow-up b6bde56 that makes /factory a utility page so the
sidebar shows Back. The Seams list in design.md stopped at eight.

Issue: #61
renderAt installed a window with only matchMedia and kept it until
unmount or the next render, and most tests never unmount. Register the
restore with onTestFinished so no test leaves its window behind;
unmount still restores too.

Issue: #61
…g path

The attention message is still a flex-1 sibling of the repo span, so it is
squeezed into a sliver at 375 px, and the config path sits inside one
unbreakable text node. Both new tests fail: the message is not a full-width
paragraph after the age, and no element holds the path with break-all.

Issue: #61
… 375 px

An attention item now has a first row of badge, repo and issue, and age
(pushed right), and the message is a full-width paragraph below it instead
of a flex-1 sibling that collapsed into a letter-by-letter sliver. The
repo span can break anywhere.

The "No config file" notice puts the path in a code element with
break-all so it wraps inside the box rather than being clipped. The
attention test's repo needle drops the "#20" suffix, since the test
helper joins the split text nodes with a space.

Issue: #61
@rawphp
rawphp marked this pull request as ready for review October 17, 2026 06:57
@rawphp

rawphp commented Oct 17, 2026

Copy link
Copy Markdown
Author

Factory gate: passed (attempt 2, run by the factory in a fresh checkout of the pull request's head 443e4f8 merged with the base branch tip 672a8ab)

Command Exit Time
factory/scripts/check.sh 0 204.3 s

@rawphp
rawphp marked this pull request as draft October 17, 2026 07:03
# Conflicts:
#	apps/web/src/components/ui/input.tsx
@rawphp
rawphp marked this pull request as ready for review October 17, 2026 07:15
@rawphp

rawphp commented Oct 17, 2026

Copy link
Copy Markdown
Author

Factory gate: passed (attempt 4, run by the factory in a fresh checkout of the pull request's head 4180601 merged with the base branch tip a77a681)

Command Exit Time
factory/scripts/check.sh 0 142.4 s

… InlineButton

Review round 1 on #99 (f1 to f5).

A factory call waited up to the timeout for a connection and then gave the
request the whole timeout again, so a server that hung could take twice the
documented time to fail. The call now works out one deadline when it starts
and hands the request only what the wait left, and the timeout message names
the whole budget. Tests cover both orders: the request outliving the time left,
and an answer that arrives within it.

The primary-connection lookup had no test that reached it: every api test set
the override. The upstream atoms it reads are now writable in the test, so one
case proves the call goes to the primary environment's connection and not
another's, one that a missing primary gives no_connection, and one that a
primary whose connection is still None is waited for.

Button's base class already gives every button a 44 px tap area on a coarse
pointer, so the fork's `touch` size duplicated it. button.tsx is upstream's
again and off the seam list; the pages use the sizes they had before #61.
Input keeps `touch`, having no such mechanism.

The 44 px links were hand-styled anchors without the design system's focus
ring. They render through InlineButton now, with the tap height named once in
the fork's TouchLink, and the table and the cards share one Ref.
@tomkaczocha
tomkaczocha merged commit 2625e09 into main Oct 17, 2026
2 checks passed
@rawphp
rawphp deleted the t3/factory-issue-61 branch October 28, 2026 04:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Factory pages on a phone and over the tailnet

2 participants