Skip to content

Commit d8fe636

Browse files
committed
Release 2.9.0: hide individual AI summaries shared across users
1 parent 157074e commit d8fe636

4 files changed

Lines changed: 77 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
### 2.9.0: 2026-05-27
2+
3+
* Add hide button on AI summary cards to mark individual summaries hidden in the shared database
4+
15
### 2.8.0: 2026-05-22
26

37
* Add manual received and not-received toggle for income that overrides auto-match

src/app/api/summary/route.ts

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,15 @@ export async function GET(request: Request) {
3232

3333
const requestLocale = url.searchParams.get("locale") || "en";
3434

35-
// Check for cached summary — shared across all users
35+
// Check for cached summary — shared across all users, excluding hidden ones
3636
if (!forceRefresh) {
3737
const cached = db
38-
.prepare("SELECT content, created_at FROM ai_summaries WHERE locale = ? ORDER BY created_at DESC LIMIT 1")
39-
.get(requestLocale) as { content: string; created_at: string } | undefined;
38+
.prepare("SELECT id, content, created_at FROM ai_summaries WHERE locale = ? AND is_hidden = 0 ORDER BY created_at DESC LIMIT 1")
39+
.get(requestLocale) as { id: number; content: string; created_at: string } | undefined;
4040

4141
if (cached) {
4242
console.debug("[summary] Returning cached", requestLocale, "summary from", cached.created_at);
43-
return NextResponse.json({ summary: cached.content, cached: true, created_at: cached.created_at });
43+
return NextResponse.json({ id: cached.id, summary: cached.content, cached: true, created_at: cached.created_at });
4444
}
4545
}
4646

@@ -350,16 +350,37 @@ ${(() => { const goals = db.prepare("SELECT name, target_amount, saved_amount, t
350350
proc.stdin.end();
351351
});
352352

353+
let newId: number | undefined;
353354
if (summaryText) {
354-
db.prepare("INSERT INTO ai_summaries (user_id, locale, content) VALUES (?, ?, ?)")
355+
const result = db.prepare("INSERT INTO ai_summaries (user_id, locale, content) VALUES (?, ?, ?)")
355356
.run(user.id, requestLocale, summaryText);
356-
357-
console.info("[summary] Summary generated and cached, length:", summaryText.length);
357+
newId = Number(result.lastInsertRowid);
358+
console.info("[summary] Summary generated and cached, length:", summaryText.length, "id:", newId);
358359
}
359360

360-
return NextResponse.json({ summary: summaryText, cached: false, created_at: new Date().toISOString() });
361+
return NextResponse.json({ id: newId, summary: summaryText, cached: false, created_at: new Date().toISOString() });
361362
} catch (error) {
362363
console.error("[summary] Error:", error);
363364
return NextResponse.json({ summary: null, error: "Failed to generate summary" }, { status: 500 });
364365
}
365366
}
367+
368+
export async function PATCH(request: Request) {
369+
try {
370+
const user = await getSession();
371+
if (!user) return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
372+
373+
const body = await request.json();
374+
const { id, is_hidden } = body;
375+
if (!id) return NextResponse.json({ error: "id required" }, { status: 400 });
376+
377+
const db = getDb();
378+
db.prepare("UPDATE ai_summaries SET is_hidden = ? WHERE id = ?").run(is_hidden ? 1 : 0, id);
379+
console.info("[summary] Set is_hidden=", is_hidden ? 1 : 0, "on summary id", id, "by user", user.id);
380+
381+
return NextResponse.json({ success: true });
382+
} catch (error) {
383+
console.error("[summary] PATCH error:", error);
384+
return NextResponse.json({ error: "Failed to update summary" }, { status: 500 });
385+
}
386+
}

src/components/dashboard/ai-summary.tsx

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { useState, useEffect } from "react";
44
import { Card } from "@/components/ui/card";
5-
import { RefreshCw, Sparkles, Copy, Check } from "lucide-react";
5+
import { RefreshCw, Sparkles, Copy, Check, EyeOff } from "lucide-react";
66
import { useLocale } from "@/lib/locale-context";
77
import ReactMarkdown from "react-markdown";
88
import remarkGfm from "remark-gfm";
@@ -26,6 +26,7 @@ function relativeTime(dateStr: string, locale: string): string {
2626

2727
export function AiSummary() {
2828
const [summary, setSummary] = useState<string | null>(null);
29+
const [summaryId, setSummaryId] = useState<number | null>(null);
2930
const [loading, setLoading] = useState(true);
3031
const [refreshing, setRefreshing] = useState(false);
3132
const [copied, setCopied] = useState(false);
@@ -48,9 +49,13 @@ export function AiSummary() {
4849
.then((r) => r.json())
4950
.then((data) => {
5051
if (data.summary) {
51-
console.info("[ai-summary] Got summary, cached:", data.cached);
52+
console.info("[ai-summary] Got summary, cached:", data.cached, "id:", data.id);
5253
setSummary(data.summary);
54+
setSummaryId(data.id ?? null);
5355
if (data.created_at) setCreatedAt(data.created_at);
56+
} else {
57+
setSummary(null);
58+
setSummaryId(null);
5459
}
5560
})
5661
.catch((err) => console.error("[ai-summary] Failed:", err))
@@ -81,6 +86,22 @@ export function AiSummary() {
8186
setTimeout(() => fetchSummary(true), 300);
8287
};
8388

89+
const handleHide = async () => {
90+
if (!summaryId) return;
91+
console.info("[ai-summary] Hiding summary id", summaryId);
92+
try {
93+
await fetch("/api/summary", {
94+
method: "PATCH",
95+
headers: { "Content-Type": "application/json" },
96+
body: JSON.stringify({ id: summaryId, is_hidden: 1 }),
97+
});
98+
// Refresh to show next non-hidden summary or none
99+
fetchSummary();
100+
} catch (err) {
101+
console.error("[ai-summary] Hide failed:", err);
102+
}
103+
};
104+
84105
if (!loading && !summary && !refreshing) return null;
85106

86107
return (
@@ -100,6 +121,16 @@ export function AiSummary() {
100121
{copied ? <Check /> : <Copy />}
101122
</button>
102123
)}
124+
{summary && summaryId !== null && (
125+
<button
126+
type="button"
127+
className="ai-summary-refresh"
128+
onClick={handleHide}
129+
title={locale === "fi" ? "Piilota tämä yhteenveto" : "Hide this summary"}
130+
>
131+
<EyeOff />
132+
</button>
133+
)}
103134
<button
104135
type="button"
105136
className="ai-summary-refresh"

src/lib/db.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,17 @@ function initializeDb(db: Database.Database) {
388388
}
389389

390390
// Add discretionary_target column to daily_budget_history if missing
391+
// Add is_hidden column to ai_summaries if missing
392+
try {
393+
const aiCols = db.prepare("PRAGMA table_info(ai_summaries)").all() as { name: string }[];
394+
if (aiCols.length > 0 && !aiCols.some((c) => c.name === "is_hidden")) {
395+
console.info("[db] Adding is_hidden column to ai_summaries");
396+
db.exec("ALTER TABLE ai_summaries ADD COLUMN is_hidden INTEGER NOT NULL DEFAULT 0");
397+
}
398+
} catch (err) {
399+
console.warn("[db] ai_summaries migration:", err);
400+
}
401+
391402
const dbhCols = db.prepare("PRAGMA table_info(daily_budget_history)").all() as { name: string }[];
392403
if (dbhCols.length > 0 && !dbhCols.some((c) => c.name === "discretionary_target")) {
393404
console.info("[db] Adding discretionary_target column to daily_budget_history");

0 commit comments

Comments
 (0)