添加家人绑定

This commit is contained in:
feie9456 2026-04-17 11:32:17 +08:00
parent 399bdf079b
commit 8c9d86e858
25 changed files with 2631 additions and 87 deletions

View File

@ -0,0 +1,74 @@
import { ConversationRole } from "@prisma/client";
import { NextResponse } from "next/server";
import { recordConversationTurns } from "@/lib/monitor-data";
function toConversationRole(rawRole: unknown) {
switch (rawRole) {
case ConversationRole.USER:
return ConversationRole.USER;
case ConversationRole.TOOL:
return ConversationRole.TOOL;
default:
return ConversationRole.ASSISTANT;
}
}
export async function POST(request: Request) {
const body = (await request.json().catch(() => null)) as
| {
deviceUuid?: string;
sessionId?: string;
turns?: unknown;
}
| null;
if (!body?.deviceUuid || typeof body.deviceUuid !== "string") {
return NextResponse.json(
{ error: "deviceUuid 是必填项。" },
{ status: 400 },
);
}
if (!Array.isArray(body.turns)) {
return NextResponse.json(
{ error: "turns 必须是数组。" },
{ status: 400 },
);
}
const turns = body.turns
.filter((turn): turn is Record<string, unknown> => !!turn && typeof turn === "object")
.map((turn) => ({
role: toConversationRole(turn.role),
content: typeof turn.content === "string" ? turn.content : "",
metaJson: typeof turn.metaJson === "string" ? turn.metaJson : undefined,
createdAt: typeof turn.createdAt === "string" ? turn.createdAt : undefined,
}))
.filter((turn) => turn.content.trim().length > 0);
if (turns.length === 0) {
return NextResponse.json(
{ error: "至少需要一条有效对话内容。" },
{ status: 400 },
);
}
try {
const result = await recordConversationTurns({
deviceUuid: body.deviceUuid,
sessionId: typeof body.sessionId === "string" ? body.sessionId : undefined,
turns,
});
return NextResponse.json({ ok: true, count: result.count });
} catch (error) {
return NextResponse.json(
{
error:
error instanceof Error ? error.message : "对话记录保存失败,请稍后再试。",
},
{ status: 400 },
);
}
}

View File

@ -0,0 +1,39 @@
import { NextResponse } from "next/server";
import { markFamilyMessagesAsRead } from "@/lib/monitor-data";
export async function POST(request: Request) {
const body = (await request.json().catch(() => null)) as
| {
deviceUuid?: string;
messageIds?: unknown;
}
| null;
if (!body?.deviceUuid || typeof body.deviceUuid !== "string") {
return NextResponse.json(
{ error: "deviceUuid 是必填项。" },
{ status: 400 },
);
}
const messageIds = Array.isArray(body.messageIds)
? body.messageIds.filter(
(messageId): messageId is string => typeof messageId === "string",
)
: undefined;
try {
await markFamilyMessagesAsRead(body.deviceUuid, messageIds);
return NextResponse.json({ ok: true });
} catch (error) {
return NextResponse.json(
{
error:
error instanceof Error ? error.message : "更新留言状态失败,请稍后再试。",
},
{ status: 400 },
);
}
}

View File

@ -0,0 +1,48 @@
import { NextRequest, NextResponse } from "next/server";
import { listFamilyMessagesForDevice } from "@/lib/monitor-data";
export async function GET(request: NextRequest) {
const rawDeviceValue =
request.nextUrl.searchParams.get("deviceUuid") ||
request.nextUrl.searchParams.get("device") ||
"";
const take = Number.parseInt(
request.nextUrl.searchParams.get("take") || "12",
10,
);
if (!rawDeviceValue) {
return NextResponse.json(
{ error: "deviceUuid 是必填项。" },
{ status: 400 },
);
}
try {
const data = await listFamilyMessagesForDevice(rawDeviceValue, take);
return NextResponse.json({
deviceUuid: data.device.deviceUuid,
unreadCount: data.unreadCount,
summaryText: data.summaryText,
messages: data.messages.map((message) => ({
id: message.id,
content: message.content,
importance: message.importance,
status: message.status,
createdAt: message.createdAt.toISOString(),
deliveredAt: message.deliveredAt?.toISOString() || null,
readAt: message.readAt?.toISOString() || null,
})),
});
} catch (error) {
return NextResponse.json(
{
error:
error instanceof Error ? error.message : "读取家属留言失败,请稍后再试。",
},
{ status: 400 },
);
}
}

View File

@ -0,0 +1,43 @@
import { NextResponse } from "next/server";
import { createBindUrl, ensureDeviceRegistration } from "@/lib/monitor-data";
export async function POST(request: Request) {
const body = (await request.json().catch(() => null)) as
| {
deviceUuid?: string;
displayName?: string;
appVersion?: string;
}
| null;
if (!body?.deviceUuid || typeof body.deviceUuid !== "string") {
return NextResponse.json(
{ error: "deviceUuid 是必填项。" },
{ status: 400 },
);
}
try {
const device = await ensureDeviceRegistration({
deviceUuid: body.deviceUuid,
displayName:
typeof body.displayName === "string" ? body.displayName : undefined,
appVersion:
typeof body.appVersion === "string" ? body.appVersion : undefined,
});
return NextResponse.json({
deviceUuid: device.deviceUuid,
bindUrl: createBindUrl(device.deviceUuid),
});
} catch (error) {
return NextResponse.json(
{
error:
error instanceof Error ? error.message : "设备注册失败,请稍后再试。",
},
{ status: 500 },
);
}
}

View File

@ -0,0 +1,73 @@
import { ToolCallStatus } from "@prisma/client";
import { NextResponse } from "next/server";
import { recordToolCall } from "@/lib/monitor-data";
function toToolCallStatus(rawStatus: unknown) {
return rawStatus === ToolCallStatus.FAILURE
? ToolCallStatus.FAILURE
: ToolCallStatus.SUCCESS;
}
function toArguments(rawArguments: unknown) {
if (
rawArguments &&
typeof rawArguments === "object" &&
!Array.isArray(rawArguments)
) {
return rawArguments as Record<string, unknown>;
}
return {};
}
export async function POST(request: Request) {
const body = (await request.json().catch(() => null)) as
| {
deviceUuid?: string;
toolName?: string;
arguments?: unknown;
outputText?: string;
status?: string;
callId?: string;
sessionId?: string;
}
| null;
if (!body?.deviceUuid || typeof body.deviceUuid !== "string") {
return NextResponse.json(
{ error: "deviceUuid 是必填项。" },
{ status: 400 },
);
}
if (!body.toolName || typeof body.toolName !== "string") {
return NextResponse.json(
{ error: "toolName 是必填项。" },
{ status: 400 },
);
}
try {
const log = await recordToolCall({
deviceUuid: body.deviceUuid,
toolName: body.toolName,
arguments: toArguments(body.arguments),
outputText: typeof body.outputText === "string" ? body.outputText : undefined,
status: toToolCallStatus(body.status),
callId: typeof body.callId === "string" ? body.callId : undefined,
sessionId:
typeof body.sessionId === "string" ? body.sessionId : undefined,
});
return NextResponse.json({ ok: true, logId: log.id });
} catch (error) {
return NextResponse.json(
{
error:
error instanceof Error ? error.message : "工具调用记录失败,请稍后再试。",
},
{ status: 400 },
);
}
}

View File

@ -0,0 +1,65 @@
import { UsageEventType } from "@prisma/client";
import { NextResponse } from "next/server";
import { recordUsageEvent } from "@/lib/monitor-data";
function toUsageEventType(rawType: unknown) {
switch (rawType) {
case UsageEventType.APP_OPEN:
return UsageEventType.APP_OPEN;
case UsageEventType.SETTINGS_OPENED:
return UsageEventType.SETTINGS_OPENED;
case UsageEventType.AI_SESSION_STARTED:
return UsageEventType.AI_SESSION_STARTED;
case UsageEventType.AI_SESSION_ENDED:
return UsageEventType.AI_SESSION_ENDED;
case UsageEventType.CAMERA_ENABLED:
return UsageEventType.CAMERA_ENABLED;
case UsageEventType.CAMERA_DISABLED:
return UsageEventType.CAMERA_DISABLED;
default:
return UsageEventType.TOOL_CALLED;
}
}
export async function POST(request: Request) {
const body = (await request.json().catch(() => null)) as
| {
deviceUuid?: string;
eventType?: string;
detailJson?: string;
}
| null;
if (!body?.deviceUuid || typeof body.deviceUuid !== "string") {
return NextResponse.json(
{ error: "deviceUuid 是必填项。" },
{ status: 400 },
);
}
if (!body.eventType || typeof body.eventType !== "string") {
return NextResponse.json(
{ error: "eventType 是必填项。" },
{ status: 400 },
);
}
try {
await recordUsageEvent({
deviceUuid: body.deviceUuid,
eventType: toUsageEventType(body.eventType),
detailJson: typeof body.detailJson === "string" ? body.detailJson : undefined,
});
return NextResponse.json({ ok: true });
} catch (error) {
return NextResponse.json(
{
error:
error instanceof Error ? error.message : "使用记录保存失败,请稍后再试。",
},
{ status: 400 },
);
}
}

View File

@ -0,0 +1,38 @@
import { NextResponse } from "next/server";
import { bindDeviceToCaregiver, createBindUrl } from "@/lib/monitor-data";
import { getOrCreateCaregiverSession } from "@/lib/session";
export async function POST(request: Request) {
const body = (await request.json().catch(() => null)) as
| {
deviceUuid?: string;
}
| null;
if (!body?.deviceUuid || typeof body.deviceUuid !== "string") {
return NextResponse.json(
{ error: "请先提供设备码或二维码链接。" },
{ status: 400 },
);
}
try {
const caregiver = await getOrCreateCaregiverSession();
const device = await bindDeviceToCaregiver(caregiver.id, body.deviceUuid);
return NextResponse.json({
ok: true,
deviceUuid: device.deviceUuid,
bindUrl: createBindUrl(device.deviceUuid),
});
} catch (error) {
return NextResponse.json(
{
error:
error instanceof Error ? error.message : "绑定失败,请稍后再试。",
},
{ status: 400 },
);
}
}

View File

@ -0,0 +1,48 @@
import { NextResponse } from "next/server";
import { createFamilyMessageFromCaregiver } from "@/lib/monitor-data";
import { getOrCreateCaregiverSession } from "@/lib/session";
export async function POST(request: Request) {
const body = (await request.json().catch(() => null)) as
| {
deviceUuid?: string;
content?: string;
importance?: string;
}
| null;
if (!body?.deviceUuid || typeof body.deviceUuid !== "string") {
return NextResponse.json(
{ error: "请先提供目标老人设备码。" },
{ status: 400 },
);
}
if (!body.content || typeof body.content !== "string" || !body.content.trim()) {
return NextResponse.json(
{ error: "留言内容不能为空。" },
{ status: 400 },
);
}
try {
const caregiver = await getOrCreateCaregiverSession();
const message = await createFamilyMessageFromCaregiver({
caregiverId: caregiver.id,
rawDeviceValue: body.deviceUuid,
content: body.content,
importance: typeof body.importance === "string" ? body.importance : undefined,
});
return NextResponse.json({ ok: true, messageId: message.id });
} catch (error) {
return NextResponse.json(
{
error:
error instanceof Error ? error.message : "留言发送失败,请稍后再试。",
},
{ status: 400 },
);
}
}

