Skip to content

Commit c005456

Browse files
schwaaampclaude
andcommitted
Fix experiment day counter timezone bug and remove metrics selection step
Parse experiment_start as local calendar date instead of UTC midnight to fix "Day 2 of 30" appearing on creation day for US timezone users. Remove the metrics picker wizard step — all available metrics are now discovered dynamically at analysis time from the full METRIC_REGISTRY. The wizard is simplified to 2 steps with a read-only metric summary on the duration step. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d6c60fc commit c005456

File tree

6 files changed

+188
-146
lines changed

6 files changed

+188
-146
lines changed

mobile/src/app/create-experiment.tsx

Lines changed: 35 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
/**
22
* Create Experiment Wizard
33
*
4-
* Multi-step wizard (3 steps) for creating a new self-experiment:
4+
* Multi-step wizard (2 steps) for creating a new self-experiment:
55
* Step 1 - Define variable and goal
6-
* Step 2 - Choose metrics to track
7-
* Step 3 - Set duration, baseline, and reminders
6+
* Step 2 - Set duration, baseline, and reminders (shows tracked metrics read-only)
87
*/
98

109
import React, { useState, useMemo, useCallback } from "react";
@@ -28,22 +27,20 @@ import * as Haptics from "expo-haptics";
2827

2928
import { useColors, ColorPalette } from "@/components/useColors";
3029
import { ProtectedRoute } from "@/components/ProtectedRoute";
31-
import MetricPicker from "@/components/Experiments/MetricPicker";
3230
import ExperimentGuidance from "@/components/Experiments/ExperimentGuidance";
3331
import { METRIC_REGISTRY } from "@/utils/experiments/metrics";
3432
import { supabase } from "@/utils/supabaseClient";
3533
import { requireUserId } from "@/utils/auth/getUserId";
3634
import useUser from "@/utils/auth/useUser";
37-
import type { CreateExperimentInput, MetricDefinition } from "@/utils/experiments/types";
35+
import type { CreateExperimentInput } from "@/utils/experiments/types";
3836

3937
// ---------------------------------------------------------------------------
4038
// Constants
4139
// ---------------------------------------------------------------------------
4240

43-
const TOTAL_STEPS = 3;
41+
const TOTAL_STEPS = 2;
4442
const DURATION_OPTIONS = [7, 14, 21, 30] as const;
4543
const DEFAULT_DURATION = 14;
46-
const MAX_METRICS = 5;
4744

