From c7eb982cf7c6d163d246cfb4d4aaa34094960518 Mon Sep 17 00:00:00 2001
From: Bohdan
Date: Tue, 2 Jun 2026 20:23:45 +0300
Subject: [PATCH 1/5] feat(reports): localStorage cache per range, auto-load on
switch, createdAt badge
---
.../app/dashboard/reports/report-form.tsx | 107 ++++++++++++++----
1 file changed, 83 insertions(+), 24 deletions(-)
diff --git a/dashboard/app/dashboard/reports/report-form.tsx b/dashboard/app/dashboard/reports/report-form.tsx
index 945f269..349ee88 100644
--- a/dashboard/app/dashboard/reports/report-form.tsx
+++ b/dashboard/app/dashboard/reports/report-form.tsx
@@ -26,7 +26,34 @@ type Status =
| { kind: "idle" }
| { kind: "loading" }
| { kind: "error"; message: string }
- | { kind: "success"; result: GenerateReportResponse };
+ | { kind: "success"; result: GenerateReportResponse; createdAt: string };
+
+// ─── localStorage cache ───────────────────────────────────────────────────────
+
+type CachedReport = { result: GenerateReportResponse; createdAt: string };
+
+function reportCacheKey(range: RangePreset, from: string, to: string): string {
+ return range === "custom"
+ ? `wt:report:custom:${from}:${to}`
+ : `wt:report:${range}`;
+}
+
+function loadCache(key: string): CachedReport | null {
+ try {
+ const raw = localStorage.getItem(key);
+ return raw ? (JSON.parse(raw) as CachedReport) : null;
+ } catch { return null; }
+}
+
+function saveCache(key: string, data: CachedReport): void {
+ try { localStorage.setItem(key, JSON.stringify(data)); } catch { /* quota */ }
+}
+
+function fmtCreatedAt(iso: string): string {
+ return new Date(iso).toLocaleString(undefined, {
+ month: "short", day: "numeric", hour: "2-digit", minute: "2-digit",
+ });
+}
function todayISO(): string {
const d = new Date();
@@ -53,7 +80,14 @@ export function ReportForm() {
const [range, setRange] = useState("last_7d");
const [from, setFrom] = useState("");
const [to, setTo] = useState(todayISO());
- const [status, setStatus] = useState({ kind: "idle" });
+ // Lazy init: read localStorage for the default range before first render
+ const [status, setStatus] = useState(() => {
+ try {
+ const cached = loadCache(reportCacheKey("last_7d", "", ""));
+ if (cached) return { kind: "success", result: cached.result, createdAt: cached.createdAt };
+ } catch { /* localStorage unavailable (SSR guard) */ }
+ return { kind: "idle" };
+ });
const [apiKey, setApiKey] = useState(null);
const [modalOpen, setModalOpen] = useState(false);
@@ -65,6 +99,35 @@ export function ReportForm() {
return () => { cancelled = true; };
}, []);
+ // Range button click — load cache in the event handler (not in an effect)
+ function handleRangeChange(newRange: RangePreset) {
+ setRange(newRange);
+ if (newRange === "custom") return;
+ const cached = loadCache(reportCacheKey(newRange, "", ""));
+ setStatus(cached
+ ? { kind: "success", result: cached.result, createdAt: cached.createdAt }
+ : { kind: "idle" });
+ }
+
+ // Custom date changes — check cache when both dates are present
+ function handleFromChange(newFrom: string) {
+ setFrom(newFrom);
+ if (!newFrom || !to) { setStatus({ kind: "idle" }); return; }
+ const cached = loadCache(reportCacheKey("custom", newFrom, to));
+ setStatus(cached
+ ? { kind: "success", result: cached.result, createdAt: cached.createdAt }
+ : { kind: "idle" });
+ }
+
+ function handleToChange(newTo: string) {
+ setTo(newTo);
+ if (!from || !newTo) { setStatus({ kind: "idle" }); return; }
+ const cached = loadCache(reportCacheKey("custom", from, newTo));
+ setStatus(cached
+ ? { kind: "success", result: cached.result, createdAt: cached.createdAt }
+ : { kind: "idle" });
+ }
+
const isCustom = range === "custom";
const isBusy = status.kind === "loading";
@@ -81,7 +144,9 @@ export function ReportForm() {
}
try {
const result = await generateReport(input);
- setStatus({ kind: "success", result });
+ const createdAt = new Date().toISOString();
+ saveCache(reportCacheKey(range, from, to), { result, createdAt });
+ setStatus({ kind: "success", result, createdAt });
} catch (err) {
const message = err instanceof Error ? err.message : "Failed to generate report";
setStatus({ kind: "error", message });
@@ -116,7 +181,7 @@ export function ReportForm() {
-
+
);
}
diff --git a/dashboard/app/dashboard/reports/report-form.tsx b/dashboard/app/dashboard/reports/report-form.tsx
index 349ee88..7641167 100644
--- a/dashboard/app/dashboard/reports/report-form.tsx
+++ b/dashboard/app/dashboard/reports/report-form.tsx
@@ -32,10 +32,11 @@ type Status =
type CachedReport = { result: GenerateReportResponse; createdAt: string };
-function reportCacheKey(range: RangePreset, from: string, to: string): string {
+function reportCacheKey(email: string, range: RangePreset, from: string, to: string): string {
+ const u = email.toLowerCase().trim();
return range === "custom"
- ? `wt:report:custom:${from}:${to}`
- : `wt:report:${range}`;
+ ? `wt:report:${u}:custom:${from}:${to}`
+ : `wt:report:${u}:${range}`;
}
function loadCache(key: string): CachedReport | null {
@@ -75,7 +76,7 @@ function downloadMarkdown(markdown: string, rangeLabel: string): void {
URL.revokeObjectURL(url);
}
-export function ReportForm() {
+export function ReportForm({ userEmail }: { userEmail: string }) {
const toast = useToast();
const [range, setRange] = useState("last_7d");
const [from, setFrom] = useState("");
@@ -83,7 +84,7 @@ export function ReportForm() {
// Lazy init: read localStorage for the default range before first render
const [status, setStatus] = useState(() => {
try {
- const cached = loadCache(reportCacheKey("last_7d", "", ""));
+ const cached = loadCache(reportCacheKey(userEmail, "last_7d", "", ""));
if (cached) return { kind: "success", result: cached.result, createdAt: cached.createdAt };
} catch { /* localStorage unavailable (SSR guard) */ }
return { kind: "idle" };
@@ -103,7 +104,7 @@ export function ReportForm() {
function handleRangeChange(newRange: RangePreset) {
setRange(newRange);
if (newRange === "custom") return;
- const cached = loadCache(reportCacheKey(newRange, "", ""));
+ const cached = loadCache(reportCacheKey(userEmail, newRange, "", ""));
setStatus(cached
? { kind: "success", result: cached.result, createdAt: cached.createdAt }
: { kind: "idle" });
@@ -113,7 +114,7 @@ export function ReportForm() {
function handleFromChange(newFrom: string) {
setFrom(newFrom);
if (!newFrom || !to) { setStatus({ kind: "idle" }); return; }
- const cached = loadCache(reportCacheKey("custom", newFrom, to));
+ const cached = loadCache(reportCacheKey(userEmail, "custom", newFrom, to));
setStatus(cached
? { kind: "success", result: cached.result, createdAt: cached.createdAt }
: { kind: "idle" });
@@ -122,7 +123,7 @@ export function ReportForm() {
function handleToChange(newTo: string) {
setTo(newTo);
if (!from || !newTo) { setStatus({ kind: "idle" }); return; }
- const cached = loadCache(reportCacheKey("custom", from, newTo));
+ const cached = loadCache(reportCacheKey(userEmail, "custom", from, newTo));
setStatus(cached
? { kind: "success", result: cached.result, createdAt: cached.createdAt }
: { kind: "idle" });
@@ -145,7 +146,7 @@ export function ReportForm() {
try {
const result = await generateReport(input);
const createdAt = new Date().toISOString();
- saveCache(reportCacheKey(range, from, to), { result, createdAt });
+ saveCache(reportCacheKey(userEmail, range, from, to), { result, createdAt });
setStatus({ kind: "success", result, createdAt });
} catch (err) {
const message = err instanceof Error ? err.message : "Failed to generate report";
From 04445fffd062d7cf47bcc8214f844f96b3dd4bef Mon Sep 17 00:00:00 2001
From: Bohdan
Date: Tue, 2 Jun 2026 20:30:09 +0300
Subject: [PATCH 4/5] fix(account): escape key, error display, localStorage
cleanup on logout/delete, hydration fix
---
dashboard/app/dashboard/account-menu.tsx | 51 +++++++++++++++++++-----
dashboard/app/dashboard/app-header.tsx | 2 +-
2 files changed, 42 insertions(+), 11 deletions(-)
diff --git a/dashboard/app/dashboard/account-menu.tsx b/dashboard/app/dashboard/account-menu.tsx
index 0bee4f6..5dc607d 100644
--- a/dashboard/app/dashboard/account-menu.tsx
+++ b/dashboard/app/dashboard/account-menu.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useState } from "react";
+import { useEffect, useState } from "react";
import { createPortal } from "react-dom";
import { AnimatePresence, motion } from "framer-motion";
import { useRouter } from "next/navigation";
@@ -22,16 +22,39 @@ const panel = {
transition: { type: "spring" as const, stiffness: 400, damping: 28, mass: 0.8 },
};
-export function AccountMenu() {
+// Remove all cached report keys for a given email (or all wt:report:* keys)
+function clearReportCache(email: string) {
+ try {
+ const prefix = `wt:report:${email.toLowerCase().trim()}:`;
+ Object.keys(localStorage)
+ .filter((k) => k.startsWith(prefix))
+ .forEach((k) => localStorage.removeItem(k));
+ } catch { /* localStorage unavailable */ }
+}
+
+export function AccountMenu({ userEmail }: { userEmail: string }) {
const router = useRouter();
const queryClient = useQueryClient();
- const [menu, setMenu] = useState
setMenu("delete-confirm")}
+ onClick={() => { setDeleteErr(null); setMenu("delete-confirm"); }}
disabled={pending}
className="w-full cursor-pointer rounded border border-pink/40 px-4 py-2.5 text-[10px] font-bold tracking-widest text-pink transition-colors hover:bg-pink/10 disabled:opacity-50"
>
@@ -145,6 +172,10 @@ export function AccountMenu() {
sessions, music data, and settings. This action cannot be undone.
+ {deleteErr ? (
+ {deleteErr}
+ ) : null}
+
From ce8bb55d1c56d8ac001b7c241830460b99f159b8 Mon Sep 17 00:00:00 2001
From: Bohdan
Date: Tue, 2 Jun 2026 20:35:14 +0300
Subject: [PATCH 5/5] fix(account): SSR-safe portal via typeof document check,
remove dead logout-button.tsx
---
dashboard/app/dashboard/account-menu.tsx | 2 +-
dashboard/app/dashboard/logout-button.tsx | 33 -----------------------
2 files changed, 1 insertion(+), 34 deletions(-)
delete mode 100644 dashboard/app/dashboard/logout-button.tsx
diff --git a/dashboard/app/dashboard/account-menu.tsx b/dashboard/app/dashboard/account-menu.tsx
index 5dc607d..183a24a 100644
--- a/dashboard/app/dashboard/account-menu.tsx
+++ b/dashboard/app/dashboard/account-menu.tsx
@@ -93,7 +93,7 @@ export function AccountMenu({ userEmail }: { userEmail: string }) {
⎋
- {createPortal(
+ {typeof document !== "undefined" && createPortal(
{/* ── Modal 1: account options ── */}
diff --git a/dashboard/app/dashboard/logout-button.tsx b/dashboard/app/dashboard/logout-button.tsx
deleted file mode 100644
index 0f3854e..0000000
--- a/dashboard/app/dashboard/logout-button.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-"use client";
-
-import { useState } from "react";
-import { useRouter } from "next/navigation";
-import { useQueryClient } from "@tanstack/react-query";
-
-export function LogoutButton() {
- const router = useRouter();
- const queryClient = useQueryClient();
- const [pending, setPending] = useState(false);
-
- return (
- {
- setPending(true);
- try {
- await fetch("/api/auth/logout", { method: "POST" });
- } finally {
- queryClient.clear();
- router.replace("/login");
- router.refresh();
- }
- }}
- title="Sign out"
- aria-label="Sign out"
- className="flex h-7 w-7 cursor-pointer items-center justify-center rounded border border-border bg-surface text-base text-muted transition-all hover:border-pink hover:bg-pink/10 hover:text-pink hover:shadow-[0_0_8px_color-mix(in_srgb,var(--c-pink)_30%,transparent)] disabled:cursor-not-allowed disabled:opacity-30"
- >
- {pending ? "…" : "⎋"}
-
- );
-}