ghostguild-org/docs/BACKLOG.md
Jennie Robinson Faber d5c91dd66b
Some checks failed
Test / vitest (push) Successful in 12m28s
Test / playwright (push) Failing after 12m9s
Test / Notify on failure (push) Successful in 2s
Design and UI tweaks
2026-05-25 13:28:54 +01:00

142 lines
18 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.

# Ghost Guild — Open Backlog
_Last consolidated: 2026-05-24. Single source of truth for every open issue across the codebase. Pulls from `LAUNCH_READINESS.md`, `TODO.md`, the post-launch backlog memory, and a fresh sweep of in-code TODO/FIXME comments._
Cutover has not happened yet. Deploy steps + Activation + Open decisions live separately in [`LAUNCH_READINESS.md`](./LAUNCH_READINESS.md). This file is the everything-else.
**Launch shape (2026-05-18):** site live with events ASAP, applications open immediately, Slack invites delivered in waves. Entire waitlist invited to apply at launch. See `LAUNCH_READINESS.md` for the full shape, the activation steps, and the open product decisions that gate the launch comms.
---
## Pre-cutover (do once)
Operational steps that have to run during cutover. Full details + env-var list in [`LAUNCH_READINESS.md`](./LAUNCH_READINESS.md).
- [ ] Provision the Dokploy app, set env vars (full list in LAUNCH_READINESS.md), confirm `BASE_URL` exact-matches the public origin and `NODE_ENV=production`.
- [ ] Add the daily Dokploy Scheduled Task that POSTs to `/api/internal/reconcile-payments` with `X-Reconcile-Token`.
- [ ] **Run `node scripts/migrate-contribution-amount.cjs --apply` against prod Mongo BEFORE the new code serves traffic.**
- [ ] **After deploying `feature/contribution-form-polish`: run `node scripts/migrate-annual-contribution-to-cadence-unit.cjs --apply` against prod Mongo.** Converts existing annual Member rows from monthly-equivalent storage to cadence-unit storage (×12 on `contributionAmount` for `billingCadence='annual'` rows). Idempotent via transient `contributionAmountConverted` marker. Without this step, the UI will render `$15/yr` for existing annual members until they update their contribution.
- [ ] Set `NUXT_HELCIM_MONTHLY_PLAN_ID=50302` and `NUXT_HELCIM_ANNUAL_PLAN_ID=50303` in Dokploy.
- [ ] Set `NUXT_RECONCILE_TOKEN` to a 32+ char random string.
- [ ] Push local `main` to `origin/main`.
- [ ] Deploy.
- [ ] **Run `node scripts/reconcile-helcim-payments.mjs --apply` against prod Mongo AFTER the new code serves traffic.**
- [ ] Audit prod for pre-fix series-pass bypass registrations (registrations on pass-only series children with `registeredAt < 2026-04-20` from non-pass-holders). Decide per case.
- [ ] In Helcim dashboard: disable the default payment-confirmation email for plans 50302 + 50303 (we send our own CRA-safe version via Resend).
- [ ] Run one real test charge and verify (a) Payment doc in Mongo and (b) exactly one CRA-compliant confirmation email.
- [ ] Rotate `HELCIM_API_TOKEN` in the Helcim merchant portal and update the Dokploy env var.
- [ ] Trigger the daily reconcile task once manually in Dokploy to confirm it's wired correctly.
## Pilot smoke walks (before first wave)
Once cutover lands, before the first Slack onboarding wave goes out:
- [ ] **Pilot smoke walk for Slack-invited workflow.** One admin manually clicks "Mark as Slack invited" against a real test member in production, confirms the row updates in place, and confirms the dashboard "Slack coming" note disappears for that member. Unit tests cover the pieces; nothing covers the live admin-to-member round-trip.
---
## Bylaws-decoupling (waiting on amendment ratification)
Membership status is being decoupled from payment status. Copy + UI gates already align; behavioral changes below remain.
- ~~B1 cancel-subscription leaves status `active`.~~ Verified shipped 2026-05-18: `server/api/members/cancel-subscription.post.js:31,50` writes `status: 'active'`. Test coverage in `tests/server/api/cancel-subscription.test.js` (Fix #9 in LAUNCH_READINESS).
- ~~B3 cancelled.~~ `pending_payment` stays.
- ~~B4 admin "Pending Payment" → "Payment setup incomplete"~~ shipped 2026-04-29 (`59d2be2`).
---
## Known gotchas (post-launch)
- **Admin edit does not sync Helcim `recurringAmount`.** `/admin/members/[id]` PUT writes `contributionAmount` direct to Mongo by design. Admins must PATCH Helcim manually. The admin form already shows an `--ember`-bordered notice (commit `e756170`); a real sync flow is a future enhancement.
- **Cadence switch rejected on active subscriptions.** `server/api/members/update-contribution.post.js:206` refuses cadence changes mid-subscription with a TODO comment pointing here. No UI toggle exists on `/member/account``ContributionAmountField` is rendered with `:allow-cadence-change="false"` there. Adding cadence switch requires a Helcim subscription replacement flow, not a plain update.
- **`contributionAmount` is now cadence-unit (not monthly-equivalent).** Post `feature/contribution-form-polish`: `Member.contributionAmount` reads as $180 for "$180/year" annual members, not $15. Server no longer multiplies by 12 anywhere; UI uses `formatContribution(amount, cadence)` from `app/config/contributions.js`. See memory `project_contribution_form_polish`.
- ~~**S2 test fixture id/slug mismatch (local dev only).**~~ Verified resolved 2026-05-24: no `test-s2-drop-in-allowed` reference remains anywhere in `scripts/`, `tests/`, or JSON — the fixture was removed.
- ~~**`/admin/series-management` "Delete" button doesn't actually delete.**~~ Fixed 2026-05-24 (`fix/backlog-batch-2026-05-24`): `deleteSeries` (`app/pages/admin/series/index.vue`) now calls `DELETE /api/admin/series/[id]` (the endpoint already unlinks events server-side), replacing the redundant per-event PUT loop. `e2e/admin-series.spec.js` delete test un-skipped.
- **Past-deadline events and sold-out events render identically.** `EventTicketPurchase.vue` falls through to "Event Sold Out" panel for both `tickets.available.reason === 'Registration deadline has passed'` and zero-stock cases. If "Registration closed" is meant to read differently from "Sold out," add a distinct branch. Flagged 2026-04-30 (no e2e written — gated on this UX decision).
---
## Accessibility / a11y
- [ ] **Button minimum target size.** Site-wide `.btn` renders ~35px tall. WCAG AA 2.5.8 (24×24) passes; AAA 2.5.5 (44×44) fails. Bumping padding affects every button — design call, not a drop-in fix. Flagged 2026-04-11.
- ~~**`/board` color-contrast violations (WCAG AA).**~~ Verified done 2026-05-24: `.block-label` and `.slack-handle` in `BoardPostCard.vue` now use `var(--text-dim)` on the cream card bg (code comments note `--text-faint` was insufficient there). `--text-faint` itself was also darkened from `#746a58` to `#665c4b` (`main.css:33`, 4.94:1 on `--surface`).
---
## Deferred features (own session each)
- [ ] **Email automation system.** Patterned after Tranzac's implementation (separate project, already built). HTML email bodies with template management and drip sequences. Deferred 2026-04-20 — ruled wasted work given the larger system is designed elsewhere. Current transactional email lives in `server/utils/resend.js` + inline in `server/api/auth/login.post.js`, `server/routes/oidc/interaction/login.post.ts`, `server/api/admin/{members,pre-registrants}/invite.post.js`. Copy dump at `docs/email-copy-dump.md`. See memory: `project_email_automation_future`.
- [ ] **Receipts for event ticket purchases (Phase 2).** Phase 1 receipts only cover membership payments. Event tickets — especially guest purchases without member accounts — need a receipt flow. Likely an emailed PDF/HTML receipt at purchase time. Build target: JuneOct 2026, live Jan 2027. See memory: `project_receipts`.
- [ ] **Series/event waitlist.** Admin can configure `tickets.waitlist.enabled` and `maxSize`; `server/utils/tickets.js` returns `waitlistAvailable: true` when full; `app/components/SeriesPassPurchase.vue:341` and `EventTicketPurchase.vue` have stub `handleJoinWaitlist` that toasts "Waitlist Coming Soon." No server endpoint, no confirmation email, no `event_waitlisted` activity hook. Either implement end-to-end or hide the button by removing the `v-if="availability?.waitlistAvailable"` branches in `EventSeriesTicketCard.vue:175` and `EventTicketCard.vue:73`.
- [ ] **ASVS Phase 4.** File-upload validation pipeline, granular RBAC, credential encryption.
---
## Wave-Slack pilot follow-ups
- ~~**`/api/auth/member` doesn't return `slackInvited`.**~~ Verified done 2026-05-24: `server/api/auth/member.get.js:20-21` returns both `slackInvited` and `slackInvitedAt`.
- ~~**Admin members list row mutation isn't reactive.**~~ Verified done 2026-05-24: `markSlackInvited` (`app/pages/admin/members/index.vue:843`) now does `members.value[idx] = { ...members.value[idx], ...res.member }`.
- [ ] **Deprecated `slackInviteStatus` — optional DB cleanup only.** No longer serialized: removed from the `Member` schema and the UI, and `server/api/admin/members.get.js` projects only current schema paths (`Object.keys(Member.schema.paths)`), so stale doc values can't leak into the payload. Remaining: an optional one-shot `$unset` to tidy old Mongo docs — nothing reads the field. Narrowed 2026-05-24.
- [ ] **Spec vs shipped-UI mismatch on wave language.** `docs/specs/wave-based-slack-onboarding-tests.md` §7.5 asserts "no wave/cohort/batch language" in the dashboard note, but the shipped welcome-email and dashboard copy say "monthly onboarding waves." Decide which side wins; update the other.
- [ ] **E2E coverage for `e2e/wave-slack-onboarding.spec.js`.** 9 of 16 scaffolded tests now passing (admin Slack-invited button + non-trivial dashboard cases). 7 remain skipped pending the bugs above (7.2, 6.2), seeding gaps (7.4 — no dev endpoint to mint members of arbitrary status), Open Questions (7.8, 6.9), or spec-vs-UI conflicts (7.5, 6.7).
- [ ] **Pilot exit decision (~8 weeks post-launch).** Either restore `server/_archive/utils/checkSlackJoins.js` + its plugin if polling is needed, or delete the archive permanently. Driven by whether the manual-invite cadence is sustainable post-pilot.
- [ ] **`slack_invite_failed` enum slug cleanup.** Detector and alert removed in `d15458b`, but the slug remains in `server/models/adminAlertDismissal.js` enum so historical dismissal rows continue to validate. Full removal needs a one-shot cleanup of stale dismissal rows in the DB. Roll into a future schema-tidy pass.
---
## Contribution-form-polish follow-ups
From `feature/contribution-form-polish` (14 commits, see memory `project_contribution_form_polish`). Captured 2026-05-23.
- ~~**`account.vue` `currentContributionLabel` uses long-form `/year`, `/month`.**~~ Done 2026-05-24: now returns `formatContribution(amount, cadence.value)` (`/yr`, `/mo`).
- [ ] **Annual-abandoned-signup `billingCadence` corruption — `/join` half still open + existing-row cleanup.** A member who picks annual but abandons before `/api/helcim/subscription` runs is left at `billingCadence: 'monthly'` + cadence-unit `contributionAmount` (e.g. 180) + `status: 'pending_payment'`, rendering `$180/mo` in admin views (member can't see it — not yet signed in). `billingCadence` is only corrected to `'annual'` once the subscription call runs.
- ~~Invite-accept path.~~ Fixed 2026-05-24 (`c3b1c59`): `inviteAcceptSchema` now accepts `cadence`, `accept-invite.vue` sends it, and `accept.post.js` persists `billingCadence` at `Member.create` ($0 forced to `'monthly'`).
- ~~`/join` path had the same gap.~~ Fixed 2026-05-24 (`426f233`): `helcimCustomerSchema` accepts `cadence`, `join.vue` sends it to `/api/helcim/customer`, and `customer.post.js` persists `billingCadence` in both the new-member create branch and the guest-upgrade `$set` branch ($0 forced to `'monthly'`). (`/api/members/create` carries the same omission but is uncalled by any frontend — left as-is.)
- ~~One-shot cleanup for rows corrupted before these fixes.~~ Verified no-op 2026-05-24: swept the `ghost-guild` DB (59 members) — 0 rows with `billingCadence: 'annual'`, 0 with an annual-magnitude `contributionAmount` (≥60), and all 17 `pending_payment` rows sit at monthly presets {0,5,15,30,50}. No corruption exists, so no script was written or run. Because both code fixes land before cutover, there's no pre-fix production window in which this corruption could occur. (If the fixes somehow ship *after* real annual signups have happened, re-run the sweep post-launch.)
- [ ] **`/member/payment-setup` is monthly-only by design — lossy `Math.floor(amount/12)` redirect from account.vue.** Spec already deferred this. Annual members landing on payment-setup via account.vue's `requiresPaymentSetup` recovery path get their cadence-unit amount floor-divided to monthly at the redirect (e.g. $100/yr → tier=8 → $96/yr after re-charge). Reachable only for corrupt-state annual members who lost their `helcimSubscriptionId`. Long-term: payment-setup accepts `?cadence=` and passes through to `update-contribution`. WHY comment lives at `account.vue:472-474`.
- [ ] **No unit tests on `ContributionAmountField.vue`.** The cadence-toggle math (×12 / floor(/12)) and soft-max threshold ($500/mo equiv) live entirely in the component. E2E tests on `/join` + `/accept-invite` cover the happy paths. A small Vitest suite around toggle math, preset selection, and emission would protect against regressions as the component gains more consumers. _Deferred 2026-05-24: a mounted test needs `@vue/test-utils` (not installed; not bundled by `@nuxt/test-utils`) plus a `~` alias / Nuxt env for the `client` vitest project. Out of scope for the low-risk batch — pick up with a deliberate decision on the test approach._
- [ ] **Migration script TOCTOU.** `scripts/migrate-annual-contribution-to-cadence-unit.cjs` reads then writes in two separate ops per doc. If a member updates their contribution via the UI between the read and the write, the migration overwrites the new value with `(stale × 12)`. Mitigation: run during a brief maintenance window, or refactor to a single `updateOne` with `$mul` + marker guard. Low-impact given the small annual-member population.
---
## Simplify-pass follow-ups (still open)
Items surfaced during the 2026-04-29 /simplify review. The 2026-04-30 small-wins batch shipped 3 items (STATUS_LABELS dedup, ImageUpload focus, signupBridge rename). Remaining:
- ~~**Extract `.tint-candle` / `.tint-ember` utility classes.**~~ Done 2026-05-24 (`fix/backlog-batch-2026-05-24`): added `.tint-candle` / `.tint-ember` to `app/assets/css/main.css` and replaced the five inline `style=""` candle tints in `SeriesPassPurchase.vue` + `EventSeriesTicketCard.vue` with the class. (`NaturalDateInput.vue` had no such inline tint; `ImageUpload.vue:29` is a conditional drag-state `:style` with only `border-color`, left as-is.)
- ~~**Audit `member &&` truthy checks in sibling ticket/subscription routes.**~~ Verified resolved 2026-05-24: `tickets/purchase.post.js:34` already gates access/pricing via `hasMemberAccess(member) ? member : null`; the remaining `member &&` lines are harmless existence checks (recording `memberId`, the auto-login decision). No truthy pricing/access gating remains in `events`/`members`/`helcim` routes.
- ~~**STATUS_LABELS dedup — verify.**~~ Verified done 2026-05-24: single shared module `app/config/memberStatus.js`; `index.vue`, `account.vue`, and `[id].vue` all import `STATUS_LABELS` with no inline copies remaining.
- ~~**`app/pages/admin/members/[id].vue` status select still hand-written.**~~ Verified done 2026-05-24: `[id].vue:65-67` drives the status `<select>` from `STATUS_LABELS` (`v-for="(label, value) in STATUS_LABELS"`).
---
## Optional / low-priority
- ~~**Welcome-email Slack-timing mention.**~~ Done 2026-05-24 (`fix/backlog-batch-2026-05-24`): `sendWelcomeEmail` (`server/utils/resend.js`) now includes "Your Slack invitation arrives in our monthly onboarding waves — there may be a short wait."
---
## E2e infrastructure gaps
Surfaced during the 2026-04-30 e2e expansion. None block a green suite, but each blocks specific coverage from being added.
- ~~**Other email routes still send real emails in dev mode.**~~ Fixed 2026-05-24 (`fix/backlog-batch-2026-05-24`): all five `server/utils/resend.js` wrappers now early-return via a shared `skipEmailInDev` guard when `ALLOW_DEV_TEST_ENDPOINTS=true`, mirroring `pre-registrants/invite.post.js`. Covered by `tests/server/utils/resend.test.js`. (The wrappers still keep their own try/catch — full `sendEmail` dedup deferred as a non-blocking cleanup.)
- [ ] **No dev endpoint to seed members of arbitrary status.** Wave-slack §7.4 (note hidden for suspended/cancelled/guest) is gated on this. `/api/dev/test-login` only mints an `active` admin. A minimal `/api/dev/members.post` accepting `{ email, status, slackInvited, ... }` would unblock many more dashboard-state e2e tests.
- [ ] **SSR `useFetch` blocks `page.route` mocking.** Page-level fetches in `[slug].vue` files run during SSR and can't be intercepted client-side. Affects: hidden-event 404 e2e, any test that needs a mocked event payload before client hydration. Either expose a client-side fetch alternative, add a server-side test mock layer, or accept that DB seeding is required for these cases.
- [ ] **Self-cancel block on paid event registrations not e2e-tested.** Requires seeding a logged-in member with a paid registration row. Out of scope for this round.
- [ ] **Visual snapshot for `join — desktop` is stale.** 12,676px diff (2% of image) from layout drift. Regenerate via `npx playwright test --update-snapshots e2e/visual/pages.spec.js` once a designer eyeballs the diff.
- [ ] **E2e cross-file races on admin specs.** With `fullyParallel: false` + `workers: 4` + `retries: 1`, ~1 admin CRUD test still fails per full-suite run (rotates between `admin-events` CRUD, `board` page-loads, and wave-slack §6.4). Each passes 100% in isolation. Root cause: tests anchor on "first row" / "any visible button" rather than uniquely-identified data, so they race when other admin specs mutate the shared dev DB. Proper fix is per-test data isolation: each test creates its own scoped record with a `Date.now()` suffix and queries by that exact identifier. Out of scope for the e2e expansion.
---
## Deeplink memories
- `project_post_launch_backlog.md` — high-level digest of this file
- `project_launch_readiness.md` — cutover status (NOT YET happened)
- `project_launch_flow_map.md` — onboarding flow + Slack wave model
- `project_pre_registrants.md` — invitation system + pre-reg lifecycle
- `project_helcim_plan_model.md` — cadence-keyed plan model
- `project_contribution_amount_redesign.md` — arbitrary $ amount + guidance presets
- `project_contribution_form_polish.md` — ContributionAmountField component + cadence-unit contract shift (supersedes the cadence-math claims in `project_contribution_amount_redesign`)
- `project_receipts.md` — Phase 1 done, Phase 2 pending
- `project_email_automation_future.md` — Tranzac reference for full system