Implement activity log (task 22)

- activity_log table in _core/schema.ts with household+created_at index;
  migration 0008_activity_log.sql applied
- logActivity() in _core/activity.ts reads current session and inserts a row
- ActivityLogEntry type + renderActivity?(entry): string added to
  EntityTypeRegistration so modules declare human-readable labels without
  any if/else branches in the widget
- core.activity widget replaced with a real async server component that
  queries the last 20 rows and renders via the registry
- logActivity wired into every create/update/delete in calendar, lists,
  and notes server actions; getAuthorizedItem also returns text so
  toggle/delete can include item text in the payload
- pnpm typecheck, lint, build, and all 4 E2E specs pass

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ginnoir
2026-05-06 13:57:03 -05:00
co-authored by Claude Sonnet 4.6
parent 1133fa8aac
commit 28475e483d
13 changed files with 182 additions and 11 deletions
+48 -10
View File
@@ -1,5 +1,49 @@
import type { ModuleManifest } from "./module";
import { desc, eq } from "drizzle-orm";
import { z } from "zod";
import { db } from "@/lib/db";
import { getCurrentSession } from "@/lib/session";
import type { ActivityLogEntry, ModuleManifest } from "./module";
import { getEntityType } from "./registry";
import { activityLog } from "./schema";
const activityConfigSchema = z.object({
limit: z.number().int().min(1).max(50).optional(),
});
async function ActivityWidget({ config }: { config: unknown }) {
const parsed = activityConfigSchema.parse(config);
const { household } = await getCurrentSession();
const entries = await db
.select()
.from(activityLog)
.where(eq(activityLog.householdId, household.id))
.orderBy(desc(activityLog.createdAt))
.limit(parsed.limit ?? 20);
if (entries.length === 0) {
return <p className="text-sm text-muted-foreground">No recent activity</p>;
}
return (
<ul className="space-y-2">
{entries.map((entry) => {
const reg = getEntityType(entry.entityType);
const description =
reg?.renderActivity?.(entry as ActivityLogEntry) ??
`${entry.action} ${entry.entityType}`;
return (
<li key={entry.id} className="flex items-start gap-2 text-sm">
<span className="mt-0.5 shrink-0 text-xs text-muted-foreground">
{entry.createdAt.toLocaleDateString(undefined, { month: "short", day: "numeric" })}
</span>
<span className="leading-snug">{description}</span>
</li>
);
})}
</ul>
);
}
const coreManifest: ModuleManifest = {
id: "_core",
@@ -14,16 +58,10 @@ const coreManifest: ModuleManifest = {
defaultSize: { w: 4, h: 3 },
minSize: { w: 3, h: 2 },
defaultPriority: 50,
configSchema: z.object({
limit: z.number().int().min(1).max(50).optional(),
}),
defaultConfig: { limit: 10 },
configSchema: activityConfigSchema,
defaultConfig: { limit: 20 },
resolveConfigOptions: async () => undefined,
render: () => (
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
Activity log coming in task 22
</div>
),
render: (props) => <ActivityWidget {...props} />,
},
],
};