Skip to content

web: social activity feed (pings + sightings) #66

Closed

Description

@TSVRN9

Part of #63. Blocked by: #64.

What to build

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.
  • Keyboard accessibility for mark-as-read (closes web a11y: notifications markRead is mouse-only (onmouseenter) #62 if you land it here — say so in the PR).
  • Red-green TDD with pasted red evidence; tokens/components only; PORT-isolated Playwright.

Acceptance criteria

  • Feed renders mixed pings+sightings chronologically with unread badges; ping action works end-to-end against mocks
  • Signed-out state verified; no anonymous-path regression
  • Mark-as-read reachable by keyboard (red-first spec)
  • Full suite green, check clean, CI green

Activity

  1. TSVRN9 commented on Aug 19, 2026

    @TSVRN9
    OwnerAuthor

    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.

  2. TSVRN9 commented on Aug 19, 2026

    @TSVRN9
    OwnerAuthor

    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).

    Required carry-over from PR #69's review

    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
    

    Restored the route files, re-ran → all 9 passed.

    Green evidence

    PORT=4307 npx playwright test
    45 passed (48.5s)
    

    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

    • 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.
  3. TSVRN9 commented on Aug 19, 2026

    @TSVRN9
    OwnerAuthor

    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.

    2. Two small tightenings

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    ready-for-agentFully specified, ready for an AFK agent

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions