feat: garden care reminder bodies, task sync, upload cleanup

- care reminders now fire with plant-specific body ("Time to water Pothos")
  via title/body columns on the reminders table (migration 0016)
- bi-directional task sync: checking off a garden-linked task list item
  creates a care log; logging care from garden marks the linked task done
  (list_items.metadata stores gardenPlantId + gardenCareType linkage)
- upload error response no longer leaks debug detail; error message
  corrected from "5 MB" to "100 MB"
This commit is contained in:
ginnoir
2026-06-01 23:58:00 -05:00
parent dd980c6932
commit 7f2c5a44dd
12 changed files with 205 additions and 28 deletions
+20
View File
@@ -73,6 +73,26 @@ export function getWidgetMetas(): SerializedWidgetMeta[] {
);
}
// ─── Item toggle hooks ────────────────────────────────────────────────────────
type ItemTogglePayload = {
itemId: string;
metadata: Record<string, unknown>;
done: boolean;
userId: string;
};
type ItemToggleHook = (payload: ItemTogglePayload) => Promise<void>;
const itemToggleHooks = new Map<string, ItemToggleHook>();
export function registerItemToggleHook(id: string, hook: ItemToggleHook): void {
itemToggleHooks.set(id, hook);
}
export async function fireItemToggleHooks(payload: ItemTogglePayload): Promise<void> {
await Promise.allSettled([...itemToggleHooks.values()].map((h) => h(payload)));
}
export function getQuickAdds(): SerializedQuickAddItem[] {
return [...modules.values()].flatMap((manifest) =>
(manifest.quickAdds ?? []).map(({ id, label, icon, url }) => ({
+13 -3
View File
@@ -13,6 +13,8 @@ export async function scheduleReminder(input: {
fireAt: Date;
createdBy: string;
channel?: string;
title?: string;
body?: string;
}) {
await db
.insert(reminders)
@@ -22,12 +24,20 @@ export async function scheduleReminder(input: {
entityId: input.entityId,
fireAt: input.fireAt,
channel: input.channel ?? "auto",
title: input.title ?? null,
body: input.body ?? null,
createdBy: input.createdBy,
firedAt: null,
})
.onConflictDoUpdate({
target: [reminders.entityType, reminders.entityId],
set: { fireAt: input.fireAt, firedAt: null, createdBy: input.createdBy },
set: {
fireAt: input.fireAt,
title: input.title ?? null,
body: input.body ?? null,
firedAt: null,
createdBy: input.createdBy,
},
});
}
@@ -82,8 +92,8 @@ export async function tickReminders() {
if (!reminder.createdBy) return;
try {
await notify(reminder.createdBy, {
title: "Reminder",
body: `You have a reminder`,
title: reminder.title ?? "Reminder",
body: reminder.body ?? "You have a reminder",
url: reminder.entityType === "notes.note" ? `/notes/${reminder.entityId}` : "/",
channels: ["push", "inapp"],
});
+2
View File
@@ -166,6 +166,8 @@ export const reminders = pgTable(
entityId: uuid("entity_id").notNull(),
fireAt: timestamp("fire_at", { withTimezone: true }).notNull(),
channel: text("channel").notNull().default("auto"),
title: text("title"),
body: text("body"),
firedAt: timestamp("fired_at", { withTimezone: true }),
createdBy: uuid("created_by").references(() => users.id, { onDelete: "set null" }),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),