Fix usage timestamp persistence and clean up debug code

This commit is contained in:
Avery Felts 2026-02-01 12:09:42 -07:00
parent 711b5d838a
commit 7ee0aff52f
5 changed files with 79 additions and 22 deletions

View File

@ -71,22 +71,25 @@ export async function POST(request: NextRequest) {
lastWeedUsageTime?: string; lastWeedUsageTime?: string;
}; };
// Validation // Validation & Normalization
if (body.substance && !['nicotine', 'weed'].includes(body.substance)) { if (body.substance && !['nicotine', 'weed'].includes(body.substance)) {
return NextResponse.json({ error: 'Invalid substance' }, { status: 400 }); return NextResponse.json({ error: 'Invalid substance' }, { status: 400 });
} }
if (body.trackingStartDate && !/^\d{4}-\d{2}-\d{2}$/.test(body.trackingStartDate)) { if (body.trackingStartDate && !/^\d{4}-\d{2}-\d{2}$/.test(body.trackingStartDate)) {
return NextResponse.json({ error: 'Invalid trackingStartDate format' }, { status: 400 }); return NextResponse.json({ error: 'Invalid trackingStartDate format' }, { status: 400 });
} }
if (body.dailyGoal !== undefined && (typeof body.dailyGoal !== 'number' || body.dailyGoal < 0)) {
// Loose type checking for numbers (allow strings that parse to numbers)
const dailyGoal = Number(body.dailyGoal);
if (body.dailyGoal !== undefined && body.dailyGoal !== null && (isNaN(dailyGoal) || dailyGoal < 0)) {
return NextResponse.json({ error: 'Invalid dailyGoal' }, { status: 400 }); return NextResponse.json({ error: 'Invalid dailyGoal' }, { status: 400 });
} }
if (body.userName && (typeof body.userName !== 'string' || body.userName.length > 100)) {
return NextResponse.json({ error: 'Invalid userName' }, { status: 400 }); const userAge = Number(body.userAge);
} if (body.userAge !== undefined && body.userAge !== null && (isNaN(userAge) || userAge < 0 || userAge > 120)) {
if (body.userAge !== undefined && (typeof body.userAge !== 'number' || body.userAge < 0 || body.userAge > 120)) {
return NextResponse.json({ error: 'Invalid userAge' }, { status: 400 }); return NextResponse.json({ error: 'Invalid userAge' }, { status: 400 });
} }
if (body.religion && !['christian', 'secular'].includes(body.religion)) { if (body.religion && !['christian', 'secular'].includes(body.religion)) {
return NextResponse.json({ error: 'Invalid religion' }, { status: 400 }); return NextResponse.json({ error: 'Invalid religion' }, { status: 400 });
} }

View File

@ -218,8 +218,17 @@ export function Dashboard({ user }: DashboardProps) {
...preferences, ...preferences,
[substance === 'nicotine' ? 'lastNicotineUsageTime' : 'lastWeedUsageTime']: now, [substance === 'nicotine' ? 'lastNicotineUsageTime' : 'lastWeedUsageTime']: now,
}; };
await savePreferencesAsync(latestPrefs);
setPreferences(latestPrefs); // Force specific fields to be present to avoid partial update issues
// This ensures that even if preferences is stale, we explicitly set the usage time
const payload: UserPreferences = {
...latestPrefs,
lastNicotineUsageTime: substance === 'nicotine' ? now : (latestPrefs.lastNicotineUsageTime ?? null),
lastWeedUsageTime: substance === 'weed' ? now : (latestPrefs.lastWeedUsageTime ?? null),
};
await savePreferencesAsync(payload);
setPreferences(payload);
} }
setActiveLoggingSubstance(null); setActiveLoggingSubstance(null);

View File

