Skip to content

feat: full frontend redesign + applicant portal#7

Open
GravityDarkLab wants to merge 86 commits into
mainfrom
worktree-feature-frontend-redesign
Open

feat: full frontend redesign + applicant portal#7
GravityDarkLab wants to merge 86 commits into
mainfrom
worktree-feature-frontend-redesign

Conversation

@GravityDarkLab

@GravityDarkLab GravityDarkLab commented Jun 9, 2026

Copy link
Copy Markdown
Owner

Summary

Admin panel & applicant portal redesign

  • Admin panel redesign — collapsible sidebar, new topbar with role badge, all 7 admin pages rebuilt (Login, Dashboard, Applicants, ApplicantDetail, Matching, Matches, AuditLogs)
  • Applicant portal — brand-new self-service portal (ProfileLoginPage, MatchCard, MatchList, ProfileDashboard, ProfileSettingsDrawer) with magic-link auth and an HttpOnly session cookie
  • Design system update — accent palette shifted from terracotta to warm gold (#C9A96E), new CSS design tokens in index.css
  • Success page rework — shows copyable magic link instead of plaintext password
  • New API clientapi/profile.client.ts covering all portal endpoints (login, password, profile, matches, contact, outcomes, deactivation)
  • Living landing page — beating heart / bloodstream canvas backdrop extended to the apply wizard, with realistic Windkessel-flow hemodynamics
  • Internationalisation — applicant portal and admin panel fully translated (en/de/fr/ar)

Exclusive contact flow

  • Contacting a match expires the initiator's other proposed/in-progress matches (the target's options stay open until they respond); new withdraw endpoint; expired pairs can be revived
  • Threshold slider, contact confirm dialog with Instagram reveal, next-phase state in the portal
  • Admin-triggered matching runs promote applicants from appliedmatched

Admin recovery & data-lifecycle fixes (manual QA follow-ups)

  • Magic-link recovery — a super_admin can regenerate an applicant's magic link from ApplicantDetail (audit-logged as REGENERATE_MAGIC_LINK). The applicant's password is cleared so the new link takes them through first-login set-password again. The raw token is shown once with a copy button.
  • Soft-delete with grace period — admin "delete" now sets deletionScheduledAt (180-day grace period) instead of just deactivating. A new "Scheduled for deletion" tab on the Applicants page shows pending deletions with their deletion date; the default "All" tab excludes them.
  • Re-matching exclusion — applicants with an active (in_progress) contact are excluded from new proposals/candidates, so a contacted applicant doesn't get fresh matches mid-conversation.
  • Match score breakdown — proposals persist their per-dimension score breakdown; the portal MatchCard is expandable to show a "why this match" panel with labeled bars.
  • Password reveal on first login — "Suggest one for me" now reveals the suggested password with a copy button instead of silently dropping it into the masked field.
  • CORS / toolingALLOWED_ORIGINS accepts comma- or semicolon-separated values (trimmed, trailing slashes stripped); pinned Node 22-24 LTS for frontend tooling via .nvmrc/.node-version/engines.

Test Plan

  • bun run test — 344 API tests + 183 frontend tests pass
  • bun run typecheck — clean on both workspaces
  • Admin login → dashboard → applicants → detail → matching → matches → audit logs
  • Sidebar collapse/expand toggle
  • /success?alias=X&token=Y shows magic link, copy + download work
  • /profile?token=abc → first-login set-password flow
  • /profile with session cookie → dashboard with match list
  • ProfileSettingsDrawer: change password, deactivate account
  • ApplicantDetail (super_admin) → "Regenerate magic link" → confirm dialog → new link revealed + copy works → audit log shows REGENERATE_MAGIC_LINK
  • New magic link logs in with firstLogin: true (password reset confirmed)
  • Applicants → "Scheduled for deletion" tab shows correct deletion date; "All" tab excludes scheduled deletions
  • Portal match card → expand → score breakdown bars render with labels
  • Contact a match, then re-run admin matching — contacted applicant receives no new proposals while in_progress
  • Portal first-login → "Suggest one for me" → password shown with working copy button

- ApplicantStatus: applied/matched/dating/inactive (was active/inactive/matched/withdrawn)
- MatchStatus: proposed/in_progress/dating/success/failed/declined/expired (was proposed/contacted/matched/failed)
- Add deleteApplicant and deleteMatch aliases in API client
- Expose role in AuthContext and AuthState
- Update all pages and tests to use new status values
Replace the password-based success page with a magic link display.
The API client now types magicToken in the submit response; Apply.tsx
passes it as a URL param; Success.tsx shows the profile link with
copy-to-clipboard and .txt download actions and a one-time-view warning.
All four locale files updated with the new i18n keys.
…identity

