Skip to content

Commit bfa0b21

Browse files
schwaaampclaude
andcommitted
Insight Engine v3, timezone registration, Reanimated easing fix
- Add dynamic metric discovery, intraday analyzers (excursion clustering, stability trends, temporal distribution, sequential change detection) - Add metric type catalog and auto-blacklisting for low-quality patterns - Update pattern spotter and ranker for v3 pipeline - Fix Reanimated v4 easing error in BioPortrait (bezier returns worklet object) - Add timezone registration and tests - Add admin insights route and blacklist migration - Add glucose trend analysis scripts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 902679b commit bfa0b21

32 files changed

+8790
-113
lines changed

docs/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ Active feature planning and architectural strategy documents.
3939
| [Integration Test Plan](./planning/INTEGRATION_TEST_PLAN.md) | Mobile integration test strategy (Phases 1-4) | 🔄 Phase 4 In Progress |
4040
| [Phase 4 E2E Test Spec](./planning/PHASE_4_E2E_TEST_SPEC.md) | E2E test implementation details | 🔄 In Progress (3/13 passing) |
4141
| [Unit Test Coverage Plan](./planning/UNIT_TEST_COVERAGE_PLAN.md) | Comprehensive unit test coverage roadmap | ✅ Active |
42+
| [Insight Engine v3](./planning/insight-engine-v3.md) | Dynamic metric discovery, auto-blacklisting, intraday analyzers | 📋 Planning |
43+
| [Timezone Strategy](./planning/timezone-strategy.md) | Cross-system timezone normalization for all data sources | 📋 Planning (prerequisite for Insight v3) |
4244

4345
---
4446

docs/planning/insight-engine-v3.md

Lines changed: 2328 additions & 0 deletions
Large diffs are not rendered by default.

