Files
terminalShop/ROADMAP.md

277 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.