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
+47 -13
View File
@@ -7,10 +7,13 @@ import { db } from "@/lib/db";
import { getCurrentSession } from "@/lib/session";
import { logActivity } from "@/modules/_core/activity";
import { cancelReminder, scheduleReminder } from "@/modules/_core/reminders";
import { listItems } from "@/modules/lists/schema";
import { notifyListChanged } from "@/modules/lists/server/realtime";
import { gardenCareLogs, gardenCareSchedules, gardenContainers, gardenPlants } from "../schema";
import { createCalendarEvent } from "./calendar-bridge";
import { buildCareReminderBody, buildCareTitle } from "./care-utils";
import { updateScheduleAfterCare } from "./care-schedule";
import { addListItem, getList, listLists } from "./lists-bridge";
import { addGardenCareTask, getList, listLists } from "./lists-bridge";
const containerInput = z.object({
name: z.string().trim().min(1).max(120),
@@ -347,6 +350,29 @@ export async function logCare(input: z.input<typeof careLogInput>) {
action: "update",
payload: { careType: parsed.careType },
});
// Mark linked task item done if one exists for this plant + care type
const linkedItems = await db
.select({ id: listItems.id, listId: listItems.listId })
.from(listItems)
.where(
and(
sql`${listItems.metadata}->>'gardenPlantId' = ${parsed.plantId}`,
sql`${listItems.metadata}->>'gardenCareType' = ${parsed.careType}`,
eq(listItems.done, false),
),
)
.limit(1);
if (linkedItems[0]) {
await db
.update(listItems)
.set({ done: true, updatedAt: new Date() })
.where(eq(listItems.id, linkedItems[0].id));
await notifyListChanged(linkedItems[0].listId);
revalidatePath(`/lists/${linkedItems[0].listId}`);
}
revalidatePath(`/garden/plants/${parsed.plantId}`);
return log;
}
@@ -416,6 +442,12 @@ export async function upsertCareSchedule(input: z.input<typeof careScheduleInput
if (!schedule) throw new Error("Schedule was not created");
const [plantRow] = await db
.select({ name: gardenPlants.name })
.from(gardenPlants)
.where(eq(gardenPlants.id, parsed.plantId))
.limit(1);
await cancelReminder("garden.schedule", schedule.id);
if (enabled) {
await scheduleReminder({
@@ -424,6 +456,8 @@ export async function upsertCareSchedule(input: z.input<typeof careScheduleInput
entityId: schedule.id,
fireAt: nextDueAt,
createdBy: user.id,
title: "Garden care reminder",
body: plantRow ? buildCareReminderBody(parsed.careType, plantRow.name) : undefined,
});
}
@@ -472,12 +506,20 @@ export async function toggleCareSchedule(input: { id: string; enabled: boolean }
await cancelReminder("garden.schedule", parsed.id);
if (parsed.enabled && row.nextDueAt) {
const [togglePlantRow] = await db
.select({ name: gardenPlants.name })
.from(gardenPlants)
.where(eq(gardenPlants.id, row.plantId))
.limit(1);
await scheduleReminder({
householdId: household.id,
entityType: "garden.schedule",
entityId: parsed.id,
fireAt: row.nextDueAt,
createdBy: user.id,
title: "Garden care reminder",
body: togglePlantRow ? buildCareReminderBody(row.careType, togglePlantRow.name) : undefined,
});
}
@@ -486,17 +528,6 @@ export async function toggleCareSchedule(input: { id: string; enabled: boolean }
// ─── Calendar integration ─────────────────────────────────────────────────────
function buildCareTitle(careType: string, plantName: string): string {
const verbs: Record<string, string> = {
watering: "Water",
fertilizing: "Fertilize",
repotting: "Repot",
pruning: "Prune",
};
const verb = verbs[careType];
return verb ? `${verb} ${plantName}` : `${careType}${plantName}`;
}
const scheduleOnCalendarInput = z.object({
scheduleId: z.string().uuid(),
calendarId: z.string().uuid(),
@@ -550,6 +581,7 @@ export async function pushOverdueToTaskList(): Promise<{ added: number }> {
const overduePairs = await db
.select({
plantId: gardenPlants.id,
plantName: gardenPlants.name,
careType: gardenCareSchedules.careType,
nextDueAt: gardenCareSchedules.nextDueAt,
@@ -582,11 +614,13 @@ export async function pushOverdueToTaskList(): Promise<{ added: number }> {
? Math.abs(Math.floor((Date.now() - pair.nextDueAt.getTime()) / (1000 * 60 * 60 * 24)))
: 0;
await addListItem({
await addGardenCareTask({
listId: taskList.id,
text: title,
notes: `Overdue by ${daysOverdue} day(s)`,
dueAt: pair.nextDueAt,
gardenPlantId: pair.plantId,
gardenCareType: pair.careType,
});
existingTexts.add(title);
added++;