| Symptom | Root Cause | Fix |
|---|---|---|
/api/agent/token returns Unknown parameter: 'session.voice' or 'model' |
client_secrets was called directly and the body contained unsupported fields. |
Use the OpenAI SDK openai.beta.realtime.sessions.create({ model }), or follow the official example and send {"session": {"type": "realtime", "model": "..."}}. |
Console shows Unknown parameter: 'session.type' |
@openai/agents-realtime@0.3.x includes deprecated fields in the session.update payload. |
Pin @openai/agents / @openai/agents-realtime / openai to 0.0.10 / 5.8.2 (matching the sample), or wait for an upstream fix and then upgrade. |
Failed to parse SessionDescription. { Expect line: v= } |
The Realtime connection URL was not explicitly set, so the endpoint returned JSON instead of SDP. | Call session.connect({ apiKey, url: 'https://api.openai.com/v1/realtime?model=...' }). |
Zod reports .optional() without .nullable() |
Optional fields in the tool schema did not allow null, which the older SDK does not accept. |
Use nullable().optional(), and strip any fields that are null from the payload before sending. |
/api/agent/tools/context returns 422 Expected object, received null |
The payload still contained fields like range: null. |
Remove all null fields before invoking the tool. |
Console shows getInitialSessionConfig is not a function |
The older SDK does not expose this helper. | Remove the debug call. |
| Echo’s answers about “today / yesterday / last week” don’t match real dates | The unified timezone + UTC truncation caused anchorDate to lag by one day, and the model did not know the current date. |
For scope === 'today' query by eq('date', anchorDate), and inject “Today is ${anchorDate} (Australia/Sydney)” into the system prompt so the model sets anchorDate / range explicitly when calling tools. |
| Session keeps running after closing the Voice panel and times out a few minutes later | The dialog closes without explicitly disconnecting the Realtime session, so it continues until the 10‑minute limit. | In the panel component’s onOpenChange callback, detect nextOpen === false and call disconnect() so closing the panel always ends the session. |
Error:
Error: ENOENT: no such file or directory, open '/path/to/.next/static/development/_buildManifest.js.tmp.xxxxx'
Cause: Next.js cache corruption during development, often occurs when:
- Multiple files are modified simultaneously
- Development server is forcefully terminated
- File system permissions issues
Solution:
# Quick fix
rm -rf .next
pnpm dev
# Complete clean (if problem persists)
rm -rf .next node_modules
pnpm install
pnpm devPrevention:
- Use proper shutdown procedures (Ctrl+C)
- Avoid rapid file modifications during build
- Ensure proper file system permissions
Error:
⚠ Port 3000 is in use, using available port 3001 instead
Cause: Another process is using the default port 3000
Solution:
# Kill process using port 3000
lsof -ti:3000 | xargs kill -9
# Or use alternative port
pnpm dev --port 3001Error:
Error [PageNotFoundError]: Cannot find module for page: /_not-found
Cause: Known Next.js 15 regression (particularly after large client-component updates) where the build step briefly loses the generated _not-found or _document entries even though src/app/not-found.tsx exists.
Workarounds:
# Clear build artifacts then retry
rm -rf .next
pnpm build
# If the error reappears, rerun the build once more
pnpm buildNotes:
- The issue does not affect
pnpm dev; only production builds are impacted. - Track Next.js release notes and upgrade once the upstream fix lands. Until then, the retry strategy above has consistently recovered the build.
Error:
Error: Event handlers cannot be passed to Client Component props
Cause: Server components cannot pass event handlers to client components
Solution:
// Add 'use client' directive to components with event handlers
'use client';
import { Button } from '@/components/ui/button';
export default function MyComponent() {
return (
<Button onClick={() => console.log('clicked')}>
Click me
</Button>
);
}Error:
Each child in a list should have a unique "key" prop
Cause: React requires unique keys for list items to optimize rendering
Solution:
// Bad
{items.map((item) => (
<div>{item.name}</div>
))}
// Good
{items.map((item) => (
<div key={item.id}>{item.name}</div>
))}
// For arrays without unique IDs
{items.map((item, index) => (
<div key={`item-${index}`}>{item.name}</div>
))}Error:
React Hook useEffect has a missing dependency: 'functionName'
Cause: useEffect depends on variables that aren't in the dependency array
Solution:
// Use useCallback for stable function references
const stableFunction = useCallback(() => {
// function logic
}, [dependencies]);
useEffect(() => {
stableFunction();
}, [stableFunction]);Error:
'props' is declared but its value is never read
Solution:
// Use underscore for unused parameters
const MyComponent = forwardRef<RefType, Props>((_props, ref) => {
// component logic
});
// Or destructure only needed props
const MyComponent = ({ neededProp, ...rest }: Props) => {
// component logic
};Error
TS7016: Could not find a declaration file for module 'wavesurfer.js/dist/plugins/record.esm.js'
Cause: The plugin ships its type definitions as record.d.ts while the runtime import path is record.esm.js, so TypeScript cannot auto-resolve the declaration when moduleResolution is set to node.
Fix:
- Create
src/types/wavesurfer-record.d.ts:declare module 'wavesurfer.js/dist/plugins/record.esm.js' { import RecordPlugin from 'wavesurfer.js/dist/plugins/record.js'; export default RecordPlugin; }
- Ensure
tsconfig.jsonincludes thesrcdirectory (already true in this repo). Restart the dev server so the shim is picked up.
Symptoms: The audio recorder works, but the live waveform never animates.
Cause: renderMicStream() was called before the Record plugin finished registering, so no analyser loop ever ran.
Fix:
- Track an
isPluginReadyflag inLiveWaveform. - Only call
renderMicStream(stream)once WaveSurfer + plugin are instantiated. - Tear down analyser intervals in the cleanup function to avoid zombie listeners.
Symptoms: In an iOS WebView or embedded browser on devices with a notch, the header avatar/profile dropdown on /dashboard/overview appears to vanish.
Root Cause: The header is sticky top-0 and the default top padding on the page container was removed. Without reserving safe-area space for the sticky header, the WebView status bar/notch overlaps the header, making it look like it has moved out of the viewport. If viewport-fit=cover is not set, env(safe-area-inset-top) stays at 0 and padding compensation fails.
Fix:
- Enable safe-area support: add
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">to<head>insrc/app/layout.tsx:1. - Reserve safe-area height for the header (choose one):
- A. On the
<header>insrc/components/layout/header.tsx:11, add
style={{ paddingTop: 'calc(env(safe-area-inset-top, 0px) + 6px)', minHeight: 'calc(64px + env(safe-area-inset-top, 0px) + 6px)' }} - B. On the page container in
src/app/dashboard/overview/layout.tsx:22, add
<PageContainer scrollable={false} className='pt-[env(safe-area-inset-top,0px)] ...'>
- A. On the
Notes:
- If the header is still clipped, first confirm
viewport-fit=coveris set. - You can tweak constants (like
+ 6pxor the base64pxheight) to fine‑tune the visual height.
Cause: The idle fact banner used white-space: nowrap and overflow: hidden, so long copy was clipped, especially on mobile.
Fix: Switch to white-space: pre-wrap / word-break: break-word and drive the typing effect with a typedChars counter so the text naturally wraps while it grows.
Cause: Wrapping the entire string in a gradient text span causes emoji glyphs to inherit the gradient, leaving them monochrome outlines.
Fix: While rendering each character, detect emoji via the Unicode Extended_Pictographic regex and render them inside a plain text-foreground span (without gradient). Non-emoji characters remain in the gradient span so the overall typography stays on-brand.
Error:
new row violates row-level security policy for table "daily_question"
Cause: Row Level Security (RLS) policies are not properly configured
Solution:
-- Enable RLS
ALTER TABLE daily_question ENABLE ROW LEVEL SECURITY;
-- Create policy for authenticated users
CREATE POLICY "Users can manage their own records" ON daily_question
FOR ALL USING (auth.uid()::text = user_id);Problem: Queries not returning expected results for "today's" data
Cause: Timezone differences between client and server
Solution:
// Use consistent date formatting
const today = new Date().toISOString().split('T')[0];
const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString().split('T')[0];
const { data, error } = await supabase
.from('daily_question')
.select('*')
.eq('user_id', user.id)
.gte('created_at', today)
.lt('created_at', tomorrow)
.single();Error:
user is undefined in useUser hook
Cause: Component renders before Clerk authentication initializes
Solution:
const { user, isLoaded } = useUser();
if (!isLoaded) {
return <div>Loading...</div>;
}
if (!user) {
return <div>Please sign in</div>;
}Problem: Components re-render too frequently
Solution:
// Use React.memo for expensive components
const ExpensiveComponent = React.memo(({ data }) => {
return <div>{data}</div>;
});
// Use useCallback for event handlers
const handleClick = useCallback(() => {
// handler logic
}, [dependency]);
// Use useMemo for expensive calculations
const expensiveValue = useMemo(() => {
return calculateExpensiveValue(data);
}, [data]);Error:
NEXT_PUBLIC_SUPABASE_URL is not defined
Solution:
# Ensure .env.local exists and contains required variables
cp env.example.txt .env.local
# Edit .env.local with your values
NEXT_PUBLIC_SUPABASE_URL=your_supabase_url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_anon_keyError:
Type error: Cannot find module '@/components/ui/button'
Solution:
# Regenerate TypeScript types
pnpm supabase:generate-types
# Check TypeScript configuration
npx tsc --showConfig
# Clear TypeScript cache
rm -rf node_modules/.cacheProblem: Components with hooks fail in tests
Solution:
// Wrap components with required providers
import { render } from '@testing-library/react';
import { ClerkProvider } from '@clerk/nextjs';
const renderWithProviders = (component) => {
return render(
<ClerkProvider>
{component}
</ClerkProvider>
);
};- Install React DevTools browser extension
- Use Profiler to identify performance bottlenecks
- Inspect component props and state
// Log Supabase queries
const { data, error } = await supabase
.from('daily_question')
.select('*')
.eq('user_id', user.id);
console.log('Supabase query:', { data, error });// Create error boundary for better error handling
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}- Use feature-based folder structure
- Keep components small and focused
- Extract custom hooks for reusable logic
- Use TypeScript for better type safety
- Implement proper loading states
- Use React.memo for expensive components
- Implement proper error boundaries
- Optimize database queries
- Always validate user input
- Use proper authentication checks
- Implement RLS policies in Supabase
- Never expose sensitive data in client-side code
Error:
Unable to access microphone. Please check permissions.
Solution:
- Browser level: Check site permissions in browser settings
- System level: Ensure browser has microphone access in OS settings
- HTTPS requirement: MediaRecorder requires secure context (HTTPS or localhost)
Problem: Click record button but nothing happens
Causes & Solutions:
- Check browser compatibility (Chrome/Edge/Firefox recommended)
- Ensure microphone is not in use by another application
- Check console for specific MediaRecorder errors
Problem: Recorded audio won't play
Solution:
// Ensure proper MIME type support
const options = {
mimeType: 'audio/webm;codecs=opus' // Primary choice
};
// Fallback options
if (!MediaRecorder.isTypeSupported(options.mimeType)) {
options.mimeType = 'audio/webm';
}Error:
Storage error: {
statusCode: '404',
error: 'Bucket not found',
message: 'Bucket not found'
}
Solution:
- Create bucket in Supabase Dashboard:
- Navigate to Storage section
- Create bucket named
audio-files - Set appropriate permissions (public/private)
Error:
new row violates row-level security policy
Solutions:
- For Clerk + Supabase: Use service role key in API routes
// Create admin client for bypassing RLS
import { createAdminClient } from '@/lib/supabase/admin';
const supabase = createAdminClient();- Disable RLS (development only):
ALTER TABLE audio_files DISABLE ROW LEVEL SECURITY;
ALTER TABLE transcripts DISABLE ROW LEVEL SECURITY;- Configure proper RLS (production):
-- Since using Clerk auth, policies need custom implementation
-- Consider using service role key for authenticated operationsError:
File too large
Solution:
- Whisper API limit: 25MB
- Configure Supabase bucket limit accordingly
- Implement client-side validation:
const maxSize = 25 * 1024 * 1024; // 25MB
if (audioFile.size > maxSize) {
return NextResponse.json({ error: 'File too large' }, { status: 400 });
}Error:
OpenAI API configuration error
Solution:
- Add to
.env.local:
OPENAI_API_KEY=sk-proj-****- Restart development server
Error:
API quota exceeded
Solutions:
- Check OpenAI dashboard for usage limits
- Implement rate limiting in your application
- Consider queuing system for high traffic
Problem: Whisper API returns empty or incorrect transcription
Solutions:
- Verify audio quality and format
- Check language settings
- Ensure audio contains speech (not silence)
- Handle edge cases:
if (!transcription || transcription.trim().length === 0) {
return NextResponse.json({ error: 'No speech detected in audio' }, { status: 400 });
}Problem: Summary not generating or too generic
Solution: Refine system prompt
const systemPrompt = `You are a helpful assistant that summarizes personal journal entries.
Create a concise, structured summary that:
- Removes redundancies and filler words
- Corrects grammar while preserving the original meaning
- Highlights key themes and emotions
- Aims for 2-3 sentences maximum`;- After switching models to GPT‑5/5‑mini, recording/transcribe sometimes returned 500/400 and Echos cards failed to generate; JSON parsing errors like
Unexpected end of JSON input. - Daily Summaries (table) showed “multiple entries today”, but
transcriptsappeared to have only a few rows; the frontend list could see and edit them, and Studio showed additional rows later. /api/transcriberesponses felt slow and the UI wait time was noticeable.
- GPT‑5 chat parameter incompatibility: it does not support
temperature/top_p/max_tokens, requiringmax_completion_tokensor a move to the Responses API. In addition,choices[0].message.contentcan be empty or an array, so directly callingJSON.parse(content)can throw. - Timezone window mismatch: the frontend treats “today” as the local day, while Studio filters by UTC; using
toISOString().split('T')[0]further diverges from user expectations. - Blocking pipeline: daily summary + Echos sync ran synchronously after recording, lengthening
/api/transcriberesponse times.
- Model fallback: standardised on the GPT‑4 family (
gpt-4o/gpt-4o-mini) withmax_tokens+temperaturefor stability (src/app/api/transcribe/route.ts,src/app/api/generate-daily-summary/route.ts,src/lib/reflections/generator.ts). - JSON parsing hardening (recommended): guard
choices[0].message.content; if it is an array, join thetextfields; wrapJSON.parsein try/catch and provide a fallback. - Non‑blocking pipeline: move “generate daily summary + Echos sync” into background work so the recording endpoint returns quickly (
src/app/api/transcribe/route.ts), and trigger Echos asynchronously after manual summary generation (src/app/api/generate-daily-summary/route.ts). - Timezone consistency (recommended): either have the client use local start/end times converted to ISO for “today” queries, or let the backend compute windows by user timezone and store/query accordingly to avoid “GUI can’t see it” issues.
- Use DevTools MCP to capture
/api/transcribe: confirm 200 and that the response containsaudioFileIdandtranscriptId. - SQL quick check (UTC vs local timezone):
-- UTC count for “today”
WITH b AS (
SELECT date_trunc('day', now() AT TIME ZONE 'UTC') AS s,
date_trunc('day', now() AT TIME ZONE 'UTC') + interval '1 day' AS e)
SELECT 'transcripts_utc' AS bucket, COUNT(*)
FROM transcripts t, b WHERE t.created_at >= b.s AND t.created_at < b.e
UNION ALL
SELECT 'audio_utc', COUNT(*) FROM audio_files a, b WHERE a.created_at >= b.s AND a.created_at < b.e;
-- Sydney count for “today” (replace with your timezone)
WITH tz AS (SELECT (now() AT TIME ZONE 'Australia/Sydney')::date AS d)
SELECT 'transcripts_syd', COUNT(*) FROM transcripts t, tz
WHERE (t.created_at AT TIME ZONE 'Australia/Sydney')::date = (SELECT d FROM tz)
UNION ALL
SELECT 'audio_syd', COUNT(*) FROM audio_files a, tz
WHERE (a.created_at AT TIME ZONE 'Australia/Sydney')::date = (SELECT d FROM tz);- Studio view: disable date filters, sort by
created_at DESCto see the latest rows, then search byidfor precise verification.
- Local auth bypass for DevTools testing: add
DEV_DISABLE_AUTH=trueto.env.local, effective only whenNODE_ENV=development; the middleware then allows pages and APIs through (src/middleware.ts). - Recording data flow: Storage →
audio_files→transcripts→ (background)daily_summaries→ (background)period_reflections.
- Before switching models, verify parameter/response shape differences (for GPT‑5, prefer the Responses API).
- The UI/frontend definition of “today” should match the query logic and Studio inspection (explicit timezone).
- Long‑running backend tasks should be made asynchronous when possible to reduce perceived latency.
If monthly/weekly card titles do not match their content (UTC vs local month boundary mismatch), recover as follows:
- Find the cards that need cleanup
-- Review the most recent month/week cards (read‑only)
SELECT period_type, period_start, period_end, created_at
FROM period_reflections
WHERE user_id = '<your_user_id>'
ORDER BY period_start DESC
LIMIT 20;- Precisely delete the incorrect months (explicit
period_startrecommended)
-- Example (replace <your_user_id> with your real user_id)
DELETE FROM period_reflections
WHERE user_id = '<your_user_id>'
AND period_type = 'monthly'
AND period_start IN ('2025-10-31','2025-09-30');Optional: if there are many bad rows, you can delete by time window, but always run the same WHERE clause as a SELECT first:
-- Example: delete “bad month” cards within a time window
-- Important: run this as SELECT first, then convert to DELETE
SELECT id, period_start, period_end, created_at
FROM period_reflections
WHERE user_id = '<your_user_id>'
AND period_type = 'monthly'
AND period_start < '2025-11-01';
-- After confirming, perform the delete:
DELETE FROM period_reflections
WHERE user_id = '<your_user_id>'
AND period_type = 'monthly'
AND period_start < '2025-11-01';- Regenerate the “current period” card from the frontend
- Go to
/dashboard/echos, switch to Monthly, and click “Refresh current period”. - The refresh anchor logic has been fixed: if the top card is not the in‑progress card, the system uses “today” as the anchor to generate the current monthly card (local month boundary).
- Similarly, to correct weekly cards, switch to Weekly and click refresh.
- Verify the result
-- The new “current month” card should have period_start = local month 1st
SELECT period_type, period_start, period_end, last_generated_at
FROM period_reflections
WHERE user_id = '<your_user_id>'
AND period_type = 'monthly'
ORDER BY period_start DESC
LIMIT 3;Notes
- Only clean up incorrect weekly/monthly cards in
period_reflections;daily_summariesis the daily base table and should not be deleted. - Always run a
SELECTwith the same WHERE clause before executingDELETEto ensure the target set is correct. - The codebase now uses local timezone period boundaries, so regenerated cards will no longer cross months/weeks incorrectly.
Error
x You are attempting to export "metadata" from a component marked with "use client"...
Cause: /dashboard/journals/stats/page.tsx is a client component ('use client') but still exports metadata.
Fix
diff --git a/src/app/dashboard/journals/stats/page.tsx b/src/app/dashboard/journals/stats/page.tsx
@@
-'use client';
-
-import PageContainer from '@/components/layout/page-container';
-
-export const metadata = {
- title: 'Dashboard: Journal Stats'
-};
+'use client';
+
+import PageContainer from '@/components/layout/page-container';
+
+// metadata export is not allowed in a client component page.
+// Move it to a parent layout or convert this page to a server wrapper.Error
Type error: Argument of type '{ ... stats: { [k: string]: unknown } }' is not assignable to TablesUpdate<'daily_summaries'>...
Cause: cleanStats returns Record<string, unknown>, which cannot be assigned to the Supabase Json type; updatePayload / upsertPayload do not explicitly use Supabase types.
Fix (excerpt)
diff --git a/src/lib/reflections/generator.ts b/src/lib/reflections/generator.ts
@@
-import { reflectionAISchema } from './schema';
-import type { ReflectionCard, ReflectionMode } from './types';
+import { reflectionAISchema } from './schema';
+import type { ReflectionCard, ReflectionMode } from './types';
+import type { Json, TablesInsert, TablesUpdate } from '@/types/supabase';
-const MODEL_NAME = process.env.OPENAI_REFLECTION_MODEL ?? 'gpt-5';
+const MODEL_NAME = process.env.OPENAI_REFLECTION_MODEL ?? 'gpt-4o-mini';
-const cleanStats = (stats: Record<string, unknown> | null | undefined) => {
- if (!stats) return null;
- const filteredEntries = Object.entries(stats).filter(([_, value]) => value != null);
- if (filteredEntries.length === 0) return null;
- return Object.fromEntries(filteredEntries);
-};
+type StatsShape = {
+ entryCount?: number;
+ topEmotions?: string[];
+ keywords?: string[];
+} | null | undefined;
+
+const cleanStats = (stats: StatsShape): Json | null => {
+ if (!stats) return null;
+ const obj: Record<string, unknown> = {};
+ if (typeof stats.entryCount === 'number') obj.entryCount = stats.entryCount;
+ if (Array.isArray(stats.topEmotions)) obj.topEmotions = stats.topEmotions;
+ if (Array.isArray(stats.keywords)) obj.keywords = stats.keywords;
+ return Object.keys(obj).length ? (obj as unknown as Json) : null;
+};
-const updatePayload = {
+const updatePayload: TablesUpdate<'daily_summaries'> = {
achievements: ...,
stats: cleanStats(stats),
...
};
-const upsertPayload = {
+const upsertPayload: TablesInsert<'period_reflections'> = {
user_id: userId,
stats: cleanStats(baseStats),
...
};Tip: if you change models in future, make sure
MODEL_NAMEstays in sync with the Supabase JSON type.
Error
Type error: Type '(event: Sentry.Event) => Sentry.Event' is not assignable to type '(event: ErrorEvent, hint: EventHint) => ErrorEvent | PromiseLike<ErrorEvent | null> | null'.
Cause
src/instrumentation-client.tsreused the server‑sidescrubEvent(typed asSentry.Event), but the browser SDK expectsbeforeSendto receive anErrorEvent.next build(husky pre‑push) re‑runs TypeScript checks; even if the editor shows no errors, this can still block pushes.
Fix
const scrubEvent = (
event: Sentry.ErrorEvent,
_hint: Sentry.EventHint
): Sentry.ErrorEvent | null => {
if (event.request?.headers) {
delete event.request.headers.authorization;
delete event.request.headers.Authorization;
}
return event ?? null;
};
Sentry.init({
// ...
beforeSend: scrubEvent,
// ...
});Verification
- Run
pnpm run buildlocally and confirm it passes; afterwards the husky pre‑push hook will also succeed. - The server‑side
src/instrumentation.tscan keep theSentry.Eventsignature; no coordinated change is needed.
This section records how we fixed the “recording → transcription → daily summary → Echos sync” pipeline and provides code diffs that can be applied to the current codebase.
Reason: once audio and transcripts are saved successfully we can return immediately. Daily summary and Echos generation are value‑added operations and should not block the main request path.
Files involved: src/app/api/transcribe/route.ts, src/app/api/generate-daily-summary/route.ts
Example diff:
diff --git a/src/app/api/transcribe/route.ts b/src/app/api/transcribe/route.ts
@@
- // Step 6: Generate daily summary directly
- try {
- const summaryData = await generateDailySummary(
- userId,
- supabase,
- openai
- );
- if (summaryData?.date) {
- await syncReflectionsForDate({
- supabase,
- openai,
- userId,
- anchorDate: summaryData.date
- });
- }
- } catch (summaryError) {
- // Log but don't fail the main request
- console.error('Failed to generate daily summary:', summaryError);
- }
+ // Step 6: Kick off daily summary + echos sync in background (non-blocking)
+ (async () => {
+ try {
+ const summaryData = await generateDailySummary(
+ userId,
+ supabase,
+ openai
+ );
+ if (summaryData?.date) {
+ await syncReflectionsForDate({
+ supabase,
+ openai,
+ userId,
+ anchorDate: summaryData.date
+ });
+ }
+ } catch (summaryError) {
+ console.error('Background daily summary failed:', summaryError);
+ }
+ })();
diff --git a/src/app/api/generate-daily-summary/route.ts b/src/app/api/generate-daily-summary/route.ts
@@
- try {
- await syncReflectionsForDate({
- supabase,
- openai,
- userId,
- anchorDate: date
- });
- } catch (reflectionError) {
- console.error('Failed to sync reflections after summary:', reflectionError);
- }
+ // Trigger echos sync in background to keep summary response fast
+ (async () => {
+ try {
+ await syncReflectionsForDate({
+ supabase,
+ openai,
+ userId,
+ anchorDate: date
+ });
+ } catch (reflectionError) {
+ console.error('Background reflections sync failed:', reflectionError);
+ }
+ })();Reason: the client previously used toISOString().split('T')[0] to calculate today’s boundary (UTC day), which diverged from local/Studio views. We recommend using local 00:00:00–23:59:59.999, then converting to ISO for queries.
Files involved: src/lib/supabase/queries.ts
Example diff (changes):
diff --git a/src/lib/supabase/queries.ts b/src/lib/supabase/queries.ts
@@
export const getTodayMoodEntry = cache(
async (supabase: TypedSupabaseClient, userId: string) => {
if (!userId) return null;
- const today = new Date().toISOString().split('T')[0];
- const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000)
- .toISOString()
- .split('T')[0];
+ const start = new Date();
+ start.setHours(0, 0, 0, 0);
+ const end = new Date();
+ end.setHours(23, 59, 59, 999);
const { data, error } = await supabase
.from('daily_question')
.select('*')
.eq('user_id', userId)
- .gte('created_at', today)
- .lt('created_at', tomorrow)
+ .gte('created_at', start.toISOString())
+ .lte('created_at', end.toISOString())
.single();
@@
export const getTodayAudioJournals = cache(
async (supabase: TypedSupabaseClient, userId: string) => {
if (!userId) return [];
- const today = new Date().toISOString().split('T')[0];
- const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000)
- .toISOString()
- .split('T')[0];
+ const start = new Date();
+ start.setHours(0, 0, 0, 0);
+ const end = new Date();
+ end.setHours(23, 59, 59, 999);
const { data, error } = await supabase
.from('audio_files')
.select(
`
*,
transcripts (
id,
text,
language,
created_at
)
`
)
.eq('user_id', userId)
- .gte('created_at', today)
- .lt('created_at', tomorrow)
+ .gte('created_at', start.toISOString())
+ .lte('created_at', end.toISOString())
.order('created_at', { ascending: false });Reason: the LLM may return an empty string, or content as a chunked array; calling JSON.parse(choices[0].message.content) directly can throw.
Files involved: src/lib/reflections/generator.ts
Example diff (recommended changes for both daily and period paths):
diff --git a/src/lib/reflections/generator.ts b/src/lib/reflections/generator.ts
@@
- const parsed = reflectionAISchema.parse(
- JSON.parse(completion.choices[0]?.message?.content ?? '{}')
- );
+ const raw = completion.choices[0]?.message?.content as any;
+ const text = Array.isArray(raw)
+ ? raw.map((chunk) => chunk?.text ?? '').join('')
+ : (raw ?? '');
+ let parsed;
+ try {
+ parsed = reflectionAISchema.parse(JSON.parse(text));
+ } catch (err) {
+ console.error('Reflection JSON parse failed (daily):', { text, err });
+ throw err;
+ }
@@
- const parsed = reflectionAISchema.parse(
- JSON.parse(completion.choices[0]?.message?.content ?? '{}')
- );
+ const raw2 = completion.choices[0]?.message?.content as any;
+ const text2 = Array.isArray(raw2)
+ ? raw2.map((chunk) => chunk?.text ?? '').join('')
+ : (raw2 ?? '');
+ let parsed;
+ try {
+ parsed = reflectionAISchema.parse(JSON.parse(text2));
+ } catch (err) {
+ console.error('Reflection JSON parse failed (period):', { text2, err });
+ throw err;
+ }Note: if you later migrate to GPT‑5, we recommend moving to the Responses API and extracting
output_textoroutput[*].textfrom its response before performing JSON validation.
These changes can be adopted in stages: A (already implemented) improves response time; B and C (recommended) eliminate timezone/JSON edge cases and improve stability and observability.
Problem: Modal not opening when button clicked
Solution: Verify event listener setup
// In layout component
useEffect(() => {
const handleOpenModal = () => {
modalRef.current?.openModal();
};
window.addEventListener('openAudioJournalModal', handleOpenModal);
return () => {
window.removeEventListener('openAudioJournalModal', handleOpenModal);
};
}, []);Problem: Audio journal stats remain at 0
Solutions:
- Check database queries are returning data
- Verify user ID matching between Clerk and database
- Ensure proper date filtering in queries
- Check for event dispatching after successful save:
const event = new CustomEvent('audioJournalUpdated');
window.dispatchEvent(event);Problem: TypeScript errors with query results
Solution: Ensure types match actual query structure
// Define custom types for joined queries
type AudioJournalWithTranscript = Tables<'audio_files'> & {
transcripts: {
id: string;
text: string | null;
language: string | null;
created_at: string | null;
}[];
};Problem: API takes too long to respond
Solutions:
- Show proper loading states
- Implement progress indicators
- Consider audio compression before upload
- Add timeout handling:
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 60000); // 60s timeoutProblem: Browser memory usage increases
Solution: Clean up audio URLs
useEffect(() => {
return () => {
if (audioUrl) {
URL.revokeObjectURL(audioUrl);
}
};
}, [audioUrl]);- 2025‑11‑07 AEST (UTC+11) morning:
DailyMoodWidgetrendered fallback (“Log mood”) with no emoji animation even though Supabase had same‑day mood rows. - Journal Library under
/dashboard/journalsfailed to show audio entries recorded minutes earlier; data appeared only under the previous day when checking raw tables. - Daily summary generation produced a November 6th record even though user had already started November 7th in Sydney.
- Commit
3948ca8755b5ecfdreplacedtoday/tomorrowstring comparisons with a “start/end of day” calculation usingDate#setHours(...)followed by.toISOString().- Calling
.toISOString()converts the local midnight (Sydney 00:00) to UTC (previous day 13:00). - Queries now included the window
[previous-day-13:00Z, same-day-12:59:59Z], so every row inserted before ~11:00 AEST was earlier than the lower bound and invisible to client code.
- Calling
- Server routes
/api/transcribeand/api/generate-daily-summarystill builtcurrentDatevianew Date().toISOString().split('T')[0], so summaries kept the UTC day stamp while front-end filters switched to “local day” logic. The mismatch meant a user could never get a “today” card until ~11:00.
- Mood modal auto-trigger logic (
src/features/daily-record/components/daily-mood-modal.tsx) saw no entry for “today” and spammed prompts every morning. - Journal list filters and “today streak” stats treated the Sydney morning as missing, breaking streak counts and filtering by date.
- Daily summaries and Echos cards lagged one day and never referenced the actual local date, producing misleading reflections and misaligned cron jobs.
- Confirmed Supabase tables via MCP queries:
daily_question.created_at = 2025-11-06 00:00:59+00even though the user entered data on the 7th AEST. - Ran
git logand isolated3948ca8as the commit changing the date filter implementation. - Blamed
src/lib/supabase/queries.tsto see thatstart/endwere generated locally but serialized to UTC. - Noticed server routes still using UTC strings for summary date fields, exacerbating list grouping.
- Designed a reusable timezone utility to compute “local day → UTC range” instead of peppering ad‑hoc logic across files.
- Added
src/lib/timezone.tsproviding:getLocalDayRange({ date?, timeZone? })returning{ date, start, end }.getUtcRangeForDate(dateString, timeZone?)for server lookups when a YYYY-MM-DD (local) date must be converted back to UTC.- A cached
Intl.DateTimeFormatto avoid perf regressions.
- Updated all “today” queries (
getTodayMoodEntry,getTodayAudioJournals, streak calculators, mood modal checks) to use local ranges. - Ensured summary generation and journal API routes convert stored
created_atto the correct local date before grouping or triggering follow-up jobs. - Adjusted Echos UI logic so “current card” detection compares against
getLocalDayRange().daterather thannew Date().toISOString().
diff --git a/src/lib/supabase/queries.ts b/src/lib/supabase/queries.ts
@@
-import type { SupabaseClient } from '@supabase/supabase-js';
-import { cache } from 'react';
+import type { SupabaseClient } from '@supabase/supabase-js';
+import { cache } from 'react';
+import { getLocalDayRange, getUtcRangeForDate } from '@/lib/timezone';
@@
- const start = new Date();
- start.setHours(0, 0, 0, 0);
- const end = new Date();
- end.setHours(23, 59, 59, 999);
+ const { start, end } = getLocalDayRange();
@@
- .gte('created_at', start.toISOString())
- .lte('created_at', end.toISOString())
+ .gte('created_at', start)
+ .lte('created_at', end)
@@
- const startOfDay = new Date(summary.date);
- startOfDay.setHours(0, 0, 0, 0);
- const endOfDay = new Date(summary.date);
- endOfDay.setHours(23, 59, 59, 999);
+ const { start: dayStart, end: dayEnd } = getUtcRangeForDate(
+ summary.date
+ );
@@
- .gte('created_at', startOfDay.toISOString())
- .lte('created_at', endOfDay.toISOString())
+ .gte('created_at', dayStart)
+ .lte('created_at', dayEnd)- The helper currently defaults to
Australia/Sydney. Future work should read user profile or organization timezone to avoid hardcoding. - Add regression tests (unit + integration) ensuring morning inserts in UTC+ offsets remain queryable as “today”.
- Document environment variables (
NEXT_PUBLIC_APP_TIMEZONE,APP_TIMEZONE) so deployment targets can override defaults without touching code.
Last Updated: 2025-11-07
Maintainer: Development Team
Next Review: Monthly