Skip to content

fix: template bugs found in the first product's code - #496

Merged
agjs merged 6 commits into
mainfrom
fix/template-lessons-from-tinkercaster
Oct 10, 2026
Merged

agjs merged 6 commits into
mainfrom
fix/template-lessons-from-tinkercaster

Conversation

@agjs

@agjs agjs commented Oct 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Tinkercaster was created from the template at cdacac0. This PR diffs every template file it changed since, reads the commit behind each change, keeps the ones that fixed a template bug or gap, and checks each against current main. Everything below was still present on main. The findings are recorded as Report 3 (entries 102–119) in docs/maintenance/product-feedback.md.

Billing

  • Paid plans granted nothing. Plans were seeded without plan_features, so a Pro subscriber resolved Free entitlements. One typed catalog (billing.plans.ts) now seeds plans and their features. Pro must give every feature key a value, enforced by the type check and a unit test. Pro grants export, team invites and 10 seats.
  • Duplicate subscriptions. Checkout now locks the account row and returns 409 when a live Stripe-backed subscription exists, locally or in Stripe (its webhook may lag). It reuses an open session for the same plan and price. An admin-granted plan does not block buying.
  • Delayed payments. checkout.session.completed activates the plan only for paid or no_payment_required. async_payment_succeeded and async_payment_failed settle the rest.
  • Smaller gaps. Checkout rejects the Free plan. GET /billing/plans is public, for the public site's pricing page, and works with billing off. The optional STRIPE_PRICE_ID_PRO_YEARLY adds a monthly/yearly choice on the billing page.

Sessions

  • Reloads logged users out. After the 15-minute access cookie expired, a reload logged the user out, because /users/me answered { user: null } and the UI refreshes only on 401. It now returns 401 while a refresh cookie exists. The UI refreshes, and an unrecoverable 401 resolves to logged out.
  • Revoked refresh cookies. /auth/refresh now clears both cookies when the session is gone.
  • Lost write after token expiry. The UI's refresh retry re-read a consumed request body, so the first write after expiry failed. The body is now captured before dispatch.

CI and repo

  • Every PR check failed in private repos. dorny/paths-filter lists PR files through the API, which needs pull-requests: read in a private repository. BoringStack is public, so this never showed here, but every PR check failed in Tinkercaster. The permission is added to 14 workflows and enforced by the new lint-meta rule github-actions-paths-filter-permissions.
  • pre-push could silently skip a gate. With many changed paths, echo | grep -q under pipefail returned 141 and the gate was skipped without a message. Here-strings replace it everywhere. The hook also unsets inherited GIT_* variables.
  • packages/* never reached the images. It now reaches them through a BuildKit packages build context and the dev containers through a read-only mount. packages/README.md keeps the directory present.
  • CI. The API suite runs once, agent verification cancels superseded PR runs, and dev mail defaults to Mailpit when the overlay is on.

UI

  • Fixes. Verify-email no longer posts its single-use token twice under StrictMode. The billing page polls after a Stripe return. nginx returns 404 for /.well-known/* and missing .json/.txt/.xml.
  • Conveniences Tinkercaster added. List errors get a retry, the error page gets a home action, devtools appear only with ?devtools, login returns to the page that asked for it (same-origin paths only), and the test setup gains jsdom stubs.

API housekeeping

  • Dev logs. No stack trace for validation errors, since it carried the request body.
  • Import cycle. Notification events no longer import the lib/notifications barrel.
  • Test cleanup. A test derives every table from the schema and fails when a table is neither cleaned nor allowlisted.

Not adopted (Report 3, entry 119): fast-by-default pre-push and skipping main runs after a verified PR weaken gates by policy. Removing the per-app release workflows would break Compose, which publishes through them.

Test plan

  • apps/api: bun run check, then bun run test:coverage against a real Postgres (TEST_DATABASE_URL): 1346 pass, 0 fail, coverage gate passed (86.8% lines)
  • apps/ui: bun run validate: 741 tests, build, size budgets, lint-meta, knip, format
  • apps/docs: bun run build:ci
  • bun run agent:check (101 pass), bun test infra/k3s/tests scripts/release, Compose guardrails, shellcheck, actionlint (only style notes already on main)
  • nginx config: nginx -t on the pinned image, and a throwaway container answered 404 for /.well-known/security.txt and 200 for SPA routes
  • packages/ build context: a scratch app importing a package built with --build-context packages, and the package files are present in the image
  • UI client regenerated from an in-process OpenAPI dump (not port 7330). The diff is billing only.
  • Real API/UI prod image builds with the new context: the local Docker disk is full, so CI's image build jobs cover this
  • Stripe test-mode run of checkout and the async payment events: covered by unit tests with a fake Stripe HTTP layer only
  • Pushed with --no-verify: the local pre-push e2e would run against another project's stack on :7331

agjs added 6 commits October 10, 2026 10:39
Plans were seeded without plan_features, so Pro granted nothing. One typed
catalog now seeds plans and features. Checkout locks the account row,
returns 409 for a live Stripe subscription found locally or in Stripe,
reuses an open session and rejects the Free plan. A delayed payment no
longer activates a plan. The plan list is public, and an optional yearly
price is supported.
…kies

/users/me answered { user: null } once the 15-minute access cookie
expired, and the UI only refreshes on 401, so a reload logged the user
out. It now returns 401 while a refresh cookie exists. /auth/refresh
clears both cookies when the session is gone. Also: no stack for
validation errors in dev logs, notification events no longer import the
barrel, and a test keeps the cleanup table list complete.
The refresh retry re-read a consumed body, so the first write after token
expiry failed. An unrecoverable 401 on /me now means logged out. The
billing page polls after a Stripe return and offers a yearly price.
Verify-email no longer runs its token twice under StrictMode, nginx 404s
.well-known and missing data files, list errors get a retry, the error
page gets a home action, devtools are opt-in and login returns to the
page that asked for it.
…mages

dorny/paths-filter lists PR files through the API, which needs
pull-requests: read in a private repository; without it every PR check
failed in the first product. A lint-meta rule now enforces it. pre-push no
longer skips gates on SIGPIPE under pipefail and unsets inherited git
variables. The API suite runs once in CI, agent verification cancels
superseded PR runs, dev mail defaults to Mailpit, and packages/ reaches
the API and UI images through a BuildKit context and dev containers
through a read-only mount.
Mapping prices only through the built-in catalog dropped updates for plans
a product adds outside it (the F10 security spec seeds one). Catalog
prices still map first, so the yearly Pro price resolves to Pro; any other
price matches plans.stripePriceId as before.
@agjs
agjs merged commit 35fc4ce into main Oct 10, 2026
35 checks passed
@agjs
agjs deleted the fix/template-lessons-from-tinkercaster branch October 10, 2026 09:01
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.

1 participant