Skip to content

feat(buddy): per-user session history and journal after shared sit (#119)#125

Merged
auerbachb merged 3 commits into
mainfrom
issue-119-per-user-buddy-persistence
Apr 14, 2026
Merged

feat(buddy): per-user session history and journal after shared sit (#119)#125
auerbachb merged 3 commits into
mainfrom
issue-119-per-user-buddy-persistence

Conversation

@auerbachb
Copy link
Copy Markdown
Owner

@auerbachb auerbachb commented Apr 14, 2026

Summary

Implements per-user persistence after a completed buddy session: each participant gets their own sessions row linked to the shared buddy_sessions id, optional in-sit thoughts persisted with that row, and the existing solo CompletionScreen for a personal completion note. POST /api/thoughts/batch now verifies session ownership and replaces prior completion notes (time_in_session = -1) to avoid duplicate journal entries.

Schema

  • sessions.buddy_session_idbuddy_sessions.id, ON DELETE SET NULL
  • Partial unique index on (user_id, buddy_session_id) where buddy_session_id IS NOT NULL (idempotency)
  • Reference SQL: drizzle/sessions_buddy_session_id_incremental.sql; prefer npx drizzle-kit push for dev DBs

Deferred / follow-ups

  • Analytics labels (solo vs buddy source, funnel by buddy_session_id) — not in this PR

Closes #119

Test plan

  • npm run build passes
  • Two accounts: complete same buddy sit; each sees own History day; each saves own completion note; other account cannot read/write that note (batch + session APIs)
  • Repeat record-personal-session returns already: true without double day increment
  • Solo session complete + note unchanged

Made with Cursor

Summary by CodeRabbit

  • New Features

    • Record a personal meditation session immediately after a buddy session finishes; the app can auto-save your personal record when the shared timer ends.
  • Behavior & Validation

    • Personal session inputs are validated, normalized, trimmed, and truncated; duplicate submissions return the existing record.
    • Thought uploads are validated, scoped to your user/session, and handled transactionally (including special “completion” notes).
  • UX

    • New “Saving…” state with retry and an option to return home without saving; user data refreshes after completion.
  • Messages

    • Added message: personal records save only after the shared timer finishes.

Add optional buddy_session_id on sessions with a partial unique index so each
user gets one personal row per completed buddy sit. New record-personal-session
API creates that row (idempotent), stores in-sit thoughts, updates participant
metadata, and increments current day like solo sessions.

Harden POST /api/thoughts/batch with session ownership checks and replace
prior completion notes (time_in_session = -1) to avoid duplicate journal
entries. Filter session detail thoughts by user id.

Web app: when the shared timer finishes, save the personal session, leave the
buddy room, and route through the existing CompletionScreen for the post-sit
note. Solo flow unchanged.

Includes incremental SQL reference for existing databases.

Made-with: Cursor
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 14, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
still-point Ready Ready Preview, Comment Apr 14, 2026 1:19am

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 14, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Adds per-user personal-session recording for buddy sessions: DB schema extends sessions with buddy_session_id, a new POST API to record personal sessions, frontend auto-finalization and callback flow, thought normalization/validation, and related policy/enum additions.

Changes

Cohort / File(s) Summary
Database schema & migration
drizzle/sessions_buddy_session_id_incremental.sql, src/db/schema.ts
Adds sessions.buddy_session_id (UUID) with FK → buddy_sessions(id) ON DELETE SET NULL, btree index, and partial unique index (user_id, buddy_session_id) WHERE buddy_session_id IS NOT NULL. Introduces buddySessions and buddySessionParticipants tables/relations.
Personal session API
src/app/api/buddy/sessions/[id]/record-personal-session/route.ts
New POST route: authenticates, validates UUID, enforces completed-phase guard, normalizes inputs (date, clearPercent, thoughts, mindStateLog), transactional insert of sessions, updates to users and buddy_sessions, participant update, optional thoughts insert, and idempotent handling for unique-constraint races.
Thoughts batch endpoint
src/app/api/thoughts/batch/route.ts
Adds session ownership/dayNumber validation, per-item normalization (trim/truncate text, validate timeInSession), drops invalid items, special handling for timeInSession === -1 completion notes, and transactional delete/insert logic.
Sessions query auth
src/app/api/sessions/[dayNumber]/route.ts
Tightens thoughts query to filter by both thoughts.sessionId === session.id and thoughts.userId === auth.userId.
Frontend: auto-finalize & UI wiring
src/components/BuddySessionRoom.tsx, src/app/app/page.tsx
Adds exported BuddyPersonalRecordPayload type and optional onPersonalRecordComplete prop; auto-finalize flow that posts personal record when shared timer completes, retry/alternate-exit UI, and page-level handler that clears state and refreshes user.
Client API & types
src/lib/api.ts
Extends Session type with optional buddySessionId; adds api.recordBuddyPersonalSession(sessionId, body) helper calling the new endpoint.
Policy & error codes
src/lib/buddySessionControlsPolicy.ts, src/lib/buddyPolicyCodes.ts
Adds requireBuddySessionCompletedForPersonalRecord guard and new BUDDY_RECORD_PERSONAL_WRONG_PHASE code (RECORD_PERSONAL_WRONG_PHASE) with a user-facing message.
Config helper
drizzle.config.ts
Adds local env-file loader (loadEnvFile) to populate process.env from .env.local and .env when keys are unset.
Docs/comments
src/lib/buddySession.ts
Updated comment documenting the new per-user recording flow (no runtime behavior changes).

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor User
    participant Frontend as BuddySessionRoom (Component)
    participant Page as App Page (State)
    participant API as recordPersonalSession API
    participant DB as Database

    User->>Frontend: Shared timer completes
    Frontend->>Frontend: compute clearPercent, thoughts, mindStateLog
    Frontend->>API: POST /api/buddy/sessions/{id}/record-personal-session
    API->>API: auth, validate sessionId, requireBuddySessionCompletedForPersonalRecord
    API->>API: normalize inputs (date, clearPercent, thoughts)
    API->>DB: BEGIN transaction
    API->>DB: SELECT users.currentDay
    API->>DB: INSERT INTO sessions (with buddy_session_id)
    API->>DB: UPDATE users (increment currentDay, updatedAt)
    API->>DB: UPDATE buddy_sessions (increment revision, updatedAt)
    API->>DB: UPDATE participant (participantCompletedAt, lastSeenAt)
    API->>DB: INSERT thoughts (if any)
    API->>DB: COMMIT
    API-->>Frontend: { session, already? }
    Frontend->>Page: onPersonalRecordComplete(payload)
    Page->>Page: set completionData, clear buddySessionId, refresh user (api.me)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hopped when timers ended, pen in paw,

saved my thoughts from the hush I saw.
Each session tucked in my own small trail,
a quiet bloom where memories sail.
Hooray — each heart gets its own tale!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.65% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat(buddy): per-user session history and journal after shared sit (#119)' clearly and concisely describes the main feature: enabling per-user session history and journal notes after a buddy sit.
Linked Issues check ✅ Passed All acceptance criteria from issue #119 are met: per-user session records created, personal completion/journal notes supported, user-scoped isolation enforced, completion UI flow adapted, and existing single-user flow preserved.
Out of Scope Changes check ✅ Passed All changes align with #119 objectives. The DB schema extension, API endpoints, thought-batch validation, and UI updates directly support per-user session recording and journal after buddy sits. No unrelated changes detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

📋 Issue Planner

Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).

View plan for ticket: #119

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-119-per-user-buddy-persistence

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/app/api/buddy/sessions/[id]/record-personal-session/route.ts (1)

59-61: Date format validation could be stricter.

The regex validates the format but not actual date validity (e.g., "2023-99-99" would pass). While PostgreSQL's date column will reject invalid dates, a stricter validation provides clearer error messages.

♻️ Proposed stricter validation
 function sessionDateOk(s: string): boolean {
-  return /^\d{4}-\d{2}-\d{2}$/.test(s);
+  if (!/^\d{4}-\d{2}-\d{2}$/.test(s)) return false;
+  const d = new Date(s + "T00:00:00Z");
+  return !Number.isNaN(d.getTime()) && d.toISOString().startsWith(s);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/buddy/sessions/`[id]/record-personal-session/route.ts around
lines 59 - 61, The sessionDateOk function currently only checks format; update
it to also verify the date is real by parsing and comparing components: split
the input in sessionDateOk into year/month/day, construct a Date (or use a
reliable date util), ensure the Date is valid (not NaN) and that the Date's
year, month, and day match the parsed values (so values like "2023-99-99" fail),
then return true only if both the regex format and the constructed Date match
the input.
src/components/BuddySessionRoom.tsx (1)

37-53: Add guard against division by zero.

If totalSeconds is 0, line 52 will divide by zero, returning NaN. While a completed buddy session should have a positive duration in practice, a defensive check would make this more robust.

🛡️ Proposed fix
 function calcBuddyClearPercent(
   mindStateLog: Array<{ time: number; state: string }>,
   totalSeconds: number,
 ): number {
-  if (mindStateLog.length === 0) return 100;
+  if (mindStateLog.length === 0 || totalSeconds <= 0) return 100;
   let clearTime = 0;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/BuddySessionRoom.tsx` around lines 37 - 53, The
calcBuddyClearPercent function can divide by zero when totalSeconds is 0; add a
defensive guard at the top (e.g., if totalSeconds <= 0) to return 100 (matching
the empty-log behavior) or otherwise avoid the division, then proceed as before
using endTime for the denominator; update function calcBuddyClearPercent to
check totalSeconds and early-return to prevent NaN from Math.round((clearTime /
endTime) * 100).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@drizzle/sessions_buddy_session_id_incremental.sql`:
- Around line 6-7: The ALTER TABLE adding the foreign key constraint
sessions_buddy_session_id_buddy_sessions_id_fk on table "sessions" is not
idempotent and will fail on reruns; wrap the constraint creation so it first
checks for existence (e.g., query pg_constraint/pg_class to see if
sessions_buddy_session_id_buddy_sessions_id_fk already exists) or enclose the
ALTER TABLE in a DO $$ BEGIN ... EXCEPTION WHEN duplicate_object THEN NULL; END
$$ block; modify the statement that creates the FK constraint (the ALTER TABLE
"sessions" ... FOREIGN KEY ("buddy_session_id") REFERENCES
"public"."buddy_sessions"("id") ...) to use one of these guards so the migration
becomes rerunnable.

In `@src/app/api/thoughts/batch/route.ts`:
- Around line 56-86: The current flow deletes existing completion notes
(timeInSession === -1) outside a transaction and then inserts rows, risking data
loss if the insert fails and allowing multiple -1 rows in one batch; wrap the
delete + insert into a single transactional operation using the database
transaction API (db.transaction or equivalent) so the delete and insert are
atomic, and before inserting deduplicate normalized to ensure at most one item
with timeInSession === -1 (e.g., keep the last/first -1 entry) when building the
values for db.insert(thoughts).returning(...); ensure you reference and operate
on the same sessionId, auth.userId, and thoughts table inside that transaction.

---

Nitpick comments:
In `@src/app/api/buddy/sessions/`[id]/record-personal-session/route.ts:
- Around line 59-61: The sessionDateOk function currently only checks format;
update it to also verify the date is real by parsing and comparing components:
split the input in sessionDateOk into year/month/day, construct a Date (or use a
reliable date util), ensure the Date is valid (not NaN) and that the Date's
year, month, and day match the parsed values (so values like "2023-99-99" fail),
then return true only if both the regex format and the constructed Date match
the input.

In `@src/components/BuddySessionRoom.tsx`:
- Around line 37-53: The calcBuddyClearPercent function can divide by zero when
totalSeconds is 0; add a defensive guard at the top (e.g., if totalSeconds <= 0)
to return 100 (matching the empty-log behavior) or otherwise avoid the division,
then proceed as before using endTime for the denominator; update function
calcBuddyClearPercent to check totalSeconds and early-return to prevent NaN from
Math.round((clearTime / endTime) * 100).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cb733939-8b3f-443a-bf56-eafced2099b0

📥 Commits

Reviewing files that changed from the base of the PR and between 8439beb and 3bf0d6a.

📒 Files selected for processing (11)
  • drizzle/sessions_buddy_session_id_incremental.sql
  • src/app/api/buddy/sessions/[id]/record-personal-session/route.ts
  • src/app/api/sessions/[dayNumber]/route.ts
  • src/app/api/thoughts/batch/route.ts
  • src/app/app/page.tsx
  • src/components/BuddySessionRoom.tsx
  • src/db/schema.ts
  • src/lib/api.ts
  • src/lib/buddyPolicyCodes.ts
  • src/lib/buddySession.ts
  • src/lib/buddySessionControlsPolicy.ts

Comment thread drizzle/sessions_buddy_session_id_incremental.sql Outdated
Comment thread src/app/api/thoughts/batch/route.ts Outdated
Drizzle Kit does not load Next.js env files; read .env.local and .env so
POSTGRES_URL is set when running npx drizzle-kit push from the repo root.

Made-with: Cursor
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
drizzle.config.ts (1)

32-33: Consider loading NODE_ENV-specific env files for CLI parity

Line 32Line 33 only load .env.local and .env. If Drizzle is run in test/CI contexts, supporting .env.<NODE_ENV> variants can reduce surprises.

♻️ Suggested enhancement
+const nodeEnv = process.env.NODE_ENV;
+if (nodeEnv) {
+  loadEnvFile(`.env.${nodeEnv}.local`);
+  loadEnvFile(`.env.${nodeEnv}`);
+}
 loadEnvFile(".env.local");
 loadEnvFile(".env");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@drizzle.config.ts` around lines 32 - 33, Add loading for NODE_ENV-specific
env files so CLI runs mirror runtime environments: compute a safe NODE_ENV
(e.g., fallback to "development" if process.env.NODE_ENV is undefined) and call
loadEnvFile for the environment variants (e.g., `.env.${NODE_ENV}.local` and
`.env.${NODE_ENV}`) in the appropriate precedence around the existing
loadEnvFile(".env.local") and loadEnvFile(".env") calls so `.env.<NODE_ENV>`
files are loaded when present.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@drizzle.config.ts`:
- Around line 19-25: The code extracts env values into val from trimmed.slice(eq
+ 1) but leaves inline comments (e.g. "POSTGRES_URL=... # local") and also
mishandles quoted values with trailing comments; update the logic that populates
val so it strips inline comments outside of quotes and correctly handles quoted
values with trailing comments: after computing val (and before removing
surrounding quotes), if val starts with a quote character (' or "), locate the
matching closing unescaped quote (not just endsWith) and extract the substring
between them; otherwise for unquoted values, cut at the first unescaped '#' and
trim the remainder; ensure you continue to preserve existing unescaping/trim
behavior for val, referencing the variables trimmed, eq and val in your change.

---

Nitpick comments:
In `@drizzle.config.ts`:
- Around line 32-33: Add loading for NODE_ENV-specific env files so CLI runs
mirror runtime environments: compute a safe NODE_ENV (e.g., fallback to
"development" if process.env.NODE_ENV is undefined) and call loadEnvFile for the
environment variants (e.g., `.env.${NODE_ENV}.local` and `.env.${NODE_ENV}`) in
the appropriate precedence around the existing loadEnvFile(".env.local") and
loadEnvFile(".env") calls so `.env.<NODE_ENV>` files are loaded when present.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7b92260d-3be7-4fc2-89b3-d7b1427073b2

📥 Commits

Reviewing files that changed from the base of the PR and between 3bf0d6a and 53d965a.

📒 Files selected for processing (1)
  • drizzle.config.ts

Comment thread drizzle.config.ts
- Make sessions_buddy_session_id incremental FK rerunnable (pg_constraint guard)
- Parse .env values: strip trailing inline comments; quoted values via lastIndexOf
- Run thoughts batch delete+insert in one transaction; at most one completion note

Made-with: Cursor
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.

[Buddy MVP] Per-user session recording and journal notes after shared session

1 participant