Skip to content

Commit 9f38e2a

Browse files
InfantLabclaude
andcommitted
fix: celebration quality — milestones, active day, record labels, card UX
- Milestone labels now use actual thresholds (2,000+ hours) instead of always 100+ - Most active day includes the actual date (e.g. Wed, Mar 18) - Longest session label clarified as this month, AI payload renamed monthlyHighlights to prevent hallucinated all-time record claims - CelebrationCard redesigned with warm gradients, section icons, accent borders - cloud_factual users now see nudge to try creative mode - Also includes: HMAC token refactor, settings UI improvements, bun.lock update Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 74bca8a commit 9f38e2a

File tree

14 files changed

+258
-244
lines changed

14 files changed

+258
-244
lines changed

.claude/settings.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,15 @@
6767
"Bash(bash .specify/scripts/bash/create-new-feature.sh --json --short-name \"weekly-rhythms\" \"Weekly Rhythms — Encouragement & Celebration: Thursday mid-week nudge and Sunday end-of-week celebration summary with tiered AI options\")",
6868
"Bash(bash .specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks)",
6969
"Bash(DATABASE_URL=file:../data/db.sqlite drizzle-kit push)",
70-
"Bash(DATABASE_URL=file:../data/db.sqlite bun drizzle-kit push --config=drizzle.config.ts)"
70+
"Bash(DATABASE_URL=file:../data/db.sqlite bun drizzle-kit push --config=drizzle.config.ts)",
71+
"Bash(git log:*)",
72+
"Bash(bash .specify/scripts/bash/create-new-feature.sh --json --short-name \"daily-timelines\" \"Daily Timeline Bar — A minimalistic 24-hour timeline visualization showing when activities happened during the day\")",
73+
"Bash(bash .specify/scripts/bash/setup-plan.sh --json)",
74+
"Bash(bash .specify/scripts/bash/update-agent-context.sh speckit.plan)",
75+
"Bash(ln -s /workspaces/tada/specs/010-daily-timelines /workspaces/tada/specs/011-daily-timelines)",
76+
"Bash(bash .specify/scripts/bash/update-agent-context.sh claude)",
77+
"Bash(bash .specify/scripts/bash/check-prerequisites.sh --json)",
78+
"Bash(node --input-type=module -e \":*)"
7179
],
7280
"additionalDirectories": [
7381
"/tmp",

app/components/settings/WeeklyRhythmsSettings.vue

Lines changed: 165 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,24 @@ const emailCelebration = ref(false);
1313
const emailEncouragement = ref(false);
1414
const acknowledgeCloud = ref(false);
1515
16+
// Prevent watchers from firing during server-to-local sync
17+
const syncing = ref(false);
18+
1619
// Watch settings to sync form
1720
watch(
1821
() => settings.value,
1922
(s) => {
2023
if (!s) return;
24+
syncing.value = true;
2125
celebrationEnabled.value = s.celebrationEnabled;
2226
encouragementEnabled.value = s.encouragementEnabled;
2327
celebrationTier.value = s.celebrationTier;
2428
emailCelebration.value = s.deliveryChannels.celebration.email;
2529
emailEncouragement.value = s.deliveryChannels.encouragement.email;
2630
acknowledgeCloud.value = s.privacy.cloudAcknowledged;
31+
nextTick(() => {
32+
syncing.value = false;
33+
});
2734
},
2835
{ immediate: true },
2936
);
@@ -39,54 +46,120 @@ const needsCloudAck = computed(() => {
3946
return isCloud && !settings.value?.privacy.cloudAcknowledged && !acknowledgeCloud.value;
4047
});
4148
42-
async function handleSave() {
43-
const result = await saveSettings({
44-
celebrationEnabled: celebrationEnabled.value,
45-
encouragementEnabled: encouragementEnabled.value,
46-
celebrationTier: celebrationTier.value,
49+
// Auto-save helper
50+
async function autoSave(input: Parameters<typeof saveSettings>[0]) {
51+
const result = await saveSettings(input);
52+
if (!result?.saved && error.value) {
53+
showError(error.value);
54+
}
55+
}
56+
57+
// Per-field watchers for auto-save
58+
watch(encouragementEnabled, (val) => {
59+
if (syncing.value) return;
60+
const input: Parameters<typeof saveSettings>[0] = { encouragementEnabled: val };
61+
// Auto-enable email delivery when opting in
62+
if (val && settings.value?.email.configured && !settings.value?.email.unsubscribed) {
63+
emailEncouragement.value = true;
64+
input.deliveryChannels = {
65+
celebration: { inApp: true, email: emailCelebration.value, push: false },
66+
encouragement: { inApp: true, email: true, push: false },
67+
};
68+
}
69+
autoSave(input);
70+
});
71+
72+
watch(celebrationEnabled, (val) => {
73+
if (syncing.value) return;
74+
const input: Parameters<typeof saveSettings>[0] = { celebrationEnabled: val };
75+
// Auto-enable email delivery when opting in
76+
if (val && settings.value?.email.configured && !settings.value?.email.unsubscribed) {
77+
emailCelebration.value = true;
78+
input.deliveryChannels = {
79+
celebration: { inApp: true, email: true, push: false },
80+
encouragement: { inApp: true, email: emailEncouragement.value, push: false },
81+
};
82+
}
83+
autoSave(input);
84+
});
85+
86+
watch(celebrationTier, (val) => {
87+
if (syncing.value) return;
88+
autoSave({ celebrationTier: val });
89+
});
90+
91+
watch(emailCelebration, (val) => {
92+
if (syncing.value) return;
93+
autoSave({
4794
deliveryChannels: {
48-
celebration: {
49-
inApp: true,
50-
email: emailCelebration.value,
51-
push: false,
52-
},
53-
encouragement: {
54-
inApp: true,
55-
email: emailEncouragement.value,
56-
push: false,
57-
},
95+
celebration: { inApp: true, email: val, push: false },
96+
encouragement: { inApp: true, email: emailEncouragement.value, push: false },
5897
},
59-
acknowledgeCloudPrivacy: acknowledgeCloud.value || undefined,
6098
});
99+
});
61100
62-
if (result?.saved) {
63-
showSuccess("Weekly rhythm settings saved");
64-
} else if (error.value) {
65-
showError(error.value);
101+
watch(emailEncouragement, (val) => {
102+
if (syncing.value) return;
103+
autoSave({
104+
deliveryChannels: {
105+
celebration: { inApp: true, email: emailCelebration.value, push: false },
106+
encouragement: { inApp: true, email: val, push: false },
107+
},
108+
});
109+
});
110+
111+
// Cloud ack: only auto-save when checked (intentional friction)
112+
watch(acknowledgeCloud, (val) => {
113+
if (syncing.value || !val) return;
114+
autoSave({ acknowledgeCloudPrivacy: true });
115+
});
116+
117+
// Test email
118+
const sendingTestCelebration = ref(false);
119+
const sendingTestEncouragement = ref(false);
120+
121+
async function sendTestEmail(kind: "celebration" | "encouragement") {
122+
const sending = kind === "celebration" ? sendingTestCelebration : sendingTestEncouragement;
123+
sending.value = true;
124+
try {
125+
await $fetch("/api/weekly-rhythms/test-email", {
126+
method: "POST",
127+
body: { kind },
128+
});
129+
const label = kind === "celebration" ? "celebration" : "encouragement";
130+
showSuccess(`Test ${label} email sent! Check your inbox.`);
131+
} catch (err: unknown) {
132+
const message =
133+
err && typeof err === "object" && "data" in err
134+
? (err as { data?: { message?: string } }).data?.message
135+
: null;
136+
showError(message || "Something went wrong sending the test email. Please try again later.");
137+
} finally {
138+
sending.value = false;
66139
}
67140
}
68141
</script>
69142

70143
<template>
71144
<div>
72-
<div v-if="loading" class="text-sm text-stone-500 dark:text-stone-400 py-4">
73-
Loading weekly rhythm settings...
145+
<div v-if="loading" class="text-sm text-stone-500 dark:text-stone-400 py-4 px-4">
146+
Loading rhythm settings...
74147
</div>
75148

76-
<div v-else class="space-y-4">
77-
<!-- Celebration toggle -->
149+
<div v-else class="space-y-0">
150+
<!-- Mid-Week Encouragement toggle -->
78151
<div class="p-4 flex items-center justify-between">
79152
<div>
80153
<div class="font-medium text-sm text-stone-800 dark:text-stone-100">
81-
Weekly Celebration
154+
Mid-Week Encouragement
82155
</div>
83156
<p class="text-xs text-stone-500 dark:text-stone-400 mt-0.5">
84-
Receive a stats summary every Monday morning
157+
A gentle Thursday check-in with stretch goals
85158
</p>
86159
</div>
87160
<label class="relative inline-flex items-center cursor-pointer">
88161
<input
89-
v-model="celebrationEnabled"
162+
v-model="encouragementEnabled"
90163
type="checkbox"
91164
class="sr-only peer"
92165
/>
@@ -96,19 +169,19 @@ async function handleSave() {
96169
</label>
97170
</div>
98171

99-
<!-- Encouragement toggle -->
172+
<!-- Weekly Celebration toggle -->
100173
<div class="p-4 flex items-center justify-between">
101174
<div>
102175
<div class="font-medium text-sm text-stone-800 dark:text-stone-100">
103-
Thursday Encouragement
176+
Weekly Celebration
104177
</div>
105178
<p class="text-xs text-stone-500 dark:text-stone-400 mt-0.5">
106-
A mid-week check-in with gentle stretch goals
179+
Receive a stats summary every Monday morning
107180
</p>
108181
</div>
109182
<label class="relative inline-flex items-center cursor-pointer">
110183
<input
111-
v-model="encouragementEnabled"
184+
v-model="celebrationEnabled"
112185
type="checkbox"
113186
class="sr-only peer"
114187
/>
@@ -161,48 +234,76 @@ async function handleSave() {
161234
(unsubscribed)
162235
</span>
163236
</p>
164-
<div class="space-y-2">
165-
<label
237+
<div class="space-y-3">
238+
<!-- Email celebration toggle -->
239+
<div
166240
v-if="celebrationEnabled"
167-
class="flex items-center gap-2 text-sm text-stone-700 dark:text-stone-300"
241+
class="flex items-center justify-between"
168242
>
169-
<input
170-
v-model="emailCelebration"
171-
type="checkbox"
172-
class="rounded border-stone-300 text-emerald-500 focus:ring-emerald-500"
173-
:disabled="settings.email.unsubscribed"
174-
/>
175-
Email celebrations
176-
</label>
177-
<label
243+
<span class="text-sm text-stone-700 dark:text-stone-300">Email celebrations</span>
244+
<label class="relative inline-flex items-center cursor-pointer">
245+
<input
246+
v-model="emailCelebration"
247+
type="checkbox"
248+
class="sr-only peer"
249+
:disabled="settings.email.unsubscribed"
250+
/>
251+
<div
252+
class="w-9 h-5 bg-stone-200 peer-focus:outline-none rounded-full peer dark:bg-stone-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-stone-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all dark:border-stone-600 peer-checked:bg-emerald-500 peer-disabled:opacity-50"
253+
/>
254+
</label>
255+
</div>
256+
257+
<!-- Email encouragement toggle -->
258+
<div
178259
v-if="encouragementEnabled"
179-
class="flex items-center gap-2 text-sm text-stone-700 dark:text-stone-300"
260+
class="flex items-center justify-between"
180261
>
181-
<input
182-
v-model="emailEncouragement"
183-
type="checkbox"
184-
class="rounded border-stone-300 text-emerald-500 focus:ring-emerald-500"
185-
:disabled="settings.email.unsubscribed"
186-
/>
187-
Email encouragements
188-
</label>
262+
<span class="text-sm text-stone-700 dark:text-stone-300">Email encouragements</span>
263+
<label class="relative inline-flex items-center cursor-pointer">
264+
<input
265+
v-model="emailEncouragement"
266+
type="checkbox"
267+
class="sr-only peer"
268+
:disabled="settings.email.unsubscribed"
269+
/>
270+
<div
271+
class="w-9 h-5 bg-stone-200 peer-focus:outline-none rounded-full peer dark:bg-stone-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-stone-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all dark:border-stone-600 peer-checked:bg-emerald-500 peer-disabled:opacity-50"
272+
/>
273+
</label>
274+
</div>
189275
</div>
190-
</div>
191276

192-
<!-- Save button -->
193-
<div class="p-4 pt-0">
194-
<button
195-
class="px-4 py-2 bg-emerald-500 text-white text-sm font-medium rounded-lg hover:bg-emerald-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
196-
:disabled="saving || (needsCloudAck && !acknowledgeCloud)"
197-
@click="handleSave"
277+
<!-- Test email buttons -->
278+
<div
279+
v-if="(emailCelebration && celebrationEnabled) || (emailEncouragement && encouragementEnabled)"
280+
class="mt-3 flex gap-2"
198281
>
199-
{{ saving ? "Saving..." : "Save Weekly Rhythm Settings" }}
200-
</button>
282+
<button
283+
v-if="emailCelebration && celebrationEnabled"
284+
class="text-xs px-3 py-1.5 rounded-lg border border-stone-200 dark:border-stone-600 text-stone-600 dark:text-stone-400 hover:bg-stone-50 dark:hover:bg-stone-700 disabled:opacity-50 transition-colors"
285+
:disabled="sendingTestCelebration"
286+
@click="sendTestEmail('celebration')"
287+
>
288+
{{ sendingTestCelebration ? "Sending..." : "Send Test Celebration" }}
289+
</button>
290+
<button
291+
v-if="emailEncouragement && encouragementEnabled"
292+
class="text-xs px-3 py-1.5 rounded-lg border border-stone-200 dark:border-stone-600 text-stone-600 dark:text-stone-400 hover:bg-stone-50 dark:hover:bg-stone-700 disabled:opacity-50 transition-colors"
293+
:disabled="sendingTestEncouragement"
294+
@click="sendTestEmail('encouragement')"
295+
>
296+
{{ sendingTestEncouragement ? "Sending..." : "Send Test Encouragement" }}
297+
</button>
298+
</div>
201299
</div>
202300

203-
<!-- Error message -->
204-
<div v-if="error" class="p-4 pt-0">
205-
<p class="text-sm text-red-600 dark:text-red-400">{{ error }}</p>
301+
<!-- Status indicators -->
302+
<div v-if="saving" class="px-4 pb-3">
303+
<p class="text-xs text-stone-400 dark:text-stone-500">Saving...</p>
304+
</div>
305+
<div v-if="error" class="px-4 pb-3">
306+
<p class="text-xs text-red-600 dark:text-red-400">{{ error }}</p>
206307
</div>
207308
</div>
208309
</div>

0 commit comments

Comments
 (0)