# 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: June–Oct 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 `