4845
/**
4946
* Regex to detect multi-variable phrasing (e.g. "meditate and journal")
@@ -100,9 +97,6 @@ export default function CreateExperimentScreen() {
10097
const [goalText, setGoalText] = useState("");
10198

10299
// ---- Step 2 state ----
103-
const [selectedMetricKeys, setSelectedMetricKeys] = useState<string[]>([]);
104-
105-
// ---- Step 3 state ----
106100
const [durationDays, setDurationDays] = useState<number>(DEFAULT_DURATION);
107101
const [reminderEnabled, setReminderEnabled] = useState(true);
108102

@@ -132,26 +126,10 @@ export default function CreateExperimentScreen() {
132126

133127
const experimentStart = today;
134128

135-
// Show all metrics as available for now (device filtering comes later)
136-
const availableMetrics: MetricDefinition[] = METRIC_REGISTRY;
137-
const unavailableMetrics: MetricDefinition[] = [];
138-
139129
const styles = useMemo(() => createStyles(colors), [colors]);
140130

141131
// ---- Validation per step ----
142132
const canAdvanceStep1 = variableDescription.trim().length > 0;
143-
const canAdvanceStep2 = selectedMetricKeys.length >= 1;
144-
145-
// ---- Metric toggle handler ----
146-
const handleToggleMetric = useCallback((key: string) => {
147-
setSelectedMetricKeys((prev) => {
148-
if (prev.includes(key)) {
149-
return prev.filter((k) => k !== key);
150-
}
151-
if (prev.length >= MAX_METRICS) return prev;
152-
return [...prev, key];
153-
});
154-
}, []);
155133

156134
// ---- Navigation helpers ----
157135
const goNext = useCallback(() => {
@@ -194,22 +172,6 @@ export default function CreateExperimentScreen() {
194172
if (expError) throw expError;
195173
if (!experiment) throw new Error("Failed to create experiment");
196174

197-
// 2. Insert experiment metrics
198-
const metricRows = input.metrics.map((m) => ({
199-
experiment_id: experiment.id,
200-
metric_key: m.metric_key,
201-
metric_label: m.metric_label,
202-
data_source: m.data_source,
203-
aggregation: m.aggregation,
204-
unit: m.unit,
205-
}));
206-
207-
const { error: metricsError } = await supabase
208-
.from("experiment_metrics")
209-
.insert(metricRows);
210-
211-
if (metricsError) throw metricsError;
212-
213175
return experiment.id as string;
214176
},
215177
onSuccess: (experimentId) => {
@@ -234,10 +196,6 @@ export default function CreateExperimentScreen() {
234196
const handleCreate = useCallback(() => {
235197
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
236198

237-
const selectedMetrics = selectedMetricKeys
238-
.map((key) => METRIC_REGISTRY.find((m) => m.key === key))
239-
.filter(Boolean) as MetricDefinition[];
240-
241199
const input: CreateExperimentInput = {
242200
title,
243201
variable_description: variableDescription.trim(),
@@ -247,13 +205,6 @@ export default function CreateExperimentScreen() {
247205
baseline_end: toISODate(baselineEnd),
248206
experiment_start: toISODate(experimentStart),
249207
reminder_enabled: reminderEnabled,
250-
metrics: selectedMetrics.map((m) => ({
251-
metric_key: m.key,
252-
metric_label: m.label,
253-
data_source: m.source,
254-
aggregation: m.agg,
255-
unit: m.unit,
256-
})),
257208
};
258209

259210
createMutation.mutate(input);
@@ -266,7 +217,6 @@ export default function CreateExperimentScreen() {
266217
baselineEnd,
267218
experimentStart,
268219
reminderEnabled,
269-
selectedMetricKeys,
270220
createMutation,
271221
]);
272222

@@ -371,42 +321,6 @@ export default function CreateExperimentScreen() {
371321
);
372322

373323
const renderStep2 = () => (
374-
<View>
375-
<Text style={styles.stepTitle}>Choose Your Metrics</Text>
376-
<Text style={styles.stepSubtitle}>
377-
Select 1-{MAX_METRICS} metrics to track during your experiment.
378-
</Text>
379-
380-
<MetricPicker
381-
availableMetrics={availableMetrics}
382-
unavailableMetrics={unavailableMetrics}
383-
selectedKeys={selectedMetricKeys}
384-
onToggle={handleToggleMetric}
385-
maxSelections={MAX_METRICS}
386-
/>
387-
388-
{selectedMetricKeys.length > 0 && (
389-
<Text style={styles.selectionCount}>
390-
{selectedMetricKeys.length} of {MAX_METRICS} selected
391-
</Text>
392-
)}
393-
394-
{/* Next button */}
395-
<TouchableOpacity
396-
style={[styles.primaryButton, !canAdvanceStep2 && styles.primaryButtonDisabled]}
397-
onPress={goNext}
398-
disabled={!canAdvanceStep2}
399-
accessibilityRole="button"
400-
accessibilityLabel="Next step"
401-
>
402-
<Text style={[styles.primaryButtonText, !canAdvanceStep2 && styles.primaryButtonTextDisabled]}>
403-
Next
404-
</Text>
405-
</TouchableOpacity>
406-
</View>
407-
);
408-
409-
const renderStep3 = () => (
410324
<View>
411325
<Text style={styles.stepTitle}>Set Duration & Baseline</Text>
412326
<Text style={styles.stepSubtitle}>
@@ -471,6 +385,21 @@ export default function CreateExperimentScreen() {
471385
</Text>
472386
</View>
473387

388+
{/* Tracked Metrics (read-only) */}
389+
<View style={styles.infoCard}>
390+
<Text style={styles.infoLabel}>Tracked Metrics</Text>
391+
<Text style={styles.infoHint}>
392+
All available metrics from your connected devices will be analyzed automatically.
393+
</Text>
394+
<View style={styles.metricChipRow}>
395+
{METRIC_REGISTRY.map((m) => (
396+
<View key={m.key} style={styles.metricChip}>
397+
<Text style={styles.metricChipText}>{m.label}</Text>
398+
</View>
399+
))}
400+
</View>
401+
</View>
402+
474403
{/* Reminder toggle */}
475404
<View style={styles.reminderRow}>
476405
<View style={styles.reminderTextContainer}>
@@ -543,7 +472,6 @@ export default function CreateExperimentScreen() {
543472
>
544473
{currentStep === 1 && renderStep1()}
545474
{currentStep === 2 && renderStep2()}
546-
{currentStep === 3 && renderStep3()}
547475
</ScrollView>
548476
</KeyboardAvoidingView>
549477
</SafeAreaView>
@@ -706,13 +634,23 @@ const createStyles = (colors: ColorPalette) =>
706634
color: colors.text,
707635
},
708636

709-
// Metric selection count
710-
selectionCount: {
711-
fontSize: 13,
712-
fontFamily: "Poppins_500Medium",
637+
// Metric chips (read-only list)
638+
metricChipRow: {
639+
flexDirection: "row",
640+
flexWrap: "wrap",
641+
gap: 6,
642+
marginTop: 10,
643+
},
644+
metricChip: {
645+
backgroundColor: colors.fieldFill,
646+
borderRadius: 6,
647+
paddingHorizontal: 8,
648+
paddingVertical: 4,
649+
},
650+
metricChipText: {
651+
fontSize: 12,
652+
fontFamily: "Poppins_400Regular",
713653
color: colors.textSecondary,
714-
textAlign: "center",
715-
marginTop: 12,
716654
},
717655

718656
// Duration buttons

mobile/src/app/experiment/[id].tsx

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import type { ExperimentResult } from "@/utils/experiments/types";
2020
import VerdictBanner from "@/components/Experiments/VerdictBanner";
2121
import MetricResultCard from "@/components/Experiments/MetricResultCard";
2222
import AdherenceCalendar from "@/components/Experiments/AdherenceCalendar";
23+
import { METRIC_REGISTRY, getMetricByKey } from "@/utils/experiments/metrics";
2324

2425
// ---------------------------------------------------------------------------
2526
// Helpers
@@ -289,11 +290,11 @@ function ActiveExperimentView({
289290
</View>
290291
)}
291292

292-
{/* Metric Summary Cards */}
293-
{experiment.metrics.length > 0 && (
294-
<View>
295-
<Text style={styles.sectionTitle}>Tracked Metrics</Text>
296-
{experiment.metrics.map((metric) => (
293+
{/* Tracked Metrics */}
294+
<View>
295+
<Text style={styles.sectionTitle}>Tracked Metrics</Text>
296+
{experiment.metrics.length > 0 ? (
297+
experiment.metrics.map((metric) => (
297298
<View key={metric.id} style={[styles.card, { marginBottom: 8 }]}>
298299
<Text style={styles.metricName}>{metric.metric_label}</Text>
299300
{metric.unit && (
@@ -303,9 +304,23 @@ function ActiveExperimentView({
303304
Data will be analyzed when the experiment ends.
304305
</Text>
305306
</View>
306-
))}
307-
</View>
308-
)}
307+
))
308+
) : (
309+
<View style={[styles.card, { marginBottom: 8 }]}>
310+
<Text style={styles.metricName}>All available metrics</Text>
311+
<Text style={styles.metricNote}>
312+
All metrics from your connected devices will be analyzed automatically when the experiment ends.
313+
</Text>
314+
<View style={styles.metricChipRow}>
315+
{METRIC_REGISTRY.map((m) => (
316+
<View key={m.key} style={styles.metricChip}>
317+
<Text style={styles.metricChipText}>{m.label}</Text>
318+
</View>
319+
))}
320+
</View>
321+
</View>
322+
)}
323+
</View>
309324

310325
{/* Adherence Calendar */}
311326
<Text style={styles.sectionTitle}>Check-in History</Text>
@@ -533,6 +548,23 @@ const createActiveStyles = (colors: ColorPalette, isDark: boolean) =>
533548
marginTop: 8,
534549
fontStyle: "italic",
535550
},
551+
metricChipRow: {
552+
flexDirection: "row",
553+
flexWrap: "wrap",
554+
gap: 6,
555+
marginTop: 10,
556+
},
557+
metricChip: {
558+
backgroundColor: isDark ? colors.fieldFill : "#F3F4F6",
559+
borderRadius: 6,
560+
paddingHorizontal: 8,
561+
paddingVertical: 4,
562+
},
563+
metricChipText: {
564+
fontSize: 12,
565+
fontFamily: "Poppins_400Regular",
566+
color: colors.textSecondary,
567+
},
536568
actionsContainer: {
537569
marginTop: 8,
538570
gap: 10,
@@ -679,15 +711,17 @@ function CompletedExperimentView({
679711
<View style={styles.metricCardsList}>
680712
{results.metric_results.map((mr) => {
681713
// Find the matching metric definition for a label
682-
const metricDef = experiment.metrics.find(
714+
// Check experiment_metrics first (backward compat), then fall back to METRIC_REGISTRY
715+
const expMetric = experiment.metrics.find(
683716
(m) => m.metric_key === mr.metric_key,
684717
);
718+
const registryMetric = getMetricByKey(mr.metric_key);
685719
return (
686720
<MetricResultCard
687721
key={mr.metric_key}
688722
metricKey={mr.metric_key}
689-
metricLabel={metricDef?.metric_label ?? mr.metric_key}
690-
unit={metricDef?.unit ?? null}
723+
metricLabel={expMetric?.metric_label ?? registryMetric?.label ?? mr.metric_key}
724+
unit={expMetric?.unit ?? registryMetric?.unit ?? null}
691725
baselineAvg={mr.baseline_avg}
692726
experimentAvg={mr.experiment_avg}
693727
changePct={mr.change_pct}

mobile/src/components/Experiments/ExperimentCard.tsx

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,38 @@ export interface ExperimentCardProps {
2020
}
2121

2222
/**
23-
* Calculate the current day number of the experiment
23+
* Parse a "YYYY-MM-DD" (or ISO timestamp) string as a local-timezone
24+
* midnight Date.
25+
*/
26+
function parseLocalDate(dateStr: string): Date {
27+
const [datePart] = dateStr.split("T");
28+
const [year, month, day] = datePart.split("-").map(Number);
29+
return new Date(year, month - 1, day);
30+
}
31+
32+
/**
33+
* Calculate the current day number of the experiment.
34+
* Uses local dates to avoid UTC timezone offset bugs.
2435
*/
2536
function getCurrentDay(experimentStart: string): number {
26-
const start = new Date(experimentStart);
37+
const start = parseLocalDate(experimentStart);
2738
const now = new Date();
39+
now.setHours(0, 0, 0, 0);
2840
const diffMs = now.getTime() - start.getTime();
29-
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
41+
const diffDays = Math.round(diffMs / (1000 * 60 * 60 * 24));
3042
return Math.max(1, diffDays + 1);
3143
}
3244

3345
/**
34-
* Check if the user has already checked in today
46+
* Check if the user has already checked in today.
47+
* Uses local date formatting instead of toISOString() which returns UTC.
3548
*/
3649
function hasCheckedInToday(checkins: ExperimentCheckin[]): boolean {
37-
const today = new Date().toISOString().split("T")[0];
50+
const now = new Date();
51+
const year = now.getFullYear();
52+
const month = String(now.getMonth() + 1).padStart(2, "0");
53+
const day = String(now.getDate()).padStart(2, "0");
54+
const today = `${year}-${month}-${day}`;
3855
return checkins.some((c) => c.checkin_date === today);
3956
}
4057

@@ -89,10 +106,14 @@ export default function ExperimentCard({
89106
</View>
90107

91108
{/* Metrics summary */}
92-
{metrics.length > 0 && (
109+
{metrics.length > 0 ? (
93110
<Text style={styles.metricsText} numberOfLines={1}>
94111
Tracking: {metrics.map((m) => m.metric_label).join(", ")}
95112
</Text>
113+
) : (
114+
<Text style={styles.metricsText} numberOfLines={1}>
115+
Tracking all available metrics
116+
</Text>
96117
)}
97118

98119
{/* Adherence */}

0 commit comments

Comments
 (0)