mobile/src/components/BioPortrait.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ export default function BioPortrait({ activationLevel, size = 200 }: BioPortrait
4242
const breathScale = useSharedValue(1.0);
4343

4444
useEffect(() => {
45-
const easing = Easing.bezier(0.25, 0.1, 0.25, 1) as unknown as Parameters<typeof Easing.inOut>[0];
46-
const timing = { duration: 600, easing: Easing.inOut(easing) };
45+
const easing = Easing.bezier(0.25, 0.1, 0.25, 1);
46+
const timing = { duration: 600, easing };
4747

4848
if (activationLevel >= 1) {
4949
// Core activates with soft pulsing breath

mobile/src/utils/__tests__/timezoneRegistration.test.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22
* Tests for timezoneRegistration.ts
33
*
44
* Covers:
5-
* - Successful timezone registration (upserts IANA timezone to user_profiles)
5+
* - Successful timezone registration (updates user_profiles)
6+
* - Returns null and logs warning when profile row doesn't exist (count=0)
67
* - Deduplication of concurrent calls
78
* - Re-registration after previous call completes
8-
* - Graceful handling of missing Intl.DateTimeFormat timezone
99
* - Graceful handling of Supabase update errors
1010
*/
1111

12-
const mockEq = jest.fn().mockResolvedValue({ error: null });
12+
const mockEq = jest.fn().mockResolvedValue({ error: null, count: 1 });
1313
const mockUpdate = jest.fn().mockReturnValue({ eq: mockEq });
1414

1515
jest.mock('@/utils/supabaseClient', () => ({
@@ -28,39 +28,46 @@ import { registerTimezone, getDeviceTimezone } from '../timezoneRegistration';
2828
describe('timezoneRegistration', () => {
2929
beforeEach(() => {
3030
jest.clearAllMocks();
31+
mockEq.mockResolvedValue({ error: null, count: 1 });
3132
});
3233

3334
describe('getDeviceTimezone', () => {
3435
it('returns the IANA timezone from Intl.DateTimeFormat', () => {
3536
const tz = getDeviceTimezone();
36-
// Jest runs in Node which has Intl support — should return a valid IANA string
3737
expect(typeof tz).toBe('string');
3838
expect(tz!.length).toBeGreaterThan(0);
39-
// IANA timezones contain a slash (e.g., "America/New_York", "UTC" is the exception)
4039
expect(tz === 'UTC' || tz!.includes('/')).toBe(true);
4140
});
4241
});
4342

4443
describe('registerTimezone', () => {
45-
it('upserts timezone to user_profiles', async () => {
44+
it('updates timezone on user_profiles with count option', async () => {
4645
await registerTimezone('user-123');
4746

4847
expect(mockUpdate).toHaveBeenCalledWith(
4948
expect.objectContaining({
5049
timezone: expect.any(String),
5150
tz_source: 'device',
5251
tz_updated_at: expect.any(String),
53-
})
52+
}),
53+
{ count: 'exact' },
5454
);
5555
expect(mockEq).toHaveBeenCalledWith('user_id', 'user-123');
5656
});
5757

58-
it('returns the detected timezone string', async () => {
58+
it('returns the detected timezone string on success', async () => {
5959
const tz = await registerTimezone('user-123');
6060
expect(typeof tz).toBe('string');
6161
expect(tz!.length).toBeGreaterThan(0);
6262
});
6363

64+
it('returns null when no profile row exists (count=0)', async () => {
65+
mockEq.mockResolvedValueOnce({ error: null, count: 0 });
66+
67+
const tz = await registerTimezone('user-123');
68+
expect(tz).toBeNull();
69+
});
70+
6471
it('deduplicates concurrent calls', async () => {
6572
const [r1, r2, r3] = await Promise.all([
6673
registerTimezone('user-123'),
@@ -84,7 +91,7 @@ describe('timezoneRegistration', () => {
8491
});
8592

8693
it('returns null on Supabase update error', async () => {
87-
mockEq.mockResolvedValueOnce({ error: { message: 'DB error' } });
94+
mockEq.mockResolvedValueOnce({ error: { message: 'DB error' }, count: null });
8895

8996
const tz = await registerTimezone('user-123');
9097
expect(tz).toBeNull();

mobile/src/utils/auth/useAuth.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,19 @@ import { registerTimezone } from '@/utils/timezoneRegistration';
2222

2323
let sessionInitPromise: Promise<SupabaseSession | null> | null = null;
2424

25+
/** Extract user ID (sub claim) from a JWT without a library. */
26+
function extractUserIdFromJwt(token: string | undefined | null): string | null {
27+
if (!token) return null;
28+
try {
29+
const payload = token.split('.')[1];
30+
if (!payload) return null;
31+
const decoded = JSON.parse(atob(payload));
32+
return decoded.sub ?? null;
33+
} catch {
34+
return null;
35+
}
36+
}
37+
2538
/**
2639
* Runs the one-time session restore: SecureStore read, Supabase setSession,
2740
* Zustand sync, and push token registration. Concurrent callers share the
@@ -51,9 +64,14 @@ function initSession(): Promise<SupabaseSession | null> {
5164
}
5265

5366
// Fire-and-forget registrations (non-blocking, deduplicated)
54-
if (currentSession?.user?.id) {
55-
registerPushToken(currentSession.user.id).catch(() => {});
56-
registerTimezone(currentSession.user.id).catch(() => {});
67+
// The session may not include .user (Google OAuth implicit flow returns
68+
// tokens only). Extract user ID from the JWT sub claim as fallback.
69+
const userId = currentSession?.user?.id ?? extractUserIdFromJwt(currentSession?.access_token);
70+
if (userId) {
71+
registerPushToken(userId).catch(() => {});
72+
registerTimezone(userId).catch(() => {});
73+
} else if (currentSession) {
74+
console.warn('[Auth] Could not extract user ID from session — timezone and push token registration skipped');
5775
}
5876

5977
return currentSession;

mobile/src/utils/timezoneRegistration.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,20 +52,25 @@ async function doRegisterTimezone(userId: string): Promise<string | null> {
5252
return null;
5353
}
5454

55-
const { error } = await supabase
55+
const { error, count } = await supabase
5656
.from('user_profiles')
5757
.update({
5858
timezone,
5959
tz_source: 'device',
6060
tz_updated_at: new Date().toISOString(),
61-
})
61+
}, { count: 'exact' })
6262
.eq('user_id', userId);
6363

6464
if (error) {
6565
console.error('[Timezone] Failed to update timezone:', error);
6666
return null;
6767
}
6868

69+
if (count === 0) {
70+
console.warn('[Timezone] No profile row found for user — timezone not saved. Will retry on next app launch.');
71+
return null;
72+
}
73+
6974
console.log('[Timezone] Registered:', timezone);
7075
return timezone;
7176
} catch (error) {

0 commit comments

Comments
 (0)