feat: journal dashboard widgets, agent polish, and edit-mode live previews
Journal dashboard widgets and quick-add; rich-text quick-add dialogs. Dashboard draft sync for live edit previews; assistant bubble + API tools. Journal UX: stress slider, mood grid, query cap fix.
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
import { apiJson, withApiHandler } from "@/lib/api-handler";
|
||||
import { scheduleOnCalendarForScope } from "@/modules/garden/server/actions";
|
||||
import { scheduleOnCalendarInput } from "@/modules/garden/server/schemas";
|
||||
|
||||
export async function POST(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
return withApiHandler(request, async (scope, req) => {
|
||||
const body: unknown = await req.json();
|
||||
const parsed = scheduleOnCalendarInput.parse({ ...(body as object), scheduleId: id });
|
||||
await scheduleOnCalendarForScope(scope, parsed);
|
||||
return apiJson({ ok: true }, 201);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { z } from "zod";
|
||||
import { apiJson, withApiHandler } from "@/lib/api-handler";
|
||||
import {
|
||||
deleteCareScheduleForScope,
|
||||
toggleCareScheduleForScope,
|
||||
} from "@/modules/garden/server/actions";
|
||||
import { careScheduleToggleInput } from "@/modules/garden/server/schemas";
|
||||
|
||||
export async function PATCH(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
return withApiHandler(request, async (scope, req) => {
|
||||
const body: unknown = await req.json();
|
||||
const parsed = careScheduleToggleInput.parse({ ...(body as object), id });
|
||||
await toggleCareScheduleForScope(scope, parsed);
|
||||
return apiJson({ ok: true });
|
||||
});
|
||||
}
|
||||
|
||||
export async function DELETE(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
return withApiHandler(request, async (scope) => {
|
||||
z.string().uuid().parse(id);
|
||||
await deleteCareScheduleForScope(scope, { id });
|
||||
return apiJson({ ok: true });
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { apiJson, withApiHandler } from "@/lib/api-handler";
|
||||
import { pushOverdueToTaskListForScope } from "@/modules/garden/server/actions";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
return withApiHandler(request, async (scope) => {
|
||||
const result = await pushOverdueToTaskListForScope(scope);
|
||||
return apiJson(result, 201);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { z } from "zod";
|
||||
import { apiJson, withApiHandler } from "@/lib/api-handler";
|
||||
import { logCareForScope } from "@/modules/garden/server/actions";
|
||||
import { careLogInput } from "@/modules/garden/server/schemas";
|
||||
import { getCareLogsForScope } from "@/modules/garden/server/queries";
|
||||
|
||||
export async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
return withApiHandler(request, async (scope, req) => {
|
||||
z.string().uuid().parse(id);
|
||||
const url = new URL(req.url);
|
||||
const limitRaw = url.searchParams.get("limit");
|
||||
const limit = limitRaw ? z.coerce.number().int().min(1).max(100).parse(limitRaw) : 20;
|
||||
const logs = await getCareLogsForScope(scope.householdId, id, limit);
|
||||
return apiJson(logs);
|
||||
});
|
||||
}
|
||||
|
||||
export async function POST(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
return withApiHandler(request, async (scope, req) => {
|
||||
z.string().uuid().parse(id);
|
||||
const body: unknown = await req.json();
|
||||
const parsed = careLogInput.parse({ ...(body as object), plantId: id });
|
||||
const log = await logCareForScope(scope, parsed);
|
||||
return apiJson(
|
||||
{
|
||||
id: log.id,
|
||||
careType: log.careType,
|
||||
notes: log.notes,
|
||||
performedAt: log.performedAt.toISOString(),
|
||||
performedBy: log.performedBy,
|
||||
},
|
||||
201,
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { z } from "zod";
|
||||
import { apiJson, withApiHandler } from "@/lib/api-handler";
|
||||
import { upsertCareScheduleForScope } from "@/modules/garden/server/actions";
|
||||
import { careScheduleInput } from "@/modules/garden/server/schemas";
|
||||
import { getCareSchedulesForScope } from "@/modules/garden/server/queries";
|
||||
|
||||
export async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
return withApiHandler(request, async (scope) => {
|
||||
z.string().uuid().parse(id);
|
||||
const schedules = await getCareSchedulesForScope(scope.householdId, id);
|
||||
return apiJson(schedules);
|
||||
});
|
||||
}
|
||||
|
||||
export async function POST(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
return withApiHandler(request, async (scope, req) => {
|
||||
z.string().uuid().parse(id);
|
||||
const body: unknown = await req.json();
|
||||
const parsed = careScheduleInput.parse({ ...(body as object), plantId: id });
|
||||
const schedule = await upsertCareScheduleForScope(scope, parsed);
|
||||
return apiJson(
|
||||
{
|
||||
id: schedule.id,
|
||||
careType: schedule.careType,
|
||||
intervalDays: schedule.intervalDays,
|
||||
lastPerformedAt: schedule.lastPerformedAt?.toISOString() ?? null,
|
||||
nextDueAt: schedule.nextDueAt?.toISOString() ?? null,
|
||||
enabled: schedule.enabled,
|
||||
},
|
||||
201,
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -4,8 +4,11 @@ import { journalEntryInput } from "@/modules/journal/server/schemas";
|
||||
import { listJournalEntriesForScope } from "@/modules/journal/server/queries";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
return withApiHandler(request, async (scope) => {
|
||||
const entries = await listJournalEntriesForScope(scope);
|
||||
return withApiHandler(request, async (scope, req) => {
|
||||
const url = new URL(req.url);
|
||||
const limitParam = url.searchParams.get("limit");
|
||||
const limit = limitParam ? Math.min(Math.max(Number(limitParam) || 20, 1), 100) : undefined;
|
||||
const entries = await listJournalEntriesForScope(scope, limit ? { limit } : undefined);
|
||||
return apiJson(entries);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { readFile } from "fs/promises";
|
||||
import path from "path";
|
||||
import { withApiHandler } from "@/lib/api-handler";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
return withApiHandler(request, async () => {
|
||||
const specPath = path.join(process.cwd(), "docs", "api", "openapi.yaml");
|
||||
const spec = await readFile(specPath, "utf8");
|
||||
return new Response(spec, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/yaml; charset=utf-8" },
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { z } from "zod";
|
||||
import { apiJson, withApiHandler } from "@/lib/api-handler";
|
||||
import { revokeShareLinkForScope } from "@/modules/_core/share-api";
|
||||
|
||||
export async function DELETE(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
return withApiHandler(request, async (scope) => {
|
||||
z.string().uuid().parse(id);
|
||||
await revokeShareLinkForScope(scope, id);
|
||||
return apiJson({ ok: true });
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { z } from "zod";
|
||||
import { apiJson, withApiHandler } from "@/lib/api-handler";
|
||||
import {
|
||||
createShareLinkForScope,
|
||||
listShareLinksForScope,
|
||||
listShareableEntityTypes,
|
||||
} from "@/modules/_core/share-api";
|
||||
|
||||
const createShareLinkInput = z.object({
|
||||
entityType: z.string().trim().min(1),
|
||||
entityId: z.string().uuid(),
|
||||
expiresAt: z.string().datetime().nullable().optional(),
|
||||
capabilities: z
|
||||
.object({
|
||||
read: z.boolean().optional(),
|
||||
write: z.boolean().optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export async function GET(request: Request) {
|
||||
return withApiHandler(request, async (scope, req) => {
|
||||
const url = new URL(req.url);
|
||||
const entityType = url.searchParams.get("entityType") ?? undefined;
|
||||
const entityId = url.searchParams.get("entityId") ?? undefined;
|
||||
|
||||
if (url.searchParams.get("entityTypes") === "true") {
|
||||
return apiJson(listShareableEntityTypes());
|
||||
}
|
||||
|
||||
const links = await listShareLinksForScope(scope, { entityType, entityId });
|
||||
return apiJson(links);
|
||||
});
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
return withApiHandler(request, async (scope, req) => {
|
||||
const body: unknown = await req.json();
|
||||
const parsed = createShareLinkInput.parse(body);
|
||||
const expiresAt = parsed.expiresAt ? new Date(parsed.expiresAt) : null;
|
||||
|
||||
const result = await createShareLinkForScope(scope, parsed.entityType, parsed.entityId, {
|
||||
expiresAt,
|
||||
capabilities: parsed.capabilities,
|
||||
});
|
||||
|
||||
return apiJson(
|
||||
{
|
||||
url: result.url,
|
||||
expiresAt: result.expiresAt?.toISOString() ?? null,
|
||||
},
|
||||
201,
|
||||
);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user