Skip to content

Commit bbbf40b

Browse files
InfantLabclaude
andcommitted
feat: v0.6.0 — What's New popup, new-user defaults, rate limit fix
- Bump version to 0.6.0 - Add WhatsNewOverlay for existing users: prompts to enable weekly celebrations + mid-week encouragement on upgrade, navigates to settings - New user registrations auto-enable both celebrations and encouragement (stats_only tier) at signup - Fix rate limiter NaN crash: guard against corrupt non-finite values in rate_limits table rows - Fix .env.example: HMAC_SECRET was misnamed WEEKLY_RHYTHMS_TOKEN_SECRET - Fix snapshot record label: use subcategory instead of name for session labels Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9f38e2a commit bbbf40b

8 files changed

Lines changed: 150 additions & 5 deletions

File tree

app/.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ APP_URL=http://localhost:3000
8585
# ============================================
8686
# HMAC secret for one-click email unsubscribe tokens
8787
# Generate with: openssl rand -hex 32
88-
WEEKLY_RHYTHMS_TOKEN_SECRET=
88+
HMAC_SECRET=
8989

9090
# ============================================
9191
# Analytics (optional)
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
<script setup lang="ts">
2+
/**
3+
* What's New Overlay
4+
*
5+
* Shows once for existing users who upgrade to a new version.
6+
* Currently highlights the Weekly Celebrations feature (v0.6.0).
7+
* If user says "Yes", enables both celebrations + encouragement
8+
* and navigates to settings so they can see the options.
9+
*/
10+
11+
const { hasNewVersion, acknowledgeNewVersion } = useOnboarding();
12+
const router = useRouter();
13+
14+
const isVisible = ref(false);
15+
const isAuthenticated = ref(false);
16+
const enabling = ref(false);
17+
18+
onMounted(async () => {
19+
try {
20+
const response = await $fetch<{ user: { id: string } | null }>("/api/auth/session");
21+
isAuthenticated.value = !!response.user;
22+
} catch {
23+
isAuthenticated.value = false;
24+
}
25+
26+
if (isAuthenticated.value && hasNewVersion.value) {
27+
setTimeout(() => {
28+
isVisible.value = true;
29+
}, 800);
30+
}
31+
});
32+
33+
async function handleEnable() {
34+
enabling.value = true;
35+
try {
36+
await $fetch("/api/weekly-rhythms/settings", {
37+
method: "PUT",
38+
body: {
39+
celebrationEnabled: true,
40+
encouragementEnabled: true,
41+
},
42+
});
43+
} catch {
44+
// Settings will be off — user can enable manually in settings
45+
}
46+
enabling.value = false;
47+
isVisible.value = false;
48+
acknowledgeNewVersion();
49+
await router.push("/settings");
50+
}
51+
52+
function handleDismiss() {
53+
isVisible.value = false;
54+
acknowledgeNewVersion();
55+
}
56+
</script>
57+
58+
<template>
59+
<Teleport to="body">
60+
<Transition
61+
enter-active-class="transition-all duration-500 ease-out"
62+
enter-from-class="opacity-0"
63+
enter-to-class="opacity-100"
64+
leave-active-class="transition-all duration-300 ease-in"
65+
leave-from-class="opacity-100"
66+
leave-to-class="opacity-0"
67+
>
68+
<div
69+
v-if="isVisible"
70+
class="fixed inset-0 z-[100] flex items-center justify-center p-6"
71+
>
72+
<!-- Backdrop -->
73+
<div
74+
class="absolute inset-0 bg-gradient-to-b from-tada-900/80 to-stone-900/90 backdrop-blur-sm"
75+
@click="handleDismiss"
76+
/>
77+
78+
<!-- Content card -->
79+
<div
80+
class="relative bg-white dark:bg-stone-800 rounded-2xl shadow-2xl max-w-sm w-full p-6 text-center"
81+
>
82+
<div class="text-4xl mb-4">🎉</div>
83+
84+
<h2 class="text-xl font-bold text-stone-800 dark:text-stone-100 mb-2">
85+
New in Ta-Da!
86+
</h2>
87+
88+
<p class="text-sm text-stone-600 dark:text-stone-300 mb-1">
89+
<strong>Weekly Celebrations</strong> — get a stats summary every Monday
90+
to see how your week went.
91+
</p>
92+
<p class="text-sm text-stone-600 dark:text-stone-300 mb-5">
93+
Plus a gentle <strong>mid-week encouragement</strong> on Thursdays.
94+
</p>
95+
96+
<p class="text-sm text-stone-500 dark:text-stone-400 mb-5">
97+
Want to turn them on?
98+
</p>
99+
100+
<div class="flex flex-col gap-2">
101+
<button
102+
class="w-full py-2.5 px-4 rounded-xl bg-tada-600 hover:bg-tada-700 text-white font-medium text-sm transition-colors disabled:opacity-60"
103+
:disabled="enabling"
104+
@click="handleEnable"
105+
>
106+
{{ enabling ? "Turning on..." : "Yes, turn it on!" }}
107+
</button>
108+
<button
109+
class="w-full py-2 px-4 rounded-xl text-stone-500 dark:text-stone-400 hover:bg-stone-100 dark:hover:bg-stone-700 text-sm transition-colors"
110+
@click="handleDismiss"
111+
>
112+
Maybe later
113+
</button>
114+
</div>
115+
</div>
116+
</div>
117+
</Transition>
118+
</Teleport>
119+
</template>

