From ee6376162d73ca80dd2b3519a0a925f10be37e17 Mon Sep 17 00:00:00 2001 From: patrick Date: Fri, 16 Oct 2026 19:19:19 +0000 Subject: [PATCH] TSK-04: aufgaben-oberflaeche Co-Authored-By: Claude Sonnet 5 --- web/task-board/.gitignore | 2 + web/task-board/.npmrc | 1 + web/task-board/app/TaskKanban.test.tsx | 84 + web/task-board/app/TaskKanban.tsx | 138 + web/task-board/app/TaskList.test.tsx | 115 + web/task-board/app/TaskList.tsx | 114 + .../app/api/tasks/[id]/status/route.test.ts | 54 + .../app/api/tasks/[id]/status/route.ts | 34 + web/task-board/app/api/tasks/route.test.ts | 55 + web/task-board/app/api/tasks/route.ts | 36 + web/task-board/app/layout.tsx | 45 + web/task-board/app/page.tsx | 55 + web/task-board/lib/api.test.ts | 31 + web/task-board/lib/api.ts | 53 + web/task-board/lib/filters.test.ts | 122 + web/task-board/lib/filters.ts | 52 + web/task-board/next.config.mjs | 5 + web/task-board/package-lock.json | 3305 +++++++++++++++++ web/task-board/package.json | 27 + web/task-board/tsconfig.json | 21 + web/task-board/vitest.config.ts | 15 + web/task-board/vitest.setup.ts | 5 + workflow/cmd/workflow-app/main.go | 2 + workflow/docs/TSK-04-PRUEFPROTOKOLL.md | 244 ++ workflow/internal/task/task.go | 126 +- workflow/internal/task/task_status_test.go | 205 + workflow/internal/taskapi/taskapi.go | 93 + workflow/internal/taskapi/taskapi_test.go | 228 ++ .../tenant/0016_task_status.down.sql | 1 + .../migrations/tenant/0016_task_status.up.sql | 2 + 30 files changed, 5263 insertions(+), 7 deletions(-) create mode 100644 web/task-board/.gitignore create mode 100644 web/task-board/.npmrc create mode 100644 web/task-board/app/TaskKanban.test.tsx create mode 100644 web/task-board/app/TaskKanban.tsx create mode 100644 web/task-board/app/TaskList.test.tsx create mode 100644 web/task-board/app/TaskList.tsx create mode 100644 web/task-board/app/api/tasks/[id]/status/route.test.ts create mode 100644 web/task-board/app/api/tasks/[id]/status/route.ts create mode 100644 web/task-board/app/api/tasks/route.test.ts create mode 100644 web/task-board/app/api/tasks/route.ts create mode 100644 web/task-board/app/layout.tsx create mode 100644 web/task-board/app/page.tsx create mode 100644 web/task-board/lib/api.test.ts create mode 100644 web/task-board/lib/api.ts create mode 100644 web/task-board/lib/filters.test.ts create mode 100644 web/task-board/lib/filters.ts create mode 100644 web/task-board/next.config.mjs create mode 100644 web/task-board/package-lock.json create mode 100644 web/task-board/package.json create mode 100644 web/task-board/tsconfig.json create mode 100644 web/task-board/vitest.config.ts create mode 100644 web/task-board/vitest.setup.ts create mode 100644 workflow/docs/TSK-04-PRUEFPROTOKOLL.md create mode 100644 workflow/internal/task/task_status_test.go create mode 100644 workflow/internal/taskapi/taskapi.go create mode 100644 workflow/internal/taskapi/taskapi_test.go create mode 100644 workflow/migrations/tenant/0016_task_status.down.sql create mode 100644 workflow/migrations/tenant/0016_task_status.up.sql diff --git a/web/task-board/.gitignore b/web/task-board/.gitignore new file mode 100644 index 00000000..b90a368f --- /dev/null +++ b/web/task-board/.gitignore @@ -0,0 +1,2 @@ +node_modules +.next diff --git a/web/task-board/.npmrc b/web/task-board/.npmrc new file mode 100644 index 00000000..c7d35173 --- /dev/null +++ b/web/task-board/.npmrc @@ -0,0 +1 @@ +install-links=true diff --git a/web/task-board/app/TaskKanban.test.tsx b/web/task-board/app/TaskKanban.test.tsx new file mode 100644 index 00000000..cc57cc39 --- /dev/null +++ b/web/task-board/app/TaskKanban.test.tsx @@ -0,0 +1,84 @@ +import { describe, it, expect, vi, afterEach } from "vitest"; +import { render, screen, fireEvent, waitFor } from "@testing-library/react"; +import { ThemeProvider, I18nProvider, ToastProvider } from "@nexarch/shl"; +import { TaskKanban } from "./TaskKanban"; + +function renderWithProviders(ui: React.ReactElement) { + return render( + + + {ui} + + + ); +} + +function listResponse() { + return new Response( + JSON.stringify([ + { ID: "t1", Title: "Rechnung pruefen", AssigneeType: "person", AssigneeID: "alice", Priority: "high", Status: "open", DueDate: null, References: null }, + ]), + { status: 200 } + ); +} + +describe("TaskKanban", () => { + const originalFetch = global.fetch; + afterEach(() => { + global.fetch = originalFetch; + }); + + // AK4: Spalten nach Status Offen/In Bearbeitung/Erledigt; Verschieben + // (hier ueber die gleichwertige Tastaturaktion, siehe Komponenten- + // kommentar) schreibt den neuen Status ueber den /status-Endpunkt + // zurueck, der seinerseits task.Store.SetStatus (TSK-01) aufruft. + it("zeigt Aufgaben in ihrer Statusspalte und verschiebt per Aktionsknopf nach In Bearbeitung", async () => { + const statusCalls: { url: string; body: string }[] = []; + global.fetch = vi.fn(async (url: string | URL, init?: RequestInit) => { + const u = url.toString(); + if (u.includes("/status")) { + statusCalls.push({ url: u, body: init?.body as string }); + return new Response(JSON.stringify({ ID: "t1", Status: "in_progress" }), { status: 200 }); + } + return listResponse(); + }) as typeof fetch; + + renderWithProviders(); + + await screen.findByText(/Rechnung pruefen/); + expect(screen.getByRole("group", { name: /Spalte Offen/i })).toBeInTheDocument(); + + const moveButton = screen.getByRole("button", { name: /nach In Bearbeitung verschieben/i }); + fireEvent.click(moveButton); + + await waitFor(() => expect(statusCalls.length).toBe(1)); + expect(statusCalls[0].url).toContain("/api/tasks/t1/status"); + expect(JSON.parse(statusCalls[0].body)).toEqual({ status: "in_progress" }); + }); + + it("verschiebt eine Aufgabe per Drag & Drop in die Spalte Erledigt", async () => { + const statusCalls: string[] = []; + global.fetch = vi.fn(async (url: string | URL, init?: RequestInit) => { + const u = url.toString(); + if (u.includes("/status")) { + statusCalls.push(init?.body as string); + return new Response(JSON.stringify({ ID: "t1", Status: "done" }), { status: 200 }); + } + return listResponse(); + }) as typeof fetch; + + renderWithProviders(); + await screen.findByText(/Rechnung pruefen/); + + const card = screen.getByText(/Rechnung pruefen/).closest("li")!; + const doneColumn = screen.getByRole("group", { name: /Spalte Erledigt/i }); + + const dataTransfer = { getData: () => "t1", setData: () => {} }; + fireEvent.dragStart(card, { dataTransfer }); + fireEvent.dragOver(doneColumn, { dataTransfer }); + fireEvent.drop(doneColumn, { dataTransfer }); + + await waitFor(() => expect(statusCalls.length).toBe(1)); + expect(JSON.parse(statusCalls[0])).toEqual({ status: "done" }); + }); +}); diff --git a/web/task-board/app/TaskKanban.tsx b/web/task-board/app/TaskKanban.tsx new file mode 100644 index 00000000..b3cf6b50 --- /dev/null +++ b/web/task-board/app/TaskKanban.tsx @@ -0,0 +1,138 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; +import { useToast } from "@nexarch/shl"; +import { listTasks, setTaskStatus, ApiError, type Task, type Status } from "../lib/api"; + +const COLUMNS: { key: Status; label: string }[] = [ + { key: "open", label: "Offen" }, + { key: "in_progress", label: "In Bearbeitung" }, + { key: "done", label: "Erledigt" }, +]; + +// AK4: Kanban-Ansicht mit Spalten nach Aufgabenstatus. Drag & Drop +// schreibt den neuen Status ausschliesslich ueber +// task.Store.SetStatus/taskapi (TSK-01) zurueck -- siehe lib/api.ts +// setTaskStatus. Zusaetzlich gleichwertige Tastaturbedienung ueber die +// "Nach ... verschieben"-Aktionen (Drag & Drop allein ist nicht +// tastaturbedienbar, selbes Muster wie web/workflow-cases/ReminderKanban, +// UX-14-Barrierefreiheitsanspruch dieses Projekts). +export function TaskKanban({ userId }: { userId: string }) { + const [tasks, setTasks] = useState(null); + const [error, setError] = useState(null); + const [draggingId, setDraggingId] = useState(null); + const toast = useToast(); + + const reload = useCallback(async () => { + try { + const result = await listTasks(userId); + setTasks(result); + setError(null); + } catch (err) { + setError(err instanceof ApiError ? err.message : "Aufgaben konnten nicht geladen werden."); + setTasks([]); + } + }, [userId]); + + useEffect(() => { + void reload(); + }, [reload]); + + async function move(id: string, target: Status) { + try { + await setTaskStatus(userId, id, target); + await reload(); + if (target === "done") { + toast.push("Aufgabe erledigt.", "success"); + } + } catch (err) { + toast.push(err instanceof ApiError ? err.message : "Status konnte nicht geaendert werden.", "danger"); + } + } + + if (error) { + return

{error}

; + } + if (tasks === null) { + return

Lade Aufgaben …

; + } + + return ( +
+ {COLUMNS.map((col) => { + const columnTasks = tasks.filter((t) => t.Status === col.key); + return ( +
e.preventDefault()} + onDrop={(e) => { + e.preventDefault(); + const id = e.dataTransfer.getData("text/plain") || draggingId; + if (id) { + void move(id, col.key); + } + setDraggingId(null); + }} + style={{ + flex: 1, + minWidth: 220, + border: "1px solid var(--shl-color-border, #d0d5dd)", + borderRadius: 8, + padding: 12, + minHeight: 160, + background: "var(--shl-color-surface, #f9fafb)", + }} + > +

+ {col.label} ({columnTasks.length}) +

+ {columnTasks.length === 0 ? ( +

Keine Aufgaben.

+ ) : ( +
    + {columnTasks.map((t) => ( +
  • { + e.dataTransfer.setData("text/plain", t.ID); + setDraggingId(t.ID); + }} + onDragEnd={() => setDraggingId(null)} + style={{ + border: "1px solid var(--shl-color-border, #d0d5dd)", + borderRadius: 6, + padding: 8, + marginBottom: 8, + background: "var(--shl-color-background, #ffffff)", + }} + > +

    {t.Title}

    +

    + {t.AssigneeID} · {t.Priority} + {t.DueDate ? ` · ${new Date(t.DueDate).toLocaleDateString("de-DE")}` : ""} +

    +
    + {COLUMNS.filter((c) => c.key !== col.key).map((target) => ( + + ))} +
    +
  • + ))} +
+ )} +
+ ); + })} +
+ ); +} diff --git a/web/task-board/app/TaskList.test.tsx b/web/task-board/app/TaskList.test.tsx new file mode 100644 index 00000000..d9a1a3b0 --- /dev/null +++ b/web/task-board/app/TaskList.test.tsx @@ -0,0 +1,115 @@ +import { describe, it, expect, vi, afterEach } from "vitest"; +import { render, screen, fireEvent, waitFor } from "@testing-library/react"; +import { ThemeProvider, I18nProvider, ToastProvider } from "@nexarch/shl"; +import { TaskList } from "./TaskList"; +import type { Task } from "../lib/api"; + +function renderWithProviders(ui: React.ReactElement) { + return render( + + + {ui} + + + ); +} + +function makeTask(overrides: Partial): Task { + return { + ID: Math.random().toString(36).slice(2), + Title: "Aufgabe", + AssigneeType: "person", + AssigneeID: "alice", + Priority: "normal", + Status: "open", + DueDate: null, + References: null, + ...overrides, + }; +} + +describe("TaskList", () => { + const originalFetch = global.fetch; + afterEach(() => { + global.fetch = originalFetch; + }); + + // AK1: Ueberfaellige Aufgaben sind klar gekennzeichnet. + it("kennzeichnet eine ueberfaellige Aufgabe im Text, nicht nur farblich", async () => { + const overdueDue = new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(); + global.fetch = vi.fn( + async () => + new Response(JSON.stringify([makeTask({ Title: "Alte Aufgabe", DueDate: overdueDue })]), { status: 200 }) + ) as typeof fetch; + + renderWithProviders(); + + await screen.findByText(/Alte Aufgabe/); + expect(screen.getByText("Ueberfaellig", { selector: "strong" })).toBeInTheDocument(); + }); + + // AK2: Liste ist nach Faelligkeit und Prioritaet filterbar. + it("filtert die Liste nach Prioritaet", async () => { + global.fetch = vi.fn( + async () => + new Response( + JSON.stringify([ + makeTask({ Title: "Hohe Prioritaet", Priority: "high" }), + makeTask({ Title: "Niedrige Prioritaet", Priority: "low" }), + ]), + { status: 200 } + ) + ) as typeof fetch; + + renderWithProviders(); + await screen.findByText(/Hohe Prioritaet/); + expect(screen.getByText(/Niedrige Prioritaet/)).toBeInTheDocument(); + + fireEvent.change(screen.getByLabelText("Prioritaet"), { target: { value: "high" } }); + + await waitFor(() => expect(screen.queryByText(/Niedrige Prioritaet/)).not.toBeInTheDocument()); + expect(screen.getByText(/Hohe Prioritaet/)).toBeInTheDocument(); + }); + + it("filtert die Liste nach Ueberfaellig", async () => { + const overdueDue = new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(); + const futureDue = new Date(Date.now() + 5 * 24 * 60 * 60 * 1000).toISOString(); + global.fetch = vi.fn( + async () => + new Response( + JSON.stringify([ + makeTask({ Title: "Ueberfaellige Aufgabe", DueDate: overdueDue }), + makeTask({ Title: "Kuenftige Aufgabe", DueDate: futureDue }), + ]), + { status: 200 } + ) + ) as typeof fetch; + + renderWithProviders(); + await screen.findByText(/Ueberfaellige Aufgabe/); + + fireEvent.change(screen.getByLabelText("Faelligkeit"), { target: { value: "overdue" } }); + + await waitFor(() => expect(screen.queryByText(/Kuenftige Aufgabe/)).not.toBeInTheDocument()); + expect(screen.getByText(/Ueberfaellige Aufgabe/)).toBeInTheDocument(); + }); + + // AK3 / Pruefung 3: Ansicht bleibt bei 200 Aufgaben nutzbar -- misst die + // tatsaechliche Renderzeit von 200 Zeilen inklusive Filterlogik. + it("rendert 200 Aufgaben in nutzbarer Zeit", async () => { + const many = Array.from({ length: 200 }, (_, i) => + makeTask({ Title: `Aufgabe ${i}`, Priority: i % 3 === 0 ? "high" : "normal" }) + ); + global.fetch = vi.fn(async () => new Response(JSON.stringify(many), { status: 200 })) as typeof fetch; + + const start = performance.now(); + renderWithProviders(); + await screen.findByText(/Aufgabe 199/); + const elapsed = performance.now() - start; + + // eslint-disable-next-line no-console + console.log(`TaskList Render mit 200 Aufgaben: ${elapsed.toFixed(1)}ms`); + expect(elapsed).toBeLessThan(2000); + expect(screen.getByText(/200 von 200 Aufgaben/)).toBeInTheDocument(); + }); +}); diff --git a/web/task-board/app/TaskList.tsx b/web/task-board/app/TaskList.tsx new file mode 100644 index 00000000..61728b15 --- /dev/null +++ b/web/task-board/app/TaskList.tsx @@ -0,0 +1,114 @@ +"use client"; + +import { useCallback, useEffect, useMemo, useState } from "react"; +import { SelectField, Table, type TableColumn } from "@nexarch/shl"; +import { listTasks, ApiError, type Task } from "../lib/api"; +import { isOverdue, filterTasks, sortByDueDate, type DueFilter, type PriorityFilter } from "../lib/filters"; + +const PRIORITY_LABEL: Record = { + low: "Niedrig", + normal: "Normal", + high: "Hoch", +}; + +const STATUS_LABEL: Record = { + open: "Offen", + in_progress: "In Bearbeitung", + done: "Erledigt", +}; + +// AK1: Liste ist nach Faelligkeit und Prioritaet filterbar. +// AK2: Ueberfaellige Aufgaben sind klar gekennzeichnet (Text-Badge, nicht +// nur Farbe -- WCAG, Farbe allein ist keine verlaessliche Kennzeichnung). +// AK3: bleibt bei 200 Aufgaben nutzbar -- 200 Zeilen sind fuer eine +// einfache Tabelle kein Performance-Problem, siehe lib/filters.test.ts +// fuer den gemessenen Wert. +export function TaskList({ userId }: { userId: string }) { + const [tasks, setTasks] = useState(null); + const [error, setError] = useState(null); + const [priority, setPriority] = useState("all"); + const [due, setDue] = useState("all"); + + const reload = useCallback(async () => { + try { + const result = await listTasks(userId); + setTasks(result); + setError(null); + } catch (err) { + setError(err instanceof ApiError ? err.message : "Aufgaben konnten nicht geladen werden."); + setTasks([]); + } + }, [userId]); + + useEffect(() => { + void reload(); + }, [reload]); + + const now = useMemo(() => new Date(), [tasks]); + const visible = useMemo(() => { + if (!tasks) return []; + return sortByDueDate(filterTasks(tasks, priority, due, now)); + }, [tasks, priority, due, now]); + + if (error) { + return

{error}

; + } + if (tasks === null) { + return

Lade Aufgaben …

; + } + + return ( +
+
+ setPriority(e.target.value as PriorityFilter)} + > + + + + + + setDue(e.target.value as DueFilter)}> + + + + + +
+ +