View File

@ -0,0 +1,44 @@
import { type NextRequest, NextResponse } from "next/server";
import {
CAREGIVER_SESSION_COOKIE,
createCaregiverSessionCookieValue,
getCaregiverSession,
} from "@/lib/session";
import { prisma } from "@/lib/prisma";
function normalizeRedirectPath(redirectTo?: string | null) {
if (!redirectTo || !redirectTo.startsWith("/") || redirectTo.startsWith("//")) {
return "/";
}
return redirectTo;
}
export async function GET(request: NextRequest) {
const redirectTo = normalizeRedirectPath(
request.nextUrl.searchParams.get("redirectTo"),
);
const existingCaregiver = await getCaregiverSession();
const response = NextResponse.redirect(new URL(redirectTo, request.url));
if (existingCaregiver) {
return response;
}
const { sessionToken, options } = createCaregiverSessionCookieValue();
await prisma.caregiverAccount.upsert({
where: { sessionToken },
update: {},
create: { sessionToken },
});
response.cookies.set({
name: CAREGIVER_SESSION_COOKIE,
value: sessionToken,
...options,
});
return response;
}

106
app/bind/page.tsx Normal file
View File

@ -0,0 +1,106 @@
import Link from "next/link";
import { redirect } from "next/navigation";
import { bindDeviceToCaregiver } from "@/lib/monitor-data";
import {
buildCaregiverSessionBootstrapPath,
getCaregiverSession,
} from "@/lib/session";
export const dynamic = "force-dynamic";
type BindPageProps = {
searchParams: Promise<Record<string, string | string[] | undefined>>;
};
export default async function BindPage({ searchParams }: BindPageProps) {
const params = await searchParams;
const rawDeviceValue =
typeof params.deviceUuid === "string"
? params.deviceUuid
: typeof params.device === "string"
? params.device
: "";
if (!rawDeviceValue) {
return (
<main className="mx-auto flex min-h-screen max-w-3xl items-center px-4 py-8 sm:px-6">
<section className="w-full rounded-[36px] border border-white/70 bg-white/82 p-8 text-center shadow-[0_30px_70px_rgba(115,76,42,0.14)] backdrop-blur">
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
</p>
<h1 className="mt-4 font-[family-name:var(--font-display)] text-4xl text-[var(--ink)]">
</h1>
<p className="mt-4 text-base leading-8 text-[var(--muted)]">
</p>
<div className="mt-6 flex justify-center gap-3">
<Link
href="/scan"
className="inline-flex h-12 items-center justify-center rounded-full bg-[var(--copper)] px-5 text-sm font-semibold text-white transition hover:bg-[var(--copper-deep)]"
>
</Link>
<Link
href="/"
className="inline-flex h-12 items-center justify-center rounded-full border border-[var(--line)] bg-[var(--paper-soft)] px-5 text-sm font-semibold text-[var(--ink)] transition hover:bg-white"
>
</Link>
</div>
</section>
</main>
);
}
const caregiver = await getCaregiverSession();
if (!caregiver) {
redirect(
buildCaregiverSessionBootstrapPath(
`/bind?deviceUuid=${encodeURIComponent(rawDeviceValue)}`,
),
);
}
const device = await bindDeviceToCaregiver(caregiver.id, rawDeviceValue);
return (
<main className="mx-auto flex min-h-screen max-w-3xl items-center px-4 py-8 sm:px-6">
<section className="w-full rounded-[36px] border border-white/70 bg-white/84 p-8 shadow-[0_30px_70px_rgba(115,76,42,0.14)] backdrop-blur">
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
</p>
<h1 className="mt-4 font-[family-name:var(--font-display)] text-4xl text-[var(--ink)]">
</h1>
<p className="mt-4 text-base leading-8 text-[var(--muted)]">
</p>
<div className="mt-6 rounded-[28px] bg-[var(--paper-soft)] p-5">
<p className="text-sm text-[var(--muted)]"> UUID</p>
<p className="mt-2 break-all text-lg font-semibold text-[var(--ink)]">
{device.deviceUuid}
</p>
</div>
<div className="mt-6 flex flex-wrap gap-3">
<Link
href="/"
className="inline-flex h-12 items-center justify-center rounded-full bg-[var(--copper)] px-5 text-sm font-semibold text-white transition hover:bg-[var(--copper-deep)]"
>
</Link>
<Link
href="/scan"
className="inline-flex h-12 items-center justify-center rounded-full border border-[var(--line)] bg-white px-5 text-sm font-semibold text-[var(--ink)] transition hover:bg-[var(--paper-soft)]"
>
</Link>
</div>
</section>
</main>
);
}

View File

@ -1,26 +1,66 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
--paper: #f4ece0;
--paper-soft: #fbf7f1;
--paper-strong: #fffdf8;
--ink: #241b14;
--ink-soft: #3d2d20;
--muted: #726153;
--line: rgba(85, 60, 42, 0.15);
--copper: #c76733;
--copper-deep: #964722;
--olive: #76845e;
--olive-deep: #596946;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--color-background: var(--paper);
--color-foreground: var(--ink);
--font-sans: var(--font-body);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
* {
box-sizing: border-box;
}
html {
background: #ecdfcf;
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
color: var(--ink);
font-family: var(--font-body), sans-serif;
background:
radial-gradient(circle at top left, rgba(255, 255, 255, 0.82), transparent 34%),
radial-gradient(circle at 88% 10%, rgba(205, 126, 76, 0.22), transparent 22%),
linear-gradient(180deg, #f6eee1 0%, #efe2d1 52%, #e8d6c3 100%);
}
a {
color: inherit;
text-decoration: none;
}
button,
input,
textarea,
select {
font: inherit;
}
::selection {
background: rgba(199, 103, 51, 0.18);
}
.paper-panel {
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(255, 249, 241, 0.84)),
repeating-linear-gradient(
135deg,
rgba(198, 163, 127, 0.06),
rgba(198, 163, 127, 0.06) 10px,
rgba(255, 255, 255, 0.08) 10px,
rgba(255, 255, 255, 0.08) 20px
);
}

View File

