Commit graph

427 commits

Author SHA1 Message Date
c6a5e25d06 fix(ImageUpload): restore :focus styling on alt-text input
The alt-text input was hard-coding border/bg via inline style="..." after
the phantom-Tailwind sweep, which can't carry pseudo-class rules.
Per CLAUDE.md, inputs focus to --candle. Moved to a scoped style block
with a real :focus rule.
2026-04-30 15:29:35 +01:00
441a5f5608 refactor(admin): drive members status <select>s from STATUS_LABELS
The status options were duplicated three times in admin/members/index.vue
(filter dropdown, edit-modal dropdown, statusLabel helper). The recent
"Pending Payment" → "Payment setup incomplete" rename only landed in
two of the three sites. Both <select>s now v-for over the existing
STATUS_LABELS const, so any future label change happens in one place.

Side effect: the edit-modal dropdown order is now
(active, pending_payment, suspended, cancelled) to match the filter
dropdown — was previously pending_payment-first.
2026-04-30 15:28:36 +01:00
d9444b022b Merge branch 'fix/launch-flow-copy-and-pre-reg-link'
Ships the 5 launch-flow fixes decided 2026-04-30:
- /join, dashboard, welcome-email copy aligned to monthly-waves model
- welcome email now sends on free /accept-invite activations
- /join signups auto-link to matching PreRegistration records
2026-04-30 15:06:32 +01:00
da5e7efcb7 fix(launch-flow): auto-link /join signups to existing PreRegistration
When a /join submitter's email matches a pending/selected/invited
PreRegistration, mark the pre-reg as accepted and link memberId to the
new Member. Prevents the same person from appearing as both an active
member and an unaccepted pre-registrant. Silent — no email, no UI.

Adds the PreRegistration mock to helcim-customer and free-signup-flow
test suites, since both invoke the customer handler at runtime.
2026-04-30 14:43:02 +01:00
d4000c18cf fix(launch-flow): send welcome email on free /accept-invite activation
Free invite acceptance previously created a Member and signed them in
without sending the welcome email — pre-registrants got nothing as the
join confirmation. Wire sendWelcomeEmail into the free branch matching
the pattern in members/create.post.js.

Paid /accept-invite activations continue to receive the welcome email
via /api/helcim/subscription on the pending_payment → active transition,
so this only changes the free path.
2026-04-30 14:40:13 +01:00
313b8598df fix(launch-flow): align Slack-wait copy across join, dashboard, welcome email
- /join "How membership works" lists community (not Slack) as a benefit;
  adds a note that Slack invitations come in monthly onboarding waves.
- Dashboard slack-coming note drops "2–3 weeks" timeline; uses the same
  monthly-waves phrasing.
- Welcome email no longer points new members to Slack (which they don't
  yet have access to); directs them to reply instead.