+ {visible.length} von {tasks.length} Aufgaben +

+ + + caption="Aufgaben" + rows={visible} + rowKey={(t) => t.ID} + columns={columns(now)} + /> +
+ ); +} + +function columns(now: Date): TableColumn[] { + return [ + { key: "title", header: "Titel", render: (t) => t.Title }, + { key: "assignee", header: "Zustaendig", render: (t) => t.AssigneeID }, + { key: "priority", header: "Prioritaet", render: (t) => PRIORITY_LABEL[t.Priority] }, + { + key: "due", + header: "Faelligkeit", + render: (t) => ( + <> + {t.DueDate ? new Date(t.DueDate).toLocaleDateString("de-DE") : "–"} + {isOverdue(t, now) ? ( + Ueberfaellig + ) : null} + + ), + }, + { key: "status", header: "Status", render: (t) => STATUS_LABEL[t.Status] }, + ]; +} diff --git a/web/task-board/app/api/tasks/[id]/status/route.test.ts b/web/task-board/app/api/tasks/[id]/status/route.test.ts new file mode 100644 index 00000000..ae5ce9c5 --- /dev/null +++ b/web/task-board/app/api/tasks/[id]/status/route.test.ts @@ -0,0 +1,54 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { NextRequest } from "next/server"; +import { POST } from "./route"; + +describe("POST /api/tasks/[id]/status", () => { + const originalFetch = global.fetch; + const originalEnv = process.env.TASK_BOARD_API_URL; + + beforeEach(() => { + process.env.TASK_BOARD_API_URL = "http://backend.local"; + }); + afterEach(() => { + global.fetch = originalFetch; + process.env.TASK_BOARD_API_URL = originalEnv; + }); + + it("leitet den neuen Status an das Backend weiter (AK4: Schreibweg der Kanban-Ansicht)", async () => { + let capturedURL = ""; + let capturedBody = ""; + global.fetch = vi.fn(async (url: string | URL, init?: RequestInit) => { + capturedURL = url.toString(); + capturedBody = init?.body as string; + return new Response(JSON.stringify({ ID: "t1", Status: "in_progress" }), { status: 200 }); + }) as typeof fetch; + + const req = new NextRequest("http://localhost/api/tasks/t1/status?userId=alice", { + method: "POST", + body: JSON.stringify({ status: "in_progress" }), + }); + const res = await POST(req, { params: Promise.resolve({ id: "t1" }) }); + + expect(res.status).toBe(200); + expect(capturedURL).toBe("http://backend.local/tasks/t1/status"); + expect(JSON.parse(capturedBody)).toEqual({ status: "in_progress" }); + }); + + it("liefert 400 ohne status", async () => { + const req = new NextRequest("http://localhost/api/tasks/t1/status?userId=alice", { + method: "POST", + body: JSON.stringify({}), + }); + const res = await POST(req, { params: Promise.resolve({ id: "t1" }) }); + expect(res.status).toBe(400); + }); + + it("liefert 400 ohne userId", async () => { + const req = new NextRequest("http://localhost/api/tasks/t1/status", { + method: "POST", + body: JSON.stringify({ status: "done" }), + }); + const res = await POST(req, { params: Promise.resolve({ id: "t1" }) }); + expect(res.status).toBe(400); + }); +}); diff --git a/web/task-board/app/api/tasks/[id]/status/route.ts b/web/task-board/app/api/tasks/[id]/status/route.ts new file mode 100644 index 00000000..ec4a42de --- /dev/null +++ b/web/task-board/app/api/tasks/[id]/status/route.ts @@ -0,0 +1,34 @@ +import { NextRequest, NextResponse } from "next/server"; + +function backendURL(): string { + const base = process.env.TASK_BOARD_API_URL; + if (!base) { + throw new Error("TASK_BOARD_API_URL ist nicht gesetzt (Umgebungsvariable erforderlich)"); + } + return base.replace(/\/$/, ""); +} + +export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { + const { id } = await params; + const userId = request.nextUrl.searchParams.get("userId"); + if (!userId) { + return NextResponse.json({ error: "'userId' ist erforderlich" }, { status: 400 }); + } + const payload = await request.json().catch(() => null); + if (!payload?.status) { + return NextResponse.json({ error: "'status' ist erforderlich" }, { status: 400 }); + } + + let backendResponse: Response; + try { + backendResponse = await fetch(`${backendURL()}/tasks/${encodeURIComponent(id)}/status`, { + method: "POST", + headers: { "X-User-Id": userId, "Content-Type": "application/json" }, + body: JSON.stringify({ status: payload.status }), + }); + } catch (err) { + return NextResponse.json({ error: `Workflow-Dienst nicht erreichbar: ${(err as Error).message}` }, { status: 502 }); + } + const body = await backendResponse.json().catch(() => null); + return NextResponse.json(body ?? { error: "Status konnte nicht geaendert werden" }, { status: backendResponse.status }); +} diff --git a/web/task-board/app/api/tasks/route.test.ts b/web/task-board/app/api/tasks/route.test.ts new file mode 100644 index 00000000..876aecc9 --- /dev/null +++ b/web/task-board/app/api/tasks/route.test.ts @@ -0,0 +1,55 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { NextRequest } from "next/server"; +import { GET } from "./route"; + +describe("GET /api/tasks", () => { + const originalFetch = global.fetch; + const originalEnv = process.env.TASK_BOARD_API_URL; + + beforeEach(() => { + process.env.TASK_BOARD_API_URL = "http://backend-mandant-a.local"; + }); + afterEach(() => { + global.fetch = originalFetch; + process.env.TASK_BOARD_API_URL = originalEnv; + }); + + it("liefert 400 ohne userId", async () => { + const req = new NextRequest("http://localhost/api/tasks"); + const res = await GET(req); + expect(res.status).toBe(400); + }); + + it("ruft ausschliesslich das konfigurierte Backend auf und reicht die Antwort durch", async () => { + let capturedURL = ""; + let capturedHeaders: Headers | undefined; + global.fetch = vi.fn(async (url: string | URL, init?: RequestInit) => { + capturedURL = url.toString(); + capturedHeaders = new Headers(init?.headers); + return new Response(JSON.stringify([{ ID: "1", Title: "x" }]), { status: 200 }); + }) as typeof fetch; + + const req = new NextRequest("http://localhost/api/tasks?userId=alice"); + const res = await GET(req); + + expect(res.status).toBe(200); + expect(capturedURL).toBe("http://backend-mandant-a.local/tasks"); + expect(capturedHeaders?.get("X-User-Id")).toBe("alice"); + }); + + // AK5: ein Client-seitiger Mandanten-Parameter darf die Zielinstanz + // nicht umlenken -- die Route liest nur 'userId' aus der Anfrage, der + // Mandant ist durch TASK_BOARD_API_URL beim Deployment fest gebunden. + it("ignoriert einen client-seitigen Mandanten-Parameter, Ziel bleibt das konfigurierte Backend", async () => { + let capturedURL = ""; + global.fetch = vi.fn(async (url: string | URL) => { + capturedURL = url.toString(); + return new Response(JSON.stringify([]), { status: 200 }); + }) as typeof fetch; + + const req = new NextRequest("http://localhost/api/tasks?userId=alice&tenant=mandant-b&mandant=mandant-b"); + await GET(req); + + expect(capturedURL).toBe("http://backend-mandant-a.local/tasks"); + }); +}); diff --git a/web/task-board/app/api/tasks/route.ts b/web/task-board/app/api/tasks/route.ts new file mode 100644 index 00000000..1e32437b --- /dev/null +++ b/web/task-board/app/api/tasks/route.ts @@ -0,0 +1,36 @@ +import { NextRequest, NextResponse } from "next/server"; + +function backendURL(): string { + const base = process.env.TASK_BOARD_API_URL; + if (!base) { + throw new Error("TASK_BOARD_API_URL ist nicht gesetzt (Umgebungsvariable erforderlich)"); + } + return base.replace(/\/$/, ""); +} + +// GET liest ausschliesslich 'userId' aus der Anfrage und leitet ohne +// weitere Parameter an das fest konfigurierte Backend weiter (AK5: kein +// Mandanten-Parameter, den ein Client setzen koennte -- welcher Mandant +// angesprochen wird, legt TASK_BOARD_API_URL beim Deployment fest, nicht +// die Anfrage). +export async function GET(request: NextRequest) { + const userId = request.nextUrl.searchParams.get("userId"); + if (!userId) { + return NextResponse.json({ error: "'userId' ist erforderlich" }, { status: 400 }); + } + + let backendResponse: Response; + try { + backendResponse = await fetch(`${backendURL()}/tasks`, { + headers: { "X-User-Id": userId }, + cache: "no-store", + }); + } catch (err) { + return NextResponse.json({ error: `Workflow-Dienst nicht erreichbar: ${(err as Error).message}` }, { status: 502 }); + } + const body = await backendResponse.json().catch(() => null); + if (!backendResponse.ok) { + return NextResponse.json(body ?? { error: "Aufgaben konnten nicht geladen werden" }, { status: backendResponse.status }); + } + return NextResponse.json(body); +} diff --git a/web/task-board/app/layout.tsx b/web/task-board/app/layout.tsx new file mode 100644 index 00000000..d10ed3f0 --- /dev/null +++ b/web/task-board/app/layout.tsx @@ -0,0 +1,45 @@ +import Script from "next/script"; +import { ThemeProvider, I18nProvider, ToastProvider, typography, themeInitScript, focusVisibleStyles } from "@nexarch/shl"; + +export const metadata = { + title: "NEXARCH Aufgaben", +}; + +const focusVisibleStyle = ` + :focus-visible { + outline: 3px solid var(--shl-color-focus-ring, #1D4ED8); + outline-offset: 2px; + } +`; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + + + + + + + + {children} + + + + + ); +} diff --git a/web/task-board/app/page.tsx b/web/task-board/app/page.tsx new file mode 100644 index 00000000..d872017f --- /dev/null +++ b/web/task-board/app/page.tsx @@ -0,0 +1,55 @@ +"use client"; + +import { Suspense, useState } from "react"; +import { useSearchParams } from "next/navigation"; +import { TextField } from "@nexarch/shl"; +import { TaskList } from "./TaskList"; +import { TaskKanban } from "./TaskKanban"; + +type Tab = "list" | "kanban"; + +function TaskBoardPage() { + const params = useSearchParams(); + const userId = params.get("user") ?? ""; + const [tab, setTab] = useState("list"); + + return ( +
+

Aufgaben

+ {userId === "" ? ( +
{ + e.preventDefault(); + const value = (e.currentTarget.elements.namedItem("user") as HTMLInputElement).value.trim(); + if (value) { + window.location.search = `?user=${encodeURIComponent(value)}`; + } + }} + > + + + + ) : ( + <> +
+ + +
+ {tab === "list" ? : } + + )} +
+ ); +} + +export default function Page() { + return ( + Lade …

}> + +
+ ); +} diff --git a/web/task-board/lib/api.test.ts b/web/task-board/lib/api.test.ts new file mode 100644 index 00000000..6b12b3b7 --- /dev/null +++ b/web/task-board/lib/api.test.ts @@ -0,0 +1,31 @@ +import { describe, it, expect, vi, afterEach } from "vitest"; +import { ApiError, listTasks, setTaskStatus } from "./api"; + +describe("listTasks/setTaskStatus", () => { + const originalFetch = global.fetch; + afterEach(() => { + global.fetch = originalFetch; + }); + + it("listTasks liefert die dekodierte Liste", async () => { + global.fetch = vi.fn(async () => new Response(JSON.stringify([{ ID: "1", Title: "x" }]), { status: 200 })) as typeof fetch; + const tasks = await listTasks("alice"); + expect(tasks).toEqual([{ ID: "1", Title: "x" }]); + }); + + it("listTasks wirft ApiError bei Fehlerstatus", async () => { + global.fetch = vi.fn(async () => new Response(JSON.stringify({ error: "kaputt" }), { status: 500 })) as typeof fetch; + await expect(listTasks("alice")).rejects.toBeInstanceOf(ApiError); + }); + + it("setTaskStatus sendet status im Koerper und liefert die aktualisierte Aufgabe", async () => { + let capturedBody = ""; + global.fetch = vi.fn(async (_url: string | URL, init?: RequestInit) => { + capturedBody = init?.body as string; + return new Response(JSON.stringify({ ID: "1", Status: "done" }), { status: 200 }); + }) as typeof fetch; + const task = await setTaskStatus("alice", "1", "done"); + expect(JSON.parse(capturedBody)).toEqual({ status: "done" }); + expect(task.Status).toBe("done"); + }); +}); diff --git a/web/task-board/lib/api.ts b/web/task-board/lib/api.ts new file mode 100644 index 00000000..303f8a8f --- /dev/null +++ b/web/task-board/lib/api.ts @@ -0,0 +1,53 @@ +// Duenner Client fuer die eigenen Backend-for-Frontend-Routen unter +// app/api — kein direkter Zugriff auf die Workflow-API vom Client aus, +// gleiches Muster wie web/workflow-cases/lib/api.ts. + +export class ApiError extends Error { + status: number; + constructor(message: string, status: number) { + super(message); + this.status = status; + } +} + +export interface Reference { + ModuleName: string; + ObjectType: string; + ObjectID: string; +} + +export type Priority = "low" | "normal" | "high"; +export type Status = "open" | "in_progress" | "done"; + +export interface Task { + ID: string; + Title: string; + AssigneeType: "person" | "group"; + AssigneeID: string; + Priority: Priority; + Status: Status; + DueDate: string | null; + References: Reference[] | null; +} + +async function parse(res: Response, fallback: string): Promise { + const body = await res.json().catch(() => null); + if (!res.ok) { + throw new ApiError(body?.error ?? `${fallback} (${res.status})`, res.status); + } + return body as T; +} + +export async function listTasks(userId: string): Promise { + const res = await fetch(`/api/tasks?userId=${encodeURIComponent(userId)}`, { cache: "no-store" }); + return parse(res, "Aufgaben konnten nicht geladen werden"); +} + +export async function setTaskStatus(userId: string, id: string, status: Status): Promise { + const res = await fetch(`/api/tasks/${encodeURIComponent(id)}/status?userId=${encodeURIComponent(userId)}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ status }), + }); + return parse(res, "Status konnte nicht geaendert werden"); +} diff --git a/web/task-board/lib/filters.test.ts b/web/task-board/lib/filters.test.ts new file mode 100644 index 00000000..53a48c33 --- /dev/null +++ b/web/task-board/lib/filters.test.ts @@ -0,0 +1,122 @@ +import { describe, it, expect } from "vitest"; +import { isOverdue, filterTasks, sortByDueDate } from "./filters"; +import type { Task } from "./api"; + +function makeTask(overrides: Partial): Task { + return { + ID: Math.random().toString(36).slice(2), + Title: "Aufgabe", + AssigneeType: "person", + AssigneeID: "user-1", + Priority: "normal", + Status: "open", + DueDate: null, + References: null, + ...overrides, + }; +} + +const NOW = new Date("2026-10-16T12:00:00Z"); + +describe("isOverdue (AK1: Ueberfaellig-Kennzeichnung)", () => { + it("ist ueberfaellig, wenn Faelligkeit in der Vergangenheit liegt und offen ist", () => { + const t = makeTask({ DueDate: "2026-10-10T00:00:00Z", Status: "open" }); + expect(isOverdue(t, NOW)).toBe(true); + }); + + it("ist nicht ueberfaellig ohne Faelligkeit", () => { + const t = makeTask({ DueDate: null }); + expect(isOverdue(t, NOW)).toBe(false); + }); + + it("ist nicht ueberfaellig, wenn bereits erledigt, auch bei abgelaufener Faelligkeit", () => { + const t = makeTask({ DueDate: "2026-10-10T00:00:00Z", Status: "done" }); + expect(isOverdue(t, NOW)).toBe(false); + }); + + it("ist nicht ueberfaellig bei Faelligkeit in der Zukunft", () => { + const t = makeTask({ DueDate: "2026-10-20T00:00:00Z" }); + expect(isOverdue(t, NOW)).toBe(false); + }); +}); + +describe("filterTasks (AK2: Filter nach Faelligkeit und Prioritaet)", () => { + const tasks = [ + makeTask({ Priority: "high", DueDate: "2026-10-10T00:00:00Z" }), // ueberfaellig + makeTask({ Priority: "low", DueDate: "2026-10-18T00:00:00Z" }), // naechste 7 Tage + makeTask({ Priority: "normal", DueDate: null }), // ohne faelligkeit + makeTask({ Priority: "high", DueDate: "2026-12-01T00:00:00Z" }), // weit in der zukunft + ]; + + it("filtert nach Prioritaet", () => { + const result = filterTasks(tasks, "high", "all", NOW); + expect(result).toHaveLength(2); + expect(result.every((t) => t.Priority === "high")).toBe(true); + }); + + it("filtert ueberfaellige Aufgaben", () => { + const result = filterTasks(tasks, "all", "overdue", NOW); + expect(result).toHaveLength(1); + expect(result[0].DueDate).toBe("2026-10-10T00:00:00Z"); + }); + + it("filtert Aufgaben ohne Faelligkeit", () => { + const result = filterTasks(tasks, "all", "none", NOW); + expect(result).toHaveLength(1); + expect(result[0].DueDate).toBeNull(); + }); + + it("filtert Aufgaben der naechsten 7 Tage", () => { + const result = filterTasks(tasks, "all", "next7", NOW); + expect(result).toHaveLength(1); + expect(result[0].DueDate).toBe("2026-10-18T00:00:00Z"); + }); + + it("kombiniert Prioritaets- und Faelligkeitsfilter", () => { + const result = filterTasks(tasks, "high", "overdue", NOW); + expect(result).toHaveLength(1); + expect(result[0].Priority).toBe("high"); + expect(result[0].DueDate).toBe("2026-10-10T00:00:00Z"); + }); +}); + +describe("sortByDueDate", () => { + it("sortiert aufsteigend, undatierte Aufgaben zuletzt", () => { + const tasks = [ + makeTask({ Title: "ohne", DueDate: null }), + makeTask({ Title: "spaeter", DueDate: "2026-11-01T00:00:00Z" }), + makeTask({ Title: "frueher", DueDate: "2026-10-17T00:00:00Z" }), + ]; + const sorted = sortByDueDate(tasks); + expect(sorted.map((t) => t.Title)).toEqual(["frueher", "spaeter", "ohne"]); + }); +}); + +// AK3 / Pruefung 3: Performance bei 200 Aufgaben gemessen -- reine +// Filter-/Sortierlogik ueber einen synthetischen Datensatz, ohne DOM. +describe("Performance bei 200 Aufgaben (Pruefung 3)", () => { + it("filtert und sortiert 200 Aufgaben deutlich unterhalb einer spuerbaren Verzoegerung", () => { + const priorities: Task["Priority"][] = ["low", "normal", "high"]; + const many: Task[] = Array.from({ length: 200 }, (_, i) => + makeTask({ + Title: `Aufgabe ${i}`, + Priority: priorities[i % 3], + DueDate: i % 5 === 0 ? null : new Date(NOW.getTime() + (i - 100) * 24 * 60 * 60 * 1000).toISOString(), + Status: i % 7 === 0 ? "done" : "open", + }) + ); + + const start = performance.now(); + const filtered = filterTasks(many, "high", "overdue", NOW); + const sorted = sortByDueDate(filtered); + const elapsed = performance.now() - start; + + // Nur ein belegter Messwert, kein hartes Budget auf einer CI-Maschine + // -- aber 200 Aufgaben muessen weit unter 100ms bleiben, sonst waere + // etwas grundlegend falsch an der Implementierung (AK3: Ansicht + // bleibt bei 200 Aufgaben nutzbar). + expect(elapsed).toBeLessThan(100); + expect(sorted.length).toBeGreaterThan(0); + expect(sorted.every((t) => t.Priority === "high")).toBe(true); + }); +}); diff --git a/web/task-board/lib/filters.ts b/web/task-board/lib/filters.ts new file mode 100644 index 00000000..52c36cff --- /dev/null +++ b/web/task-board/lib/filters.ts @@ -0,0 +1,52 @@ +import type { Task } from "./api"; + +export type DueFilter = "all" | "overdue" | "next7" | "none"; +export type PriorityFilter = "all" | Task["Priority"]; + +function startOfDay(d: Date): Date { + return new Date(d.getFullYear(), d.getMonth(), d.getDate()); +} + +// isOverdue ist Grundlage von AK1 (Ueberfaellig-Kennzeichnung): eine +// erledigte Aufgabe ist nie ueberfaellig, auch wenn ihre Faelligkeit in +// der Vergangenheit liegt -- sie ist fertig, nicht versaeumt. +export function isOverdue(task: Task, now: Date): boolean { + if (task.Status === "done" || !task.DueDate) { + return false; + } + return new Date(task.DueDate).getTime() < startOfDay(now).getTime(); +} + +// filterTasks kombiniert Prioritaets- und Faelligkeitsfilter (AK2) als +// reine, von React unabhaengige Funktion -- so bei 200 Aufgaben (AK3) +// direkt mess- und testbar ohne DOM-Rendering. +export function filterTasks(tasks: Task[], priority: PriorityFilter, due: DueFilter, now: Date): Task[] { + const start = startOfDay(now).getTime(); + const next7End = start + 7 * 24 * 60 * 60 * 1000; + return tasks.filter((t) => { + if (priority !== "all" && t.Priority !== priority) { + return false; + } + if (due === "overdue") { + return isOverdue(t, now); + } + if (due === "none") { + return !t.DueDate; + } + if (due === "next7") { + if (!t.DueDate) return false; + const due = new Date(t.DueDate).getTime(); + return due >= start && due < next7End; + } + return true; + }); +} + +export function sortByDueDate(tasks: Task[]): Task[] { + return [...tasks].sort((a, b) => { + if (!a.DueDate && !b.DueDate) return 0; + if (!a.DueDate) return 1; + if (!b.DueDate) return -1; + return new Date(a.DueDate).getTime() - new Date(b.DueDate).getTime(); + }); +} diff --git a/web/task-board/next.config.mjs b/web/task-board/next.config.mjs new file mode 100644 index 00000000..aa96725f --- /dev/null +++ b/web/task-board/next.config.mjs @@ -0,0 +1,5 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + transpilePackages: ["@nexarch/shl"], +}; +export default nextConfig; diff --git a/web/task-board/package-lock.json b/web/task-board/package-lock.json new file mode 100644 index 00000000..d19262f3 --- /dev/null +++ b/web/task-board/package-lock.json @@ -0,0 +1,3305 @@ +{ + "name": "nexarch-task-board", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "nexarch-task-board", + "dependencies": { + "@nexarch/shl": "file:../shl", + "next": "16.3.5", + "react": "19.3.0", + "react-dom": "19.3.0" + }, + "devDependencies": { + "@testing-library/jest-dom": "^7.0.1", + "@testing-library/react": "16.3.3", + "@testing-library/user-event": "14.5.2", + "@types/node": "^22.20.4", + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", + "jsdom": "24.1.0", + "typescript": "5.5.3", + "vitest": "^5.0.1" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.10.tgz", + "integrity": "sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz", + "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz", + "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz", + "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz", + "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz", + "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz", + "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz", + "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz", + "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz", + "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz", + "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz", + "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz", + "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz", + "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz", + "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz", + "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz", + "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz", + "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz", + "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz", + "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz", + "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz", + "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz", + "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz", + "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz", + "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz", + "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz", + "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nexarch/shl": { + "version": "0.1.0", + "resolved": "file:../shl", + "peerDependencies": { + "react": "^19.3.0", + "react-dom": "^19.3.0" + } + }, + "node_modules/@next/env": { + "version": "16.3.5", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.3.5.tgz", + "integrity": "sha512-NWEXVDMqoEo0ktmU6u0sE2Vg0LOcsD7NnOTJNo3/fEaTfsg+F1bMIxuDmQbda4e3yTIQwVdUREF2yIuMOusKtg==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.3.5", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.3.5.tgz", + "integrity": "sha512-pMmGgETfKvElucLHtVaeiMRbp2zUbvKx7b1yGko0liBz3cw1mKSggWN/Rp/wPz8z+E1O82u3r4L1Co+ZS5hokQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.3.5", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.3.5.tgz", + "integrity": "sha512-76VaGYvf6HPa5/w12yLkE3dXTn9AfdEviI79oEL3aZoAmRLc9rWitjWqyjViVysK/ht/y9YKzFkBrUdi/wGkow==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.3.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.3.5.tgz", + "integrity": "sha512-zKDELJ5jSQMHeO/hmXUQsAzagX4bQD4OiMi3pQ5FbUj+yK506oLVHnKA2YXMlbg1EHHqJYtyePOgByIDXD1lqw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.3.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.3.5.tgz", + "integrity": "sha512-7Vql0pgzCoHagv6+FNOZoqmJqA52c6zeVbhtS/47qFozO1MSx4ms7x7GHiciY8R5CDsSMKMQjJEryoJLcsBIbA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.3.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.3.5.tgz", + "integrity": "sha512-NH/xzehyHEFWE2nlcZon7TB/0+H4shfWCi7S1zka815XCOhJDYZhoeJtOYy0dh0WVRWACVXSyGNFFytoMxUhRg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.3.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.3.5.tgz", + "integrity": "sha512-lV4+EhWMfS8jcC+EH2nn/Cm5cn6XsgbE07bU9tMH8fCo0tNAqhyzi1b5wQ/Tn6NGFTvKDY65w3ZH95EjwBRAnQ==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.3.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.3.5.tgz", + "integrity": "sha512-/wKzAREX2RF++MhicjDbg8tGn2AiBIM0+EFeTFKoUEUbW5D6amCJehd5Z5G1H5/gxNdgnwoXMcHz24H/c2tGkQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.3.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.3.5.tgz", + "integrity": "sha512-LNdCHzgLFc+UeqMS84LzXPaeBRKyqDN9OMyFAr1OrB0XrNw78IRrEVtZvvA7245W/HsaoeVOQX9jPjPk8jojwA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.154.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.154.0.tgz", + "integrity": "sha512-KYOUGQFlLWR+s4QMV2Q9ko38TZm7EdO87Qm7rvZLqk/webse/cDjtyxKhg/47l0byCbs/k46LT8qc4PKjAikzA==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.14.tgz", + "integrity": "sha512-aWR+XKH9+rW7m5fWq/5360J3mHycrx/o6xZM2DQFdJnTMdyP7/WzxPjhtLbVWdKo2y612/4U1bWqb3HQhiF7vg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.14.tgz", + "integrity": "sha512-QiVxHofl6A8lQU7WAtOUY4QMVZXohxwz7WJJoeh8NBEUyKfIkMRKhYyq8az/1RypNiIXzNTtiS6KwHz0erxYRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.14.tgz", + "integrity": "sha512-lTQOzHuBIooh3jIr5XINQkjPGEuGy+Dsy0meXDr6Sjty0b99kYgGh4uBC/8mJX1T0kC8d6S0aPP1Y+bHkh6aBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.14.tgz", + "integrity": "sha512-hyMKEGatjmBYCs/Q4sbIcL/GqpFEqsX3wc+HYVQ/85A4rxc1o7rPm4DUI4GV4k/h0ITddwFSGpqQunI14GXjTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.14.tgz", + "integrity": "sha512-u371GbXoVXrEgP20PwzoGNOdAOqAMhzo19JzzY+V4YmmnC+DTOoqdbMu+O+54emM59Mik/UdrtWgcuEDKWRPKA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.14.tgz", + "integrity": "sha512-BqwKsdZQf6EqVWr8fv2MrDYzAvOZ7+NYFSJESRGs87fkseEhs0TU9TXgRJTkZoRRkxTizyEg1LKUXfU3/78uqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.14.tgz", + "integrity": "sha512-SD9kY8M009x/7s57Z1dgUVlP5+uYBgKQK0Tkr1WIWngrtUJmRCW1RJFMY7QDla2khXhmg2i5Io7MUjcmZNZ/KQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.14.tgz", + "integrity": "sha512-BJ2CL19M6uuWesMgnqJzgxoI+N+eV7b0A9O9vLV42y+gitC41fNa8KElau1tHVzuLWX5GzEuxMPhAO0tQXQMkg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.14.tgz", + "integrity": "sha512-4+Kl8daPV8NDHaD7mfCcc1pRM+Qg7ab8DyMm5FG5WoZgv4h2i3KFN1CybYBNbQqmuYm7ftJLvmMI1eHYJ22S/Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.14.tgz", + "integrity": "sha512-V/h8ePq1fKDrmtFMz6xcCN3DKvuvMRrwwD1q0PkBeeo4DpK64biaKvqWGVxqCeYKqeRk7gE7/iKPxLCn08O97A==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.14.tgz", + "integrity": "sha512-WxalXzgrLVpKC93xfpaJ3gCXUWzeCr+FRCbDVD2mUAy4L6Vo3lNVz0epUEUpxPWJkLNYUwM4OSIikv36OJ9mLg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.14.tgz", + "integrity": "sha512-3G3tnAmTpdk9jO3GQtyHXqeFD2SlecfTws/Y3M2mmj9nMBwwuLT4kur8sZKeu7ENUHSpVO3cIsm5A52LEWWwlg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.14.tgz", + "integrity": "sha512-qyr5XiNGxm2ytx17vvtcJ9nXn6v1gENETi503bJx3OGwZ1tSonj7BosxbMa/xeAfSl5iS9X6KjEWYXLu6v1L7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.14.tgz", + "integrity": "sha512-oA4o5o0S2yB199YyYXDzKUv1Z56Rc+mgOjwm7Z/vJ9Z8igiDPYvoOuDyo6g7AK/ZaF09nPhDKVnGBcYHO11GXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.14.tgz", + "integrity": "sha512-9mE94bf2GZUaR/fUSXCMpBoWkctu421Mi0ORpm4zqfbkjKep7RxzPFa5etcGs5h+2KApoWBOA/hAJa/7saGgEQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@swc/helpers": { + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.2.tgz", + "integrity": "sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-7.0.1.tgz", + "integrity": "sha512-oMDTC3oA+6CXSO2JZnvOI7CA6oVub6kij5ggk9ohwye5slmkwxYDXcPOVxgMw/RQlticjtO0C1RZkR97HgrWMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=22", + "npm": ">=6", + "yarn": ">=1" + }, + "peerDependencies": { + "@testing-library/dom": ">=10 <11", + "vitest": ">= 0.32" + }, + "peerDependenciesMeta": { + "vitest": { + "optional": true + } + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.3", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.3.tgz", + "integrity": "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.5.2", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.2.tgz", + "integrity": "sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.5.tgz", + "integrity": "sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, + "node_modules/@vitest/spy": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.5.tgz", + "integrity": "sha512-lwJRExuH417257Q5wv9QYugN9IMJkoxcrKrYxGPQkBzl7wlNpTUxD/5yNX4ESKVUIShKPgkGchYPFMWOOk0wzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.29", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.29.tgz", + "integrity": "sha512-YeesxhUmR4h5y5V8KF8iBCqJb8YFrBJ++XBTeGcWIaQOYXGN19JM0FoiOjAhwuDoln8owMgXcWBmgHtYSkky2w==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001816", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001816.tgz", + "integrity": "sha512-+8Yx7BXCoqk6IF8yTicjfUR6D/ldJzdlsodlEOiOSPmhK844WIgk67/zIQaGAOxNkZyyvBlrRFnLvqKIoqbAFw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", + "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cssstyle/node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/jsdom": { + "version": "24.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.1.0.tgz", + "integrity": "sha512-6gpM7pRXCwIOKxX47cgOyvyQDN/Eh0f1MeKySBV2xGdKtqJBLj8P25eY3EVCWo2mglDDzozR2r2MW4T+JiNUZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.0.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.4", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.10", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.4", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.17.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "peer": true, + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.3.tgz", + "integrity": "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "16.3.5", + "resolved": "https://registry.npmjs.org/next/-/next-16.3.5.tgz", + "integrity": "sha512-MdtsTgzyfCPRLC6uJ1mN8ao7lyJ4BB0U6Inhnx3gta1UcCIdHK3yxLG0E8OWQteWD8/Q0qb8A5o7wJaL8M9y2w==", + "license": "MIT", + "dependencies": { + "@next/env": "16.3.5", + "@swc/helpers": "0.5.23", + "baseline-browser-mapping": "^2.9.19", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.5.23", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=20.9.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "16.3.5", + "@next/swc-darwin-x64": "16.3.5", + "@next/swc-linux-arm64-gnu": "16.3.5", + "@next/swc-linux-arm64-musl": "16.3.5", + "@next/swc-linux-x64-gnu": "16.3.5", + "@next/swc-linux-x64-musl": "16.3.5", + "@next/swc-win32-arm64-msvc": "16.3.5", + "@next/swc-win32-x64-msvc": "16.3.5", + "sharp": "^0.35.4" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/nwsapi": { + "version": "2.2.28", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.28.tgz", + "integrity": "sha512-IlVB7OS7qrOsVYlpnFIkETjMwT9jwvmocJmmM+GZU/PAB3uGi9Ezd7vcWhWBUnSc0ya4ppmQITOyP1ez9gg8cg==", + "dev": true, + "license": "MIT" + }, + "node_modules/obug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/obug/-/obug-3.0.0.tgz", + "integrity": "sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", + "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.0" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/rolldown": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.14.tgz", + "integrity": "sha512-eTBTTqMR6uHKLgcUqpJP8ntzDI6V4LYA8negAMBEI7gkgVvOdrx6VsvS2Ym+83KhOPdLCHWJu0w7UQFJZhHKNw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@oxc-project/types": "=0.154.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.14", + "@rolldown/binding-android-arm64": "1.2.14", + "@rolldown/binding-darwin-arm64": "1.2.14", + "@rolldown/binding-darwin-x64": "1.2.14", + "@rolldown/binding-freebsd-x64": "1.2.14", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.14", + "@rolldown/binding-linux-arm64-gnu": "1.2.14", + "@rolldown/binding-linux-arm64-musl": "1.2.14", + "@rolldown/binding-linux-ppc64-gnu": "1.2.14", + "@rolldown/binding-linux-s390x-gnu": "1.2.14", + "@rolldown/binding-linux-x64-gnu": "1.2.14", + "@rolldown/binding-linux-x64-musl": "1.2.14", + "@rolldown/binding-openharmony-arm64": "1.2.14", + "@rolldown/binding-win32-arm64-msvc": "1.2.14", + "@rolldown/binding-win32-x64-msvc": "1.2.14" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz", + "integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.5", + "@img/sharp-darwin-x64": "0.35.5", + "@img/sharp-freebsd-wasm32": "0.35.5", + "@img/sharp-libvips-darwin-arm64": "1.3.4", + "@img/sharp-libvips-darwin-x64": "1.3.4", + "@img/sharp-libvips-linux-arm": "1.3.4", + "@img/sharp-libvips-linux-arm64": "1.3.4", + "@img/sharp-libvips-linux-ppc64": "1.3.4", + "@img/sharp-libvips-linux-riscv64": "1.3.4", + "@img/sharp-libvips-linux-s390x": "1.3.4", + "@img/sharp-libvips-linux-x64": "1.3.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", + "@img/sharp-libvips-linuxmusl-x64": "1.3.4", + "@img/sharp-linux-arm": "0.35.5", + "@img/sharp-linux-arm64": "0.35.5", + "@img/sharp-linux-ppc64": "0.35.5", + "@img/sharp-linux-riscv64": "0.35.5", + "@img/sharp-linux-s390x": "0.35.5", + "@img/sharp-linux-x64": "0.35.5", + "@img/sharp-linuxmusl-arm64": "0.35.5", + "@img/sharp-linuxmusl-x64": "0.35.5", + "@img/sharp-webcontainers-wasm32": "0.35.5", + "@img/sharp-win32-arm64": "0.35.5", + "@img/sharp-win32-ia32": "0.35.5", + "@img/sharp-win32-x64": "0.35.5" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/std-env": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", + "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.2.tgz", + "integrity": "sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", + "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/vite": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.4.tgz", + "integrity": "sha512-IftLE8phJhdNRIGNEV4JCCvfNScoZQVfcXaN8TQropWGJtOHcu+iAB9pHcuGvmEb/kddw0LBe/jS8ztdC6xdiQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.29", + "rolldown": "~1.2.12", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/postcss": { + "version": "8.5.30", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.30.tgz", + "integrity": "sha512-zkd+e9S1AQHhmaBKye9zlFHWjOwVgqXl1q3ECYy8561K7qxR2cREf/STLskulBFd78k6AOFeA+ff1TtEzk9bAA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.20", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/vitest": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.5.tgz", + "integrity": "sha512-cPODOMF8YnkiiYtDdHo6n4wWmBrOBBbOIYH2d33M4l3CdNn72h+qMmO6uXffUN6A5AZFXNND/d+krS9mgkXRfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/mocker": "5.0.5", + "chai": "^6.3.0", + "es-module-lexer": "^2.3.2", + "expect-type": "^1.4.0", + "magic-string": "^1.4.3", + "obug": "^3.0.0", + "picomatch": "^4.0.7", + "std-env": "^4.3.0", + "tinybench": "^6.2.2", + "tinyexec": "^1.3.1", + "tinyglobby": "^0.2.17", + "why-is-node-running": "3.2.1" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^22.12.0 || ^24.0.0 || >=26.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "5.0.5", + "@vitest/browser-preview": "5.0.5", + "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", + "@vitest/coverage-istanbul": "5.0.5", + "@vitest/coverage-v8": "5.0.5", + "@vitest/ui": "5.0.5", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.5.tgz", + "integrity": "sha512-xLL3lkDegJZw+MGoGaCCsFayDZthGgpAtr3Vziyfc4p7xH89K7KRPcRBBh/kEYm0Aqkt5DtTmkYpBtRV/FrHZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.31", + "@vitest/spy": "5.0.5", + "estree-walker": "^3.0.3", + "magic-string": "^1.4.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9 || ^3.0.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/why-is-node-running": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.1.tgz", + "integrity": "sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==", + "dev": true, + "license": "MIT", + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=20.11" + } + }, + "node_modules/ws": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", + "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/web/task-board/package.json b/web/task-board/package.json new file mode 100644 index 00000000..24fb7acb --- /dev/null +++ b/web/task-board/package.json @@ -0,0 +1,27 @@ +{ + "name": "nexarch-task-board", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "test": "vitest run" + }, + "dependencies": { + "@nexarch/shl": "file:../shl", + "next": "16.3.5", + "react": "19.3.0", + "react-dom": "19.3.0" + }, + "devDependencies": { + "@testing-library/jest-dom": "^7.0.1", + "@testing-library/react": "16.3.3", + "@testing-library/user-event": "14.5.2", + "@types/node": "^22.20.4", + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", + "jsdom": "24.1.0", + "typescript": "5.5.3", + "vitest": "^5.0.1" + } +} diff --git a/web/task-board/tsconfig.json b/web/task-board/tsconfig.json new file mode 100644 index 00000000..1c9cb74f --- /dev/null +++ b/web/task-board/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": false, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [{ "name": "next" }], + "paths": { "@/*": ["./*"] } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/web/task-board/vitest.config.ts b/web/task-board/vitest.config.ts new file mode 100644 index 00000000..c5cdeca9 --- /dev/null +++ b/web/task-board/vitest.config.ts @@ -0,0 +1,15 @@ +import path from "node:path"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + resolve: { + alias: { + react: path.resolve(__dirname, "node_modules/react"), + "react-dom": path.resolve(__dirname, "node_modules/react-dom"), + }, + }, + test: { + environment: "jsdom", + setupFiles: ["./vitest.setup.ts"], + }, +}); diff --git a/web/task-board/vitest.setup.ts b/web/task-board/vitest.setup.ts new file mode 100644 index 00000000..7cf3bd6c --- /dev/null +++ b/web/task-board/vitest.setup.ts @@ -0,0 +1,5 @@ +import "@testing-library/jest-dom/vitest"; +import { afterEach } from "vitest"; +import { cleanup } from "@testing-library/react"; + +afterEach(cleanup); diff --git a/workflow/cmd/workflow-app/main.go b/workflow/cmd/workflow-app/main.go index 143f19c9..5e31f2be 100644 --- a/workflow/cmd/workflow-app/main.go +++ b/workflow/cmd/workflow-app/main.go @@ -33,6 +33,7 @@ import ( "gitea.perlbach24.de/scripte/nexarch/workflow/internal/routingruleapi" "gitea.perlbach24.de/scripte/nexarch/workflow/internal/routingsuggestclient" "gitea.perlbach24.de/scripte/nexarch/workflow/internal/task" + "gitea.perlbach24.de/scripte/nexarch/workflow/internal/taskapi" "gitea.perlbach24.de/scripte/nexarch/workflow/internal/tenantregistryclient" ) @@ -121,6 +122,7 @@ func buildMux(pool *pgxpool.Pool, taskStore *task.Store, serviceToken, tenantSlu caseoverviewapi.NewServer(caseoverviewSvc).RegisterRoutes(mux) backchannelapi.NewServer(backchannelSvc).RegisterRoutes(mux) routingruleapi.NewServer(routingruleStore, buildRoutingSuggester()).RegisterRoutes(mux) + taskapi.NewServer(taskStore).RegisterRoutes(mux) moduleadapter.NewServer(pool, serviceToken, tenantSlug).RegisterRoutes(mux) return mux, nil } diff --git a/workflow/docs/TSK-04-PRUEFPROTOKOLL.md b/workflow/docs/TSK-04-PRUEFPROTOKOLL.md new file mode 100644 index 00000000..8e20dc2a --- /dev/null +++ b/workflow/docs/TSK-04-PRUEFPROTOKOLL.md @@ -0,0 +1,244 @@ +# TSK-04 – Aufgaben-Oberfläche: Prüfprotokoll + +Datum: 2026-10-16. Host: Entwicklungshost 192.168.1.204, frischer Klon von `main` +(Head `dac7fff`) in einem Wegwerf-Verzeichnis. Branch `feature/tsk-04-aufgaben-oberflaeche`. +Kein Merge, kein Deploy. + +## Umsetzung + +### Backend (`workflow/internal/task`, TSK-01 additiv erweitert) + +- `Task` bekommt ein `Status`-Feld (`open`/`in_progress`/`done`), Migration + `0016_task_status` (additive Spalte mit CHECK-Constraint, Default `open`). + `Store.SetStatus` ist der **einzige Schreibweg** für den Kanban-Status — + genau das, was Akzeptanzkriterium 4 verlangt ("Drag & Drop … schreibt + neuen Status über TSK-01 zurück"). Bestehender Code (`Create`, `Get`, + `ByAssignee`, `ByReference`, `SetDueDate`) bleibt kompatibel, nur additiv + erweitert. +- Neue Methode `Store.List` liefert alle Aufgaben des gebundenen + Mandanten-Schemas, sortiert nach Fälligkeit (undatierte zuletzt) — die + Grundlage für Liste und Kanban-Ansicht. Bewusst zwei Abfragen (Aufgaben, + dann Referenzen gebündelt) statt eines N+1-Zugriffs wie bei + `ByAssignee`/`ByReference`, weil das bei 200 Aufgaben (Prüfung 3) sonst + separat messbar langsamer wäre. +- Bewusst **kein** Abgleich mit `escalation.Store.Complete`/`completed_at` + (TSK-03): das ist eine eigenständige Nachbar-Kachel mit eigener Spalte, + ein Verknüpfen beider Zustände wäre eine Erweiterung über den + TSK-04-Scope hinaus (nicht Teil der Akzeptanzkriterien). + +### Neues Paket `workflow/internal/taskapi` + +HTTP-Bindung nach demselben Muster wie `caseapi`/`approvalapi` +(`X-User-Id`-Zwischenlösung, siehe deren eigene Kommentare): + +- `GET /tasks` — alle Aufgaben des gebundenen Mandanten-Schemas. +- `POST /tasks/{id}/status` — der Schreibweg für AK4, ruft ausschließlich + `task.Store.SetStatus` auf. + +Verdrahtet in `workflow/cmd/workflow-app/main.go` (`buildMux`), wie alle +anderen Workflow-APIs. + +### Architektur-Entscheidung zu AK5 (Mandanten-Trennung) + +Dieses Modul hat **keine Mandanten-Spalte** in `tasks` — die Trennung ist +im gesamten Workflow-Modul physisch: ein `workflow-app`-Prozess ist über +`NEXARCH_WORKFLOW_APP_TENANT_DSN`/`_TENANT_SLUG` an genau einen +Mandanten/eine Datenbank gebunden (siehe `nexarch-state.json`: "warum +Tenancy keine RLS hat"). `taskapi.Server` erbt das: ein `Server` hält +genau einen `task.Store`/Pool, es gibt keinen Mandanten-Parameter in der +API. Die Next.js-Oberfläche folgt demselben Muster wie +`web/workflow-cases`: ihre Backend-for-Frontend-Route liest nur `userId` +aus der Anfrage und ruft ausschließlich die über `TASK_BOARD_API_URL` +fest konfigurierte Backend-Instanz auf — kein Client-Parameter kann das +Ziel umlenken. Serverseitige Filterung ist damit strukturell, nicht ein +Filter, der sich umgehen ließe. + +### Frontend `web/task-board` (neue App, Next.js 16/React 19/TypeScript, `@nexarch/shl`) + +Nach dem Muster von `web/workflow-cases` (`.npmrc` mit `install-links=true`, +`tsconfig.json` mit `jsx: react-jsx` + `.next/dev/types`-Include, wie seit +OPS-11/QA-15 für alle Apps etabliert): + +- `app/page.tsx`: Tab-Umschaltung Liste/Kanban, `Nutzerkennung`-Gate wie + bei `workflow-cases`/`caseoverview` (keine zentrale Session, siehe + VOR-01/APR-01-Kommentare). +- `app/TaskList.tsx`: Liste über `@nexarch/shl`s `Table`-Komponente, + Filter nach Priorität und Fälligkeit (Alle/Überfällig/Nächste 7 + Tage/Ohne Fälligkeit), Überfällig-Kennzeichnung als Text-Badge (nicht + nur Farbe — WCAG). +- `app/TaskKanban.tsx`: drei Spalten Offen/In Bearbeitung/Erledigt, Drag & + Drop **und** gleichwertige Tastaturaktion ("Nach … verschieben"-Buttons, + gleiches Barrierefreiheits-Muster wie `ReminderKanban` aus + `workflow-cases`, UX-14-Anspruch dieses Projekts). Beide Wege rufen + `setTaskStatus` → `POST /api/tasks/{id}/status` → `taskapi` → + `task.Store.SetStatus`. +- `app/api/tasks/route.ts`, `app/api/tasks/[id]/status/route.ts`: + Backend-for-Frontend-Routen, identisches Muster wie + `workflow-cases/app/api/...` (kein direkter Client-Zugriff auf die + Workflow-API). +- `lib/filters.ts`: reine Filter-/Sortierfunktionen (`isOverdue`, + `filterTasks`, `sortByDueDate`), unabhängig vom DOM testbar. + +## Akzeptanzkriterien + +| # | Kriterium | Ergebnis | +|---|---|---| +| 1 | Überfällige Aufgaben sind klar gekennzeichnet | **Erfüllt.** `isOverdue` (Status ≠ done, Fälligkeit < heute) + Text-Badge "Ueberfaellig" in der Tabelle. Test: `lib/filters.test.ts` (4 Fälle: überfällig, ohne Fälligkeit, erledigt+abgelaufen, Zukunft), `TaskList.test.tsx` ("kennzeichnet eine ueberfaellige Aufgabe im Text"). | +| 2 | Liste ist nach Fälligkeit und Priorität filterbar | **Erfüllt.** `filterTasks` kombiniert beide Filter. Test: `lib/filters.test.ts` (Priorität, überfällig, nächste 7 Tage, ohne Fälligkeit, Kombination), `TaskList.test.tsx` (Filterwechsel über die Select-Felder). | +| 3 | Ansicht bleibt bei 200 Aufgaben nutzbar | **Erfüllt**, siehe Performance-Messung unten. | +| 4 | Kanban-Ansicht zeigt Spalten nach Status, Drag & Drop schreibt neuen Status über TSK-01 zurück | **Erfüllt.** Go: `taskapi.TestHandleSetStatus_UpdatesStatus` (HTTP → `task.Store.SetStatus` → DB-Lesebestätigung). Frontend: `TaskKanban.test.tsx` testet sowohl den Tastaturweg (Button-Klick) als auch echtes Drag & Drop (`fireEvent.dragStart/dragOver/drop`). **Live-E2E** unten bestätigt den kompletten Weg Browser-Aktion → Next.js-BFF → `workflow-app` → Postgres. | +| 5 | Kanban-Ansicht zeigt ausschließlich Aufgaben des aktuellen Mandanten, serverseitig gefiltert | **Erfüllt**, siehe eigener Abschnitt unten (Go-Test + BFF-Test + Live-E2E mit zwei echten Mandanten-Schemas). | + +## Prüfungen vor der Abnahme + +### 1. Filterkombination gegen Testdatensatz + +`lib/filters.test.ts`, Testfall "kombiniert Prioritäts- und +Fälligkeitsfilter": 4 synthetische Aufgaben (hoch/überfällig, +niedrig/nächste 7 Tage, normal/ohne Fälligkeit, hoch/weit in der Zukunft), +Filter `priority=high` + `due=overdue` liefert genau die eine erwartete +Aufgabe. **Bestanden.** + +### 2. Überfällig-Kennzeichnung bei Fristüberschreitung + +`TaskList.test.tsx`: Aufgabe mit Fälligkeit vor 5 Tagen wird mit dem Text +"Ueberfaellig" gerendert (nicht nur eine CSS-Klasse). **Bestanden.** + +### 3. Performance bei 200 Aufgaben — gemessene Werte + +- **Backend** (`workflow/internal/task`, `TestList_PerformanceWith200Tasks`): + 200 Aufgaben angelegt, `Store.List()` (2 SQL-Abfragen statt 200) + gemessen: **2,997592 ms** (Schwelle < 1 s, bestanden mit großem + Abstand). +- **Frontend-Logik** (`lib/filters.test.ts`, reine Filter-/Sortierfunktion + über 200 synthetische Aufgaben, `priority=high` + `due=overdue`): + < 100 ms-Schwelle eingehalten (deutlich darunter, im + Mikrosekundenbereich). +- **Frontend-Rendering** (`TaskList.test.tsx`, vollständiges Rendern von + 200 Zeilen inkl. Ladevorgang über gemockten `fetch` in jsdom): + gemessene Werte in zwei Durchläufen **157,6 ms** und **182,6 ms** + (Schwelle < 2000 ms). jsdom ist üblicherweise langsamer als ein echter + Browser, der Wert ist damit ein konservativer oberer Rand. +- **Fazit:** Die Ansicht bleibt bei 200 Aufgaben nutzbar — sowohl die + Datenbankabfrage als auch Filterung und Rendering liegen weit innerhalb + spürbarer Reaktionszeiten. Keine Virtualisierung nötig/eingebaut (wäre + bei 200 Zeilen Überdimensionierung; `web/dms-browser` nutzt dafür ein + eigenes Muster bei 10.000 Zeilen, hier nicht übertragen, da nicht + gefordert). + +### 4. Drag & Drop ändert Status über TSK-01 — live nachgewiesen + +Über Unit-/Integrationstests hinaus (siehe AK4) ein echter Lauf auf 204: + +1. `workflow-app`-Binary gebaut, gegen ein frisches Mandanten-Schema + gestartet (Migrationen inkl. `0016_task_status` angewendet). +2. Aufgabe direkt über `task.Store.Create` angelegt (`Status=open`). +3. `curl -X POST .../tasks/{id}/status -d '{"status":"in_progress"}'` + gegen den **echten laufenden Dienst** — Antwort zeigt + `"Status":"in_progress"`. +4. Erneutes `GET /tasks` bestätigt den persistierten neuen Status direkt + aus Postgres (nicht nur die Antwort des Schreibaufrufs). +5. Next.js-App gebaut (`next build`) und mit `next start` gegen diese + `workflow-app`-Instanz gestartet; `POST /api/tasks/{id}/status` über + die echte BFF-Route ausgelöst, erneutes `GET /tasks` direkt gegen + `workflow-app` bestätigt den Status `done`. + +**Bestanden** — der komplette Weg Oberfläche → BFF → `taskapi` → +`task.Store.SetStatus` → Postgres ist einmal vollständig real +durchlaufen, nicht nur gemockt. + +### 5. Kanban-Ansicht mit zwei Mandanten — nur aktueller Mandant sichtbar + +Dreifach nachgewiesen, von der Modellebene bis zum echten Netzwerkaufruf: + +1. **Modellebene** (`task.TestList_TenantIsolation`): zwei getrennte + Mandanten-Schemas (`testdb.Pool`), je eine Aufgabe "Mandant A"/"Mandant + B" angelegt — `List()` je Store liefert ausschließlich die eigene. +2. **HTTP-Ebene** (`taskapi.TestTwoTenantServers_ListNeverCrossesTenants`): + zwei `taskapi.Server`-Instanzen, je an ein eigenes Schema gebunden + (genau wie `workflow-app` es produktiv je Mandant tut), Mandant B hat + zwei Aufgaben, Mandant A eine — `GET /tasks` je Server liefert exakt + die eigene Menge, nie die des anderen. +3. **Live-E2E mit zwei echten laufenden Backend-Instanzen** (nicht nur + Tests): zwei Mandanten-Schemas (`tsk04_e2e_a`, `tsk04_e2e_b`) in der + echten Postgres-Testdatenbank angelegt, migriert, mit je einer + Aufgabe ("Mandant A Aufgabe" / "Mandant B Aufgabe") befüllt. Zwei + `workflow-app`-Prozesse gestartet (Port 9601 für Mandant A, 9602 für + Mandant B, je eigene `NEXARCH_WORKFLOW_APP_TENANT_DSN` mit eigenem + `search_path`). `curl` gegen beide Ports bestätigt: Port 9601 liefert + ausschließlich "Mandant A Aufgabe", Port 9602 ausschließlich "Mandant B + Aufgabe". Die Next.js-App (`next build` + `next start`) wurde mit + `TASK_BOARD_API_URL=http://127.0.0.1:9601` (Mandant A) gestartet und + über `curl` angesprochen: + - `GET /api/tasks?userId=alice` → nur "Mandant A Aufgabe". + - `GET /api/tasks?userId=alice&tenant=mandant-b` (bewusster + Manipulationsversuch über einen erfundenen Client-Parameter) → **exakt + dieselbe Antwort**, weiterhin nur "Mandant A Aufgabe" — die Route + liest `tenant`/`mandant` gar nicht erst aus, es gibt keinen Pfad, über + den ein Client das Ziel-Backend umlenken könnte. + - `POST /api/tasks/{id}/status` über die BFF-Route verändert + nachweislich (erneutes direktes `GET` gegen Mandant-A-Backend) nur + Mandant A, Mandant B unverändert. + + Alle gestarteten Prozesse anschließend beendet, die beiden + Demo-Schemas gelöscht, das temporäre Seed-Programm + (`workflow/cmd/e2e-tsk04-seed`, nicht Teil des Commits) entfernt. + +**Bestanden** — die Mandantentrennung ist strukturell (ein Pool/Prozess +pro Mandant) und wurde nicht nur behauptet, sondern mit zwei echten +nebenläufigen Backend-Instanzen und einem echten Umgehungsversuch über +die Oberfläche geprüft. + +## Regressionstests + +- Gesamtes `workflow`-Modul (`go build ./...`, `go vet ./...`, + `go test ./... -count=1 -p 1`): alle 27 testbaren Pakete `ok`, keine + Regression in bestehenden Paketen (`task`, `escalation`, + `escalationnotify`, `caseobj`, `approval`, `routingrule` usw. + unverändert grün). +- `web/task-board`: `npx tsc --noEmit` sauber, `next build` erfolgreich + (Turbopack, Next 16.3.5/React 19.3.0 wie alle anderen Apps seit + OPS-11/QA-15), `npx vitest run`: **26/26 Tests bestanden**, 0 + fehlgeschlagen. + +## QA und Entscheidungen (Iterationen: 1 von 10) + +1. **Status-Spalte additiv in `task` (TSK-01) statt eigenem Paket:** AK4 + verlangt ausdrücklich "über TSK-01" zurückzuschreiben. Da TSK-01 bisher + keinen Lebenszyklus-Status kannte (nur `completed_at` aus TSK-03/ + `escalation`, ein anderes Paket), war eine additive Erweiterung von + `task.Store` um `Status`/`SetStatus` die einzige Lösung, die sowohl + AK4 wörtlich erfüllt als auch TSK-01s eigenen Bestandscode unverändert + lässt (gleiches Prinzip wie TSK-02/TSK-03 vorher: additive Migration, + kein Edit an bestehenden Methoden). +2. **`completed_at` (TSK-03) und `status=done` bewusst nicht + verknüpft:** eine Aufgabe auf "Erledigt" im Kanban zu verschieben setzt + nicht automatisch `escalation.Store.Complete`. Das wäre eine + funktionale Erweiterung über TSK-04 hinaus (Interaktion zwischen zwei + Kacheln regeln) und ist in keinem Akzeptanzkriterium verlangt — bewusst + ausgelassen, dokumentiert statt stillschweigend verknüpft. +3. **Keine weiteren Korrekturrunden nötig:** ein einziger Testfehler + (ambige `getByText`-Abfrage in `TaskList.test.tsx`, traf sowohl den + Dropdown-Eintrag "Ueberfaellig" als auch das Status-Badge) wurde in + derselben Runde durch einen spezifischeren Selector behoben. Keine + Backend-Iteration nötig — Go-Build/Vet/Tests liefen beim ersten + Durchlauf durch. + +## Nicht Teil dieser Kachel + +- Erstellen neuer Aufgaben über die Oberfläche (kein Akzeptanzkriterium + verlangt das; `taskapi` exponiert bewusst keinen `POST /tasks`, Aufgaben + entstehen weiterhin über aufrufende Module/`task.Store.Create`, wie bei + TSK-01 festgelegt). +- Verknüpfung mit Referenzen/Fremdobjekten in der Oberfläche (TSK-01 + Akzeptanzkriterium 3 ist Backend-Scope, hier nicht dargestellt). +- CI-Matrix-Eintrag für `task-board` (wie 10 der 13 Bestands-Apps auch + nicht in der CI-Matrix sind, siehe `QA-15-F17-NEXTJS16-PRUEFPROTOKOLL.md` + — keine eigene Entscheidung dieser Kachel, bestehendes Muster + übernommen). + +## Abschluss + +- Commit: `TSK-04: aufgaben-oberflaeche` +- Branch: `feature/tsk-04-aufgaben-oberflaeche` +- Kein Merge, kein Deploy. diff --git a/workflow/internal/task/task.go b/workflow/internal/task/task.go index de4d7e7d..450a438c 100644 --- a/workflow/internal/task/task.go +++ b/workflow/internal/task/task.go @@ -32,6 +32,18 @@ const ( PriorityHigh Priority = "high" ) +// Status ist der Bearbeitungsstatus einer Aufgabe (TSK-04): Grundlage der +// Kanban-Spalten Offen/In Bearbeitung/Erledigt. Store.SetStatus ist der +// einzige Schreibweg -- die Kanban-Oberflaeche schreibt ausschliesslich +// darueber zurueck (TSK-04 Akzeptanzkriterium 4). +type Status string + +const ( + StatusOpen Status = "open" + StatusInProgress Status = "in_progress" + StatusDone Status = "done" +) + // ErrInvalidAssigneeType wird zurueckgegeben, wenn AssigneeType weder // "person" noch "group" ist. var ErrInvalidAssigneeType = errors.New("task: assignee_type muss person oder group sein") @@ -40,6 +52,10 @@ var ErrInvalidAssigneeType = errors.New("task: assignee_type muss person oder gr // zulaessigen Werte hat. var ErrInvalidPriority = errors.New("task: unzulaessige prioritaet") +// ErrInvalidStatus wird zurueckgegeben, wenn Status keinen der drei +// zulaessigen Werte hat. +var ErrInvalidStatus = errors.New("task: unzulaessiger status") + // ErrNotFound wird zurueckgegeben, wenn keine Aufgabe mit der // angegebenen ID existiert. var ErrNotFound = errors.New("task: aufgabe nicht gefunden") @@ -59,6 +75,7 @@ type Task struct { AssigneeType AssigneeType AssigneeID string Priority Priority + Status Status References []Reference // DueDate ist die ueber calendar.Store.AddBusinessDays berechnete // Faelligkeit (TSK-02). Nil, solange keine Faelligkeit gesetzt wurde. @@ -83,8 +100,13 @@ func validPriority(p Priority) bool { return p == PriorityLow || p == PriorityNormal || p == PriorityHigh } +func validStatus(s Status) bool { + return s == StatusOpen || s == StatusInProgress || s == StatusDone +} + // Create legt eine Aufgabe mit optionalen Referenzen an. Ein leerer -// Priority-Wert wird als PriorityNormal behandelt. +// Priority-Wert wird als PriorityNormal behandelt, ein leerer Status-Wert +// als StatusOpen. // // Zuweisung an eine Gruppe legt genau eine Zeile an (Akzeptanzkriterium 2: // kein Aufteilen in je eine Aufgabe pro Gruppenmitglied hier -- das waere @@ -100,6 +122,12 @@ func (s *Store) Create(ctx context.Context, t Task) (string, error) { if !validPriority(t.Priority) { return "", ErrInvalidPriority } + if t.Status == "" { + t.Status = StatusOpen + } + if !validStatus(t.Status) { + return "", ErrInvalidStatus + } if t.AssigneeID == "" { return "", fmt.Errorf("task: assignee_id darf nicht leer sein") } @@ -115,10 +143,10 @@ func (s *Store) Create(ctx context.Context, t Task) (string, error) { var id string if err := tx.QueryRow(ctx, ` - INSERT INTO tasks (title, assignee_type, assignee_id, priority) - VALUES ($1, $2, $3, $4) + INSERT INTO tasks (title, assignee_type, assignee_id, priority, status) + VALUES ($1, $2, $3, $4, $5) RETURNING id - `, t.Title, string(t.AssigneeType), t.AssigneeID, string(t.Priority)).Scan(&id); err != nil { + `, t.Title, string(t.AssigneeType), t.AssigneeID, string(t.Priority), string(t.Status)).Scan(&id); err != nil { return "", fmt.Errorf("task: anlegen: %w", err) } @@ -143,12 +171,12 @@ func (s *Store) Create(ctx context.Context, t Task) (string, error) { // Get liest eine Aufgabe mit ihren Referenzen. func (s *Store) Get(ctx context.Context, id string) (Task, error) { var t Task - var assigneeType, priority string + var assigneeType, priority, status string var dueDate *time.Time if err := s.pool.QueryRow(ctx, ` - SELECT id, title, assignee_type, assignee_id, priority, due_date + SELECT id, title, assignee_type, assignee_id, priority, status, due_date FROM tasks WHERE id = $1 - `, id).Scan(&t.ID, &t.Title, &assigneeType, &t.AssigneeID, &priority, &dueDate); err != nil { + `, id).Scan(&t.ID, &t.Title, &assigneeType, &t.AssigneeID, &priority, &status, &dueDate); err != nil { if errors.Is(err, pgx.ErrNoRows) { return Task{}, ErrNotFound } @@ -156,6 +184,7 @@ func (s *Store) Get(ctx context.Context, id string) (Task, error) { } t.AssigneeType = AssigneeType(assigneeType) t.Priority = Priority(priority) + t.Status = Status(status) t.DueDate = dueDate rows, err := s.pool.Query(ctx, ` @@ -195,6 +224,23 @@ func (s *Store) SetDueDate(ctx context.Context, id string, dueDate time.Time) er return nil } +// SetStatus setzt den Bearbeitungsstatus (TSK-04 Akzeptanzkriterium 4: der +// einzige Schreibweg, ueber den die Kanban-Oberflaeche eine verschobene +// Aufgabe zurueckschreibt). +func (s *Store) SetStatus(ctx context.Context, id string, status Status) error { + if !validStatus(status) { + return ErrInvalidStatus + } + tag, err := s.pool.Exec(ctx, `UPDATE tasks SET status = $2 WHERE id = $1`, id, string(status)) + if err != nil { + return fmt.Errorf("task: status setzen: %w", err) + } + if tag.RowsAffected() == 0 { + return ErrNotFound + } + return nil +} + // ByAssignee liefert alle Aufgaben einer Person oder Gruppe. func (s *Store) ByAssignee(ctx context.Context, assigneeType AssigneeType, assigneeID string) ([]Task, error) { rows, err := s.pool.Query(ctx, ` @@ -264,3 +310,69 @@ func (s *Store) ByReference(ctx context.Context, moduleName, objectType, objectI } return out, nil } + +// List liefert alle Aufgaben dieses Mandanten-Schemas (TSK-04: Grundlage +// fuer Liste und Kanban-Ansicht), sortiert nach Faelligkeit (undatierte +// Aufgaben zuletzt) und Erstellzeitpunkt. Bewusst zwei Abfragen statt +// einer pro Aufgabe (wie bei ByAssignee/ByReference) -- bei 200 Aufgaben +// (TSK-04 Pruefung 3) waere ein N+1-Zugriff auf task_references separat +// messbar langsamer. +func (s *Store) List(ctx context.Context) ([]Task, error) { + rows, err := s.pool.Query(ctx, ` + SELECT id, title, assignee_type, assignee_id, priority, status, due_date + FROM tasks + ORDER BY due_date IS NULL, due_date, created_at + `) + if err != nil { + return nil, fmt.Errorf("task: liste lesen: %w", err) + } + var tasks []Task + var ids []string + for rows.Next() { + var t Task + var assigneeType, priority, status string + if err := rows.Scan(&t.ID, &t.Title, &assigneeType, &t.AssigneeID, &priority, &status, &t.DueDate); err != nil { + rows.Close() + return nil, fmt.Errorf("task: zeile scannen: %w", err) + } + t.AssigneeType = AssigneeType(assigneeType) + t.Priority = Priority(priority) + t.Status = Status(status) + tasks = append(tasks, t) + ids = append(ids, t.ID) + } + rows.Close() + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("task: iterieren: %w", err) + } + if len(ids) == 0 { + return tasks, nil + } + + refRows, err := s.pool.Query(ctx, ` + SELECT task_id, module_name, object_type, object_id + FROM task_references + WHERE task_id = ANY($1) + ORDER BY created_at + `, ids) + if err != nil { + return nil, fmt.Errorf("task: referenzen lesen: %w", err) + } + defer refRows.Close() + byTask := make(map[string][]Reference, len(ids)) + for refRows.Next() { + var taskID string + var r Reference + if err := refRows.Scan(&taskID, &r.ModuleName, &r.ObjectType, &r.ObjectID); err != nil { + return nil, fmt.Errorf("task: referenz scannen: %w", err) + } + byTask[taskID] = append(byTask[taskID], r) + } + if err := refRows.Err(); err != nil { + return nil, fmt.Errorf("task: referenzen iterieren: %w", err) + } + for i := range tasks { + tasks[i].References = byTask[tasks[i].ID] + } + return tasks, nil +} diff --git a/workflow/internal/task/task_status_test.go b/workflow/internal/task/task_status_test.go new file mode 100644 index 00000000..acd1d949 --- /dev/null +++ b/workflow/internal/task/task_status_test.go @@ -0,0 +1,205 @@ +package task + +import ( + "context" + "errors" + "testing" + "time" +) + +// TestCreate_DefaultsStatusOpen prueft, dass eine neu angelegte Aufgabe +// ohne expliziten Status auf "open" steht (Grundlage der Kanban-Spalte +// Offen, TSK-04). +func TestCreate_DefaultsStatusOpen(t *testing.T) { + s := newTestStore(t) + ctx := context.Background() + + id, err := s.Create(ctx, Task{Title: "Posteingang sichten", AssigneeType: AssigneePerson, AssigneeID: "user-1"}) + if err != nil { + t.Fatalf("create: %v", err) + } + got, err := s.Get(ctx, id) + if err != nil { + t.Fatalf("get: %v", err) + } + if got.Status != StatusOpen { + t.Fatalf("status = %q, want %q", got.Status, StatusOpen) + } +} + +// TestSetStatus_ValidTransition prueft TSK-04 Akzeptanzkriterium 4: der +// Kanban-Status einer Aufgabe laesst sich ueber SetStatus (TSK-01) +// aendern und ist danach ueber Get sichtbar. +func TestSetStatus_ValidTransition(t *testing.T) { + s := newTestStore(t) + ctx := context.Background() + + id, err := s.Create(ctx, Task{Title: "Vertrag pruefen", AssigneeType: AssigneePerson, AssigneeID: "user-2"}) + if err != nil { + t.Fatalf("create: %v", err) + } + + if err := s.SetStatus(ctx, id, StatusInProgress); err != nil { + t.Fatalf("set status in_progress: %v", err) + } + got, err := s.Get(ctx, id) + if err != nil { + t.Fatalf("get: %v", err) + } + if got.Status != StatusInProgress { + t.Fatalf("status = %q, want %q", got.Status, StatusInProgress) + } + + if err := s.SetStatus(ctx, id, StatusDone); err != nil { + t.Fatalf("set status done: %v", err) + } + got, err = s.Get(ctx, id) + if err != nil { + t.Fatalf("get: %v", err) + } + if got.Status != StatusDone { + t.Fatalf("status = %q, want %q", got.Status, StatusDone) + } +} + +// TestSetStatus_RejectsInvalidStatus prueft, dass ein unzulaessiger +// Status-Wert abgelehnt wird und die Aufgabe unveraendert bleibt. +func TestSetStatus_RejectsInvalidStatus(t *testing.T) { + s := newTestStore(t) + ctx := context.Background() + + id, err := s.Create(ctx, Task{Title: "x", AssigneeType: AssigneePerson, AssigneeID: "user-3"}) + if err != nil { + t.Fatalf("create: %v", err) + } + + if err := s.SetStatus(ctx, id, "erledigt"); !errors.Is(err, ErrInvalidStatus) { + t.Fatalf("erwartet ErrInvalidStatus, habe %v", err) + } + got, err := s.Get(ctx, id) + if err != nil { + t.Fatalf("get: %v", err) + } + if got.Status != StatusOpen { + t.Fatalf("status wurde trotz abgelehnter aenderung veraendert: %q", got.Status) + } +} + +// TestSetStatus_NotFound prueft den Fehlerfall einer nicht existierenden +// Aufgabe. +func TestSetStatus_NotFound(t *testing.T) { + s := newTestStore(t) + ctx := context.Background() + + if err := s.SetStatus(ctx, "00000000-0000-0000-0000-000000000000", StatusDone); !errors.Is(err, ErrNotFound) { + t.Fatalf("erwartet ErrNotFound, habe %v", err) + } +} + +// TestList_ReturnsAllOrderedByDueDate prueft die Grundlage von TSK-04s +// Liste/Kanban-Ansicht: List liefert alle Aufgaben des Mandanten-Schemas, +// sortiert nach Faelligkeit, undatierte Aufgaben zuletzt, inklusive +// Referenzen. +func TestList_ReturnsAllOrderedByDueDate(t *testing.T) { + s := newTestStore(t) + ctx := context.Background() + + idNoDue, err := s.Create(ctx, Task{Title: "ohne faelligkeit", AssigneeType: AssigneePerson, AssigneeID: "user-4"}) + if err != nil { + t.Fatalf("create ohne faelligkeit: %v", err) + } + idLater, err := s.Create(ctx, Task{ + Title: "spaeter faellig", AssigneeType: AssigneePerson, AssigneeID: "user-4", + References: []Reference{{ModuleName: "dms-fake", ObjectType: "document", ObjectID: "doc-9"}}, + }) + if err != nil { + t.Fatalf("create spaeter faellig: %v", err) + } + idSooner, err := s.Create(ctx, Task{Title: "frueher faellig", AssigneeType: AssigneePerson, AssigneeID: "user-4"}) + if err != nil { + t.Fatalf("create frueher faellig: %v", err) + } + + now := time.Now().UTC() + if err := s.SetDueDate(ctx, idLater, now.AddDate(0, 0, 10)); err != nil { + t.Fatalf("faelligkeit setzen (spaeter): %v", err) + } + if err := s.SetDueDate(ctx, idSooner, now.AddDate(0, 0, 1)); err != nil { + t.Fatalf("faelligkeit setzen (frueher): %v", err) + } + + list, err := s.List(ctx) + if err != nil { + t.Fatalf("list: %v", err) + } + if len(list) != 3 { + t.Fatalf("erwartet 3 aufgaben, habe %d", len(list)) + } + if list[0].ID != idSooner || list[1].ID != idLater || list[2].ID != idNoDue { + t.Fatalf("falsche reihenfolge: %s, %s, %s", list[0].ID, list[1].ID, list[2].ID) + } + if len(list[1].References) != 1 || list[1].References[0].ObjectID != "doc-9" { + t.Fatalf("referenzen in liste nicht uebernommen: %+v", list[1].References) + } +} + +// TestList_TenantIsolation prueft TSK-04 Akzeptanzkriterium 5 auf +// Modellebene: zwei getrennte Mandanten-Schemas sehen sich in List +// gegenseitig nicht, dasselbe Muster wie TestTenantIsolation. +func TestList_TenantIsolation(t *testing.T) { + s1 := newTestStore(t) + s2 := newTestStore(t) + ctx := context.Background() + + if _, err := s1.Create(ctx, Task{Title: "Mandant A", AssigneeType: AssigneePerson, AssigneeID: "user-a"}); err != nil { + t.Fatalf("create in tenant 1: %v", err) + } + if _, err := s2.Create(ctx, Task{Title: "Mandant B", AssigneeType: AssigneePerson, AssigneeID: "user-b"}); err != nil { + t.Fatalf("create in tenant 2: %v", err) + } + + list1, err := s1.List(ctx) + if err != nil { + t.Fatalf("list tenant 1: %v", err) + } + if len(list1) != 1 || list1[0].Title != "Mandant A" { + t.Fatalf("mandant 1 sieht fremde aufgaben: %+v", list1) + } + + list2, err := s2.List(ctx) + if err != nil { + t.Fatalf("list tenant 2: %v", err) + } + if len(list2) != 1 || list2[0].Title != "Mandant B" { + t.Fatalf("mandant 2 sieht fremde aufgaben: %+v", list2) + } +} + +// TestList_PerformanceWith200Tasks misst die Laufzeit von List bei 200 +// Aufgaben (TSK-04 Pruefung 3: Performance bei 200 Aufgaben gemessen). +// Schwelle grosszuegig (1s) -- es geht um einen belegten Messwert, nicht +// um ein scharfes Performance-Budget auf einem Test-Host. +func TestList_PerformanceWith200Tasks(t *testing.T) { + s := newTestStore(t) + ctx := context.Background() + + for i := 0; i < 200; i++ { + if _, err := s.Create(ctx, Task{Title: "Aufgabe", AssigneeType: AssigneePerson, AssigneeID: "user-perf"}); err != nil { + t.Fatalf("create #%d: %v", i, err) + } + } + + start := time.Now() + list, err := s.List(ctx) + elapsed := time.Since(start) + if err != nil { + t.Fatalf("list: %v", err) + } + if len(list) != 200 { + t.Fatalf("erwartet 200 aufgaben, habe %d", len(list)) + } + t.Logf("List() mit 200 Aufgaben: %s", elapsed) + if elapsed > time.Second { + t.Fatalf("List() mit 200 Aufgaben dauerte %s, erwartet < 1s", elapsed) + } +} diff --git a/workflow/internal/taskapi/taskapi.go b/workflow/internal/taskapi/taskapi.go new file mode 100644 index 00000000..03117c97 --- /dev/null +++ b/workflow/internal/taskapi/taskapi.go @@ -0,0 +1,93 @@ +// Package taskapi ist die HTTP-Bindung fuer das Aufgabenmodell (TSK-01) +// zur Aufgaben-Oberflaeche (TSK-04): Liste aller Aufgaben des gebundenen +// Mandanten-Schemas und der einzige Schreibweg fuer den Kanban-Status. +// Nutzeridentitaet kommt uebergangsweise aus X-User-Id, wie bereits bei +// caseapi/approvalapi (VOR-01/APR-01) etabliert. +package taskapi + +import ( + "encoding/json" + "errors" + "net/http" + + "gitea.perlbach24.de/scripte/nexarch/workflow/internal/task" +) + +type Server struct { + store *task.Store +} + +func NewServer(store *task.Store) *Server { + return &Server{store: store} +} + +func (s *Server) RegisterRoutes(mux *http.ServeMux) { + mux.HandleFunc("GET /tasks", s.requireUser(s.handleList)) + mux.HandleFunc("POST /tasks/{id}/status", s.requireUser(s.handleSetStatus)) +} + +func (s *Server) requireUser(next func(w http.ResponseWriter, r *http.Request, userID string)) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + userID := r.Header.Get("X-User-Id") + if userID == "" { + writeError(w, http.StatusUnauthorized, "X-User-Id fehlt") + return + } + next(w, r, userID) + } +} + +// handleList liefert alle Aufgaben des an diesen Server gebundenen +// Mandanten-Schemas (TSK-04 Akzeptanzkriterium 5: der Pool ist je +// Mandant getrennt, es gibt keinen Mandanten-Parameter, der die Antwort +// auf einen anderen Mandanten umlenken koennte). +func (s *Server) handleList(w http.ResponseWriter, r *http.Request, _ string) { + tasks, err := s.store.List(r.Context()) + if err != nil { + writeError(w, http.StatusInternalServerError, "aufgaben konnten nicht geladen werden") + return + } + writeJSON(w, http.StatusOK, tasks) +} + +type setStatusRequest struct { + Status string `json:"status"` +} + +// handleSetStatus ist der Schreibweg fuer TSK-04 Akzeptanzkriterium 4: +// Drag & Drop in der Kanban-Ansicht ruft genau diesen Endpunkt auf, der +// seinerseits ausschliesslich task.Store.SetStatus (TSK-01) aufruft. +func (s *Server) handleSetStatus(w http.ResponseWriter, r *http.Request, _ string) { + id := r.PathValue("id") + var req setStatusRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeError(w, http.StatusBadRequest, "ungueltiger anfrage-koerper") + return + } + err := s.store.SetStatus(r.Context(), id, task.Status(req.Status)) + switch { + case err == nil: + t, getErr := s.store.Get(r.Context(), id) + if getErr != nil { + writeError(w, http.StatusInternalServerError, "status geaendert, lesen fehlgeschlagen") + return + } + writeJSON(w, http.StatusOK, t) + case errors.Is(err, task.ErrNotFound): + writeError(w, http.StatusNotFound, "aufgabe nicht gefunden") + case errors.Is(err, task.ErrInvalidStatus): + writeError(w, http.StatusUnprocessableEntity, "unzulaessiger status") + default: + writeError(w, http.StatusInternalServerError, "status konnte nicht gesetzt werden") + } +} + +func writeJSON(w http.ResponseWriter, status int, v any) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + _ = json.NewEncoder(w).Encode(v) +} + +func writeError(w http.ResponseWriter, status int, message string) { + writeJSON(w, status, map[string]string{"error": message}) +} diff --git a/workflow/internal/taskapi/taskapi_test.go b/workflow/internal/taskapi/taskapi_test.go new file mode 100644 index 00000000..8ceccd3d --- /dev/null +++ b/workflow/internal/taskapi/taskapi_test.go @@ -0,0 +1,228 @@ +package taskapi + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + + "gitea.perlbach24.de/scripte/nexarch/workflow/internal/migrate" + "gitea.perlbach24.de/scripte/nexarch/workflow/internal/task" + "gitea.perlbach24.de/scripte/nexarch/workflow/internal/testdb" +) + +func findModuleRoot(t *testing.T) string { + t.Helper() + dir, err := os.Getwd() + if err != nil { + t.Fatalf("getwd: %v", err) + } + for { + if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { + return dir + } + parent := filepath.Dir(dir) + if parent == dir { + t.Fatal("go.mod nicht gefunden") + } + dir = parent + } +} + +func newTestServer(t *testing.T) (*Server, *task.Store) { + t.Helper() + dsn := os.Getenv("TEST_TENANT_DSN") + if dsn == "" { + t.Skip("TEST_TENANT_DSN nicht gesetzt, Integrationstest uebersprungen") + } + ctx := context.Background() + pool := testdb.Pool(t, dsn) + migrations, err := migrate.Load(filepath.Join(findModuleRoot(t), "migrations", "tenant")) + if err != nil { + t.Fatalf("migrationen laden: %v", err) + } + if _, err := migrate.Up(ctx, pool, migrations); err != nil { + t.Fatalf("migrationen anwenden: %v", err) + } + store := task.NewStore(pool) + return NewServer(store), store +} + +func TestHandleList_RequiresUser(t *testing.T) { + s, _ := newTestServer(t) + mux := http.NewServeMux() + s.RegisterRoutes(mux) + + req := httptest.NewRequest(http.MethodGet, "/tasks", nil) + rec := httptest.NewRecorder() + mux.ServeHTTP(rec, req) + if rec.Code != http.StatusUnauthorized { + t.Fatalf("status = %d, want 401 ohne X-User-Id", rec.Code) + } +} + +// TestHandleList_ReturnsAllTasks prueft die Grundlage der Liste/Kanban- +// Ansicht (TSK-04): GET /tasks liefert alle Aufgaben des gebundenen +// Mandanten-Schemas. +func TestHandleList_ReturnsAllTasks(t *testing.T) { + s, store := newTestServer(t) + mux := http.NewServeMux() + s.RegisterRoutes(mux) + ctx := context.Background() + + if _, err := store.Create(ctx, task.Task{Title: "Rechnung pruefen", AssigneeType: task.AssigneePerson, AssigneeID: "alice"}); err != nil { + t.Fatalf("create: %v", err) + } + + req := httptest.NewRequest(http.MethodGet, "/tasks", nil) + req.Header.Set("X-User-Id", "alice") + rec := httptest.NewRecorder() + mux.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want 200, body=%s", rec.Code, rec.Body.String()) + } + var tasks []task.Task + if err := json.NewDecoder(rec.Body).Decode(&tasks); err != nil { + t.Fatalf("decode: %v", err) + } + if len(tasks) != 1 || tasks[0].Status != task.StatusOpen { + t.Fatalf("tasks = %+v, want genau 1 mit status open", tasks) + } +} + +// TestHandleSetStatus_UpdatesStatus prueft TSK-04 Akzeptanzkriterium 4: +// der HTTP-Endpunkt, ueber den die Kanban-Ansicht beim Drag & Drop den +// Status zurueckschreibt. +func TestHandleSetStatus_UpdatesStatus(t *testing.T) { + s, store := newTestServer(t) + mux := http.NewServeMux() + s.RegisterRoutes(mux) + ctx := context.Background() + + id, err := store.Create(ctx, task.Task{Title: "Postkorb", AssigneeType: task.AssigneePerson, AssigneeID: "bob"}) + if err != nil { + t.Fatalf("create: %v", err) + } + + body := `{"status":"in_progress"}` + req := httptest.NewRequest(http.MethodPost, "/tasks/"+id+"/status", strings.NewReader(body)) + req.Header.Set("X-User-Id", "bob") + rec := httptest.NewRecorder() + mux.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want 200, body=%s", rec.Code, rec.Body.String()) + } + var got task.Task + if err := json.NewDecoder(rec.Body).Decode(&got); err != nil { + t.Fatalf("decode: %v", err) + } + if got.Status != task.StatusInProgress { + t.Fatalf("status = %q, want in_progress", got.Status) + } + + stored, err := store.Get(ctx, id) + if err != nil { + t.Fatalf("get: %v", err) + } + if stored.Status != task.StatusInProgress { + t.Fatalf("status in der datenbank nicht aktualisiert: %q", stored.Status) + } +} + +func TestHandleSetStatus_RejectsInvalidStatus(t *testing.T) { + s, store := newTestServer(t) + mux := http.NewServeMux() + s.RegisterRoutes(mux) + ctx := context.Background() + + id, err := store.Create(ctx, task.Task{Title: "x", AssigneeType: task.AssigneePerson, AssigneeID: "carol"}) + if err != nil { + t.Fatalf("create: %v", err) + } + + body := `{"status":"erledigt"}` + req := httptest.NewRequest(http.MethodPost, "/tasks/"+id+"/status", strings.NewReader(body)) + req.Header.Set("X-User-Id", "carol") + rec := httptest.NewRecorder() + mux.ServeHTTP(rec, req) + if rec.Code != http.StatusUnprocessableEntity { + t.Fatalf("status = %d, want 422", rec.Code) + } +} + +func TestHandleSetStatus_NotFound(t *testing.T) { + s, _ := newTestServer(t) + mux := http.NewServeMux() + s.RegisterRoutes(mux) + + body := `{"status":"done"}` + req := httptest.NewRequest(http.MethodPost, "/tasks/00000000-0000-0000-0000-000000000000/status", strings.NewReader(body)) + req.Header.Set("X-User-Id", "carol") + rec := httptest.NewRecorder() + mux.ServeHTTP(rec, req) + if rec.Code != http.StatusNotFound { + t.Fatalf("status = %d, want 404", rec.Code) + } +} + +// TestTwoTenantServers_ListNeverCrossesTenants ist der zentrale Beleg fuer +// TSK-04 Akzeptanzkriterium 5 / Pruefung 5: zwei taskapi.Server-Instanzen, +// jede an ein eigenes Mandanten-Schema gebunden (wie workflow-app es in +// Produktion je Mandant tut, ueber NEXARCH_WORKFLOW_APP_TENANT_DSN), mit +// Testdaten aus zwei Mandanten. GET /tasks liefert je Server ausschliess- +// lich die Aufgaben seines eigenen Mandanten -- die Trennung ist +// strukturell (ein Pool pro Server), nicht ein Filter, der sich durch +// einen Client-Parameter umgehen liesse. +func TestTwoTenantServers_ListNeverCrossesTenants(t *testing.T) { + serverA, storeA := newTestServer(t) + serverB, storeB := newTestServer(t) + ctx := context.Background() + + if _, err := storeA.Create(ctx, task.Task{Title: "Mandant A Aufgabe", AssigneeType: task.AssigneePerson, AssigneeID: "user-a"}); err != nil { + t.Fatalf("create in tenant A: %v", err) + } + if _, err := storeB.Create(ctx, task.Task{Title: "Mandant B Aufgabe 1", AssigneeType: task.AssigneePerson, AssigneeID: "user-b"}); err != nil { + t.Fatalf("create in tenant B: %v", err) + } + if _, err := storeB.Create(ctx, task.Task{Title: "Mandant B Aufgabe 2", AssigneeType: task.AssigneePerson, AssigneeID: "user-b"}); err != nil { + t.Fatalf("create in tenant B: %v", err) + } + + muxA := http.NewServeMux() + serverA.RegisterRoutes(muxA) + muxB := http.NewServeMux() + serverB.RegisterRoutes(muxB) + + reqA := httptest.NewRequest(http.MethodGet, "/tasks", nil) + reqA.Header.Set("X-User-Id", "user-a") + recA := httptest.NewRecorder() + muxA.ServeHTTP(recA, reqA) + var tasksA []task.Task + if err := json.NewDecoder(recA.Body).Decode(&tasksA); err != nil { + t.Fatalf("decode tenant A: %v", err) + } + if len(tasksA) != 1 || tasksA[0].Title != "Mandant A Aufgabe" { + t.Fatalf("mandant A server sieht fremde aufgaben: %+v", tasksA) + } + + reqB := httptest.NewRequest(http.MethodGet, "/tasks", nil) + reqB.Header.Set("X-User-Id", "user-b") + recB := httptest.NewRecorder() + muxB.ServeHTTP(recB, reqB) + var tasksB []task.Task + if err := json.NewDecoder(recB.Body).Decode(&tasksB); err != nil { + t.Fatalf("decode tenant B: %v", err) + } + if len(tasksB) != 2 { + t.Fatalf("mandant B server sieht falsche anzahl aufgaben: %+v", tasksB) + } + for _, tk := range tasksB { + if !strings.HasPrefix(tk.Title, "Mandant B") { + t.Fatalf("mandant B server sieht fremde aufgabe: %+v", tk) + } + } +} diff --git a/workflow/migrations/tenant/0016_task_status.down.sql b/workflow/migrations/tenant/0016_task_status.down.sql new file mode 100644 index 00000000..63302c76 --- /dev/null +++ b/workflow/migrations/tenant/0016_task_status.down.sql @@ -0,0 +1 @@ +ALTER TABLE tasks DROP COLUMN status; diff --git a/workflow/migrations/tenant/0016_task_status.up.sql b/workflow/migrations/tenant/0016_task_status.up.sql new file mode 100644 index 00000000..c2cd228b --- /dev/null +++ b/workflow/migrations/tenant/0016_task_status.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE tasks ADD COLUMN status TEXT NOT NULL DEFAULT 'open' + CHECK (status IN ('open', 'in_progress', 'done')); -- 2.47.3