@ -1,20 +1,23 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { Noto_Sans_SC, Noto_Serif_SC } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
const bodyFont = Noto_Sans_SC({
variable: "--font-body",
subsets: ["latin"],
weight: ["400", "500", "700"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
const displayFont = Noto_Serif_SC({
variable: "--font-display",
subsets: ["latin"],
weight: ["400", "600", "700"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
metadataBase: new URL("https://digital-human.xn--876a.net"),
title: "家人连线台",
description: "给老人设备做绑定、查看留言、追踪陪伴记录的家属端。",
};
export default function RootLayout({
@ -23,11 +26,8 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html
lang="en"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
>
<body className="min-h-full flex flex-col">{children}</body>
<html lang="zh-CN" className={`${bodyFont.variable} ${displayFont.variable}`}>
<body className="min-h-screen">{children}</body>
</html>
);
}

View File

@ -1,65 +1,439 @@
import Image from "next/image";
import Link from "next/link";
import { redirect } from "next/navigation";
import { BindDeviceForm, SendMessageForm } from "@/components/dashboard-actions";
import { getCaregiverDashboard } from "@/lib/monitor-data";
import {
buildCaregiverSessionBootstrapPath,
getCaregiverSession,
} from "@/lib/session";
export const dynamic = "force-dynamic";
function formatTime(date: Date | null | undefined) {
if (!date) {
return "暂时还没有记录";
}
return new Intl.DateTimeFormat("zh-CN", {
month: "numeric",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
}).format(date);
}
function getImportanceLabel(level: string) {
switch (level) {
case "LOW":
return "温和提醒";
case "HIGH":
return "希望尽快看到";
case "URGENT":
return "需要尽快联系";
default:
return "日常问候";
}
}
function getUsageLabel(eventType: string) {
switch (eventType) {
case "APP_OPEN":
return "打开了老人端应用";
case "SETTINGS_OPENED":
return "打开了家人连接页";
case "AI_SESSION_STARTED":
return "开始了一次陪伴对话";
case "AI_SESSION_ENDED":
return "结束了一次陪伴对话";
case "CAMERA_ENABLED":
return "打开了镜头模式";
case "CAMERA_DISABLED":
return "关闭了镜头模式";
case "TOOL_CALLED":
return "模型调用了一次桥接工具";
default:
return eventType;
}
}
export default async function Home() {
const caregiver = await getCaregiverSession();
if (!caregiver) {
redirect(buildCaregiverSessionBootstrapPath("/"));
}
const dashboard = await getCaregiverDashboard(caregiver.id);
const totalUnreadForElder = dashboard.devices.reduce(
(total, device) => total + device.familyUnreadCount,
0,
);
const totalUnreadForCaregiver = dashboard.devices.reduce(
(total, device) => total + device.elderUnreadCount,
0,
);
const totalConversations = dashboard.devices.reduce(
(total, device) => total + device.elderDevice._count.conversationTurns,
0,
);
export default function Home() {
return (
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={100}
height={20}
priority
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
To get started, edit the page.tsx file.
<main className="relative overflow-hidden px-4 py-6 sm:px-6 lg:px-8 lg:py-8">
<div className="pointer-events-none absolute inset-x-0 top-0 h-64 bg-[radial-gradient(circle_at_top,rgba(255,255,255,0.78),transparent_68%)]" />
<div className="mx-auto max-w-7xl space-y-6">
<section className="paper-panel relative overflow-hidden rounded-[36px] border border-white/70 px-6 py-8 shadow-[0_35px_90px_rgba(105,68,41,0.13)] sm:px-8 lg:px-10 lg:py-10">
<div className="absolute -right-10 top-[-52px] h-40 w-40 rounded-full bg-[rgba(200,103,51,0.10)] blur-2xl" />
<div className="absolute left-10 top-8 h-20 w-20 rounded-full border border-[rgba(118,132,94,0.18)]" />
<div className="grid gap-8 lg:grid-cols-[1.2fr_0.8fr] lg:items-end">
<div>
<p className="text-sm font-semibold tracking-[0.24em] text-[var(--copper)] uppercase">
Digital Human Bridge
</p>
<h1 className="mt-4 max-w-3xl font-[family-name:var(--font-display)] text-4xl leading-[1.15] text-[var(--ink)] sm:text-5xl">
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
<p className="mt-5 max-w-2xl text-base leading-8 text-[var(--muted)] sm:text-lg">
使
</p>
<div className="mt-6 flex flex-wrap gap-3 text-sm text-[var(--muted)]">
<span className="rounded-full border border-[var(--line)] bg-white/70 px-4 py-2">
{dashboard.caregiver.sessionToken.slice(0, 8).toUpperCase()}
</span>
<span className="rounded-full border border-[var(--line)] bg-white/70 px-4 py-2">
{dashboard.devices.length}
</span>
</div>
</div>
<div className="grid gap-3 sm:grid-cols-3 lg:grid-cols-1">
<div className="rounded-[28px] border border-white/80 bg-white/88 p-5">
<p className="text-sm text-[var(--muted)]"></p>
<p className="mt-3 font-[family-name:var(--font-display)] text-4xl text-[var(--ink)]">
{dashboard.devices.length}
</p>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
<div className="rounded-[28px] border border-white/80 bg-white/88 p-5">
<p className="text-sm text-[var(--muted)]"></p>
<p className="mt-3 font-[family-name:var(--font-display)] text-4xl text-[var(--ink)]">
{totalUnreadForElder}
</p>
</div>
<div className="rounded-[28px] border border-white/80 bg-white/88 p-5">
<p className="text-sm text-[var(--muted)]"></p>
<p className="mt-3 font-[family-name:var(--font-display)] text-4xl text-[var(--ink)]">
{totalConversations}
</p>
</div>
</div>
</div>
</section>
<BindDeviceForm />
<section className="grid gap-6 lg:grid-cols-[0.85fr_1.15fr]">
<div className="rounded-[32px] border border-white/70 bg-white/78 p-6 shadow-[0_26px_60px_rgba(115,76,42,0.12)] backdrop-blur">
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
</p>
<div className="mt-4 space-y-4">
<div className="rounded-[24px] bg-[var(--paper-soft)] px-5 py-4">
<p className="text-sm text-[var(--muted)]"></p>
<p className="mt-2 text-3xl font-semibold text-[var(--ink)]">
{totalUnreadForCaregiver}
</p>
</div>
<div className="rounded-[24px] bg-[var(--paper-soft)] px-5 py-4">
<p className="text-sm text-[var(--muted)]"></p>
<p className="mt-2 text-3xl font-semibold text-[var(--ink)]">
{totalUnreadForElder}
</p>
</div>
<div className="rounded-[24px] bg-[var(--paper-soft)] px-5 py-4">
<p className="text-sm text-[var(--muted)]"></p>
<p className="mt-2 text-sm leading-7 text-[var(--muted)]">
</p>
<Link
href="/scan"
className="mt-4 inline-flex h-11 items-center justify-center rounded-full bg-[var(--olive)] px-4 text-sm font-semibold text-white transition hover:bg-[var(--olive-deep)]"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={16}
/>
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
</Link>
</div>
</div>
</div>
<div className="rounded-[32px] border border-white/70 bg-white/78 p-6 shadow-[0_26px_60px_rgba(115,76,42,0.12)] backdrop-blur">
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
</p>
<div className="mt-4 grid gap-4 sm:grid-cols-3">
<div className="rounded-[24px] bg-[var(--paper-soft)] p-5">
<h3 className="font-[family-name:var(--font-display)] text-xl text-[var(--ink)]">
</h3>
<p className="mt-2 text-sm leading-7 text-[var(--muted)]">
function calling
</p>
</div>
<div className="rounded-[24px] bg-[var(--paper-soft)] p-5">
<h3 className="font-[family-name:var(--font-display)] text-xl text-[var(--ink)]">
</h3>
<p className="mt-2 text-sm leading-7 text-[var(--muted)]">
使便
</p>
</div>
<div className="rounded-[24px] bg-[var(--paper-soft)] p-5">
<h3 className="font-[family-name:var(--font-display)] text-xl text-[var(--ink)]">
</h3>
<p className="mt-2 text-sm leading-7 text-[var(--muted)]">
function calling
</p>
</div>
</div>
</div>
</section>
{dashboard.devices.length === 0 ? (
<section className="rounded-[32px] border border-dashed border-[var(--line)] bg-white/70 px-6 py-10 text-center shadow-[0_20px_50px_rgba(115,76,42,0.10)]">
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
</p>
<h2 className="mt-3 font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]">
</h2>
<p className="mx-auto mt-4 max-w-2xl text-base leading-8 text-[var(--muted)]">
</p>
</section>
) : (
<section className="space-y-6">
{dashboard.devices.map((binding) => {
const familyMessages = binding.elderDevice.messages.filter(
(message) => message.direction === "FAMILY_TO_ELDER",
);
const elderMessages = binding.elderDevice.messages.filter(
(message) => message.direction === "ELDER_TO_FAMILY",
);
return (
<article
key={binding.id}
className="rounded-[36px] border border-white/75 bg-white/80 p-6 shadow-[0_28px_70px_rgba(108,73,41,0.12)] backdrop-blur lg:p-7"
>
Documentation
</a>
<div className="flex flex-col gap-5 lg:flex-row lg:items-end lg:justify-between">
<div>
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
</p>
<h2 className="mt-2 font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]">
{binding.elderDevice.displayName || "未命名陪伴设备"}
</h2>
<div className="mt-3 flex flex-wrap gap-3 text-sm text-[var(--muted)]">
<span className="rounded-full border border-[var(--line)] bg-[var(--paper-soft)] px-4 py-2">
UUID{binding.elderDevice.deviceUuid}
</span>
<span className="rounded-full border border-[var(--line)] bg-[var(--paper-soft)] px-4 py-2">
线{formatTime(binding.elderDevice.lastSeenAt)}
</span>
<span className="rounded-full border border-[var(--line)] bg-[var(--paper-soft)] px-4 py-2">
{binding.bindUrl}
</span>
</div>
</div>
<div className="grid gap-3 sm:grid-cols-4">
<div className="rounded-[22px] bg-[var(--paper-soft)] px-4 py-4">
<p className="text-xs text-[var(--muted)]"></p>
<p className="mt-2 text-2xl font-semibold text-[var(--ink)]">
{binding.elderUnreadCount}
</p>
</div>
<div className="rounded-[22px] bg-[var(--paper-soft)] px-4 py-4">
<p className="text-xs text-[var(--muted)]"></p>
<p className="mt-2 text-2xl font-semibold text-[var(--ink)]">
{binding.familyUnreadCount}
</p>
</div>
<div className="rounded-[22px] bg-[var(--paper-soft)] px-4 py-4">
<p className="text-xs text-[var(--muted)]"></p>
<p className="mt-2 text-2xl font-semibold text-[var(--ink)]">
{binding.elderDevice._count.conversationTurns}
</p>
</div>
<div className="rounded-[22px] bg-[var(--paper-soft)] px-4 py-4">
<p className="text-xs text-[var(--muted)]"></p>
<p className="mt-2 text-2xl font-semibold text-[var(--ink)]">
{binding.elderDevice._count.toolCalls}
</p>
</div>
</div>
</div>
<div className="mt-6 grid gap-6 xl:grid-cols-[0.95fr_1.05fr]">
<div className="space-y-6">
<section className="rounded-[28px] bg-[var(--paper-soft)] p-5">
<div className="flex items-center justify-between gap-3">
<div>
<p className="text-sm font-medium tracking-[0.16em] text-[var(--copper)] uppercase">
</p>
<h3 className="mt-2 font-[family-name:var(--font-display)] text-2xl text-[var(--ink)]">
</h3>
</div>
<span className="rounded-full bg-white px-4 py-2 text-sm text-[var(--ink)]">
{binding.elderUnreadCount}
</span>
</div>
<div className="mt-4 space-y-3">
{elderMessages.length === 0 ? (
<p className="rounded-[22px] bg-white/80 px-4 py-4 text-sm leading-7 text-[var(--muted)]">
</p>
) : (
elderMessages.map((message) => (
<div key={message.id} className="rounded-[22px] bg-white/88 px-4 py-4">
<div className="flex flex-wrap items-center gap-2 text-xs text-[var(--muted)]">
<span className="rounded-full bg-[var(--paper-soft)] px-3 py-1">
{getImportanceLabel(message.importance)}
</span>
{message.recipientRelation ? (
<span className="rounded-full bg-[var(--paper-soft)] px-3 py-1">
{message.recipientRelation}
</span>
) : null}
<span>{formatTime(message.createdAt)}</span>
</div>
<p className="mt-3 text-sm leading-7 text-[var(--ink)]">
{message.content}
</p>
</div>
))
)}
</div>
</section>
<section className="rounded-[28px] bg-[var(--paper-soft)] p-5">
<p className="text-sm font-medium tracking-[0.16em] text-[var(--copper)] uppercase">
</p>
<h3 className="mt-2 font-[family-name:var(--font-display)] text-2xl text-[var(--ink)]">
</h3>
<div className="mt-4 space-y-3">
{binding.elderDevice.usageEvents.length === 0 ? (
<p className="rounded-[22px] bg-white/80 px-4 py-4 text-sm leading-7 text-[var(--muted)]">
使
</p>
) : (
binding.elderDevice.usageEvents.map((event) => (
<div key={event.id} className="rounded-[22px] bg-white/88 px-4 py-4">
<div className="flex items-center justify-between gap-3 text-xs text-[var(--muted)]">
<span>{getUsageLabel(event.eventType)}</span>
<span>{formatTime(event.createdAt)}</span>
</div>
</div>
))
)}
</div>
</section>
</div>
<div className="space-y-6">
<SendMessageForm deviceUuid={binding.elderDevice.deviceUuid} />
<section className="rounded-[28px] bg-[var(--paper-soft)] p-5">
<div className="flex items-center justify-between gap-3">
<div>
<p className="text-sm font-medium tracking-[0.16em] text-[var(--copper)] uppercase">
</p>
<h3 className="mt-2 font-[family-name:var(--font-display)] text-2xl text-[var(--ink)]">
</h3>
</div>
<span className="rounded-full bg-white px-4 py-2 text-sm text-[var(--ink)]">
{binding.familyUnreadCount}
</span>
</div>
<div className="mt-4 space-y-3">
{familyMessages.length === 0 ? (
<p className="rounded-[22px] bg-white/80 px-4 py-4 text-sm leading-7 text-[var(--muted)]">
</p>
) : (
familyMessages.map((message) => (
<div key={message.id} className="rounded-[22px] bg-white/88 px-4 py-4">
<div className="flex flex-wrap items-center gap-2 text-xs text-[var(--muted)]">
<span className="rounded-full bg-[var(--paper-soft)] px-3 py-1">
{getImportanceLabel(message.importance)}
</span>
<span>{formatTime(message.createdAt)}</span>
<span>
{message.readAt ? "老人已看过" : "等待老人查看"}
</span>
</div>
<p className="mt-3 text-sm leading-7 text-[var(--ink)]">
{message.content}
</p>
</div>
))
)}
</div>
</section>
<section className="rounded-[28px] bg-[var(--paper-soft)] p-5">
<p className="text-sm font-medium tracking-[0.16em] text-[var(--copper)] uppercase">
</p>
<h3 className="mt-2 font-[family-name:var(--font-display)] text-2xl text-[var(--ink)]">
</h3>
<div className="mt-4 space-y-3">
{binding.elderDevice.conversationTurns.length === 0 ? (
<p className="rounded-[22px] bg-white/80 px-4 py-4 text-sm leading-7 text-[var(--muted)]">
</p>
) : (
binding.elderDevice.conversationTurns.map((turn) => (
<div key={turn.id} className="rounded-[22px] bg-white/88 px-4 py-4">
<div className="flex items-center justify-between gap-3 text-xs text-[var(--muted)]">
<span>
{turn.role === "USER"
? "老人说"
: turn.role === "ASSISTANT"
? "数字人回复"
: "工具结果"}
</span>
<span>{formatTime(turn.createdAt)}</span>
</div>
<p className="mt-3 text-sm leading-7 text-[var(--ink)]">
{turn.content}
</p>
</div>
))
)}
</div>
</section>
</div>
</div>
</article>
);
})}
</section>
)}
</div>
</main>
</div>
);
}

40
app/scan/page.tsx Normal file
View File

@ -0,0 +1,40 @@
import Link from "next/link";
import { ScanClient } from "@/components/scan-client";
export default function ScanPage() {
return (
<main className="mx-auto min-h-screen max-w-5xl px-4 py-8 sm:px-6 lg:px-8">
<section className="grid gap-6 lg:grid-cols-[0.82fr_1.18fr]">
<div className="rounded-[36px] border border-white/70 bg-white/78 p-6 shadow-[0_30px_70px_rgba(115,76,42,0.14)] backdrop-blur">
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
</p>
<h1 className="mt-4 font-[family-name:var(--font-display)] text-4xl leading-[1.15] text-[var(--ink)]">
</h1>
<p className="mt-4 text-base leading-8 text-[var(--muted)]">
UUID 使
</p>
<div className="mt-6 space-y-3 rounded-[28px] bg-[var(--paper-soft)] p-5 text-sm leading-7 text-[var(--muted)]">
<p>1. </p>
<p>2. </p>
<p>3. </p>
</div>
<div className="mt-6 flex flex-wrap gap-3">
<Link
href="/"
className="inline-flex h-12 items-center justify-center rounded-full border border-[var(--line)] bg-white px-5 text-sm font-semibold text-[var(--ink)] transition hover:bg-[var(--paper-soft)]"
>
</Link>
</div>
</div>
<ScanClient />
</section>
</main>
);
}

226
bun.lock
View File

@ -5,7 +5,12 @@
"": {
"name": "digital-human-monitor-v2",
"dependencies": {
"@prisma/adapter-pg": "^7.7.0",
"@prisma/client": "^7.7.0",
"dotenv": "^17.4.2",
"next": "16.2.4",
"pg": "^8.20.0",
"qr-scanner": "^1.4.2",
"react": "19.2.4",
"react-dom": "19.2.4",
},
@ -16,6 +21,7 @@
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.2.4",
"prisma": "^7.7.0",
"tailwindcss": "^4",
"typescript": "^5",
},
@ -60,6 +66,12 @@
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
"@electric-sql/pglite": ["@electric-sql/pglite@0.4.1", "", {}, "sha512-mZ9NzzUSYPOCnxHH1oAHPRzoMFJHY472raDKwXl/+6oPbpdJ7g8LsCN4FSaIIfkiCKHhb3iF/Zqo3NYxaIhU7Q=="],
"@electric-sql/pglite-socket": ["@electric-sql/pglite-socket@0.1.1", "", { "peerDependencies": { "@electric-sql/pglite": "0.4.1" }, "bin": { "pglite-server": "dist/scripts/server.js" } }, "sha512-p2hoXw3Z3LQHwTeikdZNsFBOvXGqKY2hk51BBw+8NKND8eoH+8LFOtW9Z8CQKmTJ2qqGYu82ipqiyFZOTTXNfw=="],
"@electric-sql/pglite-tools": ["@electric-sql/pglite-tools@0.3.1", "", { "peerDependencies": { "@electric-sql/pglite": "0.4.1" } }, "sha512-C+T3oivmy9bpQvSxVqXA1UDY8cB9Eb9vZHL9zxWwEUfDixbXv4G3r2LjoTdR33LD8aomR3O9ZXEO3XEwr/cUCA=="],
"@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
"@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
@ -84,6 +96,8 @@
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
"@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="],
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
"@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="],
@ -152,6 +166,8 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="],
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
"@next/env": ["@next/env@16.2.4", "", {}, "sha512-dKkkOzOSwFYe5RX6y26fZgkSpVAlIOJKQHIiydQcrWH6y/97+RceSOAdjZ14Qa3zLduVUy0TXcn+EiM6t4rPgw=="],
@ -182,8 +198,54 @@
"@nolyfill/is-core-module": ["@nolyfill/is-core-module@1.0.39", "", {}, "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA=="],
"@prisma/adapter-pg": ["@prisma/adapter-pg@7.7.0", "", { "dependencies": { "@prisma/driver-adapter-utils": "7.7.0", "@types/pg": "^8.16.0", "pg": "^8.16.3", "postgres-array": "3.0.4" } }, "sha512-q33Ta8sKbgzEpAy0lx45tAq//yMv0qcb+8nj+TCA3P4wiAY+OBFEFk/NDkZncAfHaNJeGo5WJpJdpbL+ijYx8g=="],
"@prisma/client": ["@prisma/client@7.7.0", "", { "dependencies": { "@prisma/client-runtime-utils": "7.7.0" }, "peerDependencies": { "prisma": "*", "typescript": ">=5.4.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-5Ar4OsZpJ54s21sy5oDNNW9gQtd4NuxCaiM7+JDTOU07D6VvlpLjYzAVCMB1+JzokN+08dAVomlx+b7bhJd3ww=="],
"@prisma/client-runtime-utils": ["@prisma/client-runtime-utils@7.7.0", "", {}, "sha512-BLyd0UpFYOtyJFTHm7jS9vesHW7P83abibodQMiIofqjBKzDHQ1VAsQkdfvXyYDkPlONPfOTz7/rv3x/+CQqvQ=="],
"@prisma/config": ["@prisma/config@7.7.0", "", { "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", "effect": "3.20.0", "empathic": "2.0.0" } }, "sha512-hmPI3tKLO2aP0Y5vugbjcnA9qqlfJndiT6ds4tw28U5hNHLWg+mHJEWAhjsSPgxjtmxhJ/EDIeIlyh+3Us0OPg=="],
"@prisma/debug": ["@prisma/debug@7.7.0", "", {}, "sha512-12J62XdqCmpiwJHhHdQxZeY3ckVCWIFmcJP8hg5dPTceeiQ0wiojXGFYTluKqFQfu46fRLgb/rLALZMAx3+dTA=="],
"@prisma/dev": ["@prisma/dev@0.24.3", "", { "dependencies": { "@electric-sql/pglite": "0.4.1", "@electric-sql/pglite-socket": "0.1.1", "@electric-sql/pglite-tools": "0.3.1", "@hono/node-server": "1.19.11", "@prisma/get-platform": "7.2.0", "@prisma/query-plan-executor": "7.2.0", "@prisma/streams-local": "0.1.2", "foreground-child": "3.3.1", "get-port-please": "3.2.0", "hono": "^4.12.8", "http-status-codes": "2.3.0", "pathe": "2.0.3", "proper-lockfile": "4.1.2", "remeda": "2.33.4", "std-env": "3.10.0", "valibot": "1.2.0", "zeptomatch": "2.1.0" } }, "sha512-ffHlQuKXZiaDt9Go0OnCTdJZrHxK0k7omJKNV86/VjpsXu5EIHZLK0T7JSWgvNlJwh56kW9JFu9v0qJciFzepg=="],
"@prisma/driver-adapter-utils": ["@prisma/driver-adapter-utils@7.7.0", "", { "dependencies": { "@prisma/debug": "7.7.0" } }, "sha512-gZXREeu6mOk7zXfGFJgh86p7Vhj0sXNKp+4Cg1tWYo7V2dfncP2qxS2BiTmbIIha8xPqItkl0WSw38RuSq1HoQ=="],
"@prisma/engines": ["@prisma/engines@7.7.0", "", { "dependencies": { "@prisma/debug": "7.7.0", "@prisma/engines-version": "7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711", "@prisma/fetch-engine": "7.7.0", "@prisma/get-platform": "7.7.0" } }, "sha512-7fmcbT7HHXBq/b+3h/dO1JI3fd8l8q7erf7xP7pRprh58hmSSnG8mg9K3yjW3h9WaHWUwngVFpSxxxivaitQ2w=="],
"@prisma/engines-version": ["@prisma/engines-version@7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711", "", {}, "sha512-r51DLcJ8bDRSrBEJF3J4cinoWyGA7rfP2mG6lD90VqIbGNOkbfcLcXalSVjq5Y6brQS3vcjrq4GbyUb1Cb7vkw=="],
"@prisma/fetch-engine": ["@prisma/fetch-engine@7.7.0", "", { "dependencies": { "@prisma/debug": "7.7.0", "@prisma/engines-version": "7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711", "@prisma/get-platform": "7.7.0" } }, "sha512-TfyzveBQoK4xALzsTpVhB/0KG1N8zOK0ap+RnBMkzGUu3f98fnQ4QtXa2wlKPhsO2X8a3N5ugFQgcKNoHGmDfw=="],
"@prisma/get-platform": ["@prisma/get-platform@7.2.0", "", { "dependencies": { "@prisma/debug": "7.2.0" } }, "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA=="],
"@prisma/query-plan-executor": ["@prisma/query-plan-executor@7.2.0", "", {}, "sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ=="],
"@prisma/streams-local": ["@prisma/streams-local@0.1.2", "", { "dependencies": { "ajv": "^8.12.0", "better-result": "^2.7.0", "env-paths": "^3.0.0", "proper-lockfile": "^4.1.2" } }, "sha512-l49yTxKKF2odFxaAXTmwmkBKL3+bVQ1tFOooGifu4xkdb9NMNLxHj27XAhTylWZod8I+ISGM5erU1xcl/oBCtg=="],
"@prisma/studio-core": ["@prisma/studio-core@0.27.3", "", { "dependencies": { "@radix-ui/react-toggle": "1.1.10", "chart.js": "4.5.1" }, "peerDependencies": { "@types/react": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-AADjNFPdsrglxHQVTmHFqv6DuKQZ5WY4p5/gVFY017twvNrSwpLJ9lqUbYYxEu2W7nbvVxTZA8deJ8LseNALsw=="],
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
"@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ=="],
"@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
"@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="],
"@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
"@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="],
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
"@tailwindcss/node": ["@tailwindcss/node@4.2.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.2" } }, "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA=="],
@ -226,6 +288,10 @@
"@types/node": ["@types/node@20.19.39", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw=="],
"@types/offscreencanvas": ["@types/offscreencanvas@2019.7.3", "", {}, "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A=="],
"@types/pg": ["@types/pg@8.20.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow=="],
"@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
@ -322,6 +388,8 @@
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
"aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="],
"axe-core": ["axe-core@4.11.3", "", {}, "sha512-zBQouZixDTbo3jMGqHKyePxYxr1e5W8UdTmBQ7sNtaA9M2bE32daxxPLS/jojhKOHxQ7LWwPjfiwf/fhaJWzlg=="],
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
@ -330,12 +398,16 @@
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.19", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g=="],
"better-result": ["better-result@2.8.2", "", {}, "sha512-YOf0VSj5nUPI27doTtXF+BBnsiRq3qY7avHqfIWnppxTLGyvkLq1QV2RTxkwoZwJ60ywLfZ0raFF4J/G886i7A=="],
"brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="],
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
"browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="],
"c12": ["c12@3.1.0", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^16.6.1", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.4.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw=="],
"call-bind": ["call-bind@1.0.9", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
@ -348,6 +420,12 @@
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"chart.js": ["chart.js@4.5.1", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw=="],
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
"citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="],
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
@ -356,6 +434,10 @@
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
"confbox": ["confbox@0.2.4", "", {}, "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ=="],
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
@ -374,22 +456,38 @@
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
"deepmerge-ts": ["deepmerge-ts@7.1.5", "", {}, "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw=="],
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
"define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
"defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="],
"denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
"destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
"dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"effect": ["effect@3.20.0", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw=="],
"electron-to-chromium": ["electron-to-chromium@1.5.339", "", {}, "sha512-Is+0BBHJ4NrdpAYiperrmp53pLywG/yV/6lIMTAnhxvzj/Cmn5Q/ogSHC6AKe7X+8kPLxxFk0cs5oc/3j/fxIg=="],
"emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
"empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="],
"enhanced-resolve": ["enhanced-resolve@5.20.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA=="],
"env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="],
"es-abstract": ["es-abstract@1.24.2", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
@ -442,6 +540,10 @@
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
"exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="],
"fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-glob": ["fast-glob@3.3.1", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg=="],
@ -450,6 +552,8 @@
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
"fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
@ -466,24 +570,32 @@
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="],
"functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="],
"generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="],
"generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="],
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"get-port-please": ["get-port-please@3.2.0", "", {}, "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A=="],
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="],
"get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="],
"giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="],
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
"globals": ["globals@16.4.0", "", {}, "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw=="],
@ -494,6 +606,10 @@
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"grammex": ["grammex@3.1.12", "", {}, "sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ=="],
"graphmatch": ["graphmatch@1.1.1", "", {}, "sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg=="],
"has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
@ -512,6 +628,12 @@
"hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="],
"hono": ["hono@4.12.14", "", {}, "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w=="],
"http-status-codes": ["http-status-codes@2.3.0", "", {}, "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA=="],
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
@ -554,6 +676,8 @@
"is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="],
"is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="],
"is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="],
"is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="],
@ -632,10 +756,14 @@
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"lru.min": ["lru.min@1.1.4", "", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
@ -650,6 +778,10 @@
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"mysql2": ["mysql2@3.15.3", "", { "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.0", "long": "^5.2.1", "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" } }, "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg=="],
"named-placeholders": ["named-placeholders@1.1.6", "", { "dependencies": { "lru.min": "^1.1.0" } }, "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"napi-postinstall": ["napi-postinstall@0.3.4", "", { "bin": { "napi-postinstall": "lib/cli.js" } }, "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ=="],
@ -660,8 +792,12 @@
"node-exports-info": ["node-exports-info@1.6.0", "", { "dependencies": { "array.prototype.flatmap": "^1.3.3", "es-errors": "^1.3.0", "object.entries": "^1.1.9", "semver": "^6.3.1" } }, "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw=="],
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
"node-releases": ["node-releases@2.0.37", "", {}, "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg=="],
"nypm": ["nypm@0.6.5", "", { "dependencies": { "citty": "^0.2.0", "pathe": "^2.0.3", "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
@ -678,6 +814,8 @@
"object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="],
"ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
"own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="],
@ -694,38 +832,88 @@
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="],
"pg": ["pg@8.20.0", "", { "dependencies": { "pg-connection-string": "^2.12.0", "pg-pool": "^3.13.0", "pg-protocol": "^1.13.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA=="],
"pg-cloudflare": ["pg-cloudflare@1.3.0", "", {}, "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ=="],
"pg-connection-string": ["pg-connection-string@2.12.0", "", {}, "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ=="],
"pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
"pg-pool": ["pg-pool@3.13.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA=="],
"pg-protocol": ["pg-protocol@1.13.0", "", {}, "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w=="],
"pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="],
"pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
"pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="],
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
"postcss": ["postcss@8.5.10", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ=="],
"postgres": ["postgres@3.4.7", "", {}, "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw=="],
"postgres-array": ["postgres-array@3.0.4", "", {}, "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ=="],
"postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="],
"postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="],
"postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
"prisma": ["prisma@7.7.0", "", { "dependencies": { "@prisma/config": "7.7.0", "@prisma/dev": "0.24.3", "@prisma/engines": "7.7.0", "@prisma/studio-core": "0.27.3", "mysql2": "3.15.3", "postgres": "3.4.7" }, "peerDependencies": { "better-sqlite3": ">=9.0.0", "typescript": ">=5.4.0" }, "optionalPeers": ["better-sqlite3", "typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-HlgwRBt1uEFB9LStHL4HLYDvoi4BNu1rYA0hPG0zCAEyK9SaZBqp7E5Rjpc3Qh8Lex/ye/svoHZ0OWoFNhWxuQ=="],
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
"proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="],
"qr-scanner": ["qr-scanner@1.4.2", "", { "dependencies": { "@types/offscreencanvas": "^2019.6.4" } }, "sha512-kV1yQUe2FENvn59tMZW6mOVfpq9mGxGf8l6+EGaXUOd4RBOLg7tRC83OrirM5AtDvZRpdjdlXURsHreAOSPOUw=="],
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
"rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="],
"react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
"react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
"regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
"remeda": ["remeda@2.33.4", "", {}, "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ=="],
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
"resolve": ["resolve@2.0.0-next.6", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "node-exports-info": "^1.6.0", "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA=="],
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
"retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
@ -736,10 +924,14 @@
"safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"seq-queue": ["seq-queue@0.0.5", "", {}, "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="],
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
"set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="],
@ -760,10 +952,18 @@
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
"sqlstring": ["sqlstring@2.3.3", "", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="],
"stable-hash": ["stable-hash@0.0.5", "", {}, "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA=="],
"std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
"string.prototype.includes": ["string.prototype.includes@2.0.1", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3" } }, "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg=="],
@ -792,6 +992,8 @@
"tapable": ["tapable@2.3.2", "", {}, "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA=="],
"tinyexec": ["tinyexec@1.1.1", "", {}, "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg=="],
"tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
@ -826,6 +1028,8 @@
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"valibot": ["valibot@1.2.0", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="],
@ -838,10 +1042,14 @@
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
"zeptomatch": ["zeptomatch@2.1.0", "", { "dependencies": { "grammex": "^3.1.11", "graphmatch": "^1.1.0" } }, "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA=="],
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
"zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="],
@ -852,6 +1060,14 @@
"@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
"@prisma/engines/@prisma/get-platform": ["@prisma/get-platform@7.7.0", "", { "dependencies": { "@prisma/debug": "7.7.0" } }, "sha512-MEUNzvKxvYnJ7kgvd6oNRnMmmiGNS9TYLB2weMeIXplnHdL/UWEGnvavYGnN7KLJ2n0iI4dDAyzSkHI3c7AscQ=="],
"@prisma/fetch-engine/@prisma/get-platform": ["@prisma/get-platform@7.7.0", "", { "dependencies": { "@prisma/debug": "7.7.0" } }, "sha512-MEUNzvKxvYnJ7kgvd6oNRnMmmiGNS9TYLB2weMeIXplnHdL/UWEGnvavYGnN7KLJ2n0iI4dDAyzSkHI3c7AscQ=="],
"@prisma/get-platform/@prisma/debug": ["@prisma/debug@7.2.0", "", {}, "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw=="],
"@prisma/streams-local/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
@ -872,6 +1088,8 @@
"@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
"c12/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
"eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
"eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
@ -886,8 +1104,16 @@
"next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
"nypm/citty": ["citty@0.2.2", "", {}, "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w=="],
"pg-types/postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
"proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
"sharp/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"@prisma/streams-local/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="],
"@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],

View File

@ -0,0 +1,211 @@
"use client";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { startTransition, useState } from "react";
export function BindDeviceForm() {
const router = useRouter();
const [rawCode, setRawCode] = useState("");
const [submitting, setSubmitting] = useState(false);
const [notice, setNotice] = useState<string | null>(null);
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!rawCode.trim()) {
setNotice("先把老人二维码里的设备码贴进来。\n");
return;
}
setSubmitting(true);
setNotice("正在建立连接,请稍候……");
try {
const response = await fetch("/api/family/bind", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ deviceUuid: rawCode }),
});
const payload = (await response.json().catch(() => null)) as
| { error?: string }
| null;
if (!response.ok) {
throw new Error(payload?.error || "连接失败,请稍后再试。");
}
setNotice("已经连上了,这位老人的留言和陪伴记录会出现在下方。\n");
setRawCode("");
startTransition(() => {
router.refresh();
});
} catch (error) {
setNotice(
error instanceof Error ? error.message : "连接失败,请稍后再试。",
);
} finally {
setSubmitting(false);
}
}
return (
<div className="rounded-[28px] border border-white/70 bg-white/80 p-5 shadow-[0_24px_60px_rgba(125,80,46,0.12)] backdrop-blur">
<div className="flex flex-col gap-3 lg:flex-row lg:items-end lg:justify-between">
<div className="max-w-xl">
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
</p>
<h3 className="mt-2 font-[family-name:var(--font-display)] text-2xl text-[var(--ink)]">
</h3>
<p className="mt-2 text-sm leading-7 text-[var(--muted)]">
</p>
</div>
<Link
href="/scan"
className="inline-flex h-12 items-center justify-center rounded-full bg-[var(--copper)] px-5 text-sm font-semibold text-white transition hover:bg-[var(--copper-deep)]"
>
</Link>
</div>
<form className="mt-5 flex flex-col gap-3 lg:flex-row" onSubmit={handleSubmit}>
<input
value={rawCode}
onChange={(event) => setRawCode(event.target.value)}
placeholder="粘贴二维码链接,或者直接贴设备 UUID"
className="h-13 flex-1 rounded-full border border-[var(--line)] bg-[var(--paper-soft)] px-5 text-sm text-[var(--ink)] outline-none transition focus:border-[var(--copper)] focus:bg-white"
/>
<button
type="submit"
disabled={submitting}
className="inline-flex h-13 min-w-36 items-center justify-center rounded-full bg-[var(--olive)] px-5 text-sm font-semibold text-white transition hover:bg-[var(--olive-deep)] disabled:cursor-not-allowed disabled:opacity-60"
>
{submitting ? "正在连接" : "立即绑定"}
</button>
</form>
{notice ? (
<p className="mt-3 whitespace-pre-line text-sm leading-7 text-[var(--muted)]">
{notice}
</p>
) : null}
</div>
);
}
type SendMessageFormProps = {
deviceUuid: string;
};
export function SendMessageForm({ deviceUuid }: SendMessageFormProps) {
const router = useRouter();
const [content, setContent] = useState("");
const [importance, setImportance] = useState("NORMAL");
const [submitting, setSubmitting] = useState(false);
const [notice, setNotice] = useState<string | null>(null);
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!content.trim()) {
setNotice("先写下想对老人说的话。\n");
return;
}
setSubmitting(true);
setNotice("正在送达这条留言……");
try {
const response = await fetch("/api/family/messages", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
deviceUuid,
content,
importance,
}),
});
const payload = (await response.json().catch(() => null)) as
| { error?: string }
| null;
if (!response.ok) {
throw new Error(payload?.error || "留言发送失败,请稍后再试。");
}
setNotice("留言已经写进家人的收件盒,老人下次打开或询问时就能看到。\n");
setContent("");
setImportance("NORMAL");
startTransition(() => {
router.refresh();
});
} catch (error) {
setNotice(
error instanceof Error ? error.message : "留言发送失败,请稍后再试。",
);
} finally {
setSubmitting(false);
}
}
return (
<form className="rounded-[28px] border border-white/70 bg-white/88 p-5" onSubmit={handleSubmit}>
<div className="flex items-center justify-between gap-3">
<div>
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
</p>
<h4 className="mt-2 font-[family-name:var(--font-display)] text-xl text-[var(--ink)]">
</h4>
</div>
<select
value={importance}
onChange={(event) => setImportance(event.target.value)}
className="h-11 rounded-full border border-[var(--line)] bg-[var(--paper-soft)] px-4 text-sm text-[var(--ink)] outline-none"
>
<option value="LOW"></option>
<option value="NORMAL"></option>
<option value="HIGH"></option>
<option value="URGENT"></option>
</select>
</div>
<textarea
value={content}
onChange={(event) => setContent(event.target.value)}
rows={4}
placeholder="例如:妈,晚上天气凉,记得添件衣服。晚饭后我给您打电话。"
className="mt-4 w-full rounded-[24px] border border-[var(--line)] bg-[var(--paper-soft)] px-4 py-4 text-sm leading-7 text-[var(--ink)] outline-none transition focus:border-[var(--copper)] focus:bg-white"
/>
<div className="mt-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<p className="text-sm leading-7 text-[var(--muted)]">
function calling
</p>
<button
type="submit"
disabled={submitting}
className="inline-flex h-12 min-w-32 items-center justify-center rounded-full bg-[var(--ink)] px-5 text-sm font-semibold text-white transition hover:bg-[var(--ink-soft)] disabled:cursor-not-allowed disabled:opacity-60"
>
{submitting ? "正在送达" : "写好并送出"}
</button>
</div>
{notice ? (
<p className="mt-3 whitespace-pre-line text-sm leading-7 text-[var(--muted)]">
{notice}
</p>
) : null}
</form>
);
}