- Two-column layout (left: profile card, right: status stepper + match history)
- Status stepper with completed/current/future visual states
- Role-gated identity reveal: super_admin sees reveal button, admin sees locked card
- Add useOptionalAuth to AuthContext for components that run outside AuthProvider
- Match history card only rendered when matches exist
- Reset matches state on navigation to prevent stale data between applicants
Comment thread frontend/src/pages/profile/ProfileLoginPage.tsx Fixed
…ocalStorage

Bearer JWT storage in localStorage is a conscious design decision for the
applicant portal (vs. HttpOnly cookies used by the admin panel). Mark both
setPassword and profileLogin storage sites with lgtm suppression comments.
Comment thread frontend/src/pages/profile/ProfileLoginPage.tsx Fixed
@GravityDarkLab GravityDarkLab self-assigned this Jun 9, 2026
@GravityDarkLab GravityDarkLab added the enhancement New feature or request label Jun 9, 2026
@GravityDarkLab GravityDarkLab changed the title feat: full frontend redesign + applicant portal (PR2) feat: full frontend redesign + applicant portal Jun 9, 2026
…pOnly session cookie

- Backend: setPassword and login now call setCookie() with httpOnly:true,
  sameSite:Lax, secure in production; no token returned in response body
- Backend: deactivate clears the session cookie via deleteCookie()
- Backend: requireApplicant reads ons_applicant_session cookie first,
  falls back to Authorization header for API clients
- Frontend: profile.client.ts uses credentials:'include'; all localStorage
  usage removed
- Frontend: ProfileLoginPage and ProfileDashboard no longer touch localStorage
- Tests: ProfileDashboard beforeEach no longer needs a fake JWT in storage

Fixes CodeQL alert js/clear-text-storage-sensitive-data (CWE-312).
login and set-password now return no token in the body; tests verify the
ons_applicant_session Set-Cookie header is present instead.
…ion cookie

- Add ApplicantCookieAuth security scheme (ons_applicant_session cookie)
- Keep ApplicantBearerAuth as fallback scheme for API clients
- All profile endpoints list cookie auth first, bearer as fallback
- login/set-password 200 responses: remove token field, add Set-Cookie header
- ProfileLoginResponse schema: remove token property
- deactivate: note that session cookie is cleared
- Update top-level auth description to reflect cookie-first approach
… page URL

- Add X-Submission-Key to CORS allowHeaders so browser form submission is not blocked by preflight
- Add default empty-string values for all string fields in Apply.tsx to prevent uncontrolled→controlled React warnings and Zod validation failures
- Fix Success page magic link URL from /profile?token=... to /profile/login?token=... so the token is not lost on redirect
profileRequest now returns the parsed body instead of unwrapping
body.data, since not all endpoints follow that shape. Only treat 401s
from the auth middleware itself ("Unauthorized" / "Invalid or expired
token") as a session-expired event — other 401s (e.g. wrong current
password) are business-logic errors and should surface as such.
Rows had a varying number of action buttons, so flex-wrap pushed extra
buttons onto a second line and made row heights inconsistent. The
actions container now stays on a single row and scrolls horizontally
when it overflows the column's max width.
The footer hardcoded 2025. Interpolate the year from Date.now() so it
stays correct without future edits.
The run card now opens with an animated band in the style of the public
pages' LifeBackground: two gold particle streams flow in from either
side and converge on a beating heart. While a run is in flight the
heart rate and flow surge, sparks fly where the streams meet, and a
cycling status line narrates the phases; on completion the heart pops
with expanding rings and a spray of sparks, and the result card enters
with a sprung check mark and staggered stat reveals. All motion honors
prefers-reduced-motion and pauses while the tab is hidden.
The run CTA was the stark black primary button and the labels read as
generic grey. The CTA now uses the public pages' cta-gold gradient
pill, the algorithm label gets the uppercase tracked treatment used
elsewhere, and the last-run line moves into a bordered footer row with
a small clock icon.
The native select was the one control the theme couldn't reach. The
three algorithms are now selectable cards with the hint inline, a gold
Recommended badge, and a selection dot — keyboard-accessible via a
visually hidden radio group.
Sign out floated as a lone small pill between two dividers while its
neighbors both had headings. It now gets a Session heading, a short
note, an icon, and a full-width button — same rhythm as the other
sections, while staying separated from the destructive deactivate
zone below.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 117 out of 118 changed files in this pull request and generated 9 comments.

Comment thread frontend/src/admin/pages/AuditLogs.tsx
Comment thread frontend/src/admin/pages/AuditLogs.tsx
Comment thread frontend/src/api/profile.client.ts Outdated
Comment thread frontend/src/components/ui/Toast.tsx
Comment thread frontend/src/pages/Success.tsx
Comment thread frontend/src/pages/Success.tsx
Comment thread frontend/src/pages/Success.tsx
Comment thread api/src/config/env.ts
Comment thread frontend/src/components/ui/Toast.tsx
…-option warning

