In-app notification center (Phase 5, migration 051):
- notifications.read_at + partial unread index. /me Notifications is now
an inbox: unread styling, click to expand the message + mark read,
"mark all as read", unread count. markNotificationsRead is a service-
client write scoped to the caller's own member_id. Always-on channel:
email-muted ('skipped') notifications still appear in-app.
Consolidate /profile into /me:
- Delete app/(app)/profile/*; /me is the single member page. Port the
conflict-of-interest privileged-role nudge + last-disclosed date into
/me's profile editor. Remove the "My profile" sidebar + command-palette
entries; rename the /me nav label to "My membership". Repoint the
recruitment empty-state link to /me.
Theme realign:
- Revert the rounded-lg cards introduced in the prior /me visual pass to
rounded (the app norm, matching the former /profile). Route /me section
headers through the SectionTitle primitive.
Regression guard:
- integration/auth-embed.test.ts runs the auth layout's
space_members.select('*, spaces(*)') embed through PostgREST and fails
on PGRST201, so a future junction table cannot silently break auth again.
Notification preferences (migration 048):
- Per-member opt-out of muteable categories (bookings, classes, forms,
admin alerts); billing is always-on. Opt-out model, default on.
- Dispatcher reads prefs and marks muted outbox rows 'skipped' instead
of sending; pure isMuted logic, fail-open on a prefs lookup error.
- notification_preferences table (default-deny RLS); /me toggles and
self-view via validated service-client actions.
Alternate dues payment methods (migration 049):
- Admin sets external pay-here URLs per platform (PayPal/Zeffy/Venmo)
in /settings; members see active ones on /me and pay off-platform;
the treasurer reconciles manually (platform pre-tagged). https-only.
- dues_payment_methods table (SELECT space member, write admin/board).
/me visual pass:
- Restore the chip/preset profile editor (skills/interests/willing-to/
affiliations); promote ChipInput and presets to shared modules.
- Gate the Stripe pay-dues button on whether the space has Stripe
configured; show external links or a contact-admin note otherwise.
Independent adversarial audit verdict was NOT SAFE; fixed:
- P0-1 /auth/confirm no longer trusts the query `type` (pinned to
'email_change'); a public endpoint feeding an attacker-chosen OTP
type into verifyOtp was an unintended auth entrypoint.
- P0-2 email sync uses the verifyOtp response user, not a follow-up
getUser() that may not reflect the just-verified identity; /me now
reconciles space_members.email against the authoritative auth email
on load, so a missed sync self-heals (no permanent stale email).
- P1-1 requestEmailChange builds the confirm origin from
NEXT_PUBLIC_APP_URL only, never request headers (forged Host could
redirect the confirmation link / capture token_hash).
- P2-1 isSecretConfigField also catches password/token/private_key/
*_token/*_password so non-conventionally-named credentials can't be
stored in plaintext integrations.config.
521 tests; build clean.
New getMyPayments: payments SELECT is treasurer-scoped (RLS), so the
self-view uses the validated service client strictly scoped to the
caller's space_id + member_id (same convention as getMyBilling /
getMyNotifications). Read-only list (amount, date, platform, status)
in the Membership tab. Docs updated. 521 tests; build clean.
Wires existing updateMyProfile + discloseAffiliations into an editable
form (name, handle, phone, bio, skills, interests, willing-to,
affiliations). Affiliations re-disclosed only when changed so the COI
timestamp is not reset on every save. Added bio to the updateMyProfile
param type (schema + column already supported it). 521 tests; build
clean (UI not browser-tested from here).
/me restructured into a 3-tab portal (Tabs primitive): Profile
(permissions), Membership (dues + certifications), Activity
(notifications, classes, reservations, cards, visits). Server page
keeps all data fetching; sections moved verbatim into a client portal
component. No behavior change; sets the frame for P3b-e. 521 tests.
Dashboard right column gets a 'Who's here' panel: present members with
Host badges + your own Check in / Check in as host / Check out with an
optional note (router.refresh after). New /attendance page (all members)
lists recent visits with status + duration + notes. /me gains a 'My
recent visits' section. Sidebar 'Attendance' link under People.
Migration 034 adds member_cards (RFID/NFC UID -> member). The card UID
is treated as a credential: door.manage-only RLS with no member SELECT
policy; the member's own "My access cards" view goes through a service
-client action that returns only count + last4 (never the raw UID).
New door.manage / door.operate permissions (group Access), seeded to
board and backfilled, introduced now for the whole Door epic. Pure
door-logic masking is unit-tested. A per-member "Cards" panel on
/members is gated to door.manage (independent of admin). No controller
calls and no anonymous path in this phase. Mirrored in schema.sql; types
and schema docs updated.
/me gains a "My equipment reservations" section (the member's own
reservations with status and window). ARCHITECTURE documents the
equipment module; HANDOFF pass-23 records Classes deployed and the
code-complete-but-local equipment module.
/me gains a "My classes" section (the member's own signups with status
and attendance). ARCHITECTURE documents the classes module; HANDOFF
pass-22 records the code-complete-but-local module and the reaffirmed
roadmap.
/me shows the signed-in member their own certifications (status badges
from the shared logic) and a read-only view of their effective
permissions. Sidebar gets a "My access" link. ARCHITECTURE documents the
module; HANDOFF pass-21 records the UX fixes (deployed) and the
code-complete-but-local certifications module.