From 6183fb62c88a51ff967da428edfbf1b8b6d70d84 Mon Sep 17 00:00:00 2001 From: ginnoir Date: Sat, 4 Jul 2026 22:39:21 -0500 Subject: [PATCH] fix: dashboard edit previews, migration journal, and notes comments Register drizzle journal entries for 0022/0023 so prod migrations apply. Key edit-mode widget previews by index so bangs.stats shows live data. Add comments to notes detail pages. --- AGENTS.md | 2 +- CLAUDE.md | 2 +- STATUS.md | 6 +++-- drizzle/meta/_journal.json | 14 ++++++++++ src/app/d/[slug]/page.tsx | 8 +++--- src/app/notes/[id]/page.tsx | 5 ++-- src/components/dashboard-editor.tsx | 28 +++++++++++++++++--- src/lib/dashboard.ts | 7 ++++- src/modules/_core/comments.ts | 2 ++ src/modules/bangs/manifest.tsx | 4 ++- src/modules/notes/components/note-editor.tsx | 11 +++++++- 11 files changed, 72 insertions(+), 17 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 9dff4ce..9679358 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -67,7 +67,7 @@ src/ ### Core primitives every module gets - **Entity registry.** Modules declare entity types; share-link, activity log, search, reminders all work against any registered entity. -- **Dashboard widget registry.** Every widget is uniformly configurable (no singleton/parameterized split) and reusable — each placement on a dashboard is an independent instance with its own config. Each user has multiple dashboards; the active dashboard composes whatever widgets they've placed. +- **Dashboard widget registry.** Every widget is uniformly configurable (no singleton/parameterized split) and reusable — each placement on a dashboard is an independent instance with its own config. Each user has multiple dashboards; the active dashboard composes whatever widgets they've placed. **Edit mode** (`?edit=1`) must pre-render live widget content (task 81): server-side `DashboardWidgetContent` per placement, keyed by index in `widgetContents`; `render` loads real data; never show meta-description placeholders for saved placements. - **Quick-add registry.** Modules register quick actions for the dashboard's `+` menu. - **Share-link service.** `createShareLink(entityType, entityId, { expiresAt, capabilities })` → `fam.ginnoir.com/s/`. Generic. - **Notification bus.** `notify(userId, { title, body, url })` fans out to web push + in-app + (optional) ntfy. diff --git a/CLAUDE.md b/CLAUDE.md index b6ead62..e15c0a3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -68,7 +68,7 @@ src/ ### Core primitives every module gets - **Entity registry.** Modules declare entity types; share-link, activity log, search, reminders all work against any registered entity. -- **Dashboard widget registry.** Every widget is uniformly configurable (no singleton/parameterized split) and reusable — each placement on a dashboard is an independent instance with its own config. Each user has multiple dashboards; the active dashboard composes whatever widgets they've placed. +- **Dashboard widget registry.** Every widget is uniformly configurable (no singleton/parameterized split) and reusable — each placement on a dashboard is an independent instance with its own config. Each user has multiple dashboards; the active dashboard composes whatever widgets they've placed. **Edit mode** (`?edit=1`) must pre-render live widget content (task 81): server-side `DashboardWidgetContent` per placement, keyed by index in `widgetContents`; `render` loads real data; never show meta-description placeholders for saved placements. - **Quick-add registry.** Modules register quick actions for the dashboard's `+` menu. - **Share-link service.** `createShareLink(entityType, entityId, { expiresAt, capabilities })` → `fam.ginnoir.com/s/`. Generic. - **Notification bus.** `notify(userId, { title, body, url })` fans out to web push + in-app + (optional) ntfy. diff --git a/STATUS.md b/STATUS.md index 7fb50b7..6a124d5 100644 --- a/STATUS.md +++ b/STATUS.md @@ -29,7 +29,9 @@ Living progress tracker. Update at the end of each task. Codex and Claude Code b - **91 — Comments on lists and tasks** (Gitea #30, commit `e1c2a09`). Generic `comments` table + `_core/comments.ts`; reusable `EntityComments` on list detail (list + per-item compact threads). Migration `0023_comments.sql`. -- **92 — Bang stats dashboard widget** (Gitea #31). `bangs.stats` widget with monthly/yearly counts, average days between bangs, recent month breakdown. +- **92 — Bang stats dashboard widget** (Gitea #31, commit `e1c2a09`). `bangs.stats` widget with monthly/yearly counts, average days between bangs, recent month breakdown. + +- **P2 batch follow-ups** (release v0.5.4). Migration journal entries for `0022`/`0023` (prod apply fix); dashboard edit previews keyed by placement index (`bangs.stats` live in edit mode); notes comments; comment revalidation on `/notes`. - **01 — Repo init & tooling** (commit `b89690a`). pnpm 10 + TS strict + ESLint flat + Prettier. All acceptance criteria green. - **02 — Next.js app skeleton**. Next.js 15 + React 19 + Tailwind v4 + shadcn/ui (button, card, input, dialog). `pnpm dev` serves placeholder, `pnpm build` produces `.next/standalone/`, `pnpm lint` clean. Added `.npmrc` with `node-linker=hoisted` for Windows symlink compatibility. @@ -81,7 +83,7 @@ Phase 9 — Post-v0.1 (see `docs/superpowers/specs/2026-07-03-backlog-triage-des P2/P3 backlog is filed on Gitea only (no task briefs yet) — see `docs/issues-map.md` designs 7–9, 11–12, 15–19. -**How to resume:** P2 batch #28–#31 complete. Next: remaining P2/P3 Gitea backlog (#32+) or homelab LLM wiring (`LLM_BASE_URL` in homelabstack `.env`). Run `pnpm db:migrate` for migrations `0022` and `0023`. +**How to resume:** P2 batch #28–#31 complete (released v0.5.4). Next: remaining P2/P3 Gitea backlog (#32+) or homelab LLM wiring (`LLM_BASE_URL` in homelabstack `.env`). ## Development login/testing notes diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index e2553a9..04f9b73 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -155,6 +155,20 @@ "when": 1751754000000, "tag": "0021_user_assistant_enabled", "breakpoints": true + }, + { + "idx": 22, + "version": "7", + "when": 1780392000000, + "tag": "0022_multiple_reminders", + "breakpoints": true + }, + { + "idx": 23, + "version": "7", + "when": 1780393000000, + "tag": "0023_comments", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/app/d/[slug]/page.tsx b/src/app/d/[slug]/page.tsx index b518042..6c71d9f 100644 --- a/src/app/d/[slug]/page.tsx +++ b/src/app/d/[slug]/page.tsx @@ -1,7 +1,7 @@ import { notFound } from "next/navigation"; import { Suspense } from "react"; import { getCurrentSession } from "@/lib/session"; -import { parseDashboardLayout, widgetContentKey } from "@/lib/dashboard"; +import { parseDashboardLayout, widgetContentIndexKey } from "@/lib/dashboard"; import { readEditorDraftLayout } from "@/lib/dashboard-editor-draft"; import { computeDefaultLayout, normalizeDashboardLayout } from "@/lib/dashboard.server"; import { getWidget, getWidgetMetas } from "@/modules/_core"; @@ -58,10 +58,10 @@ export default async function DashboardPage({ if (isEditing) { const ctx = { userId: user.id, householdId: household.id }; const widgetContents = Object.fromEntries( - layout.widgets.map((placement) => [ - widgetContentKey(placement), + layout.widgets.map((placement, index) => [ + widgetContentIndexKey(index), , diff --git a/src/app/notes/[id]/page.tsx b/src/app/notes/[id]/page.tsx index bd7c1c2..b7178a3 100644 --- a/src/app/notes/[id]/page.tsx +++ b/src/app/notes/[id]/page.tsx @@ -1,11 +1,12 @@ import { notFound } from "next/navigation"; +import { getCurrentSession } from "@/lib/session"; import { NoteEditor } from "@/modules/notes/components/note-editor"; import { getNote } from "@/modules/notes/server/queries"; export default async function NotePage({ params }: { params: Promise<{ id: string }> }) { const { id } = await params; - const note = await getNote(id).catch(() => null); + const [{ user }, note] = await Promise.all([getCurrentSession(), getNote(id).catch(() => null)]); if (!note) notFound(); - return ; + return ; } diff --git a/src/components/dashboard-editor.tsx b/src/components/dashboard-editor.tsx index dd5b64c..3a4bab0 100644 --- a/src/components/dashboard-editor.tsx +++ b/src/components/dashboard-editor.tsx @@ -9,7 +9,7 @@ import { GridLayout } from "react-grid-layout"; import type { Layout } from "react-grid-layout"; import { GripVertical, Settings2, Trash2, RotateCcw, Plus, LayoutGrid } from "lucide-react"; import type { DashboardLayout, WidgetPlacement, PresetId } from "@/lib/dashboard"; -import { computePresetLayoutFromMetas, widgetContentKey } from "@/lib/dashboard"; +import { computePresetLayoutFromMetas, widgetContentIndexKey } from "@/lib/dashboard"; import type { SerializedWidgetMeta } from "@/modules/_core/registry"; import { saveDashboardLayout, @@ -113,10 +113,18 @@ export function DashboardEditor({ function addWidget(widgetId: string, config: unknown) { const meta = widgetMetas.find((m) => m.id === widgetId); if (!meta) return; + const resolvedConfig = resolveWidgetConfig(meta, config); const maxY = placements.reduce((m, p) => Math.max(m, p.y + p.h), 0); const next = [ ...placements, - { widgetId, config, x: 0, y: maxY, w: meta.defaultSize.w, h: meta.defaultSize.h }, + { + widgetId, + config: resolvedConfig, + x: 0, + y: maxY, + w: meta.defaultSize.w, + h: meta.defaultSize.h, + }, ]; setPlacements(next); setIsDirty(true); @@ -125,7 +133,9 @@ export function DashboardEditor({ } function updateConfig(index: number, config: unknown) { - const next = placements.map((p, i) => (i === index ? { ...p, config } : p)); + const meta = widgetMetas.find((m) => m.id === placements[index]?.widgetId); + const resolvedConfig = meta ? resolveWidgetConfig(meta, config) : config; + const next = placements.map((p, i) => (i === index ? { ...p, config: resolvedConfig } : p)); setPlacements(next); setIsDirty(true); setConfiguringIndex(null); @@ -237,7 +247,7 @@ export function DashboardEditor({ > {placements.map((placement, i) => { const meta = widgetMetas.find((m) => m.id === placement.widgetId); - const content = widgetContents[widgetContentKey(placement)]; + const content = widgetContents[widgetContentIndexKey(i)]; return (
); } + +function resolveWidgetConfig(meta: SerializedWidgetMeta, config: unknown): unknown { + if (config != null && typeof config === "object" && !Array.isArray(config)) { + return { + ...(meta.defaultConfig as Record), + ...(config as Record), + }; + } + return meta.defaultConfig; +} diff --git a/src/lib/dashboard.ts b/src/lib/dashboard.ts index e3cb42f..ce5f417 100644 --- a/src/lib/dashboard.ts +++ b/src/lib/dashboard.ts @@ -11,7 +11,12 @@ export type WidgetPlacement = { }; export function widgetContentKey(placement: Pick): string { - return `${placement.widgetId}::${JSON.stringify(placement.config)}`; + return `${placement.widgetId}::${JSON.stringify(placement.config ?? null)}`; +} + +/** Stable key for edit-mode widget preview map (index-aligned with layout.widgets). */ +export function widgetContentIndexKey(index: number): string { + return String(index); } export type DashboardLayout = { diff --git a/src/modules/_core/comments.ts b/src/modules/_core/comments.ts index 338d860..0b2e1cc 100644 --- a/src/modules/_core/comments.ts +++ b/src/modules/_core/comments.ts @@ -77,6 +77,7 @@ export async function addComment(input: z.input): Promise; } -async function BangStatsWidgetServer({ ctx }: { config: unknown; ctx: WidgetContext }) { +async function BangStatsWidgetServer({ config, ctx }: { config: unknown; ctx: WidgetContext }) { + bangStatsConfigSchema.parse(config); const aggregates = await getBangAggregates(ctx.householdId); return ; } @@ -62,6 +63,7 @@ const bangsManifest: ModuleManifest = { defaultPriority: 55, configSchema: bangStatsConfigSchema, defaultConfig: {}, + resolveConfigOptions: async () => undefined, render: (props) => , }, ], diff --git a/src/modules/notes/components/note-editor.tsx b/src/modules/notes/components/note-editor.tsx index 3136a86..6b2e5f6 100644 --- a/src/modules/notes/components/note-editor.tsx +++ b/src/modules/notes/components/note-editor.tsx @@ -4,6 +4,7 @@ import dynamic from "next/dynamic"; import { useRouter } from "next/navigation"; import { Pin, PinOff, Save, Trash2 } from "lucide-react"; import { useState, useTransition } from "react"; +import { EntityComments } from "@/components/comments/entity-comments"; import { RichTextContent } from "@/components/rich-text"; import { DetailBackLink } from "@/components/detail-back-link"; import { Button } from "@/components/ui/button"; @@ -25,7 +26,7 @@ const RichTextEditor = dynamic( }, ); -export function NoteEditor({ note }: { note?: NoteDto }) { +export function NoteEditor({ note, currentUserId }: { note?: NoteDto; currentUserId?: string }) { const router = useRouter(); const [currentNote, setCurrentNote] = useState(note); const [title, setTitle] = useState(note?.title ?? ""); @@ -147,6 +148,14 @@ export function NoteEditor({ note }: { note?: NoteDto }) {
+ + {currentNote && currentUserId ? ( + + ) : null} ); }