164
components/scan-client.tsx Normal file
View File

@ -0,0 +1,164 @@
"use client";
import QrScanner from "qr-scanner";
import { useRouter } from "next/navigation";
import {
startTransition,
useEffect,
useEffectEvent,
useRef,
useState,
} from "react";
function extractDeviceUuid(rawValue: string) {
const trimmedValue = rawValue.trim();
if (!trimmedValue) {
return null;
}
try {
const parsedUrl = new URL(trimmedValue);
return (
parsedUrl.searchParams.get("deviceUuid") ||
parsedUrl.searchParams.get("device") ||
null
);
} catch {
return trimmedValue;
}
}
export function ScanClient() {
const router = useRouter();
const videoRef = useRef<HTMLVideoElement | null>(null);
const scannerRef = useRef<QrScanner | null>(null);
const [statusText, setStatusText] = useState("正在准备镜头……");
const [errorText, setErrorText] = useState<string | null>(null);
const handleDecode = useEffectEvent(async (result: { data: string }) => {
const scannedValue = extractDeviceUuid(result.data);
if (!scannerRef.current || !scannedValue) {
return;
}
scannerRef.current.stop();
setErrorText(null);
setStatusText("识别到了设备码,正在建立连接……");
try {
const response = await fetch("/api/family/bind", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ deviceUuid: scannedValue }),
});
const payload = (await response.json().catch(() => null)) as
| { error?: string; deviceUuid?: string }
| null;
const deviceUuid = payload?.deviceUuid;
if (!response.ok || !deviceUuid) {
throw new Error(payload?.error || "绑定失败,请重新扫描一次。");
}
startTransition(() => {
router.replace(
`/bind?deviceUuid=${encodeURIComponent(deviceUuid)}&source=scan`,
);
});
} catch (error) {
setErrorText(
error instanceof Error ? error.message : "绑定失败,请重新扫描一次。",
);
setStatusText("请再把二维码对准镜头中央。");
await scannerRef.current.start().catch(() => null);
}
});
useEffect(() => {
let cancelled = false;
async function startScanner() {
if (!videoRef.current) {
return;
}
try {
const hasCamera = await QrScanner.hasCamera();
if (!hasCamera) {
setErrorText("当前设备没有可用摄像头,请改用系统扫码或手动粘贴设备码。");
setStatusText("无法打开镜头");
return;
}
const scanner = new QrScanner(
videoRef.current,
(result) => {
void handleDecode(result as { data: string });
},
{
preferredCamera: "environment",
highlightCodeOutline: true,
highlightScanRegion: true,
returnDetailedScanResult: true,
onDecodeError: () => {},
},
);
scannerRef.current = scanner;
await scanner.start();
if (!cancelled) {
setStatusText("请把老人的二维码放到取景框中央。");
}
} catch (error) {
setErrorText(
error instanceof Error
? error.message
: "镜头启动失败,请检查权限后重试。",
);
setStatusText("镜头启动失败");
}
}
void startScanner();
return () => {
cancelled = true;
scannerRef.current?.destroy();
scannerRef.current = null;
};
}, []);
return (
<div className="rounded-[32px] border border-white/70 bg-white/82 p-5 shadow-[0_30px_70px_rgba(115,72,44,0.14)] backdrop-blur">
<div className="relative overflow-hidden rounded-[28px] border border-[var(--line)] bg-[var(--ink)]/95">
<video
ref={videoRef}
muted
playsInline
className="aspect-[3/4] w-full object-cover"
/>
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_center,transparent_0_28%,rgba(10,7,5,0.48)_29%_100%)]" />
<div className="pointer-events-none absolute inset-x-[16%] top-[20%] aspect-square rounded-[28px] border-2 border-white/85 shadow-[0_0_0_999px_rgba(0,0,0,0.18)]" />
</div>
<div className="mt-4 space-y-2">
<p className="text-base font-semibold text-[var(--ink)]">{statusText}</p>
<p className="text-sm leading-7 text-[var(--muted)]">
</p>
{errorText ? (
<p className="rounded-[20px] bg-[var(--paper-soft)] px-4 py-3 text-sm leading-7 text-[var(--copper-deep)]">
{errorText}
</p>
) : null}
</div>
</div>
);
}

