Skip to content

M2 worlds frontend integration kareem - #16

Closed
KareemN1936 wants to merge 10 commits into
developfrom
m2-worlds-frontend-integration-kareem
Closed

KareemN1936 wants to merge 10 commits into
developfrom
m2-worlds-frontend-integration-kareem

Conversation

@KareemN1936

Copy link
Copy Markdown
Collaborator

No description provided.

@github-actions

github-actions Bot commented Aug 5, 2026 •

Copy link
Copy Markdown

Vercel preview is ready:

https://hivelore-e8ivjofcb-kareemn1936s-projects.vercel.app

Commit: ac026e2

@KareemN1936
KareemN1936 changed the base branch from main to develop August 5, 2026 22:20
@mcfarhat

mcfarhat commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

@KareemN1936 quick coordination note — you have two open PRs changing the same three worlds pages in incompatible ways, and they conflict with each other.

This one (#16) is the right approach. You correctly branched off Perla's #14 branch rather than duplicating her backend (I verified the 7 shared files are identical blobs — good practice), and you wired the pages to the real API via lib/api/worlds.ts.

#17 replaces those same three pages with mock-worlds.ts fixtures — hardcoded data, and worlds/new has no submit path at all. Merging #17 after this one would overwrite your live API integration with mock data. Confirmed conflict on all three files.

Plan: please consolidate into ONE PR — take #17's styling/responsive layout and put it on top of #16's API wiring — then close the other. Keep this one as the base.

Sequencing: #15 (auth hardening) is merged, and Perla is rebasing #14 with review fixes. Once #14 lands, rebase this onto develop and it should shrink to just your 7 frontend files.

Two things to fix regardless of which PR survives:

  1. layout.tsx deletes the pre-paint theme script and theme-provider moved from useSyncExternalStore to useState/useEffect, so the theme now resolves only after hydration — dark-mode users get a white flash on every page load, and cross-tab/OS theme sync is dropped. If the goal was removing dangerouslySetInnerHTML, use a nonce'd <Script strategy="beforeInteractive"> instead of deleting the pre-paint step.
  2. lib/api/session.ts stores the access token in localStorage, which is XSS-readable. Worth aligning with the httpOnly-cookie pattern the refresh token already uses — let's discuss before you change it.

The home/login page in #17 does use the real auth API properly, so I know you can wire this — just needs to be consistent.

@mcfarhat mcfarhat mentioned this pull request Aug 6, 2026
@KareemN1936
KareemN1936 force-pushed the m2-worlds-frontend-integration-kareem branch from 0ce14f6 to d44a50a Compare August 7, 2026 12:01
@KareemN1936

Copy link
Copy Markdown
Collaborator Author

@mcfarhat Got it. I closed #17 and will keep #16 as the base. I’ll only port any useful responsive styling manually without reintroducing mock-worlds or replacing the real API wiring. I’ll wait for #14 to land before rebasing onto develop. I’ll also leave the localStorage access-token issue for discussion before changing auth storage.

@mcfarhat

mcfarhat commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

@KareemN1936 thanks for closing #17 and keeping this as the base — you did exactly what you said you would, and I checked rather than assumed: zero mock-worlds/fixture references anywhere in apps/web/src, all three pages call the real lib/api/worlds.ts, and worlds/new now has a genuine submit path (handleSubmit → createWorld → redirect) which #17 didn't. You also did not port the theme regression — layout.tsx and theme-provider.tsx are byte-identical to develop, so the pre-paint script and cross-tab sync are intact. Good.

And the auth fix is excellent. You changed authOptions() to take database, widened the route option types, and added the missing await at routes/worlds.ts:137 — so the silent 401 bypass on world creation is properly gone. You also fixed all six requireSession(authOptions(database)) sites in contributions.ts, which I hadn't explicitly named. Zero remaining call sites missing database or await. That was the single most important thing on this branch and you nailed it.

🔴 One blocker, and it's the same failure mode we discussed:

Your styling commit d44a50a (12:01) overwrote your own API wiring from a0ab185 (12:00), one minute earlier. It pasted #17's app/page.tsx over the current file, deleting the import plus all five storeAccessToken(session.accessToken) / clearStoredAccessToken() calls.

Net effect: storeAccessToken and clearStoredAccessToken in lib/api/session.ts now have no callers anywhere in the repo, so getStoredAccessToken() at worlds/new/page.tsx:72 always returns null, and every world-creation submit fails at :74-77 with "Please sign in before creating a world" — including for fully authenticated users. The POST /worlds endpoint you just fixed is unreachable from the UI. CI can't catch it: the symbols are exported so lint stays quiet, and nothing tests the flow.

Suggested fix that also closes an earlier note: refreshAuthSession() already works off the httpOnly cookie (client.ts:69 sends credentials: 'include'), so hold the access token in React state/context rather than restoring the localStorage writes. That fixes the wiring and removes the XSS-readable token in one move.

Also to fix before merge:

  • Two sort options are fabrications. worlds/page.tsx:49-51 "Highest reputation" sorts alphabetically by founder display name; :53-56 "Most canon entries" sorts by bible versionNumber, not canon count (the list endpoint doesn't return one). Both ship to users as working controls — either implement them properly or remove them.
  • worlds/new has no auth gate — the page renders fully when signed out and you only discover you're not authenticated after filling ~10 fields. Also :358-363 keeps a "Sign in" link beside the submit button even when signed in.
  • Placeholder buttons styled as live controls — worlds/[worldId]/page.tsx:101-102 ("View Timeline", "View Map") and :225 ("Ask AI Guide") have no href, no handler, and aren't disabled.

Nits: worlds.ts:137 re-runs authenticateRequest even though the requireSession preHandler already authenticated and set request.user — an extra DB round-trip per creation; read request.user like contributions.ts does. The rewritten theme-switcher.tsx dropped the "System" option (2-state toggle now), so one click pins a user to light/dark permanently with no way back to OS sync. Search/filter is client-side over a hardcoded page=1&pageSize=100, so your own server-side q/genre/tone params are never used and it breaks past 100 worlds; :224 also prints "Showing X of Y" using the fetched page length instead of pagination.total.

One for Perla, not you — I'll raise it on #14: lib/worlds.ts:54-59 returns the latest bible version with no publishedAt filter, so an unpublished world bible is exposed via the public GET /worlds and /hub. Your hub page then renders it as "World Codex preview" (worlds/[worldId]/page.tsx:197-208), which is what makes it user-visible. Lore entries are correctly gated to PUBLISHED_CANON — it's only the bible.

Genuinely nice work in here too: lore-atlas-background.tsx is aria-hidden, rAF-throttled, respects prefers-reduced-motion, and cleans up its listener. That's careful.

Sequencing: this now rebases cleanly and carries Perla's backend, but merging it would land #14's unreviewed items by the back door — so it waits for #14, then rebase.

@KareemN1936

Copy link
Copy Markdown
Collaborator Author

@mcfarhat Addressed the remaining review notes in fe6e038.

Changes:

  • Moved access-token handling out of localStorage into an in-memory AuthSessionProvider, backed by refreshAuthSession() via the httpOnly cookie.
  • Removed apps/web/src/lib/api/session.ts.
  • Updated login/session refresh/logout flows to use the provider.
  • Updated /worlds/new to read auth from context, redirect signed-out users, and remove the redundant Sign in link for signed-in users.
  • Updated POST /worlds to use request.user from the requireSession preHandler instead of calling authenticateRequest again.
  • Restored the System theme preference in the theme switcher.
  • Switched worlds browse to server-backed search/filter/pagination using q, genre, page, pageSize, and pagination.total.

Validation:

  • npm run typecheck --workspace=@hivelore/api
  • npm run typecheck --workspace=@hivelore/web
  • npm run lint --workspace=@hivelore/api
  • npm run lint --workspace=@hivelore/web
    -npx tsx --test apps/api/src/routes/worlds.test.ts with test env injected.

@mcfarhat

mcfarhat commented Aug 8, 2026 •

Copy link
Copy Markdown
Contributor

@KareemN1936 every item from the 08-07 review is fixed — and you fixed the big one the better way rather than the quick way. Credit where it's due:

  • World creation works again, and you deleted lib/api/session.ts entirely instead of restoring the localStorage writes. auth-session-provider.tsx holds the token in React state seeded from the httpOnly-cookie refresh — grep localStorage apps/web/src now returns only the theme keys. That removes the XSS-readable token surface as a side effect.
  • Fabricated sorts removed (only newest/most-active, both backed by real date fields) · worlds/new auth-gated with a /login redirect · placeholder buttons now disabled with explanatory titles · redundant authenticateRequest dropped for request.user · "System" restored in the theme switcher · server-side q/genre/pagination with real Previous/Next · "Showing X of Y" now uses pagination.total.

Regression checks all clean: no mocks, layout.tsx/theme-provider.tsx still byte-identical to develop, and all nine database+await auth sites intact.

Four things before merge — two are new:

1. 🔴 The world bible is now permanently invisible on every world. 646ddb3 added publicWorldInclude (lib/worlds.ts:72-88) filtering bibleVersions to publishedAt: { not: null } — but nothing in the codebase ever sets publishedAt. createWorld (:257-264) doesn't, there's no default in schema.prisma:408, and no publish path exists. So every world made through your own /worlds/new returns currentBibleVersion: null from list, get and hub — killing the "Bible v{n}" badge, showing "Not set", and permanently displaying "This world bible does not expose readable rules yet."

Your tests codify this as correct (worlds.test.ts:563,566,568 assert currentBibleVersion === null right after a successful create); the one passing draft-hiding test injects a publishedAt row the app can never produce. Fix: either set publishedAt on the founder's initial bible (and on updateWorld), or gate on membership role instead of publishedAt. Then add a test asserting hub.world.currentBibleVersion.versionNumber === 1 after create.

Good initiative picking this up — it was assigned to Perla on #14 — it just needs the publish path to go with it.

2. 🔴 Unconditional refresh-on-mount can log users out everywhere. auth-session-provider.tsx:59-83 calls refreshAuthSession() on every mount. rotateRefreshSession (lib/auth-sessions.ts:184-187) has strict reuse detection: if a session was already rotated it revokes the whole session family. Two tabs opened together, a browser session-restore, or React StrictMode's dev double-invoke (Next 15 defaults reactStrictMode: true) send the same refresh cookie concurrently → one rotates, the other trips reuse detection → user logged out everywhere. Needs at minimum a module-level in-flight promise so concurrent mounts share one refresh.

3. 🟠 Response shape is inconsistent. createWorld (:290) and updateWorld (:544) still use the unfiltered worldInclude, so POST /worlds returns a bible while an immediate GET of the same world returns null. Use the same include for both.

4. 🟠 Internal roadmap copy shipped as product copy — worlds/[worldId]/page.tsx:265: "Entity CRUD is the next M2 card; this hub is ready to receive those entries." Same class of string I asked you to remove from #18.

Nits: no refresh-on-401, so a user spending >15 min on the 10-field form gets a 401 on submit with no retry; the hand-written 40-line relation type at :88-129 will drift from the schema — keep Prisma.WorldGetPayload<{include: typeof publicWorldInclude}>; search fires a request per keystroke (no debounce); disabled buttons explain themselves via title, which screen readers don't announce — use visible helper text; dead sort branch at worlds/page.tsx:52.

Sequencing unchanged: this is based on develop now, but the diff is still 6,226 insertions across 26 files carrying Perla's entire backend (lib/contributions.ts, lib/worlds.ts, routes/contributions.ts, schema + two migrations). Merging it would land #14's unreviewed backend through the back door, so it still waits for #14.

@KareemN1936

Copy link
Copy Markdown
Collaborator Author

@mcfarhat Pushed fixes in 53fdd04.

Addressed latest PR #16 review items:

  • Founder-created initial world bibles now get publishedAt, so Bible v1 is visible from list/get/hub immediately after world creation.
  • Founder bible edits publish the version if it was not already published.
  • POST /worlds and PATCH /worlds now return the same public bible projection as GET responses.
  • Auth session refresh now uses a module-level in-flight promise so concurrent mounts share one refresh call and do not trigger refresh-token reuse revocation.
  • Removed the internal roadmap copy from the world hub UI.

Validation passed:

  • API typecheck
  • Web typecheck
  • API lint
  • Web lint
  • Web build
  • Focused worlds route tests

@mcfarhat

Copy link
Copy Markdown
Contributor

@KareemN1936 re-reviewed at 53fdd041. All four items from the 08-08 round are addressed in code, and I verified each rather than going by the commit messages. Two are partial, and there's one blocker that isn't in this PR's diff at all but is caused by an earlier commit on this branch.

Confirmed fixed: response shape is now consistent — all four read/write paths use publicWorldInclude. The roadmap copy is corrected. The earlier rounds all still hold: no mocks left, no localStorage token, the theme pre-paint regression has not come back.

Your validation claims check out. typecheck, lint, format:check and build all pass for me too. Worth saying, because that hasn't been true of every claim I've checked this week.

Blocker — this branch's API test suite is red, and it's from your own commit

contributions.test.ts fails on every authenticated route with 401. The cause is 0a9f3404, which changed routes/contributions.ts to requireSession(authOptions(database)) — that path goes through isRefreshSessionActive() → database.refreshSession.findUnique. The fake database in contributions.test.ts has no refreshSession (grep: zero occurrences), so the lookup throws, authenticateRequest swallows it, and every request 401s.

You hit exactly this in worlds.test.ts and fixed it there in 646ddb3 — worlds.test.ts has the stub, contributions.test.ts doesn't. The fix is mirroring that one stub.

It's been red since 08-07 and nobody noticed, because CI runs no test step. One of the dead tests is "valid draft submission creates exactly one proposal and locks immutable proposed content" — which happens to guard the exact property under review in your #20. I've carded the CI gap separately: https://trello.com/c/TM6P5w2s

Partially fixed

1. Bible visibility — the fix is forward-only. lib/worlds.ts:263 sets publishedAt on the founder's v1 and :538 publishes on edit, and the tests now assert currentBibleVersion.versionNumber === 1 from the hub, which is what I asked for. But prisma/seed.ts:148-168 still creates its v1 bible with no publishedAt, and there's no backfill migration. So npm run db:seed then opening the seeded world still shows "Bible version: Not set" and "This world bible does not expose readable rules yet." Same for any world created on preview/staging before this commit. Either set it in the seed and add a backfill, or accept it and say so.

2. Refresh-on-mount — closes one case, not the other. The module-level in-flight promise at auth-session-provider.tsx:31-39 is exactly right for React StrictMode's double invoke. It can't help the case I actually described: module scope is per-JS-context, so two tabs opening together still send the same refresh cookie concurrently and one trips reuse detection. That needs server-side tolerance (a short grace window on the rotated token), not client-side dedupe.

Should-fix

The Sort control is misleading again, for a new reason. worlds/page.tsx:113 sorts the current page client-side, but lib/api/worlds.ts sends no sort param and listWorldsSchema doesn't accept one — the server always orders createdAt desc. With 50 worlds and a page size of 24, "Most active" reorders only the 24 newest; a world updated today but created months ago sits on page 3 and never surfaces. Same class as the fabricated sorts I asked you to remove — it came back when pagination moved server-side. Either add sort to the API or drop the control.

The publishedAt gate is now unreachable. No code path can produce an unpublished bible any more — create publishes, edit publishes. So the draft-hiding filter is inert and the only test covering it injects rows the app can't create. Fine as scaffolding for a future publish workflow, but right now it's buying nothing and it's masking the seed gap above.

Sequencing — this is the one to sort out before merge

git diff origin/develop...53fdd041 --stat is 26 files, 6,241 insertions, and 11 of them are under apps/api — including schema.prisma, two migrations, seed.ts, and lib/worlds.ts / lib/contributions.ts / routes/contributions.ts, which are Perla's work from #14.

Merging this "frontend integration" PR would land a chunk of the backend through the back door, ahead of its own review. Please rebase onto develop once #14 lands and drop the apps/api files from this PR, so it contains only apps/web.

Nits

site-header.tsx:29-34 hardcodes a "Sign in" link and is a server component with no access to useAuthSession, so signed-in users see "Sign in" on every page — the same complaint I made about /worlds/new, which you fixed locally but not globally. clearSession doesn't reset refreshSessionPromise, so signing out during an in-flight refresh re-populates the session afterwards. Changing a filter while on page 3 fires a request with the stale page then another with page 1 (harmless, but two calls per keystroke with no debounce). worldBibleSchema.content accepts any JSON with no size or depth cap — a deeply nested ~1MB payload recurses to a RangeError → 500. key={character} / key={rule} over user-supplied strings will collide on duplicates.

@KareemN1936

Copy link
Copy Markdown
Collaborator Author

@mcfarhat Pushed 7a01bb7.

Addressed the latest review follow-ups:

  • Added the missing refreshSession fake in contributions.test.ts, so the API route tests no longer 401 from the fake DB.
  • Published seeded initial bible rows and added a backfill migration for existing v1 bible rows.
  • Removed the misleading client-side sort from Browse Worlds since sorting was only happening inside the current server page.

Verification:

  • GitHub Quality ✅
  • Vercel Preview ✅
  • Focused worlds/contributions route tests passed locally with test env
  • Web lint/typecheck passed locally

I’ll still rebase/drop inherited API files once #14 lands so this PR becomes frontend-only.

@mcfarhat

Copy link
Copy Markdown
Contributor

@KareemN1936 re-reviewed at 7a01bb7. All three claims verified mechanically — three for three.

1. The API test suite is green. I ran it rather than trusting the claim:

cd apps/api && npx tsx --test "src/**/*.test.ts"
# tests 107  # pass 107  # fail 0

And I checked it's a real fix rather than a test neutered into passing: restoring only 53fdd04's copy of contributions.test.ts reproduces exactly the 8 failures, so your stub is the sole delta. The fake at contributions.test.ts:234-256 mirrors the one in worlds.test.ts, returns a genuine record (expiresAt: 2099, revokedAt: null, userId) so isRefreshSessionActive still evaluates all three of its conditions, and returns null for unknown ids so the unauthenticated and wrong-user paths still fail properly. The assertions at :628-636 and :820 are exercising the routes again for real.

Small gap, not a defect: the stub can't express a revoked or expired session, so route-level revocation isn't covered — auth-middleware.test.ts:73 has it at unit level.

2. Seed and backfill. seed.ts:148 and :168 both set publishedAt, so a re-seed over an existing database publishes too. The migration is correct and safe:

UPDATE "WorldBibleVersion" SET "publishedAt" = COALESCE("publishedAt","createdAt")
WHERE "versionNumber" = 1 AND "publishedAt" IS NULL;

Idempotent thanks to the IS NULL guard, a no-op on a fresh database, and using createdAt rather than now() is the right call. I checked coverage rather than assuming it: there are exactly three writers of WorldBibleVersion in the repo, and because updateWorld mutates v1 in place rather than creating v2, every row in any existing database is versionNumber = 1 — so WHERE versionNumber = 1 genuinely reaches all of them. No risk of publishing something meant to stay hidden, since no draft/publish workflow exists to produce one.

3. The sort control is gone entirely, not re-pointed at a param that doesn't exist — sortLabels, sortWorlds(), the state, the useMemo and the Select block are all deleted, and I confirmed listWorlds and listWorldsSchema now agree exactly on page/pageSize/q/genre/tone. Nothing renders that doesn't work.

Worth one line in the PR description: the effective default order changes from updatedAt (client-side) to server createdAt desc.

No regressions in the fix commit — typecheck, lint and the full API suite are clean.

Still outstanding

The apps/api files are still in this PR — now 12 of them (your migration added one), 3,984 of the 6,226 insertions. You've said you'll drop them once #14 lands, which matches the sequencing I asked for, so this is a deferral rather than a miss. Just flagging that the merge blocker is unchanged: merging today would still land Perla's backend ahead of its own review.

One correction to how you're reading CI: "GitHub Quality ✅" does not mean the tests ran. ci.yml runs format, prisma validate/generate, lint, typecheck and build — there is no test step, and the root package.json has no test script. The suite you just got green isn't enforced by anything, so it can go red again silently, exactly as it did on 08-07. That's carded: https://trello.com/c/TM6P5w2s — and honestly it's the highest-value thing anyone could pick up on this repo right now.

Two dead-code leftovers, cosmetic: the exported WorldSort type now has no consumers, and components/ui/select.tsx has none repo-wide.

Good work — this is the cleanest turnaround of the three.

@KareemN1936

Copy link
Copy Markdown
Collaborator Author

No code changes needed from the latest review. I’ll keep this blocked on the sequencing note: wait for #14 to land, then drop the backend/API pieces before merging this frontend PR.

@mcfarhat

mcfarhat commented Aug 11, 2026 •

Copy link
Copy Markdown
Contributor

Closing this as superseded by #20 — no work is lost, and this isn't a rejection of anything here.

The check: git merge-base --is-ancestor pr16 pr20 → true, and git rev-list --count pr16 ^pr20 → 0. #20 contains every commit and every file change from this branch, and it ends in a merge commit that brought this branch's head in. So everything reviewed and fixed here is already carried by #20.

I've retargeted #20's base from this branch to develop, so #20 is now the single live PR for the worlds-frontend + contribution-editor stream. It's still MERGEABLE/CLEAN after the retarget (31 files, +7143/−294). I have deliberately not deleted this branch — closing a PR whose branch other work is stacked on is what left #21 pointing at a dead end, and I'd rather not repeat it.

This also means the hold I asked you to keep was costing you more than it was worth, and that's on me. You said on 08-10 you'd keep this "blocked on the sequencing note: wait for #14 to land" — and you honoured it for four days. But you had already rebased onto develop back on 08-07, which means the git dependency I described was gone by then. I kept repeating "waiting on #14" long after it stopped being true. The real constraint was only ever that this PR carries copies of Perla's backend files, so merging it would land her backend without its own review — that's a review-ordering problem, not something you needed to sit still for.

What's actually next, on #20: when Perla's backend lands on develop, rebase and drop the 12 apps/api files. I compared the two copies rather than assuming: your versions add no exported symbols that hers don't have — the difference is an inlined WorldWithRelations type and a publicWorldInclude const, i.e. a divergent internal refactor of the same code. Hers is 36 lines longer and has one extra export. So dropping them loses no functionality.

Two things from your side that must survive that drop, because they are uniquely yours:

  • apps/api/prisma/seed.ts — the two publishedAt: new Date() lines
  • the migration 20260810133000_publish_initial_world_bibles

I measured the end state: develop + Perla's backend, then your web-only changes (18 files, 3719 lines of patch) applies completely clean, zero conflicts. So the rebase should be uneventful once her side is in.

Continue on #20 — I'll review it there.

@mcfarhat mcfarhat closed this Aug 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants