Skip to content

feat: full frontend redesign + applicant portal#7

Merged
GravityDarkLab merged 95 commits into
mainfrom
worktree-feature-frontend-redesign
Jun 17, 2026
Merged

feat: full frontend redesign + applicant portal#7
GravityDarkLab merged 95 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.
…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.
…rithms

cosine and embedding-cosine duplicated the same REL_TYPE_ENCODING table,
type coercion helpers (str/bool/num), vector builder, and cosine
similarity math. Move the shared pieces into
matching/scorers/numeric.scorer.ts so both algorithms import one
implementation instead of two copies.
admin and applicant auth middleware each reimplemented JWT signing,
verification, expiry-to-seconds conversion, and bearer/cookie token
extraction. Move the shared logic into middleware/jwt.util.ts and have
both middlewares consume it. applicant.auth.middleware now also exports
APPLICANT_COOKIE_MAX_AGE so the profile controller can stop maintaining
its own copy of the cookie lifetime.
…alidation

- Split match.service.ts's status-transition/match-view kernel into a new
  match-state.service.ts (toMatchView, assertMatchTransition,
  expireConflictingMatches, transitionApplicantStatus,
  applyMatchStatusSideEffects, DELETION_GRACE_MS), shared by
  match.service, admin.service, profile.service, and the matching job.
- Add errorResponse() (utils/error-response.ts) and a ValidatedContext
  type alias (utils/validated-context.ts), and use them across the
  admin/matching/form/match/profile controllers — replacing ad-hoc
  `(err as {statusCode?:number})` casts, the local matchErrorResponse
  helper, and `c.req.valid('json' as never) as T` double-casts.
- Move the inline patch-match schema into validators/match.validator.ts
  and export input types from the admin/profile validators for the new
  ValidatedContext handlers.
- Convert remaining plain Error throws (engine.ts, form.service.ts,
  admin.service.ts) to AppError with correct status codes, and extract
  escapeRegex (utils/regex.ts) shared by the admin/match search filters.
- Fix requestContact to reveal the partner's identity before expiring the
  actor's other matches, rolling the claim back to "proposed" if the
  reveal fails instead of leaving it stuck in_progress.
- Fix cancelAccountDeletion to restore "dating" status when the applicant
  still has a dating match, instead of always "applied".
- Fix reportOutcome to also expire the partner's other proposed/in_progress
  matches on a successful outcome.
- Rename AuditContext.adminId to actorId since the field also carries
  applicant ids for self-service actions.
- Update tests for the new module boundaries and AppError status codes.
The mutual-reveal description still described the old flow where both
sides had to accept before handles were decrypted. Update it to match
the current behavior: the initiator sees the partner's handle
immediately on contact request, and the partner sees it as soon as
they view that match.
Both admin/types.ts and api/profile.client.ts declared their own copies
of ApplicantStatus and MatchStatus. Move them to types/status.ts and
re-export from both call sites.
…e UI

- Add missing translation keys (admin nav/login/dashboard/applicants,
  portal dashboard/success, common.dismiss) across en/fr/ar/de and
  replace the remaining hardcoded English strings in AdminLayout, Toast,
  Dashboard, ApplicantDetail, Applicants, Success, and ProfileDashboard.
- Move useTimeAgo from admin/utils into lib/ so both the admin panel and
  the profile portal can use it.
- Extract shared admin table primitives (Th, PageButton, TrashIcon) into
  admin/components/Table.tsx, and a shared useStatusLabels hook used by
  both Matches and Dashboard.
- Extract AuthPageShell for ProfileLoginPage's four near-identical layout
  wrappers, and replace duplicated spinner SVGs with the shared Spinner
  component in InviteGate and MatchCard.
- Switch Applicants to deactivateApplicant and drop the dead
  deleteApplicant/deleteMatch aliases from the admin API client.
- Fix InviteGate's hardcoded hover color and Textarea's ml-1 (now ms-1)
  so it respects RTL layouts.
- Stop passing the magic token through the /success and /profile/login
  URLs — pass it via router state instead.
…e fixture

- Add unit coverage for transitionApplicantStatus and
  applyMatchStatusSideEffects (dating/success/failed/no-op branches),
  closing most of the match-state.service.ts gap.
- Rework the accept->success leg of match-flow.smoke.ts to use a 4th
  applicant (D) instead of reusing A, since contacting B in the A-B
  flow now expires A's other proposed matches via the exclusive-contact
  side effect.
@GravityDarkLab GravityDarkLab deleted the worktree-feature-frontend-redesign branch June 14, 2026 17:46
@GravityDarkLab GravityDarkLab changed the title feat: full frontend redesign + applicant portal feat: applicant portal frontend, admin redesign & API consolidation (PR2) Jun 14, 2026
@GravityDarkLab GravityDarkLab restored the worktree-feature-frontend-redesign branch June 14, 2026 17:57
@GravityDarkLab GravityDarkLab changed the title feat: applicant portal frontend, admin redesign & API consolidation (PR2) feat: full frontend redesign + applicant portal Jun 14, 2026
…entions

Adds a Product section (elevator pitch for new sessions), Error handling &
status transitions, Design system (frontend) — covering the token system,
dark mode, brand identity, shared UI primitives, and i18n/RTL rules — and
a Conventions section with Conventional Commits style and the no-Co-Authored-By
rule. Also fills in the Applicant portal bullet and the magicToken/plainPassword
contract note that were missing from the Frontend section.
CORS middleware in 4.12.23 reflects any Origin with credentials when
the origin option is unset. Fixed in 4.12.25. Regenerates standalone
api/bun.lock via bun run lockfiles.
@GravityDarkLab GravityDarkLab merged commit e76ce7a into main Jun 17, 2026
12 checks passed
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