486
lib/monitor-data.ts Normal file
View File

@ -0,0 +1,486 @@
import {
ConversationRole,
MessageDirection,
MessageImportance,
MessageStatus,
ToolCallStatus,
UsageEventType,
} from "@prisma/client";
import { prisma } from "@/lib/prisma";
type RegisterDeviceInput = {
deviceUuid: string;
displayName?: string;
appVersion?: string;
};
type UsageEventInput = {
deviceUuid: string;
eventType: UsageEventType;
detailJson?: string;
};
type ConversationTurnInput = {
deviceUuid: string;
sessionId?: string;
turns: Array<{
role: ConversationRole;
content: string;
metaJson?: string;
createdAt?: string;
}>;
};
type ToolCallInput = {
deviceUuid: string;
toolName: string;
arguments: Record<string, unknown>;
outputText?: string;
status: ToolCallStatus;
callId?: string;
sessionId?: string;
};
const PUBLIC_BASE_URL = (
process.env.NEXT_PUBLIC_APP_URL || "https://digital-human.xn--876a.net"
).replace(/\/+$/, "");
const DEVICE_UUID_PATTERN =
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
export function normalizeDeviceUuid(rawValue: string) {
return rawValue.trim().toLowerCase();
}
export function createBindUrl(deviceUuid: string) {
return `${PUBLIC_BASE_URL}/bind?deviceUuid=${encodeURIComponent(
normalizeDeviceUuid(deviceUuid),
)}`;
}
export function extractDeviceUuid(rawValue: string) {
const trimmedValue = rawValue.trim();
if (!trimmedValue) {
return null;
}
try {
const parsedUrl = new URL(trimmedValue);
const queryDeviceUuid =
parsedUrl.searchParams.get("deviceUuid") ||
parsedUrl.searchParams.get("device");
if (queryDeviceUuid && DEVICE_UUID_PATTERN.test(queryDeviceUuid)) {
return normalizeDeviceUuid(queryDeviceUuid);
}
const lastSegment = parsedUrl.pathname.split("/").filter(Boolean).at(-1);
if (lastSegment && DEVICE_UUID_PATTERN.test(lastSegment)) {
return normalizeDeviceUuid(lastSegment);
}
} catch {
if (DEVICE_UUID_PATTERN.test(trimmedValue)) {
return normalizeDeviceUuid(trimmedValue);
}
}
return null;
}
export async function ensureDeviceRegistration(input: RegisterDeviceInput) {
const deviceUuid = normalizeDeviceUuid(input.deviceUuid);
return prisma.elderDevice.upsert({
where: { deviceUuid },
update: {
displayName: input.displayName?.trim() || undefined,
appVersion: input.appVersion?.trim() || undefined,
lastSeenAt: new Date(),
},
create: {
deviceUuid,
displayName: input.displayName?.trim() || undefined,
appVersion: input.appVersion?.trim() || undefined,
lastSeenAt: new Date(),
},
});
}
export async function bindDeviceToCaregiver(
caregiverId: string,
rawDeviceValue: string,
) {
const deviceUuid = extractDeviceUuid(rawDeviceValue);
if (!deviceUuid) {
throw new Error("设备码格式不正确,请重新扫码或粘贴。");
}
const device = await ensureDeviceRegistration({ deviceUuid });
await prisma.deviceBinding.upsert({
where: {
caregiverId_elderDeviceId: {
caregiverId,
elderDeviceId: device.id,
},
},
update: {},
create: {
caregiverId,
elderDeviceId: device.id,
},
});
return device;
}
function parseImportance(rawImportance?: string | null) {
const normalizedImportance = rawImportance?.trim().toUpperCase();
switch (normalizedImportance) {
case MessageImportance.LOW:
return MessageImportance.LOW;
case MessageImportance.HIGH:
return MessageImportance.HIGH;
case MessageImportance.URGENT:
return MessageImportance.URGENT;
default:
return MessageImportance.NORMAL;
}
}
function buildFamilyMessageSummary(
messages: Array<{ content: string }>,
unreadCount: number,
) {
if (messages.length === 0) {
return "家里人暂时还没有新的留言。";
}
const preview = messages
.slice(0, 3)
.map((message, index) => `${index + 1}. ${message.content}`)
.join(" ");
if (unreadCount > 0) {
return `家里人给您留了${unreadCount}条新话:${preview}`;
}
return `家里人最近说:${preview}`;
}
export async function listFamilyMessagesForDevice(
rawDeviceValue: string,
take = 12,
) {
const deviceUuid = extractDeviceUuid(rawDeviceValue);
if (!deviceUuid) {
throw new Error("设备码格式不正确。");
}
const device = await ensureDeviceRegistration({ deviceUuid });
const messages = await prisma.familyMessage.findMany({
where: {
elderDeviceId: device.id,
direction: MessageDirection.FAMILY_TO_ELDER,
},
orderBy: { createdAt: "desc" },
take,
});
const unreadCount = await prisma.familyMessage.count({
where: {
elderDeviceId: device.id,
direction: MessageDirection.FAMILY_TO_ELDER,
readAt: null,
},
});
await prisma.familyMessage.updateMany({
where: {
elderDeviceId: device.id,
direction: MessageDirection.FAMILY_TO_ELDER,
deliveredAt: null,
},
data: {
deliveredAt: new Date(),
status: MessageStatus.DELIVERED,
},
});
const summarySource =
messages.filter((message) => message.readAt === null).length > 0
? messages.filter((message) => message.readAt === null)
: messages;
return {
device,
unreadCount,
summaryText: buildFamilyMessageSummary(summarySource, unreadCount),
messages,
};
}
export async function markFamilyMessagesAsRead(
rawDeviceValue: string,
messageIds?: string[],
) {
const deviceUuid = extractDeviceUuid(rawDeviceValue);
if (!deviceUuid) {
throw new Error("设备码格式不正确。");
}
const device = await ensureDeviceRegistration({ deviceUuid });
const now = new Date();
await prisma.familyMessage.updateMany({
where: {
elderDeviceId: device.id,
direction: MessageDirection.FAMILY_TO_ELDER,
readAt: null,
id: messageIds?.length ? { in: messageIds } : undefined,
},
data: {
readAt: now,
deliveredAt: now,
status: MessageStatus.READ,
},
});
}
export async function createFamilyMessageFromCaregiver(input: {
caregiverId: string;
rawDeviceValue: string;
content: string;
importance?: string;
}) {
const deviceUuid = extractDeviceUuid(input.rawDeviceValue);
if (!deviceUuid) {
throw new Error("设备码格式不正确,请重新扫码或粘贴。");
}
const binding = await prisma.deviceBinding.findFirst({
where: {
caregiverId: input.caregiverId,
elderDevice: {
deviceUuid,
},
},
include: {
elderDevice: true,
},
});
if (!binding) {
throw new Error("这位老人还没有绑定到当前家属账号。");
}
return prisma.familyMessage.create({
data: {
elderDeviceId: binding.elderDeviceId,
caregiverId: input.caregiverId,
direction: MessageDirection.FAMILY_TO_ELDER,
content: input.content.trim(),
importance: parseImportance(input.importance),
status: MessageStatus.PENDING,
source: "FAMILY_PANEL",
},
});
}
export async function recordUsageEvent(input: UsageEventInput) {
const device = await ensureDeviceRegistration({ deviceUuid: input.deviceUuid });
return prisma.usageEvent.create({
data: {
elderDeviceId: device.id,
eventType: input.eventType,
detailJson: input.detailJson || undefined,
},
});
}
export async function recordConversationTurns(input: ConversationTurnInput) {
const device = await ensureDeviceRegistration({ deviceUuid: input.deviceUuid });
const validTurns = input.turns
.map((turn) => ({
role: turn.role,
content: turn.content.trim(),
metaJson: turn.metaJson?.trim() || null,
createdAt: turn.createdAt ? new Date(turn.createdAt) : new Date(),
}))
.filter((turn) => turn.content.length > 0);
if (validTurns.length === 0) {
return { count: 0 };
}
const result = await prisma.conversationTurn.createMany({
data: validTurns.map((turn) => ({
elderDeviceId: device.id,
sessionId: input.sessionId?.trim() || null,
role: turn.role,
content: turn.content,
metaJson: turn.metaJson,
createdAt: turn.createdAt,
})),
});
return { count: result.count };
}
export async function recordToolCall(input: ToolCallInput) {
const device = await ensureDeviceRegistration({ deviceUuid: input.deviceUuid });
return prisma.$transaction(async (tx) => {
const log = await tx.toolCallLog.create({
data: {
elderDeviceId: device.id,
sessionId: input.sessionId?.trim() || null,
callId: input.callId?.trim() || null,
toolName: input.toolName.trim(),
argumentsJson: JSON.stringify(input.arguments),
outputText: input.outputText?.trim() || null,
status: input.status,
},
});
await tx.usageEvent.create({
data: {
elderDeviceId: device.id,
eventType: UsageEventType.TOOL_CALLED,
detailJson: JSON.stringify({
toolName: input.toolName,
callId: input.callId || null,
status: input.status,
}),
},
});
const messageText =
typeof input.arguments.message === "string"
? input.arguments.message.trim()
: "";
if (
input.status === ToolCallStatus.SUCCESS &&
input.toolName === "leave_message_for_family" &&
messageText
) {
await tx.familyMessage.create({
data: {
elderDeviceId: device.id,
direction: MessageDirection.ELDER_TO_FAMILY,
content: messageText,
recipientRelation:
typeof input.arguments.recipientRelation === "string"
? input.arguments.recipientRelation.trim()
: null,
importance: parseImportance(
typeof input.arguments.importance === "string"
? input.arguments.importance
: null,
),
status: MessageStatus.DELIVERED,
deliveredAt: new Date(),
source: "MODEL_TOOL",
},
});
}
return log;
});
}
export async function getCaregiverDashboard(caregiverId: string) {
const caregiver = await prisma.caregiverAccount.findUnique({
where: { id: caregiverId },
include: {
bindings: {
orderBy: { createdAt: "desc" },
include: {
elderDevice: {
include: {
messages: {
orderBy: { createdAt: "desc" },
take: 8,
},
usageEvents: {
orderBy: { createdAt: "desc" },
take: 6,
},
conversationTurns: {
orderBy: { createdAt: "desc" },
take: 10,
},
toolCalls: {
orderBy: { createdAt: "desc" },
take: 4,
},
_count: {
select: {
messages: true,
usageEvents: true,
conversationTurns: true,
toolCalls: true,
},
},
},
},
},
},
},
});
if (!caregiver) {
throw new Error("当前家属账号不存在。");
}
const elderDeviceIds = caregiver.bindings.map((binding) => binding.elderDeviceId);
const familyUnreadCounts = elderDeviceIds.length
? await prisma.familyMessage.groupBy({
by: ["elderDeviceId"],
_count: { _all: true },
where: {
elderDeviceId: { in: elderDeviceIds },
direction: MessageDirection.FAMILY_TO_ELDER,
readAt: null,
},
})
: [];
const elderUnreadCounts = elderDeviceIds.length
? await prisma.familyMessage.groupBy({
by: ["elderDeviceId"],
_count: { _all: true },
where: {
elderDeviceId: { in: elderDeviceIds },
direction: MessageDirection.ELDER_TO_FAMILY,
readAt: null,
},
})
: [];
const familyUnreadMap = new Map(
familyUnreadCounts.map((item) => [item.elderDeviceId, item._count._all]),
);
const elderUnreadMap = new Map(
elderUnreadCounts.map((item) => [item.elderDeviceId, item._count._all]),
);
return {
caregiver,
devices: caregiver.bindings.map((binding) => ({
...binding,
familyUnreadCount: familyUnreadMap.get(binding.elderDeviceId) || 0,
elderUnreadCount: elderUnreadMap.get(binding.elderDeviceId) || 0,
bindUrl: createBindUrl(binding.elderDevice.deviceUuid),
})),
};
}

21
lib/prisma.ts Normal file
View File

@ -0,0 +1,21 @@
import { PrismaPg } from "@prisma/adapter-pg";
import { PrismaClient } from "@prisma/client";
const globalForPrisma = globalThis as unknown as {
prisma?: PrismaClient;
};
if (!process.env.DATABASE_URL) {
throw new Error("DATABASE_URL 尚未配置,无法初始化 Prisma。");
}
const adapter = new PrismaPg({
connectionString: process.env.DATABASE_URL,
});
export const prisma =
globalForPrisma.prisma ?? new PrismaClient({ adapter });
if (process.env.NODE_ENV !== "production") {
globalForPrisma.prisma = prisma;
}

72
lib/session.ts Normal file
View File

@ -0,0 +1,72 @@
import { cookies } from "next/headers";
import { randomUUID } from "node:crypto";
import { prisma } from "@/lib/prisma";
export const CAREGIVER_SESSION_COOKIE = "dh-caregiver-session";
const CAREGIVER_SESSION_MAX_AGE = 60 * 60 * 24 * 365;
function createSessionToken() {
return randomUUID().replaceAll("-", "");
}
function getCookieOptions() {
return {
httpOnly: true,
sameSite: "lax" as const,
secure: process.env.NODE_ENV === "production",
path: "/",
maxAge: CAREGIVER_SESSION_MAX_AGE,
};
}
function normalizeRedirectPath(redirectTo?: string) {
if (!redirectTo || !redirectTo.startsWith("/") || redirectTo.startsWith("//")) {
return "/";
}
return redirectTo;
}
async function upsertCaregiverByToken(sessionToken: string) {
return prisma.caregiverAccount.upsert({
where: { sessionToken },
update: {},
create: { sessionToken },
});
}
export function buildCaregiverSessionBootstrapPath(redirectTo: string) {
const normalizedRedirect = normalizeRedirectPath(redirectTo);
return `/api/family/session?redirectTo=${encodeURIComponent(normalizedRedirect)}`;
}
export async function getCaregiverSession() {
const sessionToken = (await cookies()).get(CAREGIVER_SESSION_COOKIE)?.value;
if (!sessionToken) {
return null;
}
return upsertCaregiverByToken(sessionToken);
}
export async function getOrCreateCaregiverSession() {
const cookieStore = await cookies();
let sessionToken = cookieStore.get(CAREGIVER_SESSION_COOKIE)?.value;
if (!sessionToken) {
sessionToken = createSessionToken();
cookieStore.set(CAREGIVER_SESSION_COOKIE, sessionToken, getCookieOptions());
}
return upsertCaregiverByToken(sessionToken);
}
export function createCaregiverSessionCookieValue() {
return {
sessionToken: createSessionToken(),
options: getCookieOptions(),
};
}