The @config directive for the legacy JS config triggered Tailwind's
internal PostCSS reprocessing without a from path, which Vite's dev
server warned about on every CSS request. Moving the theme overrides
into @theme/@Keyframes in index.css removes the @config directive
(and the now-unused autoprefixer, which Tailwind v4 already covers).
The portal matches endpoint now batch-loads each partner's public
answers and attaches them as partnerProfile on the applicant match
view. Consent checkboxes are filtered out, and instagram_handle is
excluded as defense in depth (identity is stored encrypted in a
separate collection and never reaches the answers field).

The MatchCard expand toggle now also opens an "About {alias}"
section rendering the raw answers, shown above the score breakdown
on proposed, in-progress (initiator) and dating cards.
…e accepting

The target of a contact request now sees the initiator's Instagram
handle, the partner profile and the score breakdown on the
"wants to meet you" card before accepting or declining. The handle
is attached as partnerInstagram on the match view for in_progress
and dating matches only (never while proposed), with an audit log
entry per decryption — consistent with the existing design where the
initiator already sees the target's handle at contact time.

This also fixes initiator/dating cards losing the revealed handle on
page reload, since the handle previously lived only in session state
from the contact response.
…longer clips it

The desktop table wrapper uses overflow-hidden for its rounded corners,
which cut off the status menu on the last rows. The menu now renders via
createPortal with fixed positioning from the trigger's bounding rect, and
closes on outside click, scroll, or resize.
…y layer

- revealIdentityById() in identity.service decrypts and writes the audit
  log in one place; raw resolveIdentityById is documented as repeat-view
  only, and identityExistsById allows pre-flight checks without decrypting
- admin getApplicantIdentity logs RESOLVE_IDENTITY via the central
  function instead of the controller hand-rolling it
- drop the LIST_APPLICANTS audit action: listing applicants exposes no
  sensitive data and flooded the log
- MatchDoc.identityViewLoggedFor tracks which applicants already had a
  reveal logged so repeat matches-page loads don't write duplicates
…ile tab

API:
- GET/PUT /api/v1/profile/answers — same Zod field rules as submission,
  derived from the submit schema with instagram_handle and
  disclaimer_agreed omitted; .strict() rejects them and unknown keys (422)
- updateMyAnswers merges over stored answers so non-editable keys survive,
  and refreshes embeddings in the background like a fresh submission
- profile.service also adopts the centralized revealIdentityById and the
  once-per-match audit dedup for matches-page reveals

Frontend:
- Matches | My profile tab bar on the portal dashboard (hidden for
  inactive accounts)
- EditProfileForm reuses the apply-wizard step components (Step2-4) plus
  the final slider/date fields as stacked cards, with a locked Instagram
  row pointing applicants to the admins and a sticky save bar gated on
  dirty state
- i18n keys for en/de/fr/ar
… edits

Applicants now enter a date of birth instead of a raw age, with age
derived consistently on both API and frontend via a shared helper.
birth_date and gender_identity are shown read-only in the profile
editor and rejected by the answers-update API — only an admin can
change them. Partners continue to see a derived age, never the exact
birth date. Also fixes the floating save bar hiding the Save button
from assistive tech when the form is clean.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 139 out of 140 changed files in this pull request and generated 6 comments.

Comment thread tests/smoke/portal.smoke.ts
Comment thread tests/smoke/match-flow.smoke.ts
Comment thread frontend/src/pages/profile/DeletionCountdown.tsx
Comment thread tests/smoke/match-flow.smoke.ts
Comment thread frontend/src/App.tsx Outdated
Comment thread frontend/src/api/profile.client.ts
- Smoke test payload helpers now send birth_date instead of the
  removed age field, so /form/submit no longer 422s
- injectBCMatch fails fast with a clear error if setup didn't
  populate applicant B/C IDs, instead of a non-null-assertion crash
- DeletionCountdown resets cancelLoading in a finally and closes the
  confirm dialog on a successful cancel
- Fix stale "Bearer JWT auth" comment on the applicant portal routes
  (it's an HttpOnly session cookie)

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 139 out of 140 changed files in this pull request and generated 2 comments.

Comment thread package.json
Comment thread frontend/package.json
Update the three top-level READMEs to reflect the current app: the
/profile applicant portal (session-cookie auth, edit answers, mutual
identity reveal), admin match management endpoints, i18n, and the
birth_date question (replacing the old age field in the form-steps
table). Also fixes a stale frontend dev port and test count.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants