Skip to content

feat(presence): derivation foundation for online status (spec 006)#66

Merged
rmjoia merged 1 commit into
mainfrom
claude/presence-foundation
May 28, 2026
Merged

feat(presence): derivation foundation for online status (spec 006)#66
rmjoia merged 1 commit into
mainfrom
claude/presence-foundation

Conversation

@rmjoia

@rmjoia rmjoia commented May 27, 2026

Copy link
Copy Markdown
Owner

What this PR delivers

PR 1 of the spec 006 (online presence) series — pure logic + types, no wiring yet. The base that the write-path (touchLastSeen coalescing) and the /api/profiles isOnline join build on. Splitting 006-A this way keeps each review small and the privacy-critical derivation isolated.

  • api/src/lib/presence.tsderivePresence(user, now): the coarse online boolean.
    online ⟺ presenceVisible !== false AND lastSeenAt parses AND (now − lastSeenAt) ≤ 5min (inclusive boundary). Fails safe to offline for missing/garbage timestamps and the explicit opt-out. now injectable for deterministic tests. PRESENCE_WINDOW_MS (5min) exported + pinned.
  • api/src/lib/users.tsUserRecord gains lastSeenAt?: string + presenceVisible?: boolean (both optional → backwards-compatible: missing lastSeenAt reads offline, missing presenceVisible reads visible). Doc comments stress the raw timestamp never leaves the server (SC-604) — only the derived boolean does.

Verified by

  • api/src/lib/presence.test.ts::1 minute ago → online
  • api/src/lib/presence.test.ts::exactly 5 minutes ago → online (inclusive boundary) — the boundary the spec calls out explicitly
  • api/src/lib/presence.test.ts::one millisecond past 5 minutes → offline
  • api/src/lib/presence.test.ts::6 minutes ago → offline
  • api/src/lib/presence.test.ts::missing lastSeenAt → offline + garbage / unparseable → offline (defensive)
  • api/src/lib/presence.test.ts::presenceVisible:false → offline even when freshly seen + opt-out wins even at the inclusive boundary
  • api/src/lib/presence.test.ts::presenceVisible true/undefined + fresh → online
  • api/src/lib/presence.test.ts::a future lastSeenAt reads online (negative delta ≤ window) — clock-skew defensiveness
  • api/src/lib/presence.test.ts::exposes a 5-minute window constant — pins the threshold

Constitution compliance

  • P1 — no secrets
  • P2 Code Quality — 341 API tests pass (+13); lint/typecheck/build clean
  • P3 Security (NON-NEGOTIABLE) — derivation only; no new endpoint/surface in this PR. The raw lastSeenAt is explicitly never returned (enforced when the join lands in PR 3, asserted there)
  • P5 Privacy (NON-NEGOTIABLE) — only a coarse boolean is derivable; opt-out (presenceVisible:false) overrides recency unconditionally
  • P9 Verified Quality — see "Verified by"
  • P4/P6/P7/P8 — N/A (no UI / network / perf surface in this slice)

Out of scope (queued)

  • PR 2touchLastSeen write-coalescing (≤1 write/min) + withPresenceTracking handler wrapper + wire into authenticated handlers
  • PR 3/api/profiles isOnline join + response-privacy strip (userId + lastSeenAt never leave)
  • LaterPOST /api/presence/visibility opt-out endpoint + OnlineIndicator UI on /find + profile + edit-page toggle (+ Playwright DOM smoke for the indicator)

Test plan

  • API unit tests: 341 pass (+13 presence)
  • API tsc typecheck + build clean; API audit clean
  • Frontend unit + lint green (no frontend surface touched)
  • Post-deploy E2E unaffected (no runtime behaviour change yet — field is dormant until PR 2 writes it)

https://claude.ai/code/session_018WA3SvALW1EmxJKVmRUGrR


Generated by Claude Code

PR 1 of the spec-006 (online presence) series — pure logic + types, no
wiring yet. Establishes the base the write-path (touchLastSeen) and the
/api/profiles isOnline join build on.

Pieces (tasks T-610..T-612):
- api/src/lib/presence.ts — derivePresence(user, now): the coarse
  "online" boolean. online ⟺ presenceVisible !== false AND lastSeenAt
  parses AND (now - lastSeenAt) <= 5min (INCLUSIVE boundary). Fails safe
  to offline for missing/garbage timestamps and the explicit opt-out.
  `now` is injectable for deterministic tests. PRESENCE_WINDOW_MS (5min)
  exported + pinned.
- api/src/lib/users.ts — UserRecord gains `lastSeenAt?: string` and
  `presenceVisible?: boolean` (both optional → backwards-compatible;
  missing lastSeenAt reads offline, missing presenceVisible reads
  visible). Doc comments stress the raw timestamp NEVER leaves the
  server (SC-604) — only the derived boolean does.

Design notes carried from the spec:
- Coarse + server-derived: "interacted within 5 min", not "tab open
  now". No client heartbeat (privacy + cost).
- The opt-out (presenceVisible:false) wins over any recency — even at
  the inclusive boundary. Pinned by test.

Tests (T-610, 13 cases): 1m→online, 5m boundary→online, 5m+1ms→offline,
6m→offline, missing→offline, garbage/empty string→offline, opt-out
overrides fresh, presenceVisible true/undefined→online when fresh,
opt-out wins at boundary, future-timestamp (clock skew)→online,
default-now path, window constant pinned.

Out of scope for this PR (queued): touchLastSeen write-coalescing +
with-presence handler wrapper (PR 2), /api/profiles isOnline join +
response-privacy strip (PR 3), presence-visibility endpoint + UI
indicator (later).

All 341 API tests pass (+13); lint, API typecheck/build, API audit clean.

https://claude.ai/code/session_018WA3SvALW1EmxJKVmRUGrR

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

This PR adds the backend foundation for spec 006 online presence by introducing server-side presence derivation logic and extending user records with dormant presence fields.

Changes:

  • Adds derivePresence with a pinned inclusive 5-minute window and opt-out handling.
  • Adds unit coverage for boundary, invalid timestamp, opt-out, and default-clock behavior.
  • Extends UserRecord with optional lastSeenAt and presenceVisible fields.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
api/src/lib/users.ts Adds optional presence-related fields to persisted user records.
api/src/lib/presence.ts Introduces coarse online-status derivation logic and exported window constant.
api/src/lib/presence.test.ts Covers the presence truth table and boundary conditions.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@rmjoia rmjoia merged commit 1c4cfa9 into main May 28, 2026
5 checks passed
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