@ -18,6 +18,7 @@ import {
Cigarette, Cigarette,
Leaf Leaf
} from 'lucide-react'; } from 'lucide-react';
import { getTodayString, getLocalDateString } from '@/lib/date-utils';
interface HealthTimelineCardProps { interface HealthTimelineCardProps {
usageData: UsageEntry[]; usageData: UsageEntry[];
@ -225,29 +226,57 @@ function HealthTimelineCardComponent({
// Calculate last usage timestamps only when data changes // Calculate last usage timestamps only when data changes
const lastUsageTimes = useMemo(() => { const lastUsageTimes = useMemo(() => {
const getTimestamp = (substance: 'nicotine' | 'weed') => { const getTimestamp = (substance: 'nicotine' | 'weed') => {
// 1. Check for stored timestamp first let lastTime = 0;
const stored = substance === 'nicotine' ? preferences?.lastNicotineUsageTime : preferences?.lastWeedUsageTime;
if (stored) return new Date(stored).getTime();
// 2. Fallback to usage data // 1. Check for stored timestamp
const stored = substance === 'nicotine' ? preferences?.lastNicotineUsageTime : preferences?.lastWeedUsageTime;
if (stored) {
lastTime = new Date(stored).getTime();
}
// 2. Check usage data (usually more up-to-date for "just logged")
const lastEntry = usageData const lastEntry = usageData
.filter(e => e.substance === substance && e.count > 0) .filter(e => e.substance === substance && e.count > 0)
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())[0]; .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())[0];
if (lastEntry) { if (lastEntry) {
const todayStr = getTodayString();
// Calculate local midnight for today
const now = new Date();
const localMidnight = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
// If usage recorded today
if (lastEntry.date === todayStr) {
// Check if the stored timestamp belongs to today (is after midnight)
if (lastTime >= localMidnight) {
return lastTime;
}
// Fallback: If we have usage "Today" but no valid timestamp,
// we must assume it just happened or we missed the timestamp.
// Returning Date.now() resets the timer to 0.
return Date.now();
}
const d = new Date(lastEntry.date); const d = new Date(lastEntry.date);
d.setHours(23, 59, 59, 999); d.setHours(23, 59, 59, 999);
return d.getTime(); const entryTime = d.getTime();
// Take the more recent of the two
lastTime = Math.max(lastTime, entryTime);
} }
// 3. Fallback to start date
if (preferences?.trackingStartDate) {
// 3. Fallback to start date if no usage found
if (lastTime === 0 && preferences?.trackingStartDate) {
const d = new Date(preferences.trackingStartDate); const d = new Date(preferences.trackingStartDate);
d.setHours(0, 0, 0, 0); d.setHours(0, 0, 0, 0);
return d.getTime(); lastTime = d.getTime();
} }
return null; return lastTime || null;
}; };
return { return {
@ -302,6 +331,7 @@ function HealthTimelineCardComponent({
<TimelineColumn substance="nicotine" minutesFree={nicotineMinutes} theme={theme} /> <TimelineColumn substance="nicotine" minutesFree={nicotineMinutes} theme={theme} />
<TimelineColumn substance="weed" minutesFree={weedMinutes} theme={theme} /> <TimelineColumn substance="weed" minutesFree={weedMinutes} theme={theme} />
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
); );

View File

@ -74,17 +74,29 @@ export async function upsertPreferencesD1(userId: string, data: Partial<UserPref
if (data.userName !== undefined) { updates.push('userName = ?'); values.push(data.userName); } if (data.userName !== undefined) { updates.push('userName = ?'); values.push(data.userName); }
if (data.userAge !== undefined) { updates.push('userAge = ?'); values.push(data.userAge); } if (data.userAge !== undefined) { updates.push('userAge = ?'); values.push(data.userAge); }
if (data.religion !== undefined) { updates.push('religion = ?'); values.push(data.religion); } if (data.religion !== undefined) { updates.push('religion = ?'); values.push(data.religion); }
if (data.lastNicotineUsageTime !== undefined) { updates.push('lastNicotineUsageTime = ?'); values.push(data.lastNicotineUsageTime); }
if (data.lastWeedUsageTime !== undefined) { updates.push('lastWeedUsageTime = ?'); values.push(data.lastWeedUsageTime); } // Explicit checks for usage times
if (data.lastNicotineUsageTime !== undefined) {
updates.push('lastNicotineUsageTime = ?');
values.push(data.lastNicotineUsageTime);
}
if (data.lastWeedUsageTime !== undefined) {
updates.push('lastWeedUsageTime = ?');
values.push(data.lastWeedUsageTime);
}
if (data.quitPlanJson !== undefined) { updates.push('quitPlanJson = ?'); values.push(data.quitPlanJson); } if (data.quitPlanJson !== undefined) { updates.push('quitPlanJson = ?'); values.push(data.quitPlanJson); }
updates.push('updatedAt = ?'); updates.push('updatedAt = ?');
values.push(now); values.push(now);
values.push(userId); values.push(userId);
await db.prepare( if (updates.length > 1) { // At least updatedAt is always there
`UPDATE UserPreferences SET ${updates.join(', ')} WHERE userId = ?` await db.prepare(
).bind(...values).run(); `UPDATE UserPreferences SET ${updates.join(', ')} WHERE userId = ?`
).bind(...values).run();
}
} else { } else {
// Insert // Insert
await db.prepare( await db.prepare(

View File

@ -181,6 +181,9 @@ export async function savePreferencesAsync(preferences: UserPreferences): Promis
}); });
if (response.ok) { if (response.ok) {
preferencesCache = preferences; preferencesCache = preferences;
} else {
const err = await response.json();
console.error('[Storage] Error saving preferences:', response.status, err);
} }
} catch (error) { } catch (error) {
console.error('Error saving preferences:', error); console.error('Error saving preferences:', error);