app/layouts/default.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,7 @@ onUnmounted(() => {
347347
<ErrorTrackerPanel />
348348
<CookieConsent />
349349
<onboarding-WelcomeOverlay />
350+
<OnboardingWhatsNewOverlay />
350351
<ContextualHelpPanel v-model:open="showHelpPanel" />
351352
</div>
352353
</template>

app/nuxt.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export default defineNuxtConfig({
4141
runtimeConfig: {
4242
public: {
4343
appName: "Tada",
44-
appVersion: "0.5.0",
44+
appVersion: "0.6.0",
4545
appUrl: process.env["APP_URL"] || "http://localhost:3000",
4646
isCloudMode:
4747
process.env["TADA_CLOUD_MODE"] === "true" ||

app/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "tada",
3-
"version": "0.5.0",
3+
"version": "0.6.0",
44
"private": true,
55
"type": "module",
66
"description": "Personal lifelogger - Track Activities, Discover Achievements",

app/server/api/auth/register.post.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { nanoid } from "nanoid";
77
import { createLogger } from "~/server/utils/logger";
88
import { hashPassword } from "~/server/utils/password";
99
import { apiError, internalError } from "~/server/utils/response";
10+
import { upsertWeeklyRhythmSettings, refreshNextDueTimes } from "~/server/services/weekly-rhythms/settings";
1011

1112
const logger = createLogger("api:auth:register");
1213

@@ -68,6 +69,20 @@ export default defineEventHandler(async (event) => {
6869
const session = await createSession(userId);
6970
setSessionCookie(event, session.id);
7071

72+
// Auto-enable weekly celebrations + encouragement for new users
73+
try {
74+
await upsertWeeklyRhythmSettings(userId, {
75+
celebrationEnabled: true,
76+
encouragementEnabled: true,
77+
celebrationTier: "stats_only",
78+
onboardingCompletedAt: new Date().toISOString(),
79+
});
80+
await refreshNextDueTimes(userId);
81+
} catch (settingsErr) {
82+
// Non-fatal — user can enable manually in settings
83+
logger.error("Failed to create default weekly rhythm settings", settingsErr);
84+
}
85+
7186
logger.info("User registered successfully", { username: body.username });
7287

7388
return {

app/server/services/rate-limit.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,17 @@ export function checkRateLimit(
6363
.where(eq(rateLimits.key, cacheKey))
6464
.get();
6565

66-
if (!existing || existing.windowEnd <= now) {
66+
// Guard against corrupt data — treat non-finite values as expired
67+
if (
68+
existing &&
69+
(!Number.isFinite(existing.windowEnd) ||
70+
!Number.isFinite(existing.windowStart) ||
71+
!Number.isFinite(existing.count))
72+
) {
73+
db.delete(rateLimits).where(eq(rateLimits.key, cacheKey)).run();
74+
}
75+
76+
if (!existing || !Number.isFinite(existing.windowEnd) || existing.windowEnd <= now) {
6777
// No entry or window has expired — start a new window
6878
const windowEnd = now + WINDOW_DURATION;
6979

app/server/services/weekly-rhythms/snapshots.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ async function extractPersonalRecords(
304304
}
305305
}
306306
if (longestSession && longestSession.durationSeconds) {
307-
const sessionName = longestSession.name ?? longestSession.category ?? "session";
307+
const sessionName = longestSession.subcategory ?? longestSession.category ?? "session";
308308
records.push({
309309
type: "longest_session",
310310
label: `Longest ${sessionName} session this month`,

0 commit comments

Comments
 (0)