2026-04-30 14:39:47 +01:00
d06c83cfc4 Merge pull request 'chore(serena): update project.yml to current schema' (#3) from chore/serena-config-update into main
Some checks failed
Test / vitest (push) Successful in 11m6s
Test / playwright (push) Failing after 9m36s
Test / visual (push) Failing after 9m23s
Test / Notify on failure (push) Successful in 2s
Reviewed-on: #3
2026-04-30 12:51:00 +00:00
9c7d6fa446 Merge pull request 'chore/visual-fidelity-fixes' (#2) from chore/visual-fidelity-fixes into main
Some checks failed
Test / Notify on failure (push) Blocked by required conditions
Test / visual (push) Blocked by required conditions
Test / vitest (push) Successful in 11m11s
Test / playwright (push) Has been cancelled
Reviewed-on: #2
2026-04-30 12:36:08 +00:00
07943266b7 chore(serena): update project.yml to current schema
Some checks failed
Test / vitest (pull_request) Successful in 11m6s
Test / playwright (pull_request) Failing after 9m39s
Test / visual (pull_request) Failing after 9m31s
Test / Notify on failure (pull_request) Successful in 1s
Auto-generated update from Serena — adds new language entries
(ansible, crystal, haxe, hlsl, json, lean4, luau, msl, ocaml,
python_ty, solidity, systemverilog), trims the inline tool list
in favor of a docs link, and adds the 'added_modes' field.
2026-04-30 12:57:21 +01:00
5a69d6ab75 style(visual-fidelity): missed Batch B row in admin/members
Some checks failed
Test / vitest (pull_request) Successful in 11m59s
Test / playwright (pull_request) Failing after 9m53s
Test / visual (pull_request) Failing after 9m20s
Test / Notify on failure (pull_request) Successful in 1s
.row-error background was the one rgba leftover from the
pages-admin slice — line had shifted from 1309 to 1307 after
earlier Batch B edits.
2026-04-30 11:47:44 +01:00
d6cdf45838 style(visual-fidelity): components — batches B,E,G,H
- B: token-equivalent rgba → color-mix in SignupFlowOverlay, OnboardingWidget
- E: drop text-white Tailwind utility from ImageUpload remove-button (now color: var(--parch-text) inline)
- G: typography off-scale snaps (9→10, 14→13, 15→16, 19→18 px)
- H: padding off-scale snaps in BoardPostCard/Form, CirclePicker, FilterBar, LoginModal
2026-04-30 00:13:13 +01:00
cb93f14160 style(visual-fidelity): pages-admin — batches B,C,F
- B: token-equivalent rgba → color-mix(srgb, var(--ember|green|candle) X%, transparent) so colors track dark mode
- C: drop stale var(--green, #...) fallbacks (canonical token now defined in main.css)
- F: inline circle badge → <CircleBadge/> in admin/index, members/[id], members/index
2026-04-30 00:13:09 +01:00
d93c16fbf7 style(visual-fidelity): pages-auth — batches D,G
font-weight 700 → 600 across auth pages; wiki-login hero 32→36
2026-04-30 00:13:05 +01:00
cad57b0083 style(visual-fidelity): pages-public — batches A,D,F,G,H
- about.vue: promote h3 → h2 on circle headings (h1→h2→h2→h2)
- coming-soon.vue: font-weight 700 → 600
- members/[id].vue: inline circle badge → <CircleBadge/>; hero size 42→36
- community-guidelines.vue: padding + font-size off-scale snaps
- board.vue: loading/empty padding 60→64
- series/index.vue, join.vue: padding off-scale snaps
2026-04-30 00:13:02 +01:00
1c2d1537a8 docs(backlog): log 2026-04-29 simplify-pass and deferred follow-ups 2026-04-29 21:50:43 +01:00
26791cc0e3 chore(simplify): trim narrating comments and dedup test body
Test file: drop step markers, regression explainers, and the lead
comment block that restated the contract; hoist the shared subscription
request body to a const; move Member mock defaults into the test that
uses them. Two it() cases unchanged.

Events page: drop WCAG comment that narrated what the
.past-toggle:focus-visible selector already says.
2026-04-29 21:50:00 +01:00
6527bbbe4e test(api): cover free-signup → subscription bridge-cookie hand-off
Two tests guarding the regression where /api/helcim/customer skipped
setPaymentBridgeCookie for $0 signups and left the user unable to
complete activation. Second test confirms the auth gate on
/api/helcim/subscription still rejects fresh unauthenticated calls.
2026-04-29 21:00:27 +01:00
90acc35792 fix(helcim): always issue payment-bridge cookie on signup
Free ($0) signups need the same short-lived bridge cookie as paid signups
so /api/helcim/subscription can identify the member during activation
without a verified auth session. Drops the contributionAmount > 0 guard
that broke free-tier activation in the same flow.
2026-04-29 21:00:22 +01:00
dbd46cc157 docs(backlog): strike EventSeriesBadge dead-code follow-up as shipped 2026-04-29 20:57:06 +01:00
a9acc4c2dc docs(backlog): strike past-events toggle as audited and fixed 2026-04-29 20:56:21 +01:00
dadec1a273 fix(events): add focus-visible outline to past-events toggle
Custom .past-toggle button had no focus indicator — keyboard users
got nothing. Match the canonical WCAG 2.4.7 outline used on .btn
and .zine-select (dashed candle, 3px offset).
2026-04-29 20:39:31 +01:00
f85f284ea5 chore(series): delete unused EventSeriesBadge component
Zero usages across app/ and server/. Migrated to design tokens in commit
350d6c2 before the dead-code status was confirmed; safe to remove now.
2026-04-29 20:38:29 +01:00
55c57d263d docs(backlog): strike shipped items in launch-readiness post-launch list
Strikes:
- memberSavings inactive-member block (shipped f66455e)
- Success-state color convention 4-instances (gold chosen, shipped dc2becf)
- Sidebar 1024px breakpoint verified clean
- EventTicketPurchase magic 24px padding (shipped 7e44809)
- .section-label extraction (already extracted at main.css:128)
- Contribution-amount cosmetic cleanup (shipped 955217a)
- Reconcile customerCode bug (shipped 3c38333, pre-existing on main)

Adds:
- Pointer noting EventSeriesBadge.vue is unused — delete in a future pass.
- Pointer noting Simplify-pass follow-ups are documented in memory.
2026-04-29 20:26:52 +01:00
1da76b11cb fix(series): replace phantom Tailwind on SeriesPassPurchase
Error state and main registration card swap bg-ember-*/border-ember-* and
bg-guild-*/border-guild-* utilities for design tokens in a scoped style
block. Error state uses the codebase's --ember + 8% color-mix pattern;
registration card uses --surface + dashed --border per the zine spec.
2026-04-29 20:22:35 +01:00
350d6c219c fix(series): replace phantom guild Tailwind on EventSeriesBadge
Swap bg-guild-*/border-guild-*/text-guild-* utility classes for design tokens
in a scoped style block. Drops rounded-* per the no-rounded-corners rule and
uses dashed borders for the structural block per the zine spec.
2026-04-29 20:22:30 +01:00
05c47c4499 docs(backlog): close out admin layout token migration as stale
Verified clean 2026-04-29: grep for guild-[0-9]|candlelight-[0-9]|ember-[0-9]
across app/layouts/, app/pages/admin/, and app/components/admin/ returns zero
matches. All admin surfaces already use design tokens.
2026-04-29 20:22:25 +01:00
59d2be2df8 docs(backlog): close out a11y triage items
Strike two stale entries (verified 2026-04-29) and the OIDC routing
quirk (fixed in 23154ff).
2026-04-29 20:10:38 +01:00
23154ff232 fix(oidc): disable devInteractions so custom interactions.url runs in dev
oidc-provider's devInteractions is a quick-start scaffold that, when
enabled, mutates configuration.url to its own urlFor('interaction')
helper — emitting /interaction/UID instead of our /oidc/interaction/UID.
That made /oidc/auth redirect to a 404 in local dev and forced a stale
TODO entry. We already have our own interaction handler at
server/routes/oidc/interaction/[uid].get.ts, so devInteractions is
unnecessary; disabling it makes dev match prod and clears the
oidc-provider warning "your configuration is not in effect".
2026-04-29 19:59:49 +01:00
a69c9d9b49 fix(uploads): replace phantom Tailwind palette with design tokens
Sibling sweep to dc2becf: NaturalDateInput.vue and ImageUpload.vue used
candlelight-/ember-/guild-* utility classes that aren't defined in the
project's Tailwind palette and rendered as no-ops. Swapped to inline
styles using --candle, --ember, --text-dim/faint/bright, --border,
--input-bg, --surface. Drag-state and parsed-date notices follow the
color-mix(... 15%) + 1px solid pattern from dc2becf.
2026-04-29 19:46:59 +01:00
dc2becf63e fix(events): replace phantom candlelight Tailwind with --candle var 2026-04-29 18:30:29 +01:00
e19b16a5cc chore(members): TODO comment for cadence-switch sub-replacement flow 2026-04-29 18:26:40 +01:00
e756170884 feat(admin): warn that contribution edit doesn't sync Helcim 2026-04-29 18:25:59 +01:00
7e44809a83 fix(events): grid-align consent hint, drop magic 24px padding 2026-04-29 18:22:45 +01:00
f66455eda5 fix(tickets): gate memberSavings on hasMemberAccess
Previously the publicTicket comparison block ran whenever a Member record
existed, which surfaced "$0 saved" for cancelled/suspended/guest accounts.
Use the canonical hasMemberAccess helper so only active/pending_payment
members see the savings comparison.
2026-04-29 17:54:58 +01:00
955217a941 chore(admin): rename pending_payment label and tier→contribution
Backlog cleanup from docs/LAUNCH_READINESS.md:
- B4: admin status filter + form options + STATUS_LABELS now read
  "Payment setup incomplete" so admins stop conflating with membership state
- CSV import preview header "Tier" → "Contribution"
- handleUpdateTier → handleUpdateContribution on /member/account
- update-contribution error log "tier" → "amount"
2026-04-29 17:54:53 +01:00
d15458b30a chore(slack): remove dead invite path, archive checkSlackJoins poller
Some checks failed
Test / vitest (push) Successful in 12m6s
Test / playwright (push) Failing after 9m39s
Test / visual (push) Failing after 9m28s
Test / Notify on failure (push) Successful in 2s
Wave-based onboarding makes the auto-invite + polling path obsolete.

- Removes SlackService.inviteUserToSlack — admins now send invites
  through Slack's UI and flip the flag in our admin endpoint.
- Removes the slack_invite_failed admin alert + its detector. The
  alert no longer has a meaningful trigger (we don't attempt invites).
- Archives server/utils/checkSlackJoins.js (and its test) under
  _archive/ in case the polling pattern is needed again post-pilot.
- Deletes the Nitro plugin that scheduled checkSlackJoins on boot
  + hourly. Nothing in nitro.config / nuxt.config / package.json
  registered it elsewhere.
- Drops the slack_invite_failed branch from adminAlerts.test; the
  enum slug stays in adminAlertDismissal so historical dismissal
  rows continue to validate.

notifyNewMember (vetting-channel notification) and findUserByEmail
(used by the auto-flag helper) are retained.
2026-04-29 12:34:21 +01:00
7b326f879d feat(dashboard): one-line note for active members awaiting Slack invite
Renders only when status==='active' && !slackInvited. Hidden for
pending_payment, suspended, cancelled, guest, and any member already
flagged as invited. Lives inside the existing ClientOnly tree at the
top of the dashboard so it never SSRs.

Plain inline text in the welcome region — no banner, no callout. The
2–3 week window is admin-side workflow; the copy avoids cohort/wave
language.
2026-04-29 12:26:51 +01:00
c2999810c6 feat(admin/members): mark-as-Slack-invited button + date display
Replaces the placeholder Slack-invite handler with a call to the new
PATCH /api/admin/members/:id/slack-status endpoint. Status labels are
reworded to match reality (no Slack API call from this app):

- Pending → Not yet invited
- Invited → Invited <slackInvitedAt>
- Action button copy → 'Mark as Slack invited'
- Removes slackInviteStatus reads from the member detail page (the
  remaining repo-wide sweep lands in the cleanup task).
2026-04-29 12:25:18 +01:00
0981596ea2 feat(admin): PATCH /api/admin/members/:id/slack-status
Endpoint that flips a member's slackInvited flag manually after the
admin has actually sent the Slack invitation through Slack's UI. No
Slack API call is made from this app.

- Body validated via Zod literal-true schema (no undo path for the
  pilot — admins correct mistakes in the database if needed).
- Idempotent: re-marking an already-invited member is a no-op,
  preserving the original slackInvitedAt and not duplicating the
  activity log entry.
- Activity log: slack_invited_manually, actor = admin from
  requireAdmin, subject = the target member.
2026-04-29 12:23:07 +01:00
55029e7eb7 feat(activation): wire autoFlagPreExistingSlackAccess into self-serve paths
Replaces the per-file inviteToSlack helpers with a single auto-flag
call. Self-serve activation paths now check for pre-existing workspace
membership (silent on miss) instead of attempting an admin-only invite.

- helcim/subscription.post.js: removed local inviteToSlack; both
  free- and paid-tier activation branches now call the helper, then
  notifyNewMember with the canonical 'manual_invitation_required' arg.
- members/create.post.js: same shape — helper + canonical notify arg.
- invite/accept.post.js (free-tier branch): added the helper call after
  member creation. Free-tier had no prior Slack call (audit confirmed);
  paid-tier remains untouched and activates via the Helcim webhook.

Admin-created and CSV-imported members intentionally do NOT call the
helper — admins flip the flag manually after sending the invite.

Test stub for autoFlagPreExistingSlackAccess added to server setup.
2026-04-29 12:21:12 +01:00
b1d8cb1966 feat(slack): autoFlagPreExistingSlackAccess helper
Best-effort lookup of an activating member's email in the Slack
workspace. On a hit, flips slackInvited:true and stamps slackInvitedAt
without sending a fresh invite. Races against a 3s timeout and swallows
all errors so activation never blocks on Slack.

- Promotes SlackService.findUserByEmail from private to public so the
  helper can call it without a wrapper.
- New activity-log action: slack_access_auto_detected (actor = subject).
- Idempotent: short-circuits when slackInvited is already true.

Callers wired in next commit.
2026-04-29 12:13:59 +01:00
2f6a92ac61 refactor(member): replace slackInviteStatus with slackInvitedAt
Schema change for wave-based Slack onboarding. The string enum
slackInviteStatus is replaced with a simple slackInvitedAt: Date —
boolean slackInvited is the source of truth, the date records when.

Call sites that flip slackInvited:true must stamp slackInvitedAt
in the same update (no pre-save hook, per findByIdAndUpdate convention).

Sweeps of remaining slackInviteStatus references land in later tasks.
2026-04-29 12:11:25 +01:00
3c49317437 chore: remove dead TierPicker + strike resolved gotchas
Some checks failed
Test / vitest (push) Successful in 10m57s
Test / playwright (push) Failing after 9m32s
Test / visual (push) Failing after 9m16s
Test / Notify on failure (push) Successful in 2s
TierPicker.vue is a 5-tier preset picker from before the arbitrary-
amount contribution redesign. Zero imports across app/ and server/ —
purely dead code (99 lines).

Strike two LAUNCH_READINESS bullets that describe already-fixed
issues: the "stale tier comment" in useMemberPayment.js (no `tier`
references remain in that file), and the SeriesPassPurchase auto-
refresh gotcha (fetchPassInfo() already runs after the success path
at line 318).
2026-04-27 21:07:17 +01:00
be24ae32fb fix(toast): rename Nuxt UI 4 toast.add timeout → duration
Some checks failed
Test / vitest (push) Successful in 11m3s
Test / playwright (push) Failing after 9m29s
Test / visual (push) Failing after 9m26s
Test / Notify on failure (push) Successful in 3s
Nuxt UI 4's Toast component reads `duration` (default 5000ms), not
`timeout` — the property was silently ignored. Behavior unchanged
since 5000ms matched the default. Fix the call site to be honest.

Strike the now-resolved gotcha from LAUNCH_READINESS.md.
2026-04-27 19:50:38 +01:00
cf59931814 fix(helcim): read dateBilling on subscription CREATE to populate next-billing cache
Some checks failed
Test / playwright (push) Blocked by required conditions
Test / Notify on failure (push) Blocked by required conditions
Test / visual (push) Blocked by required conditions
Test / vitest (push) Has been cancelled
Helcim returns next-charge as `dateBilling` on POST /subscriptions, but the
two CREATE sites were reading `subscription.nextBillingDate`, leaving
`member.nextBillingDate` empty after every signup and free→paid upgrade.
The lazy refresh in subscription.get.js (which already accepts both shapes)
masked it on next account-page load, so renders eventually populated — but
the success response we returned to the client also had `nextBillingDate:
undefined`. Mirror the GET-side resolution at both CREATE sites: prefer
`dateBilling`, fall back to `nextBillingDate`. Existing Number.isNaN guard
unchanged; defensively rejects malformed strings from either field.
2026-04-27 19:44:35 +01:00
3c38333dd1 fix(reconcile): pass customerCode (not helcimCustomerId) to Helcim transactions API
Some checks failed
Test / vitest (push) Successful in 11m5s
Test / playwright (push) Has been cancelled
Test / Notify on failure (push) Blocked by required conditions
Test / visual (push) Blocked by required conditions
2026-04-27 19:31:59 +01:00
4d44e7045c refactor(rate-limit): delegate auth limiting to handlers, add dev bypass
Some checks failed
Test / playwright (push) Blocked by required conditions
Test / Notify on failure (push) Blocked by required conditions
Test / visual (push) Blocked by required conditions
Test / vitest (push) Has been cancelled
Main's middleware-level auth limiter (5 req / 5 min, IP-only) duplicated
the handler-level limiter introduced earlier on this branch (5/hr IP +
3/hr per-email, blocks email enumeration across IPs). Drop the
middleware version and let the handlers own it.

Added ALLOW_DEV_TEST_ENDPOINTS bypass to the rateLimit utility so
parallel E2E runs from 127.0.0.1 don't exhaust per-IP/email budgets,
mirroring the existing middleware bypass.

Trimmed the obsolete middleware auth test; handler-level coverage lives
in tests/server/api/auth-{login,verify}.test.js. Switched IP-isolation
test to the payment path so it still exercises the limiter.
2026-04-27 19:18:34 +01:00
c1367ebd29 refactor(helcim): collapse redundant Member queries in subscription.post.js 2026-04-27 19:16:32 +01:00
ac5e979c78 feat(payments): persist helcimCustomerCode + skip getOrCreateCustomer on card-on-file 2026-04-27 19:16:32 +01:00
0a41b30db7 refactor(helcim): normalize listHelcimCustomerCards return shape 2026-04-27 19:16:32 +01:00