You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Evolve the notifications page + friends pings into a single social activity feed: a chronological stream of friend pings ("come eat with me" with hall + message) and favorited-food sightings, with unread state. "Ping a friend" becomes a first-class action from the feed, not buried in the friends page. Friends management (search/request/accept) stays on /friends.
Constraints
Server data already exists (pings, food_sightings, RLS-gated); NO schema changes in this issue — if the feed genuinely needs one, stop and file a separate issue instead.
Signed-out: render the feed shell with a sign-in value prop (what you'd see here), never a dead end. Anonymous-first pages must not regress.
Reuse the mocked-Supabase e2e technique from web/e2e/friends-notifications.spec.ts (cookie seeding + REST/WebSocket interception — zero live-project contact). Fix its two known nits if you touch that file: hardcoded sb-your-project-ref-auth-token cookie name (derive from PUBLIC_SUPABASE_URL) and the unawaited PATCH race at ~L149.
Carry-over from PR #69's review, REQUIRED when building this issue's Supabase-backed feed onto the home/today surfaces: the residency-guard spec web/e2e/home-dashboard.spec.ts (zero-API assertion) only intercepts **/api/** — a direct fetch to *.supabase.co passes it GREEN (proven by mutation with mode:no-cors). Replace it with a page.on('request') listener flagging any /api/ or cross-origin request (fires for no-cors too; reviewer left a snippet in the PR #69 review). Do this red-first against a mutated page.
Implemented on branch feat/social-feed, PR #75 (not merged — leaving for review).
Per-criterion results
Feed renders mixed pings+sightings chronologically with unread badges; ping action works end-to-end against mocks — DONE.
web/src/routes/notifications/+page.svelte: pings (as receiver, accepted friends only) + food_sightings merged into one <ul aria-label="Activity feed">, sorted newest-first, each item badged "New" when unread.
Sightings: unread = read_at === null (server truth, unchanged mechanism). Pings: pings has no read_at column and this issue is no-schema-changes, so unread is derived from a new device-local "last viewed the feed" marker (web/src/lib/feedLastSeen.ts, localStorage, same shape as the existing preferences.ts) — snapshotted once at mount before being overwritten, so a ping's badge doesn't flip off mid-visit.
"Ping a friend" composer (friend picker + hall + optional message) lives on the feed itself now, not buried in /friends' accepted-friends list. Sends pings.insert.
Test: friends-notifications.spec.ts — "feed mixes pings and sightings newest-first, both badged New when unread" and "ping a friend from the feed sends the right insert" (asserts the actual POST body via postDataJSON()).
Signed-out state verified; no anonymous-path regression — DONE.
/notifications signed-out: feed shell + sign-in value prop ("Sign in to see this feed — pings from friends and favorited-dish alerts, all in one place."), never a dead end. Verified no duplicate "Sign in" button is introduced (the value prop points at the header's existing control).
/friends signed-out unchanged.
Anonymous path (/, /today, halls, menus, ranking, filters) untouched by this PR — no files outside friends/, notifications/, and the two named e2e specs were modified.
Mark-as-read reachable by keyboard (red-first spec) — DONE, closes #62.
Sighting unread items now render a real <button>Mark as read</button> (keyboard-reachable via focus + Enter), alongside the pre-existing onmouseenter for mouse users (unchanged, per web a11y: notifications markRead is mouse-only (onmouseenter) #62's "existing mouse behavior unchanged").
Mutation-verified: temporarily removed the button, re-ran just that test → red (locator.focus: Test timeout of 30000ms exceeded, waiting on the now-absent button). Restored, re-ran → green.
Full suite green, check clean, CI green — DONE (CI itself not observable from here — remote runners disabled per CLAUDE.md's "CI is local-only" — all lanes run and green locally, see Verification).
home-dashboard.spec.ts's residency guard only intercepted **/api/** via page.route(), so a direct cross-origin fetch (e.g. to *.supabase.co) with mode: "no-cors" passed it green undetected. Replaced with a page.on("request") listener that flags any same-origin /api/ path or disallowed cross-origin request — this is request-level, not route-glob-level, so it fires for no-cors requests too.
Added an explicit, commented allow-list for fonts.googleapis.com/fonts.gstatic.com: app.css (#42) already @imports Google Fonts on every page, so a blanket cross-origin guard would false-positive on every run, not just a mutated one — confirmed this by running the new guard unmutated first (green, no false positive) before doing the red-first mutation proof below.
Red evidence (mutation, not initial-implementation red — this guard replaces working code, so there's no "before" state to run new tests against other than the mutation):
Temporarily added to web/src/routes/+page.svelte's onMount:
fetch("https://your-project-ref.supabase.co/rest/v1/whatever", { mode: "no-cors" });
PORT=4307 npx playwright test home-dashboard.spec.ts -g "cross-origin"
1) home-dashboard.spec.ts › home issues zero API/cross-origin requests...
Error: unexpected /api/ or cross-origin request(s): https://your-project-ref.supabase.co/rest/v1/whatever
Expected: []
Received: ["https://your-project-ref.supabase.co/rest/v1/whatever"]
1 failed
Reverted the mutation — git diff on +page.svelte is empty (not owned by this PR; used only to prove the guard). Re-ran → green, and confirmed against the old**/api/**-only guard logic that the same mutation would have passed silently (that was the whole bug).
Red evidence (before implementation, friends.svelte / notifications.svelte)
Wrote the full new friends-notifications.spec.ts first, then ran it against the pre-#66 route files (git stash on just web/src/routes/friends/+page.svelte and web/src/routes/notifications/+page.svelte):
PORT=4307 npx playwright test friends-notifications.spec.ts
5 failed:
Notifications — signed out › shows the feed shell with a sign-in value prop, not a dead end
(old copy was "Sign in to enable favorited-food alerts." only)
Friends — signed in › accepted friends are shown without ping controls -- pinging moved to the activity feed (#66)
Error: expect(locator).toHaveCount(expected) failed — Expected: 0, Received: 1 (old Ping button still there)
Notifications — signed in › feed mixes pings and sightings newest-first, both badged New when unread
Error: expect(locator).toHaveCount(expected) failed — Expected: 2, Received: 0 (no unified feed existed yet)
Notifications — signed in › ping a friend from the feed sends the right insert
Error: expect(locator).toBeVisible() failed — "Ping a friend" heading didn't exist on the old page
Notifications — signed in › unread sighting mark-as-read is reachable by keyboard, not just onmouseenter (closes #62)
Test timeout of 30000ms exceeded — waiting for a "Mark as read" button that didn't exist yet
4 passed (pre-existing behavior, confirming the rewritten spec mechanics — cookie derivation, PATCH-race poll fix — don't themselves regress anything):
Friends — signed out › shows a styled sign-in prompt instead of the friends UI
Friends — signed in › pending (incoming/outgoing) and accepted friend states are visually distinct, and Accept sends the right update
Notifications — signed in › the alert toggle is visually clear and flips state with the right PATCH
Notifications — signed in › read sightings stay visually de-emphasized and don't offer mark-as-read again
pnpm --filter web build — clean (adapter-auto's "could not detect a production environment" notice is pre-existing/benign).
Notes for reviewers
Fixed both known nits in friends-notifications.spec.ts: cookie name now derived from PUBLIC_SUPABASE_URL (sb-<project-ref>-auth-token, matching @supabase/supabase-js's own default storage-key derivation) instead of hardcoded "your-project-ref"; the unawaited-PATCH race fixed at both the originally-flagged instance (friendships Accept, ~L149) and its unfixed twin (notifications toggle PATCH, ~L195), both now via expect.poll.
No schema changes — pings table untouched; unread state for pings is entirely client-derived.
Data residency: pings/sightings remain server-backed only for signed-in users (unchanged, per CLAUDE.md's table); the new feedLastSeen marker is device-local UI state, not health/food data.
Follow-up to my previous comment on this issue — self-review caught two things before I'd call this actually done, both fixed in a second commit on feat/social-feed (also updated on PR #75).
1. The "old guard would have passed" claim was asserted, not run
My first comment said the old **/api/**-only guard would have passed silently against the no-cors mutation "(that was the whole bug)" — I hadn't actually executed that. Ran it for real:
# Old home-dashboard.spec.ts guard (checked out from the pre-#66 commit), with the same
# no-cors mutation still in +page.svelte's onMount:
PORT=4307 npx playwright test home-dashboard.spec.ts -g "zero API"
1 passed (8.6s)
Confirmed: the old guard passes green with the leak present. That's the actual evidence for why the replacement in this PR is necessary, not an assumption. Both the mutation and the swapped-in old spec file were reverted afterward — git diff on +page.svelte and home-dashboard.spec.ts is clean relative to the committed state.
Fixed a PR-description error: I'd written "9 tests (2 pre-existing + 7 new/changed)" — the file actually had 6 tests before this PR and has 9 now, matching the earlier red-run log (5 failed / 4 passed). Corrected on the PR.
Also, while re-checking: scoped the alert-toggle badge locator to its own <section> instead of .badge.first() (would've silently drifted if the feed's own badges ever came first in DOM order), and reordered refresh() in notifications/+page.svelte so friends resolves before pings is assigned (a ping could otherwise flash "A friend" for one render before its sender's name loaded).
Re-verified after these: PORT=4307 npx playwright test — 45/45 passed; pnpm --filter web check — 0 errors/warnings; pnpm --filter web build — clean.
Activity
TSVRN9 commentedon Aug 19, 2026
Carry-over from PR #69's review, REQUIRED when building this issue's Supabase-backed feed onto the home/today surfaces: the residency-guard spec
web/e2e/home-dashboard.spec.ts(zero-API assertion) only intercepts**/api/**— a direct fetch to*.supabase.copasses it GREEN (proven by mutation with mode:no-cors). Replace it with apage.on('request')listener flagging any /api/ or cross-origin request (fires for no-cors too; reviewer left a snippet in the PR #69 review). Do this red-first against a mutated page.TSVRN9 commentedon Aug 19, 2026
Implemented on branch
feat/social-feed, PR #75 (not merged — leaving for review).Per-criterion results
Feed renders mixed pings+sightings chronologically with unread badges; ping action works end-to-end against mocks — DONE.
web/src/routes/notifications/+page.svelte:pings(as receiver, accepted friends only) +food_sightingsmerged into one<ul aria-label="Activity feed">, sorted newest-first, each item badged "New" when unread.read_at === null(server truth, unchanged mechanism). Pings:pingshas noread_atcolumn and this issue is no-schema-changes, so unread is derived from a new device-local "last viewed the feed" marker (web/src/lib/feedLastSeen.ts, localStorage, same shape as the existingpreferences.ts) — snapshotted once at mount before being overwritten, so a ping's badge doesn't flip off mid-visit./friends' accepted-friends list. Sendspings.insert.friends-notifications.spec.ts— "feed mixes pings and sightings newest-first, both badged New when unread" and "ping a friend from the feed sends the right insert" (asserts the actual POST body viapostDataJSON()).Signed-out state verified; no anonymous-path regression — DONE.
/notificationssigned-out: feed shell + sign-in value prop ("Sign in to see this feed — pings from friends and favorited-dish alerts, all in one place."), never a dead end. Verified no duplicate "Sign in" button is introduced (the value prop points at the header's existing control)./friendssigned-out unchanged./,/today, halls, menus, ranking, filters) untouched by this PR — no files outsidefriends/,notifications/, and the two named e2e specs were modified.Mark-as-read reachable by keyboard (red-first spec) — DONE, closes #62.
<button>Mark as read</button>(keyboard-reachable via focus + Enter), alongside the pre-existingonmouseenterfor mouse users (unchanged, per web a11y: notifications markRead is mouse-only (onmouseenter) #62's "existing mouse behavior unchanged").locator.focus: Test timeout of 30000ms exceeded, waiting on the now-absent button). Restored, re-ran → green.Full suite green, check clean, CI green — DONE (CI itself not observable from here — remote runners disabled per CLAUDE.md's "CI is local-only" — all lanes run and green locally, see Verification).
Required carry-over from PR #69's review
home-dashboard.spec.ts's residency guard only intercepted**/api/**viapage.route(), so a direct cross-origin fetch (e.g. to*.supabase.co) withmode: "no-cors"passed it green undetected. Replaced with apage.on("request")listener that flags any same-origin/api/path or disallowed cross-origin request — this is request-level, not route-glob-level, so it fires forno-corsrequests too.Added an explicit, commented allow-list for
fonts.googleapis.com/fonts.gstatic.com:app.css(#42) already@imports Google Fonts on every page, so a blanket cross-origin guard would false-positive on every run, not just a mutated one — confirmed this by running the new guard unmutated first (green, no false positive) before doing the red-first mutation proof below.Red evidence (mutation, not initial-implementation red — this guard replaces working code, so there's no "before" state to run new tests against other than the mutation):
Reverted the mutation —
git diffon+page.svelteis empty (not owned by this PR; used only to prove the guard). Re-ran → green, and confirmed against the old**/api/**-only guard logic that the same mutation would have passed silently (that was the whole bug).Red evidence (before implementation, friends.svelte / notifications.svelte)
Wrote the full new
friends-notifications.spec.tsfirst, then ran it against the pre-#66 route files (git stashon justweb/src/routes/friends/+page.svelteandweb/src/routes/notifications/+page.svelte):Restored the route files, re-ran → all 9 passed.
Green evidence
Verification
PORT=4307 npx playwright test— 45/45 passed.pnpm --filter web check—COMPLETED 399 FILES 0 ERRORS 0 WARNINGS 0 FILES_WITH_PROBLEMS.pnpm --filter web build— clean (adapter-auto's "could not detect a production environment" notice is pre-existing/benign).Notes for reviewers
friends-notifications.spec.ts: cookie name now derived fromPUBLIC_SUPABASE_URL(sb-<project-ref>-auth-token, matching@supabase/supabase-js's own default storage-key derivation) instead of hardcoded"your-project-ref"; the unawaited-PATCH race fixed at both the originally-flagged instance (friendships Accept, ~L149) and its unfixed twin (notifications toggle PATCH, ~L195), both now viaexpect.poll.pingstable untouched; unread state for pings is entirely client-derived.feedLastSeenmarker is device-local UI state, not health/food data.TSVRN9 commentedon Aug 19, 2026
Follow-up to my previous comment on this issue — self-review caught two things before I'd call this actually done, both fixed in a second commit on
feat/social-feed(also updated on PR #75).1. The "old guard would have passed" claim was asserted, not run
My first comment said the old
**/api/**-only guard would have passed silently against the no-cors mutation "(that was the whole bug)" — I hadn't actually executed that. Ran it for real:Confirmed: the old guard passes green with the leak present. That's the actual evidence for why the replacement in this PR is necessary, not an assumption. Both the mutation and the swapped-in old spec file were reverted afterward —
git diffon+page.svelteandhome-dashboard.spec.tsis clean relative to the committed state.2. Two small tightenings
toBeFocused()before pressing Enter — proves the "Mark as read" button is actually reachable via Tab (matching web a11y: notifications markRead is mouse-only (onmouseenter) #62's literal "keyboard-only walkthrough" wording), not just that it's clickable once focus lands on it some other way.Also, while re-checking: scoped the alert-toggle badge locator to its own
<section>instead of.badge.first() (would've silently drifted if the feed's own badges ever came first in DOM order), and reorderedrefresh()innotifications/+page.sveltesofriendsresolves beforepingsis assigned (a ping could otherwise flash "A friend" for one render before its sender's name loaded).Re-verified after these:
PORT=4307 npx playwright test— 45/45 passed;pnpm --filter web check— 0 errors/warnings;pnpm --filter web build— clean.web: unified social activity feed with honest unread state (#75)