View File

@ -9,7 +9,12 @@
"lint": "eslint"
},
"dependencies": {
"@prisma/adapter-pg": "^7.7.0",
"@prisma/client": "^7.7.0",
"dotenv": "^17.4.2",
"next": "16.2.4",
"pg": "^8.20.0",
"qr-scanner": "^1.4.2",
"react": "19.2.4",
"react-dom": "19.2.4"
},
@ -20,6 +25,7 @@
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.2.4",
"prisma": "^7.7.0",
"tailwindcss": "^4",
"typescript": "^5"
},

13
prisma.config.ts Normal file
View File

@ -0,0 +1,13 @@
import "dotenv/config";
import { defineConfig } from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
},
datasource: {
url: process.env.DATABASE_URL ?? "",
},
});

View File

@ -0,0 +1,165 @@
-- CreateEnum
CREATE TYPE "MessageDirection" AS ENUM ('ELDER_TO_FAMILY', 'FAMILY_TO_ELDER');
-- CreateEnum
CREATE TYPE "MessageImportance" AS ENUM ('LOW', 'NORMAL', 'HIGH', 'URGENT');
-- CreateEnum
CREATE TYPE "MessageStatus" AS ENUM ('PENDING', 'DELIVERED', 'READ');
-- CreateEnum
CREATE TYPE "ConversationRole" AS ENUM ('USER', 'ASSISTANT', 'TOOL');
-- CreateEnum
CREATE TYPE "UsageEventType" AS ENUM ('APP_OPEN', 'SETTINGS_OPENED', 'AI_SESSION_STARTED', 'AI_SESSION_ENDED', 'CAMERA_ENABLED', 'CAMERA_DISABLED', 'TOOL_CALLED');
-- CreateEnum
CREATE TYPE "ToolCallStatus" AS ENUM ('SUCCESS', 'FAILURE');
-- CreateTable
CREATE TABLE "CaregiverAccount" (
"id" TEXT NOT NULL,
"sessionToken" TEXT NOT NULL,
"nickname" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "CaregiverAccount_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ElderDevice" (
"id" TEXT NOT NULL,
"deviceUuid" TEXT NOT NULL,
"displayName" TEXT,
"appVersion" TEXT,
"lastSeenAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ElderDevice_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "DeviceBinding" (
"id" TEXT NOT NULL,
"caregiverId" TEXT NOT NULL,
"elderDeviceId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "DeviceBinding_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "FamilyMessage" (
"id" TEXT NOT NULL,
"elderDeviceId" TEXT NOT NULL,
"caregiverId" TEXT,
"direction" "MessageDirection" NOT NULL,
"content" TEXT NOT NULL,
"recipientRelation" TEXT,
"importance" "MessageImportance" NOT NULL DEFAULT 'NORMAL',
"status" "MessageStatus" NOT NULL DEFAULT 'PENDING',
"source" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deliveredAt" TIMESTAMP(3),
"readAt" TIMESTAMP(3),
CONSTRAINT "FamilyMessage_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ConversationTurn" (
"id" TEXT NOT NULL,
"elderDeviceId" TEXT NOT NULL,
"sessionId" TEXT,
"role" "ConversationRole" NOT NULL,
"content" TEXT NOT NULL,
"metaJson" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ConversationTurn_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "UsageEvent" (
"id" TEXT NOT NULL,
"elderDeviceId" TEXT NOT NULL,
"eventType" "UsageEventType" NOT NULL,
"detailJson" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "UsageEvent_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ToolCallLog" (
"id" TEXT NOT NULL,
"elderDeviceId" TEXT NOT NULL,
"sessionId" TEXT,
"callId" TEXT,
"toolName" TEXT NOT NULL,
"argumentsJson" TEXT NOT NULL,
"outputText" TEXT,
"status" "ToolCallStatus" NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ToolCallLog_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "CaregiverAccount_sessionToken_key" ON "CaregiverAccount"("sessionToken");
-- CreateIndex
CREATE UNIQUE INDEX "ElderDevice_deviceUuid_key" ON "ElderDevice"("deviceUuid");
-- CreateIndex
CREATE INDEX "DeviceBinding_elderDeviceId_createdAt_idx" ON "DeviceBinding"("elderDeviceId", "createdAt");
-- CreateIndex
CREATE UNIQUE INDEX "DeviceBinding_caregiverId_elderDeviceId_key" ON "DeviceBinding"("caregiverId", "elderDeviceId");
-- CreateIndex
CREATE INDEX "FamilyMessage_elderDeviceId_createdAt_idx" ON "FamilyMessage"("elderDeviceId", "createdAt" DESC);
-- CreateIndex
CREATE INDEX "FamilyMessage_direction_status_createdAt_idx" ON "FamilyMessage"("direction", "status", "createdAt" DESC);
-- CreateIndex
CREATE INDEX "ConversationTurn_elderDeviceId_createdAt_idx" ON "ConversationTurn"("elderDeviceId", "createdAt" DESC);
-- CreateIndex
CREATE INDEX "ConversationTurn_sessionId_createdAt_idx" ON "ConversationTurn"("sessionId", "createdAt");
-- CreateIndex
CREATE INDEX "UsageEvent_elderDeviceId_createdAt_idx" ON "UsageEvent"("elderDeviceId", "createdAt" DESC);
-- CreateIndex
CREATE INDEX "UsageEvent_eventType_createdAt_idx" ON "UsageEvent"("eventType", "createdAt" DESC);
-- CreateIndex
CREATE INDEX "ToolCallLog_elderDeviceId_createdAt_idx" ON "ToolCallLog"("elderDeviceId", "createdAt" DESC);
-- CreateIndex
CREATE INDEX "ToolCallLog_toolName_createdAt_idx" ON "ToolCallLog"("toolName", "createdAt" DESC);
-- AddForeignKey
ALTER TABLE "DeviceBinding" ADD CONSTRAINT "DeviceBinding_caregiverId_fkey" FOREIGN KEY ("caregiverId") REFERENCES "CaregiverAccount"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "DeviceBinding" ADD CONSTRAINT "DeviceBinding_elderDeviceId_fkey" FOREIGN KEY ("elderDeviceId") REFERENCES "ElderDevice"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "FamilyMessage" ADD CONSTRAINT "FamilyMessage_elderDeviceId_fkey" FOREIGN KEY ("elderDeviceId") REFERENCES "ElderDevice"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "FamilyMessage" ADD CONSTRAINT "FamilyMessage_caregiverId_fkey" FOREIGN KEY ("caregiverId") REFERENCES "CaregiverAccount"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ConversationTurn" ADD CONSTRAINT "ConversationTurn_elderDeviceId_fkey" FOREIGN KEY ("elderDeviceId") REFERENCES "ElderDevice"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "UsageEvent" ADD CONSTRAINT "UsageEvent_elderDeviceId_fkey" FOREIGN KEY ("elderDeviceId") REFERENCES "ElderDevice"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ToolCallLog" ADD CONSTRAINT "ToolCallLog_elderDeviceId_fkey" FOREIGN KEY ("elderDeviceId") REFERENCES "ElderDevice"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"

145
prisma/schema.prisma Normal file
View File

@ -0,0 +1,145 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
}
enum MessageDirection {
ELDER_TO_FAMILY
FAMILY_TO_ELDER
}
enum MessageImportance {
LOW
NORMAL
HIGH
URGENT
}
enum MessageStatus {
PENDING
DELIVERED
READ
}
enum ConversationRole {
USER
ASSISTANT
TOOL
}
enum UsageEventType {
APP_OPEN
SETTINGS_OPENED
AI_SESSION_STARTED
AI_SESSION_ENDED
CAMERA_ENABLED
CAMERA_DISABLED
TOOL_CALLED
}
enum ToolCallStatus {
SUCCESS
FAILURE
}
model CaregiverAccount {
id String @id @default(cuid())
sessionToken String @unique
nickname String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
bindings DeviceBinding[]
messages FamilyMessage[]
}
model ElderDevice {
id String @id @default(cuid())
deviceUuid String @unique
displayName String?
appVersion String?
lastSeenAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
bindings DeviceBinding[]
messages FamilyMessage[]
conversationTurns ConversationTurn[]
usageEvents UsageEvent[]
toolCalls ToolCallLog[]
}
model DeviceBinding {
id String @id @default(cuid())
caregiverId String
elderDeviceId String
createdAt DateTime @default(now())
caregiver CaregiverAccount @relation(fields: [caregiverId], references: [id], onDelete: Cascade)
elderDevice ElderDevice @relation(fields: [elderDeviceId], references: [id], onDelete: Cascade)
@@unique([caregiverId, elderDeviceId])
@@index([elderDeviceId, createdAt])
}
model FamilyMessage {
id String @id @default(cuid())
elderDeviceId String
caregiverId String?
direction MessageDirection
content String
recipientRelation String?
importance MessageImportance @default(NORMAL)
status MessageStatus @default(PENDING)
source String?
createdAt DateTime @default(now())
deliveredAt DateTime?
readAt DateTime?
elderDevice ElderDevice @relation(fields: [elderDeviceId], references: [id], onDelete: Cascade)
caregiver CaregiverAccount? @relation(fields: [caregiverId], references: [id], onDelete: SetNull)
@@index([elderDeviceId, createdAt(sort: Desc)])
@@index([direction, status, createdAt(sort: Desc)])
}
model ConversationTurn {
id String @id @default(cuid())
elderDeviceId String
sessionId String?
role ConversationRole
content String
metaJson String?
createdAt DateTime @default(now())
elderDevice ElderDevice @relation(fields: [elderDeviceId], references: [id], onDelete: Cascade)
@@index([elderDeviceId, createdAt(sort: Desc)])
@@index([sessionId, createdAt])
}
model UsageEvent {
id String @id @default(cuid())
elderDeviceId String
eventType UsageEventType
detailJson String?
createdAt DateTime @default(now())
elderDevice ElderDevice @relation(fields: [elderDeviceId], references: [id], onDelete: Cascade)
@@index([elderDeviceId, createdAt(sort: Desc)])
@@index([eventType, createdAt(sort: Desc)])
}
model ToolCallLog {
id String @id @default(cuid())
elderDeviceId String
sessionId String?
callId String?
toolName String
argumentsJson String
outputText String?
status ToolCallStatus
createdAt DateTime @default(now())
elderDevice ElderDevice @relation(fields: [elderDeviceId], references: [id], onDelete: Cascade)
@@index([elderDeviceId, createdAt(sort: Desc)])
@@index([toolName, createdAt(sort: Desc)])
}