Repository navigation
feat(web-ui): templ + Datastar UI with en/ja parity pages, served by the server - #6
Merged
Merged
Conversation
Messages are the original apps/web-app/messages files with additive keys only. Locale comes from the locale cookie, then Accept-Language (q-ordered). Tests fail when a key used by templates is missing in any locale. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Datastar is served from go:embed with its version and SHA-256 pinned and tested. Tailwind uses the standalone CLI v4.3.3, downloaded into a gitignored bin/ and checksum-verified. templ and datastar-go are added. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ports every parity page (home, why, login, signup, forgot/reset password, verify email and success, dashboard with settings and delete account) with the original Tailwind classes. Forms post via Datastar and stream patched fragments or redirects over SSE, with a no-JS fallback. The dashboard live card is the server-push example: clock, session expiry, and account card updates across tabs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
go run ./apps/web/ui/cmd/preview serves all pages with fakes on 0.0.0.0:3001. Handler tests cover status codes, SSE fragments, cookies and the live stream over a real connection. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The header controls stopped at max-w-3xl while pages run to max-w-6xl. The nav now uses the same container as the content (max-w-6xl, px-4/6). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ntract Pages link through config.AppRoutes and read config.Features; ui/routes keeps only the UI's own action endpoints. ui.Users is now the method set of *services.UserService, so the service plugs in without an adapter, and the settings form is validated with validate.UpdateUserSettings. Email changes belong to the auth flows, so the settings form edits the name only and shows the email read-only (copy updated in en and ja). ui.Auth follows packages/auth: CallbackURL on sign-up/sign-in inputs, a zero Session from SignUpEmail/VerifyEmail when there is nothing to sign in, and ui.Err* values named after auth.Err* (new messages for too many requests and passwords over 128 characters). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With packages/auth on main, ui.Auth uses its types (User, Session and the inputs are aliases) and *auth.Auth satisfies it without an adapter. The handlers match auth.Err* values directly instead of UI copies, the session is read through Deps.UserFrom (default auth.UserFrom), and Handler() returns the mux for server.Deps.UI. Compile-time assertions keep ui.Auth and ui.Users in step with *auth.Auth and *services.UserService. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mounts ui.New(...).Handler() as server.Deps.UI with the real *auth.Auth and *services.UserService, and adds make css to rebuild the committed Tailwind output. templ generation is already part of make generate and the CI generated-code check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jayleaton
marked this pull request as ready for review
October 22, 2026 07:15
This was referenced Oct 22, 2026
jayleaton
added a commit
that referenced
this pull request
Oct 31, 2026
The boundary check ran only inside make lint, which a change confined to deploy/ (its own module) never selects, so a test file there merged unchecked and failed the next full make lint on main (sparkrack #6). Plan now adds scripts/check-test-boundaries.sh on its own whenever make lint is not selected; its reuse input is every file. deploy/ has no tests again (AGENTS.md "Test boundaries"): make deploy-check and make deploy-vars cover the deploy config. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jayleaton
added a commit
that referenced
this pull request
Oct 31, 2026
* ci: verify pull requests locally (make ci-local), Lockpick format Every check now runs in `make ci-local` on the agent's machine, in a clean worktree of the exact pushed head, selected from the diff against the PR's base. It reuses steps whose inputs are unchanged since an earlier attested head, takes a machine-wide slot, waits for memory, and can offload the steps to a Davit box. It posts the `local-ci` commit status and fills the PR body's "Local verification" section. A failed head is refused for good. GitHub runs only two small jobs, started by that body edit: a gitleaks secret scan and a check that `local-ci` and the section hold for the head. Main and dev run no CI; Deploy gate passes only attested commits for Railway's "Wait for CI". `make pr-merge` merges on local verification, and on `local-ci` alone where Actions cannot run. The rules live in scripts/cilocal/verify, a new test boundary (100% covered). The migration order check moves into it, so it no longer needs bash 4. The old single CI job, its check action and scripts are removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ci): a Davit box drops files its image has and the head does not Davit's sync adds and updates files but never deletes them, so a box made from a project image built at an older commit kept the four files this branch removes, and cilocal exec refused the tree (rightly: nothing ran). The payload now carries the head's paths (gzipped), and exec removes the rest before it checks the tree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ci): a Davit box commits the head's tree before its steps run Davit syncs the index but no commit, so git status in the box listed every file as added and make check-generated failed on a clean head. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ci: deploy main to staging and releases to production, Lockpick format Railway's GitHub auto-deploy is off (platform.yaml repo: ""), so only the workflows ship code: - Deploy staging: every push to main that make ci-local attested, to the Railway staging environment (web, admin, backup, then /healthz). - Staging catch-up: hourly, dispatches Deploy staging when staging is behind. - Draft release: drafts vX.Y.Z at the commit staging runs. - Deploy production: a published release, after a gate proving staging ran it, still runs it or later, is healthy, and the publisher may release. Until RAILWAY_DEPLOY is true the deploys dry-run. main is the only deployed branch; Deploy gate is gone. docs/RELEASES.md has the flow and setup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ci: each deploy environment holds its own RAILWAY_TOKEN; deploy-check runs the deploy tests - The staging and production GitHub environments each hold a secret named RAILWAY_TOKEN, a Railway project token scoped to that environment. - Readiness needs /healthz to answer 200 with data.status ok. - make deploy-check (and so make ci-local and make ci) runs go test in the deploy/ module, and deploy/platform is a test boundary for platform.yaml's rules. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: name deploy config among the test boundaries Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ci): the deploy test probe goes outside the deploy/platform boundary deploy/platform is a test boundary now, so check-test-probes planted its deploy probe where a test is allowed and the probe was not reported. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ci): every plan checks the test boundaries; deploy/ keeps no tests The boundary check ran only inside make lint, which a change confined to deploy/ (its own module) never selects, so a test file there merged unchecked and failed the next full make lint on main (sparkrack #6). Plan now adds scripts/check-test-boundaries.sh on its own whenever make lint is not selected; its reuse input is every file. deploy/ has no tests again (AGENTS.md "Test boundaries"): make deploy-check and make deploy-vars cover the deploy config. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ci): make pr-merge reads CI jobs GitHub never started as not run On an account whose Actions are blocked by billing, GitHub still creates the CI run and fails each job within seconds, with no runner and the annotation "The job was not started because recent account payments have failed...". make pr-merge counted those as failed checks and refused, so local-ci alone could never merge there. It now reads each failed CI job's annotations and treats a never-started job as no run (--require-actions still refuses). The ci-local hint is shown only for verification problems. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(ci): make ci-local attests what you ran instead of running it make ci-local RAN="<what you ran>" now runs nothing. It refuses a dirty, unpushed or already-failed head, posts local-ci on the exact SHA ("<result> · body <digest> · by <author> on <host>") and fills the PR body's Local verification section with the head and the statement. FAILED= records a failure, final for that head; COMMIT= attests a main commit. It takes seconds: no worktree, plan, slot, Davit box or memory wait. - verify: StatementOf, RenderSection, ClaimsHead, BodyProblems and LocalVerificationProblems (the section names the head and says what ran, its digest matches local-ci, the head never failed), shared by cilocal check, merge and attest. - ci.yml: a "Local verification claim" job; the checking job is named "Local verification" only for a claimed head, "Awaiting make ci-local" otherwise, so an unattested PR shows Expected, not red. - make pr-merge: Decide() per PR (draft, stacked, conflict, attestation, failed, hollow, pending, mergeability), latest attempt per job; CI jobs GitHub never started still count as not run, and with no CI job at all it merges on local-ci alone (--require-actions refuses). Prints the squash SHA and --check-deploy <sha>, a one-read staging verdict (Deploy staging runs plus <STAGING_WEB_URL>/healthz; a dry run says nothing deployed); --check-staging and --then-check-staging. - Post-merge report: comments on the merged PR when Deploy staging fails on main; a verify test fails when a push-to-main workflow is missing from its list. - Removed: the step plan, runner and exec, reuse ledger, Davit routing, machine slot, memory wait, failure triage, report save/load. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(ci): run what the change needs, attest, stack, merge at once docs/CI.md: "What runs" (the claim job and the Awaiting name), a new "What to run" table for this stack, "Local verification" (RAN, FAILED, COMMIT), "Merging on local verification" (Decide, --check-deploy, --check-staging, the Post-merge report) and "When Actions cannot run" with the never-started-jobs rule kept. The Davit, slot, memory, reuse and plan sections are gone. AGENTS.md "Verifying", "Pull requests" and "Environments", CONTEXT.md (claimed, attested, failed head; no ledger), README, development.md and the deploy docs say the same. The github-prs skill, adapted to make ci-local and make pr-merge, is at .claude/skills and .agents/skills, linked from AGENTS.md and CLAUDE.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ci): the PR checks run only main's code; attest only what was checked Randy's review of #134 (bf0b5d3), applied to the attest-only redesign: - P1, PR-controlled gate: ci.yml ran on pull_request, so the pull request supplied the workflow and the scripts/cilocal it ran; a fork adding `func init() { os.Exit(0) }` passed Secret scan and Local verification, and the scan used the fork's .gitleaks.toml. ci.yml now runs on pull_request_target: GitHub takes the workflow and the checkout from main, and the jobs read the head as data only. `cilocal secret-scan --head <sha>` copies the head's files out of git's objects (verify.ScanTargets; a path leaving the scan directory fails closed) and scans them with main's scanner and config. A fork's head fails Local verification, make pr-merge and the staging gate (Verification.Fork, verify.Claimed), and openPull skips a fork PR that shares the branch's name. verify.TrustProblems holds ci.yml to all of it. - P1, publishing a refused run: publish now reads the checkout again just before it posts and refuses unless the report is for exactly that clean, pushed head and tree (verify.PublishRefusal). The save/--publish path Randy reproduced was removed in de66901; this is the one choke point now. - P2, first attestation: a main attestation finds the last attested first-parent ancestor (at most 50; a root or shallow-first commit has none). With none, as in a new project, the statement must name make ci (verify.FirstAttestationProblem). Documented in docs/CI.md, README, AGENTS.md and the github-prs skill. Regression tests in scripts/cilocal/verify (still 100% statement coverage, which also covers two workflows.go helpers 9068dd7 left at 99.4%): TestCIRunsOnlyTrustedCode and TestTrustProblems (they fail on the ci.yml of bf0b5d3 and 9068dd7), TestScanTargets, TestClaimed, the fork cases of TestLocalVerificationProblems, TestPublishRefusal, TestLastAttested, TestRanFullCheck and TestFirstAttestationProblem. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ci): the secret scan never takes its policy from the change it scans Randy's re-review of #134 (2f1cca7): the Secret scan job used main's .gitleaks.toml, but gitleaks also loads .gitleaksignore from the root of the directory it scans, where the head's files were copied. A head adding a .gitleaksignore with a finding's fingerprint passed the scan. --gitleaks-ignore-path alone does not stop it: gitleaks 8.30.1 still loads the scanned root's file. Inline gitleaks:allow comments were a second way for head content to act as policy. - Policy (verify.ScannerPolicy: .gitleaks.toml, .gitleaksignore) comes only from the trusted commit (the checkout's HEAD, main in the job; --base locally), written to a directory beside the scanned one, with gitleaks' defaults and an empty ignore list where it has none. - The change's own policy-named files are copied in as content under another name (verify.ScanCopyPath); gitleaks runs with --ignore-gitleaks-allow and without GITLEAKS_* env (verify.GitleaksArgs). - A change that adds, edits or removes a policy file fails the scan for review even when it finds nothing (verify.PolicyChanges, PolicyProblem). TestHeadPolicyIsNeverScannerPolicy is Randy's repro: both heads fail. scripts/cilocal/verify stays at 100% statement coverage. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(ci): the secret scan allows fixture values, not whole paths --------- Co-authored-by: JJ Eaton <58495321+oldbettie@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Danny <danny@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The Go template has a server, JSON API and auth, but no web UI:
server.Deps.UIis nil, so every page of the original Next.js app (home, why, login, signup, forgot/reset password, verify email + success, dashboard) is missing.How
Adds the HTML transport as
apps/web/ui: server-renderedtemplpages, Datastar 1.0.4 over SSE, Tailwind v4. The server mounts it incmd/serverasUI: ui.New(ui.Deps{Auth: authn, Users: users, ...}).Handler(); nothing else ininternal/*changes.make cssrebuilds the committed Tailwind output.make generateand the CI generated-code check already runtempl generate.<form>s withdata-on:submit="@post(..., {contentType: 'form'})". A Datastar request gets patched fragments or a redirect over SSE; a plain post (no JS) gets the full page or a 303 from the same handler.data-indicatordrives the loading labels, replacingloading.tsx.GET /dashboard/livestreams the clock and session expiry every second, and patches the account card when the user changes in another tab. The notice appears only in the other tabs.config.AppRoutesand readconfig.Features. The settings form is validated withvalidate.UpdateUserSettingsand callsUpdateUserSettings/DeleteAllUserDataon the service. Auth types andauth.Err*are used directly. The package imports neither pgx norpackages/database.<html class>, so there is no flash.Real server (
make db-up && make migrate && make dev, headless Chromium, real auth + Postgres)name = 'Real User Renamed'for the account./loginwithlang="ja"and the heading 「アカウントにサインイン」./healthzreturns{"data":{"status":"ok"}}, and/,/login,/signup,/why,/static/app.cssreturn 200.Checks (on 2fd7510 + this branch)
make ci: exit 0. Build,check-generated(templ + sqlc, no diff), gofmt + golangci-lint 0 issues,lint-layeringall ok (incl.apps/web/ui must not import packages/database),go test -race ./...all packages ok.go test -race -count=1 ./apps/web/ui/...: 66 tests pass, 0 fail.git rebase --exec).@playwright/cli0.1.22, Chromium) against the preview:tab,name; email read-only)Evidence
Before:
mainhas no UI (server.Deps.UIis nil).After: 42 screenshots, each page/state in en and ja, at
~/Documents/spark-work/zig-dsv41-scratch/web-ui-shots/on the dev box (local paths, not uploaded):Files:
en-NN-<state>.pngandja-NN-<state>.png. Real-server shots:real-01-dashboard-after-signup.png,real-02-settings-saved.png,real-03-dashboard-after-signin.png,real-04-login-ja.png. The original app was compared by its markup (classes ported one-to-one), not run side by side.Choices to revisit
common.home,components.themeToggle.*,pages.home.*,pages.dashboard.{settingsForm,deleteAccount,live}.*,errors.{emailTaken,invalidInput,passwordTooLong,tooManyRequests}.validate,config/features.go) instead of Next.js files. Hosting says "any container host" instead of Vercel.make cssregenerates it./logout,/locale,/dashboard/live,/dashboard/settings,/dashboard/delete-account,/static/) live inapps/web/ui/routes, becauseconfigis outside this PR. They can move intoconfig.AppRouteswith the wiring PR.LISTEN/NOTIFYor similar.data-init(link scanners doing GETs cannot spend it). It also handles links that auth's ownGET /api/auth/verify-emailalready completed.http.CrossOriginProtection.themecookie instead of localStorage, so the server can render the right class.Merge Danger
Door: two-way. Reverting removes the pages and the
UI:line. No migration, schema or stored data is added; the UI writes only through the existing auth and user service.Blast Radius: web. Every non-API path (
/and the pages) is now served by the UI instead of 404./api/*and/healthzare matched first and unchanged. Shared files: go.mod/go.sum (a-h/templincl. itstooldirective,datastar-go),cmd/server/main.go(+5 lines) and the Makefile (csstarget).🤖 Generated with Claude Code