chore: gitignore runtime artifacts, drop orphan 00 and AI roadmap

Add loadtest/run.pid and loadtest/run.err to .gitignore so k6 runtime
state stops tagging along on git add. Also ignore the orphan /00 file
(stray redirect of pkg/observability/tracing.go) and ROADMAP.md (AI-
synthesised audit doc, not part of the project canon).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-04 09:51:14 +02:00
parent 7b92636b64
commit 4e28b812bd
3 changed files with 5 additions and 308 deletions

5
.gitignore vendored
View File

@@ -94,4 +94,9 @@ migrations/*.state
loadtest/run.pid
loadtest/run.err
/00
ROADMAP.md

32
00
View File

@@ -1,32 +0,0 @@
package observability
import (
"context"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
sdktrace "go.opentelemetry.io/otel/trace"
)
func InitTracing(ctx context.Context, serviceName, endpoint string) (func(context.Context) error, error) {
exp, err := otlptracehttp.New(ctx, otlptracehttp.WithEndpoint(endpoint), otlptracehttp.WithInsecure())
if err != nil {
return nil, err
}
res, err := resource.New(ctx, resource.WithAttributes(semconv.ServiceName(serviceName)),)
if err != nil {
return nil, err
}
tp := sdktrace.NewTraceProvider(sdktrace.WithBatcher(exp, sdktrace.WithBatchTimeout(5*time.Second)), sdktrace.WithResource(res),)
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.TraceContext{})
return tp.Shutdown, nil
}

View File

@@ -1,276 +0,0 @@
# Roadmap
Synthesised from a four-pronged audit (TUI, checkout, API, code quality) comparing this project against `terminal-shop-source/`. Items are grouped by impact, not by sequence — but the **Phase** ordering reflects what unblocks what. Every item links to the file path (and line where useful) so future sessions can pick up cold.
The legend across every entry:
- **MATCHES** — parity with reference
- **LACKS** — reference has it, we don't
- **EXTRA** — we have it, reference doesn't (kept on purpose unless flagged)
- **WORSE / BETTER** — both sides have it; relative quality
---
## Phase 0 — Hygiene (do first; small, prevents footguns)
These are quick wins that are blocking nothing but should be done before opening larger refactors.
### 0.1 Remove tracked artefacts
- `terminalshop.db` (root) and `api/terminalshop.db` are committed SQLite DBs. May contain test users / Stripe IDs.
- `git rm --cached terminalshop.db api/terminalshop.db`
- Compiled binaries `main` (~20MB) and `terminalShop` (~22MB) are tracked.
- `git rm --cached main terminalShop`
- `server.go.old` is a stale duplicate of `main.go`. Delete.
- `test_stripe.go` at root is dead manual test code under `//go:build ignore`. Move to `cmd/striptest/` or delete.
- `pkg/tui/qrfefe/` — likely a typo for `qrcode`; rename and fix the unused `size` parameter in `Generate`.
- Verify `.env` isn't tracked: `git ls-files | grep .env`. If it is, `git rm --cached .env` and **rotate any Stripe keys**.
### 0.2 Resolve port mismatch
`main.go:26` hardcodes port `23456`; `pkg/config` defaults `SSH_PORT` to `23457`; `CLAUDE.md` and `README` claim `23457`. Pick one (use `cfg.SSHPort`) and update docs in `main.go` + `CLAUDE.md`.
### 0.3 Delete dead checkout code
`pkg/tui/confirmation.go:14-125` defines `RenderConfirmation`, `ConfirmView`, `ConfirmUpdate` — superseded by `ReviewSwitch`/`generateReviewContent` and never called. Remove.
### 0.4 Wire or remove `ssh_keys.go`
`api/handlers/ssh_keys.go` defines `GetSSHKeys`/`DeleteSSHKeys` but `api/routes/routes.go` never registers them. Wire or delete.
### 0.5 Lowercase error message
`api/handlers/cards.go:348` returns `fmt.Errorf("Failed to save...")` capitalized — violates code style.
### 0.6 Fix red "press enter to confirm"
`pkg/tui/confirmation.go:158` uses `m.theme.TextError().Render(...)` for an action prompt — misleading. Reference uses `TextBrand()`.
### 0.7 Breadcrumb medium-size threshold
`pkg/tui/breadcrumbs.go:21-25` only abbreviates on `< large`; reference abbreviates on `small||medium`. May overflow on medium terminals.
---
## Phase 1 — Foundational systems (these compound; fix first)
The audits identified three foundations where every new feature lands awkwardly until addressed: theme parameterization, per-page state encapsulation, and API envelope shape.
### 1.1 Parameterized theme + tokens
**Why this first:** Reference `BasicTheme(renderer, highlight *string)` swaps the highlight per selected product, giving the shop view its colour-shift effect. We hardcode highlight to `#4682B4` in `pkg/tui/theme/theme.go:36`, so the per-product theme pattern is impossible.
- Add `pkg/tui/theme/tokens.go` with named palette constants — every `lipgloss.Color("#XXXXXX")` and raw ANSI literal (`"196"`, `"205"`, `"52"`) currently embedded in `pkg/tui/theme/theme.go` and `pkg/tui/shop.go` (line 72,124), `pkg/tui/cart.go:54`.
- Change `BasicTheme(renderer, highlight *string)` to accept an optional highlight; default to brand if nil.
- Add a `Theme.ProductColor(hex string) lipgloss.Style` accessor for dynamic product colours; remove direct `lipgloss.Color(coffee.Color)` usages.
- Make `Theme.Base()` return `t.base.Copy()` (`pkg/tui/theme/theme.go:82`) — chained mutations on the cached style currently leak across renders.
- Add an `UpdateSelectedTheme(coffee)` call in shop nav (mirrors reference `shop.go:523-542`).
### 1.2 Per-page state structs (refactor `model.go`)
**Why:** `pkg/tui/model.go` is 964 lines with ~40 boolean flags. `account.go` (428) similarly farms booleans — `addressListFocused`, `cardListFocused`, `faqFocused`, etc. Reference uses one `state` struct with per-page substructs (`root.go:89-107`) and a `[]page` array for account sub-pages (`account.go:11-17`).
Files to introduce inside `model.go` (or a new `model_state.go`):
- `splashState`, `cartState`, `shippingState`, `paymentState`, `accountState`, `reviewState`, `confirmState`.
- Replace the boolean farm in `account.go` with `selected int`, `focused bool`, and an explicit `[]page` for account sub-pages.
Then split `model.go` into:
- `model.go` (~150 lines: struct + constructor)
- `model_state.go` (per-page state structs)
- `model_switch.go` (`*Switch` helpers + footer config)
- `commands.go` (every `tea.Cmd` builder: `fetchCart`, `fetchOrders`, `fetchAddresses`, `saveAddress`, `fetchCards`, `deleteCard`, `checkoutWithSavedCard`, `saveCardAndConvert`, `saveCardOnlyCmd`, `collectCardCmd`, `pollCardsCmd`).
While here, extract `resolveCartAddressCmd(m Model) error`: `checkoutWithSavedCard` and `saveCardAndConvert` (`model.go:592-726`) duplicate ~30 lines of address-resolution logic.
### 1.3 Structured validation errors
**Why:** Field-level validation is currently ad-hoc string checks (`addresses.go:69`, `cart.go:102`, `auth.go:121-127`) returning a single message. The TUI shows one error at a time even when a form has multiple bad fields. Matching the reference's exact envelope shape isn't necessary (no external client consumes it), but you do want structured per-field issues so the TUI can highlight the right input.
- Add a central validator middleware (or helper) that returns `{issues: [{path, message}]}` per failing field. Reference does this via Zod (`common.ts:33-117`); a minimal Go equivalent is ~50 lines.
- Add a `pkg/utils/codes.go` with exported error-code constants — current strings (`CART_ERROR`, `INVALID_ID`, etc.) are scattered. One file makes them greppable and prevents typos like `"ADDRESS NOT FOUND"` (the bug in old `NEXT.md`).
- Keep the `{success, data|error}` envelope as-is unless an external client ever shows up.
---
## Phase 2 — Checkout flow correctness (impact on real customers)
### 2.1 Shipping cost calculation + display
**Critical gap.** `pkg/tui/confirmation.go:46-48,154-156` always renders `$0.00`. No shipping rate calculation exists anywhere. Reference pulls `m.cart.Amount.Shipping` from the API.
- Add a `pkg/shipping` rate calculation (Shippo client already exists for address validation — extend it).
- Backend: have `/cart` and `/cart/convert` populate `cart.Amount.Shipping`.
- TUI: add `paymentCostsView()` (mirrors reference `payment.go:714,750-771`) shown above the card list AND inside the form.
- TUI: render shipping line in `confirmation.go` and `confirm.go` review.
### 2.2 Confirm-delete pattern for addresses + cards
Both pages currently delete immediately:
- `pkg/tui/shipping_form.go:414-432` — destructive, no undo.
- `pkg/tui/payment_form.go:503-521` — same.
Reference uses inline `y/n` confirmation tied to `state.deleting *int` (`shipping.go:251-277`, `payment.go:376-402`). Adopt the same pattern.
### 2.3 Auto-scroll focused form field into view
`pkg/tui/shipping_form.go` and `pkg/tui/payment_form.go` set viewport content but never scroll to the focused input. On small terminals a focused field can be off-screen. Reference: `ensureShippingFocusedInputIsVisible` (`shipping.go:73-132`) and `ensurePaymentFocusedInputIsVisible` (`payment.go:104-165`).
### 2.4 Restore field-level validators
`pkg/tui/shipping_form.go:73` removes per-field validators "so Tab/Enter move freely"; same for `payment_form.go`. Reference keeps `validate.NotEmpty` per field (`shipping.go:535-569`) for in-place feedback. Currently we rebuild the whole form on submit-error (`shipping_form.go:216,367`), losing focus.
### 2.5 Country list beyond {NO, US}
`pkg/tui/shipping_form.go:24-27` hardcodes the country list. Reference accepts free-text country (`shipping.go:556-560`). Add ISO-2 validation in the API and a wider list (or free-text) in the form.
### 2.6 Centralize footer commands in `*Switch()` methods
Footers are set ad-hoc inside `Update` branches (`shipping_form.go:350-354`, `payment_form.go:421-425, 487-491, 498-500, 531-536, 550-555`). Easy to drift. Reference sets them once on entry. After Phase 1.2 this becomes a one-liner per `*Switch`.
### 2.7 Robust `error` handling in checkout
`pkg/tui/confirmation.go:185-231` (`ReviewUpdate`) doesn't handle a generic `error` `tea.Msg`. If the checkout cmd returns a non-`CheckoutResultMsg` error, `CheckingOut` stays `true` forever. Mirror reference's `confirm.go:101-103` and `payment.go:584-592` — clear `submitting` on any `error`.
### 2.8 Profile update on payment submit
Reference `payment.go:504-514` calls `Profile.Update` to persist name/email entered during checkout. We don't (we have no profile endpoint anyway — see Phase 4.1).
### 2.9 Distinct submitting states / spinner messages
Reference distinguishes "verifying payment details…" vs "generating payment link…" (`payment.go:653-659`). We show generic strings in `payment_form.go:247,370`.
---
## Phase 3 — TUI feature parity (visible polish)
### 3.1 Reusable cursor + logo
- Add `pkg/tui/cursor.go` (700ms tick, `state.cursor.visible`) — reference has one shared blink, we only blink on splash via `splashCursorTickMsg` (`model.go:151,422`).
- Add `pkg/tui/logo.go` with `LogoView()`. Currently re-implemented inline in `splash.go:45`, `menu.go:29`, `account.go:186-210`.
### 3.2 Region toggle on shop view
`r` key cycles NA / EU / Global with re-fetch (reference `shop.go:155`, `footer.go:49-82`). Touches: `pkg/tui/shop.go`, `pkg/tui/footer.go`, API products endpoint (Phase 4.4 — variants by region).
### 3.3 Featured / originals product sections
Render `~ featured ~` / `~ originals ~` headers from `Tags.Featured` (reference `shop.go:348-389`). We flat-list everything (`shop.go:92-99`).
### 3.4 Splash error path + auth failure surface
`pkg/tui/splash.go:35-59` has no error rendering — auth failure falls through silently (`update.go:74`). Reference handles `error != nil` with an `esc quit` hint (`splash.go:115-165`).
### 3.5 Footer parity
- Top border (`footer.go:91-93` reference) — we just centre text.
- Error banner with esc-hint and word-wrap (reference `footer.go:128-156`); ours sits above content (`views.go:76-79`) and has no esc affordance.
- "Free shipping" hint line + region indicator (reference `footer.go:122-156`).
### 3.6 Account view dual viewports + scroll-pos restore
- Reference has `menuViewport` + `detailViewport` (`account.go:14-16`); we only have `detailViewport` (`model.go:188`). Left menu doesn't scroll for users with many sub-pages.
- Subscription scroll-pos restore — currently only orders saves `yOffset` (`account.go:337,411-414`). Add the equivalent for subscriptions when those land (Phase 4.2).
### 3.7 Promote about + FAQ to dedicated pages
We render both as account sub-panels. Reference has `AboutSwitch`/`FaqSwitch` as page-level. Lower-priority polish but unblocks `aboutPage`/`faqPage` in the page enum (Phase 1.2).
### 3.8 Final / final-sub post-purchase pages
Reference has `final.go` / `final-sub.go` as a separate terminal "thank-you" state with its own viewport. Our flow ends inside `confirmation.go`. Add a `finalPage` after `ReviewSubmit` succeeds.
### 3.9 Unified error-to-Visible-error pipeline
`update.go:55-60` catches raw `err.Error()` and only re-fetches cart. Reference `root.go:266-278` runs `api.GetErrorMessage` to normalise messages and re-fetches the right resource per page.
### 3.10 Command-line entry routing
Reference parses `command []string` from the SSH session — `ssh terminal.shop espresso` lands directly on the espresso product (`root.go:195-258`). We always splash → shop. Easy to add to `main.go` + `model.NewModelWithAuth`.
### 3.11 Resize debounce — keep
`pkg/tui/update.go:14-41` debounces resize via `resizeTickMsg`. Reference resizes synchronously (`root.go:279-303`). Ours is **BETTER**; document the rationale in a comment so it isn't accidentally removed during the Phase 1.2 refactor.
---
## Phase 4 — API completeness (closes the largest functional gap)
Reference TS API at `terminal-shop-source/packages/functions/src/api/`. Our Go API at `api/handlers/`.
### 4.1 Profile resource — **missing entirely**
- `pkg/models/profile.go` (or extend user model)
- `api/handlers/profile.go`: `GET /profile`, `PUT /profile {name, email}`
- Wire group in `api/routes/routes.go`
### 4.2 Subscriptions — **missing entirely**
Major gap: products carry a `subscription` flag but we have no model, table, routes, or TUI flow.
- `pkg/models/subscription.go` + `pkg/models/subscription_schedule.go` (matches `core/src/subscription/index.ts`)
- `api/handlers/subscriptions.go`: `GET /subscription`, `GET /:id`, `POST /`, `DELETE /:id`, `PUT /:id`
- Reference TUI counterparts to port: `subscribe.go` (variant picker), `subscriptions.go` (list/detail/cancel)
### 4.3 Product variants + region filtering
**Largest single product gap.** `pkg/models/coffee.go` has flat `price`; reference has `variants[]` with prices + inventory + `subscription` setting + `tags` + `order` (`core/src/product/index.ts:19-86`).
- Migrate to string IDs (`prd_*`) and add `variants` table.
- Cart `SetItemRequest` body becomes `{productVariantID, quantity}` instead of `{coffee_id, quantity}` (`api/handlers/cart.go:87-90`).
- Add IP geolocation (reference `ipinfo.ts`) and filter variants by `Region`.
### 4.4 Address / card / cart / order schema cleanup
Reference field shapes are listed for cross-reference, but since this API only ever serves the SSH TUI client, **field renames are optional cosmetic churn** — only make changes that improve correctness or unblock other work.
- **Keep** (correctness / unblocks Phase 2.1):
- Cart (`api/handlers/cart.go:43-71`): nest `amount: {subtotal, shipping, total}` and `shipping: {service, timeframe}` so the TUI can render shipping cost.
- Addresses (`api/handlers/addresses.go:54-72`): ISO-2 country validation.
- Orders: add `GET /orders/{id}` (Phase 3.6 order detail benefits); expose `tracking` once Phase 4.5 lands.
- **Optional / skip unless you feel like it**: rename `street_2` → `street2`, `state` → `province`, switch SCREAMING_SNAKE error codes to lowercase, drop `is_default` on cards. None of this matters with a single in-tree client.
- **Audit, don't necessarily remove**: `POST /orders/{id}/refund` (`orders.go:40-95`) — reference handles refunds via Stripe dashboard. If you want admin self-serve, keep it; otherwise gate behind admin auth or drop.
### 4.5 Shippo tracking webhook
`api/handlers/webhook.go` doesn't handle `/webhooks/shippo`. Without this, orders never advance past `paid` to `shipped`/`delivered`. Spec: reference `hook.ts:32-82` → `Order.UpdateTrackingStatus`.
### 4.6 Inventory tracking on order create
`api/handlers/cart.go:324-335` (`ConvertCart`) doesn't decrement inventory or reject out-of-stock. Reference writes `inventoryRecordTable` on every order.
### 4.7 Email
- Order receipt + shipped notification. Choose a transactional provider; wire on order status transitions.
- Newsletter signup (`POST /email`) — only if you actually plan to send a newsletter; otherwise skip.
### 4.8 `/view/init` parity
`api/handlers/view.go:47-54` returns `{user, products, cart-as-items, addresses, cards, orders}`. After Phase 4.1–4.2 land, expand to include `profile`, full `Cart.Info` (with `amount`/`shipping`), `subscriptions`, and `region`. Drop `tokens` / `apps` — see out-of-scope note below.
---
## Phase 5 — Tests (lift coverage to match reference)
### 5.1 API handler tests
Currently no tests for: `cards.go` (351 lines, Stripe-critical), `orders.go`, `webhook.go`, `reconcile.go`, `view.go`. Reference has matching `card.test.ts`, `order.test.ts`, `view.test.ts`.
Adopt the existing pattern (table-driven + `httptest`). Files to add: `api/handlers/cards_test.go`, `webhook_test.go`, `orders_test.go`, `reconcile_test.go`, `view_test.go`.
### 5.2 TUI Update tests
`pkg/tui/menu_test.go` is an empty table; `pkg/tui/model_test.go` tests only `updateLayout`. No tests cover `Update`, page switches, cart math, or scroll. Add `pkg/tui/update_test.go` covering `tea.KeyMsg` for nav keys + `CartSyncedMsg` + `CheckoutResultMsg`.
### 5.3 Cart math + form validation
`validatePaymentForm` is pure and easily unit-tested. `CalculateSubtotal` and cart sort order in `GetCartItemsSlice` likewise.
### 5.4 Token refresh test
The JWT rotation logic at `pkg/tui/model.go:468` has no test. Add one.
### 5.5 Subscription / email tests
After Phase 4.2 / 4.7 land, port reference `subscription.test.ts` and `email.test.ts`.
---
## Phase 6 — Things we do better (preserve and document)
These are intentional design wins worth keeping. Document them with comments at the call sites so they aren't reverted by accident.
- **Server-side authoritative cart** — DB-backed, can't desync between sessions. Reference is client-side optimistic.
- **Idempotent payments** — order written before Stripe charge, with `[CRITICAL]` log on partial failure (`docs/checkout.md`).
- **Atomic checkout transaction** — order create + cart clear + address/card sync in one DB tx.
- **Multi-key SSH support** — reference only trusts the SSH transport key.
- **Rate limiting** — auth (20/min), card creation (15/min), checkout (10/min). Reference has none visible.
- **Reconciliation job** (`api/handlers/reconcile.go`) — needed because of our eager-confirm flow; reference's webhook-driven flow doesn't need one.
- **Resize debounce** (`pkg/tui/update.go:14-41`).
- **Custom error envelope hygiene** — handlers consistently use `utils.RespondSuccess`/`RespondError`; no `http.Error` leaks (audited, 0 hits). Keep this discipline as Phase 1.3 reshapes the envelope.
- **Extra theme palette colours** (`success`, `loading`, `label`, `dim`) beyond reference's minimal set — flexibility for spinner / labels.
---
## Reference paths cheat-sheet
User project:
- TUI: `pkg/tui/` (22 files, 4,223 lines)
- API: `api/handlers/`, `api/middleware/`, `api/routes/`
- Models: `pkg/models/`
- Theme: `pkg/tui/theme/`
- Utils: `pkg/utils/response.go`
Reference (`terminal-shop-source/`):
- Go TUI: `packages/go/pkg/tui/` (27 files, 5,854 lines)
- TS API: `packages/functions/src/api/`
- TS domain models: `packages/core/src/{product,cart,address,card,order,user,subscription}/`
- Errors: `packages/core/src/error.ts`
- Tests: `packages/functions/test/api/*.test.ts`
---
## Out of scope / explicitly skipped
- **Personal access tokens (`trm_*`)** — reference has them so third parties can call the API from `curl` / SDKs without an SSH session. We only have one client (the SSH TUI), and the JWT it gets at splash is scoped to that session. Adding PATs would mean a new model, new middleware path, new TUI reveal-once UX, and a longer-lived secret to leak. Not worth it unless someone asks for programmatic access.
- **OAuth apps** — reference has them so third-party websites can act on a user's behalf without seeing the user's credentials. We have no third parties. Same reasoning as above, plus a redirect flow on top.
- **Stainless-generated SDK** — reference ships `stainless.yml` to auto-generate client libraries (JS/Python/Go). Only useful if you have programmatic API consumers, which means PATs or OAuth, which we just skipped.
- **Reference-exact API field naming** — `street1` vs `street_2`, lowercase-snake error codes, bare `{data}` envelope. With one in-tree client these are cosmetic.
- **Stripe Hosted Checkout → Stripe.js custom card form** — described in `docs/stripe-js-card-form.md`. Still a valid future option; not on the critical path. Revisit if branding/UX justifies it.
- **PostgreSQL migration** — currently SQLite via SQLite driver in `pkg/database/db.go`. Move when concurrent-write contention shows up; not before.
- **Catppuccin theme** — present in `pkg/tui/theme/theme.go:49-66` but unused. Harmless; keep or remove with no feature impact.