From 4ad7873c3363c2370713b06677120d35743ccba1 Mon Sep 17 00:00:00 2001 From: Sami Rusani Date: Tue, 28 Jul 2026 15:05:48 +0200 Subject: [PATCH] Migrate the web app to Next 16, eslint-config-next 16, and TypeScript 6 These three cannot land separately. TypeScript 6 fails under Next 15 because app/layout.tsx reports TS2882 on the side-effect import of globals.css, and it only typechecks once Next 16 is present. Next 16 in turn requires eslint-config-next 16, which ships native flat config, so routing it through FlatCompat now dies inside the eslintrc validator with a circular structure error. eslint.config.mjs imports eslint-config-next/core-web-vitals directly instead. That combination enables react-hooks/set-state-in-effect, which found 13 real violations across five components. All 13 are fixed on their merits using lazy useState initializers or guarded render-time state adjustment. No disable comment, rule downgrade, or lint-glob narrowing was added. Mount-path equivalence was checked per fix, since useEffect runs on mount and a render-time guard does not; in each case the initializer already holds the value the effect would have set. One behaviour difference remains and is deliberate. On the initial live mount the status line now reads "Loading live vNext workspace from the trusted API." for the whole request, where it previously flipped to "Refreshing live vNext workspace...". The spinner, aria-busy, and disabled states are unchanged because isRefreshing initializes from liveModeReady. Reporting a first load as a refresh was the older, weaker behaviour. Next 16 grows first-load JS by about 43.7 KB gzip on every route, which is framework weight rather than application code. The route budgets are raised by a uniform 44,000 bytes so the gate keeps its previous headroom instead of hiding the growth. Measured before and after, in gzip bytes: / 106247 -> 149940, headroom 13753 -> 14060 /continuity 113660 -> 156942, headroom 16340 -> 17058 /vnext 137901 -> 181747, headroom 17099 -> 17253 check-bundle-budget.mjs now reads either the legacy app build manifest or Next 16's .next/diagnostics/route-bundle-stats.json, so it works across both. The tsconfig jsx switch to react-jsx and the .next/dev/types include are both written by Next 16 itself during build, verified by reverting them and rebuilding, so they are kept rather than fought. Closes #300 Closes #239 Closes #214 --- apps/web/app/vnext/page.test.tsx | 66 +++ apps/web/components/approval-detail.test.tsx | 68 +++ apps/web/components/approval-detail.tsx | 16 +- .../calendar-event-ingest-form.test.tsx | 78 +++ .../components/calendar-event-ingest-form.tsx | 109 ++-- .../gmail-message-ingest-form.test.tsx | 80 +++ .../components/gmail-message-ingest-form.tsx | 92 ++-- apps/web/components/vnext-brain-workspace.tsx | 299 ++++++++--- .../components/vnext-operator-auth.test.tsx | 85 ++- .../workflow-memory-writeback-form.tsx | 25 +- apps/web/eslint.config.mjs | 15 +- apps/web/next-env.d.ts | 2 +- apps/web/package.json | 6 +- apps/web/pnpm-lock.yaml | 503 ++++++++++++++---- apps/web/scripts/check-bundle-budget.mjs | 36 +- apps/web/tsconfig.json | 5 +- 16 files changed, 1168 insertions(+), 317 deletions(-) diff --git a/apps/web/app/vnext/page.test.tsx b/apps/web/app/vnext/page.test.tsx index 6b90dd6d..e4f481c0 100644 --- a/apps/web/app/vnext/page.test.tsx +++ b/apps/web/app/vnext/page.test.tsx @@ -235,6 +235,72 @@ describe("VNextPage", () => { expect(screen.getByText("Follow up with Sam about launch owner")).toBeInTheDocument(); }, 60000); // heavy multi-action page test; allow bounded CI contention headroom + it("repopulates source, memory, and connector drafts when their selections change", async () => { + await renderVNextPage(); + + fireEvent.change(screen.getByLabelText("Selected source title"), { + target: { value: "Unsaved source draft" }, + }); + fireEvent.click(screen.getByRole("button", { name: /Vendor legal note/ })); + expect(screen.getByLabelText("Selected source title")).toHaveValue("Vendor legal note"); + expect(screen.getByLabelText("Source domain")).toHaveValue("legal"); + expect(screen.getByLabelText("Source sensitivity")).toHaveValue("internal"); + + fireEvent.click(screen.getByRole("button", { name: /Launch review note/ })); + expect(screen.getByLabelText("Selected source title")).toHaveValue("Launch review note"); + expect(screen.getByLabelText("Source domain")).toHaveValue("project"); + expect(screen.getByLabelText("Source sensitivity")).toHaveValue("private"); + + fireEvent.change(screen.getByLabelText("Edited memory title"), { + target: { value: "Unsaved memory draft" }, + }); + fireEvent.change(screen.getByLabelText("Open-loop title"), { + target: { value: "Unsaved open-loop draft" }, + }); + fireEvent.click( + screen.getByRole("button", { name: /Vendor legal review is waiting for Priya/ }), + ); + expect(screen.getByLabelText("Edited memory title")).toHaveValue( + "Vendor legal review is waiting for Priya.", + ); + expect(screen.getByLabelText("Edited memory text")).toHaveValue( + "Vendor legal review is waiting for Priya.", + ); + expect(screen.getByLabelText("Domain label")).toHaveValue("legal"); + expect(screen.getByLabelText("Sensitivity label")).toHaveValue("internal"); + expect(screen.getByLabelText("Assigned project")).toHaveValue("project-fixture-1"); + expect(screen.getByLabelText("Open-loop title")).toHaveValue( + "Vendor legal review is waiting for Priya.", + ); + + fireEvent.change(screen.getByLabelText("Connector"), { + target: { value: "browser_clipper" }, + }); + expect(screen.getByLabelText("Enabled")).toHaveValue("enabled"); + expect( + screen.getByLabelText("Default domain", { selector: "#vnext-connector-domain" }), + ).toHaveValue("professional"); + fireEvent.change( + screen.getByLabelText("Default domain", { selector: "#vnext-connector-domain" }), + { target: { value: "legal" } }, + ); + fireEvent.change(screen.getByLabelText("Connector"), { + target: { value: "local_folder" }, + }); + expect( + screen.getByLabelText("Default domain", { selector: "#vnext-connector-domain" }), + ).toHaveValue("project"); + fireEvent.change(screen.getByLabelText("Connector"), { + target: { value: "browser_clipper" }, + }); + expect( + screen.getByLabelText("Default domain", { selector: "#vnext-connector-domain" }), + ).toHaveValue("professional"); + expect(screen.getByLabelText("Trusted-client capture secret ref")).toHaveValue( + "browser.capture_token.default", + ); + }, 30000); + it("refreshes Ask Alice output and generates reviewable artifacts", async () => { await renderVNextPage(); diff --git a/apps/web/components/approval-detail.test.tsx b/apps/web/components/approval-detail.test.tsx index dfbd5099..9b72d7ee 100644 --- a/apps/web/components/approval-detail.test.tsx +++ b/apps/web/components/approval-detail.test.tsx @@ -251,4 +251,72 @@ describe("ApprovalDetail", () => { expect(screen.getByRole("button", { name: "Reject" })).toBeDisabled(); expect(screen.getByText("Approve and reject controls are disabled in fixture mode.")).toBeInTheDocument(); }); + + it("adopts refreshed detail objects without over-resetting write-back form state", () => { + const { rerender } = render( + , + ); + + fireEvent.change(screen.getByLabelText("Memory key"), { + target: { value: "user.preference.supplement.magnesium" }, + }); + fireEvent.change(screen.getByLabelText("Memory value (JSON)"), { + target: { value: "not-json" }, + }); + fireEvent.click(screen.getByRole("button", { name: "Submit memory write-back" })); + expect(screen.getByText("Memory value must be valid JSON.")).toBeInTheDocument(); + + const refreshedApproval = { + ...approval, + tool: { ...approval.tool, name: "Updated Merchant Proxy" }, + }; + const refreshedExecution = { + ...execution, + id: "execution-2", + trace_id: "trace-execution-2", + request_event_id: "event-request-2", + result_event_id: "event-result-2", + }; + rerender( + , + ); + + expect(screen.getByRole("heading", { name: "Updated Merchant Proxy" })).toBeInTheDocument(); + expect(screen.getAllByText("event-result-2").length).toBeGreaterThan(0); + expect(screen.getAllByText("event-request-2").length).toBeGreaterThan(0); + expect(screen.getByText("Memory value must be valid JSON.")).toBeInTheDocument(); + expect(screen.getByLabelText("Memory key")).toHaveValue("user.preference.supplement.magnesium"); + expect(screen.getByLabelText("Memory value (JSON)")).toHaveValue("not-json"); + + rerender( + , + ); + + expect( + screen.getByText("Execution evidence is required before memory write-back can be submitted."), + ).toBeInTheDocument(); + expect(screen.getByLabelText("Memory key")).toHaveValue("user.preference.supplement.magnesium"); + expect(screen.getByLabelText("Memory value (JSON)")).toHaveValue("not-json"); + }); }); diff --git a/apps/web/components/approval-detail.tsx b/apps/web/components/approval-detail.tsx index 1c5879cd..b71083fa 100644 --- a/apps/web/components/approval-detail.tsx +++ b/apps/web/components/approval-detail.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import type { ApprovalExecutionResponse, ApprovalItem, ApiSource, ToolExecutionItem } from "../lib/api"; import { EmptyState } from "./empty-state"; @@ -55,12 +55,20 @@ export function ApprovalDetail({ const [approval, setApproval] = useState(initialApproval); const [execution, setExecution] = useState(initialExecution); const [executionPreview, setExecutionPreview] = useState(null); - - useEffect(() => { + const [previousInitialState, setPreviousInitialState] = useState(() => ({ + approval: initialApproval, + execution: initialExecution, + })); + + if ( + initialApproval !== previousInitialState.approval || + initialExecution !== previousInitialState.execution + ) { + setPreviousInitialState({ approval: initialApproval, execution: initialExecution }); setApproval(initialApproval); setExecution(initialExecution); setExecutionPreview(null); - }, [initialApproval, initialExecution]); + } if (!approval) { return ( diff --git a/apps/web/components/calendar-event-ingest-form.test.tsx b/apps/web/components/calendar-event-ingest-form.test.tsx index cb2fcc85..cf460ffc 100644 --- a/apps/web/components/calendar-event-ingest-form.test.tsx +++ b/apps/web/components/calendar-event-ingest-form.test.tsx @@ -154,4 +154,82 @@ describe("CalendarEventIngestForm", () => { expect(screen.getByText("Select one discovered event before submitting ingestion.")).toBeInTheDocument(); expect(ingestCalendarEventMock).not.toHaveBeenCalled(); }); + + it("preserves valid workspace state and resets only prop-derived status when inputs change", async () => { + const secondWorkspace = { + ...baseWorkspaces[0], + id: "workspace-2", + task_id: "task-2", + local_path: "/tmp/task-workspaces/task-2", + }; + const replacementWorkspace = { + ...baseWorkspaces[0], + id: "workspace-3", + task_id: "task-3", + local_path: "/tmp/task-workspaces/task-3", + }; + ingestCalendarEventMock.mockResolvedValue({ + account: baseAccount, + event: { + provider_event_id: "evt-001", + artifact_relative_path: "calendar/acct-owner-001/evt-001.txt", + media_type: "text/plain", + }, + artifact: { + id: "artifact-1", + task_id: "task-1", + task_workspace_id: "workspace-2", + status: "registered", + ingestion_status: "ingested", + relative_path: "calendar/acct-owner-001/evt-001.txt", + media_type_hint: "text/plain", + created_at: "2026-03-18T10:10:00Z", + updated_at: "2026-03-18T10:11:00Z", + }, + summary: { + total_count: 1, + total_characters: 256, + media_type: "text/plain", + chunking_rule: "normalized_utf8_text_fixed_window_1000_chars_v1", + order: ["sequence_no_asc", "id_asc"], + }, + }); + + const renderForm = (account = baseAccount, taskWorkspaces = [baseWorkspaces[0], secondWorkspace]) => ( + + ); + const { rerender } = render(renderForm()); + + fireEvent.change(screen.getByLabelText("Task workspace"), { + target: { value: "workspace-2" }, + }); + fireEvent.click(screen.getByRole("button", { name: "Ingest selected event" })); + await screen.findByText(/Ingestion completed\./i); + + rerender(renderForm({ ...baseAccount }, [{ ...baseWorkspaces[0] }, { ...secondWorkspace }])); + expect(screen.getByLabelText("Task workspace")).toHaveValue("workspace-2"); + expect(screen.getByText("Select one task workspace to ingest the discovered event.")).toBeInTheDocument(); + expect(screen.getAllByText("calendar/acct-owner-001/evt-001.txt").length).toBeGreaterThan(0); + + rerender(renderForm({ ...baseAccount }, [replacementWorkspace])); + expect(screen.getByLabelText("Task workspace")).toHaveValue("workspace-3"); + fireEvent.click(screen.getByRole("button", { name: "Ingest selected event" })); + await waitFor(() => { + expect(ingestCalendarEventMock).toHaveBeenLastCalledWith( + "https://api.example.com", + "calendar-account-1", + "evt-001", + { user_id: "user-1", task_workspace_id: "workspace-3" }, + ); + }); + }); }); diff --git a/apps/web/components/calendar-event-ingest-form.tsx b/apps/web/components/calendar-event-ingest-form.tsx index 42d5fcc9..ca4cee54 100644 --- a/apps/web/components/calendar-event-ingest-form.tsx +++ b/apps/web/components/calendar-event-ingest-form.tsx @@ -1,7 +1,7 @@ "use client"; import type { FormEvent } from "react"; -import { useEffect, useMemo, useState } from "react"; +import { useMemo, useState } from "react"; import { useRouter } from "next/navigation"; @@ -28,6 +28,27 @@ type CalendarEventIngestFormProps = { userId?: string; }; +function defaultStatusText(options: { + hasAccount: boolean; + hasTaskWorkspace: boolean; + hasSelectedEvent: boolean; + liveModeReady: boolean; +}) { + if (!options.hasAccount) { + return "Select a Calendar account to enable single-event ingestion."; + } + if (!options.hasTaskWorkspace) { + return "No task workspace is available for ingestion target selection."; + } + if (!options.hasSelectedEvent) { + return "Select one discovered event before submitting ingestion."; + } + if (options.liveModeReady) { + return "Select one task workspace to ingest the discovered event."; + } + return "Event ingestion is unavailable until live API configuration, live account detail, and live task workspace list are present."; +} + export function CalendarEventIngestForm({ account, accountSource, @@ -61,55 +82,51 @@ export function CalendarEventIngestForm({ [account, accountSource, apiBaseUrl, userId, taskWorkspaceSource, taskWorkspaces.length], ); - const [statusText, setStatusText] = useState( - !account - ? "Select a Calendar account to enable single-event ingestion." - : taskWorkspaces.length === 0 - ? "No task workspace is available for ingestion target selection." - : !hasSelectedEvent - ? "Select one discovered event before submitting ingestion." - : liveModeReady - ? "Select one task workspace to ingest the discovered event." - : "Event ingestion is unavailable until live API configuration, live account detail, and live task workspace list are present.", + const [statusText, setStatusText] = useState(() => + defaultStatusText({ + hasAccount: Boolean(account), + hasTaskWorkspace: taskWorkspaces.length > 0, + hasSelectedEvent, + liveModeReady, + }), ); + const [previousStatusInputs, setPreviousStatusInputs] = useState(() => ({ + account, + hasSelectedEvent, + liveModeReady, + taskWorkspaceCount: taskWorkspaces.length, + })); - useEffect(() => { - const hasWorkspace = taskWorkspaces.some((workspace) => workspace.id === taskWorkspaceId); - if (!hasWorkspace) { - setTaskWorkspaceId(taskWorkspaces[0]?.id ?? ""); - } - }, [taskWorkspaceId, taskWorkspaces]); - - useEffect(() => { - if (!account) { - setStatusTone("info"); - setStatusText("Select a Calendar account to enable single-event ingestion."); - return; - } - - if (taskWorkspaces.length === 0) { - setStatusTone("info"); - setStatusText("No task workspace is available for ingestion target selection."); - return; - } - - if (!hasSelectedEvent) { - setStatusTone("info"); - setStatusText("Select one discovered event before submitting ingestion."); - return; - } - - if (!liveModeReady) { - setStatusTone("info"); - setStatusText( - "Event ingestion is unavailable until live API configuration, live account detail, and live task workspace list are present.", - ); - return; - } + const fallbackTaskWorkspaceId = taskWorkspaces[0]?.id ?? ""; + if ( + !taskWorkspaces.some((workspace) => workspace.id === taskWorkspaceId) && + taskWorkspaceId !== fallbackTaskWorkspaceId + ) { + setTaskWorkspaceId(fallbackTaskWorkspaceId); + } + if ( + account !== previousStatusInputs.account || + hasSelectedEvent !== previousStatusInputs.hasSelectedEvent || + liveModeReady !== previousStatusInputs.liveModeReady || + taskWorkspaces.length !== previousStatusInputs.taskWorkspaceCount + ) { + setPreviousStatusInputs({ + account, + hasSelectedEvent, + liveModeReady, + taskWorkspaceCount: taskWorkspaces.length, + }); setStatusTone("info"); - setStatusText("Select one task workspace to ingest the discovered event."); - }, [account, hasSelectedEvent, liveModeReady, taskWorkspaces.length]); + setStatusText( + defaultStatusText({ + hasAccount: Boolean(account), + hasTaskWorkspace: taskWorkspaces.length > 0, + hasSelectedEvent, + liveModeReady, + }), + ); + } async function handleSubmit(event: FormEvent) { event.preventDefault(); diff --git a/apps/web/components/gmail-message-ingest-form.test.tsx b/apps/web/components/gmail-message-ingest-form.test.tsx index 6f9378d8..67806bd8 100644 --- a/apps/web/components/gmail-message-ingest-form.test.tsx +++ b/apps/web/components/gmail-message-ingest-form.test.tsx @@ -134,4 +134,84 @@ describe("GmailMessageIngestForm", () => { ).toBeInTheDocument(); expect(ingestGmailMessageMock).not.toHaveBeenCalled(); }); + + it("preserves typed input and result while reconciling workspace and status props", async () => { + const secondWorkspace = { + ...baseWorkspaces[0], + id: "workspace-2", + task_id: "task-2", + local_path: "/tmp/task-workspaces/task-2", + }; + const replacementWorkspace = { + ...baseWorkspaces[0], + id: "workspace-3", + task_id: "task-3", + local_path: "/tmp/task-workspaces/task-3", + }; + ingestGmailMessageMock.mockResolvedValue({ + account: baseAccount, + message: { + provider_message_id: "msg-001", + artifact_relative_path: "gmail/acct-owner-001/msg-001.eml", + media_type: "message/rfc822", + }, + artifact: { + id: "artifact-1", + task_id: "task-1", + task_workspace_id: "workspace-2", + status: "registered", + ingestion_status: "ingested", + relative_path: "gmail/acct-owner-001/msg-001.eml", + media_type_hint: "message/rfc822", + created_at: "2026-03-18T10:10:00Z", + updated_at: "2026-03-18T10:11:00Z", + }, + summary: { + total_count: 1, + total_characters: 128, + media_type: "message/rfc822", + chunking_rule: "normalized_utf8_text_fixed_window_1000_chars_v1", + order: ["sequence_no_asc", "id_asc"], + }, + }); + + const renderForm = (account = baseAccount, taskWorkspaces = [baseWorkspaces[0], secondWorkspace]) => ( + + ); + const { rerender } = render(renderForm()); + + fireEvent.change(screen.getByLabelText("Provider message ID"), { + target: { value: "msg-001" }, + }); + fireEvent.change(screen.getByLabelText("Task workspace"), { + target: { value: "workspace-2" }, + }); + fireEvent.click(screen.getByRole("button", { name: "Ingest selected message" })); + await screen.findByText(/Ingestion completed\./i); + + rerender(renderForm({ ...baseAccount }, [{ ...baseWorkspaces[0] }, { ...secondWorkspace }])); + expect(screen.getByLabelText("Provider message ID")).toHaveValue("msg-001"); + expect(screen.getByLabelText("Task workspace")).toHaveValue("workspace-2"); + expect(screen.getByText("Enter one provider message ID and select one task workspace.")).toBeInTheDocument(); + expect(screen.getAllByText("gmail/acct-owner-001/msg-001.eml").length).toBeGreaterThan(0); + + rerender(renderForm({ ...baseAccount }, [replacementWorkspace])); + expect(screen.getByLabelText("Task workspace")).toHaveValue("workspace-3"); + fireEvent.click(screen.getByRole("button", { name: "Ingest selected message" })); + await waitFor(() => { + expect(ingestGmailMessageMock).toHaveBeenLastCalledWith( + "https://api.example.com", + "gmail-account-1", + "msg-001", + { user_id: "user-1", task_workspace_id: "workspace-3" }, + ); + }); + }); }); diff --git a/apps/web/components/gmail-message-ingest-form.tsx b/apps/web/components/gmail-message-ingest-form.tsx index cb5670ec..d9b280d0 100644 --- a/apps/web/components/gmail-message-ingest-form.tsx +++ b/apps/web/components/gmail-message-ingest-form.tsx @@ -1,7 +1,7 @@ "use client"; import type { FormEvent } from "react"; -import { useEffect, useMemo, useState } from "react"; +import { useMemo, useState } from "react"; import { useRouter } from "next/navigation"; @@ -26,6 +26,23 @@ type GmailMessageIngestFormProps = { userId?: string; }; +function defaultStatusText(options: { + hasAccount: boolean; + hasTaskWorkspace: boolean; + liveModeReady: boolean; +}) { + if (!options.hasAccount) { + return "Select a Gmail account to enable single-message ingestion."; + } + if (!options.hasTaskWorkspace) { + return "No task workspace is available for ingestion target selection."; + } + if (options.liveModeReady) { + return "Enter one provider message ID and select one task workspace."; + } + return "Message ingestion is unavailable until live API configuration, live account detail, and live task workspace list are present."; +} + export function GmailMessageIngestForm({ account, accountSource, @@ -57,47 +74,46 @@ export function GmailMessageIngestForm({ [account, accountSource, apiBaseUrl, userId, taskWorkspaceSource, taskWorkspaces.length], ); - const [statusText, setStatusText] = useState( - !account - ? "Select a Gmail account to enable single-message ingestion." - : taskWorkspaces.length === 0 - ? "No task workspace is available for ingestion target selection." - : liveModeReady - ? "Enter one provider message ID and select one task workspace." - : "Message ingestion is unavailable until live API configuration, live account detail, and live task workspace list are present.", + const [statusText, setStatusText] = useState(() => + defaultStatusText({ + hasAccount: Boolean(account), + hasTaskWorkspace: taskWorkspaces.length > 0, + liveModeReady, + }), ); + const [previousStatusInputs, setPreviousStatusInputs] = useState(() => ({ + account, + liveModeReady, + taskWorkspaceCount: taskWorkspaces.length, + })); - useEffect(() => { - const hasWorkspace = taskWorkspaces.some((workspace) => workspace.id === taskWorkspaceId); - if (!hasWorkspace) { - setTaskWorkspaceId(taskWorkspaces[0]?.id ?? ""); - } - }, [taskWorkspaceId, taskWorkspaces]); - - useEffect(() => { - if (!account) { - setStatusTone("info"); - setStatusText("Select a Gmail account to enable single-message ingestion."); - return; - } - - if (taskWorkspaces.length === 0) { - setStatusTone("info"); - setStatusText("No task workspace is available for ingestion target selection."); - return; - } - - if (!liveModeReady) { - setStatusTone("info"); - setStatusText( - "Message ingestion is unavailable until live API configuration, live account detail, and live task workspace list are present.", - ); - return; - } + const fallbackTaskWorkspaceId = taskWorkspaces[0]?.id ?? ""; + if ( + !taskWorkspaces.some((workspace) => workspace.id === taskWorkspaceId) && + taskWorkspaceId !== fallbackTaskWorkspaceId + ) { + setTaskWorkspaceId(fallbackTaskWorkspaceId); + } + if ( + account !== previousStatusInputs.account || + liveModeReady !== previousStatusInputs.liveModeReady || + taskWorkspaces.length !== previousStatusInputs.taskWorkspaceCount + ) { + setPreviousStatusInputs({ + account, + liveModeReady, + taskWorkspaceCount: taskWorkspaces.length, + }); setStatusTone("info"); - setStatusText("Enter one provider message ID and select one task workspace."); - }, [account, liveModeReady, taskWorkspaces.length]); + setStatusText( + defaultStatusText({ + hasAccount: Boolean(account), + hasTaskWorkspace: taskWorkspaces.length > 0, + liveModeReady, + }), + ); + } async function handleSubmit(event: FormEvent) { event.preventDefault(); diff --git a/apps/web/components/vnext-brain-workspace.tsx b/apps/web/components/vnext-brain-workspace.tsx index 8a73ffb7..992e39b3 100644 --- a/apps/web/components/vnext-brain-workspace.tsx +++ b/apps/web/components/vnext-brain-workspace.tsx @@ -131,6 +131,11 @@ export function VNextBrainWorkspace({ ]); const [operatorAgentApiKey, setOperatorAgentApiKey] = useState(""); const [operatorAgentApiKeyActive, setOperatorAgentApiKeyActive] = useState(false); + const [previousLiveRefreshInputs, setPreviousLiveRefreshInputs] = useState(() => ({ + apiBaseUrl, + liveModeReady, + userId, + })); const [captureTitle, setCaptureTitle] = useState("Launch note"); const [captureText, setCaptureText] = useState( @@ -138,7 +143,8 @@ export function VNextBrainWorkspace({ ); const [defaultDomain, setDefaultDomain] = useState("project"); const [defaultSensitivity, setDefaultSensitivity] = useState("private"); - const [selectedSourceId, setSelectedSourceId] = useState(""); + const initialSelectedSource = workspace.sources[0] ?? null; + const [selectedSourceId, setSelectedSourceId] = useState(initialSelectedSource?.id ?? ""); const selectedSource = useMemo( () => workspace.sources.find((source) => source.id === selectedSourceId) ?? workspace.sources[0] ?? null, [selectedSourceId, workspace.sources], @@ -150,23 +156,56 @@ export function VNextBrainWorkspace({ : null, [selectedSource, workspace.traceability.items], ); - const [sourceTitleDraft, setSourceTitleDraft] = useState(""); - const [sourceDomainDraft, setSourceDomainDraft] = useState("project"); - const [sourceSensitivityDraft, setSourceSensitivityDraft] = useState("private"); - const [sourceProjectDraft, setSourceProjectDraft] = useState(""); + const initialSelectedSourceMetadata = asRecord(initialSelectedSource?.metadata_json); + const [sourceTitleDraft, setSourceTitleDraft] = useState( + initialSelectedSource + ? textValue(initialSelectedSource.title) || initialSelectedSource.source_type + : "", + ); + const [sourceDomainDraft, setSourceDomainDraft] = useState( + initialSelectedSource ? asDomain(initialSelectedSource.domain) : "project", + ); + const [sourceSensitivityDraft, setSourceSensitivityDraft] = useState( + initialSelectedSource ? asSensitivity(initialSelectedSource.sensitivity) : "private", + ); + const [sourceProjectDraft, setSourceProjectDraft] = useState( + textValue(initialSelectedSourceMetadata.project_id) || workspace.projects[0]?.id || "", + ); const [sourceReviewNote, setSourceReviewNote] = useState("Reviewed from /vnext operator console."); + const [previousSourceDraftInputs, setPreviousSourceDraftInputs] = useState(() => ({ + selectedSource, + projects: workspace.projects, + })); - const [selectedReviewId, setSelectedReviewId] = useState(""); + const initialSelectedReview = workspace.reviewItems[0] ?? null; + const [selectedReviewId, setSelectedReviewId] = useState(initialSelectedReview?.id ?? ""); const selectedReview = useMemo( () => workspace.reviewItems.find((item) => item.id === selectedReviewId) ?? workspace.reviewItems[0] ?? null, [selectedReviewId, workspace.reviewItems], ); - const [draftTitle, setDraftTitle] = useState(""); - const [draftText, setDraftText] = useState(""); - const [draftDomain, setDraftDomain] = useState("project"); - const [draftSensitivity, setDraftSensitivity] = useState("private"); - const [selectedProjectId, setSelectedProjectId] = useState(""); + const initialSelectedReviewMetadata = asRecord(initialSelectedReview?.metadata_json); + const [draftTitle, setDraftTitle] = useState( + initialSelectedReview + ? textValue(initialSelectedReview.title) || memoryText(initialSelectedReview) + : "", + ); + const [draftText, setDraftText] = useState( + initialSelectedReview ? memoryText(initialSelectedReview) : "", + ); + const [draftDomain, setDraftDomain] = useState( + initialSelectedReview ? asDomain(initialSelectedReview.domain) : "project", + ); + const [draftSensitivity, setDraftSensitivity] = useState( + initialSelectedReview ? asSensitivity(initialSelectedReview.sensitivity) : "private", + ); + const [selectedProjectId, setSelectedProjectId] = useState( + textValue(initialSelectedReviewMetadata.project_id) || workspace.projects[0]?.id || "", + ); const [inlineConfirmationDrafts, setInlineConfirmationDrafts] = useState>({}); + const [previousReviewDraftInputs, setPreviousReviewDraftInputs] = useState(() => ({ + selectedReview, + projects: workspace.projects, + })); const [question, setQuestion] = useState("What should I focus on before the product review?"); const [answer, setAnswer] = useState({ @@ -181,17 +220,25 @@ export function VNextBrainWorkspace({ sensitivity: "private", }); - const [openLoopTitle, setOpenLoopTitle] = useState("Confirm launch checklist owner"); + const [openLoopTitle, setOpenLoopTitle] = useState( + initialSelectedReview ? memoryText(initialSelectedReview) : "Confirm launch checklist owner", + ); const [openLoopDescription, setOpenLoopDescription] = useState("Created from the selected memory candidate."); const [openLoopDueAt, setOpenLoopDueAt] = useState(""); const [openLoopPriority, setOpenLoopPriority] = useState("normal"); - const [selectedOpenLoopId, setSelectedOpenLoopId] = useState(""); + const [selectedOpenLoopId, setSelectedOpenLoopId] = useState(workspace.openLoops[0]?.id ?? ""); const [newProjectName, setNewProjectName] = useState("Product launch"); const [newProjectState, setNewProjectState] = useState("Launch ownership is unresolved."); - const [charterText, setCharterText] = useState("# ALICE.md\n\nKeep generated artifacts reviewable before promotion."); - const [charterSensitivity, setCharterSensitivity] = useState("private"); + const [charterText, setCharterText] = useState( + workspace.brainCharter?.content_markdown ?? + "# ALICE.md\n\nKeep generated artifacts reviewable before promotion.", + ); + const [charterSensitivity, setCharterSensitivity] = useState( + workspace.brainCharter ? asSensitivity(workspace.brainCharter.sensitivity) : "private", + ); + const [previousBrainCharter, setPreviousBrainCharter] = useState(workspace.brainCharter); const [schedulerDrafts, setSchedulerDrafts] = useState< Record >({}); @@ -208,10 +255,30 @@ export function VNextBrainWorkspace({ const [qualityVerbosity, setQualityVerbosity] = useState("right_sized"); const [qualityComments, setQualityComments] = useState(""); const [selectedConnectorId, setSelectedConnectorId] = useState("telegram"); - const [connectorEnabled, setConnectorEnabled] = useState(true); - const [connectorDomain, setConnectorDomain] = useState("personal"); - const [connectorSensitivity, setConnectorSensitivity] = useState("private"); - const [connectorSecretRef, setConnectorSecretRef] = useState(""); + const initialSelectedConnector = + INITIAL_CONNECTORS.find((connector) => connector.id === "telegram") ?? INITIAL_CONNECTORS[0]; + const initialSelectedConnectorHealth = + workspace.connectorHealth.items.find( + (item) => item.connector_name === initialSelectedConnector.id, + ) ?? null; + const [connectorEnabled, setConnectorEnabled] = useState( + Boolean(initialSelectedConnectorHealth?.enabled ?? false), + ); + const [connectorDomain, setConnectorDomain] = useState( + asDomain(initialSelectedConnectorHealth?.default_domain ?? initialSelectedConnector.defaultDomain), + ); + const [connectorSensitivity, setConnectorSensitivity] = useState( + asSensitivity( + initialSelectedConnectorHealth?.default_sensitivity ?? initialSelectedConnector.defaultSensitivity, + ), + ); + const [connectorSecretRef, setConnectorSecretRef] = useState( + initialSelectedConnector.id === "browser_clipper" ? "browser.capture_token.default" : "", + ); + const [previousConnectorDraftInputs, setPreviousConnectorDraftInputs] = useState(() => ({ + connectorHealth: workspace.connectorHealth, + selectedConnectorId, + })); const [telegramAllowedChats, setTelegramAllowedChats] = useState("999001"); const [localFolderPath, setLocalFolderPath] = useState("~/Notes"); const [localFolderExtensions, setLocalFolderExtensions] = useState(".md,.txt"); @@ -220,52 +287,32 @@ export function VNextBrainWorkspace({ const [browserClipSelection, setBrowserClipSelection] = useState("Fact: Browser clipper test content remains untrusted."); const [browserClipperPreparedOrigin, setBrowserClipperPreparedOrigin] = useState(""); const [browserClipperExpiresAt, setBrowserClipperExpiresAt] = useState(""); - - const dailyArtifact = latestArtifact(workspace.artifacts, "daily_brief"); - const weeklyArtifact = latestArtifact(workspace.artifacts, "weekly_synthesis"); - const projectUpdateArtifacts = workspace.artifacts.filter((artifact) => artifact.artifact_type === "project_update"); - - const refreshWorkspace = useCallback( - async (successMessage?: string) => { - if (!liveModeReady || !apiBaseUrl || !userId) { - return; - } + const [previousBrowserClipperInputs, setPreviousBrowserClipperInputs] = useState(() => ({ + apiBaseUrl, + browserClipUrl, + connectorDomain, + connectorSensitivity, + userId, + })); + + if ( + apiBaseUrl !== previousLiveRefreshInputs.apiBaseUrl || + liveModeReady !== previousLiveRefreshInputs.liveModeReady || + userId !== previousLiveRefreshInputs.userId + ) { + setPreviousLiveRefreshInputs({ apiBaseUrl, liveModeReady, userId }); + if (liveModeReady) { setIsRefreshing(true); setStatusTone("info"); setStatusText("Refreshing live vNext workspace..."); - try { - const payload = await getVNextWorkspace(apiBaseUrl, userId); - const nextWorkspace = workspaceFromPayload(payload); - setWorkspace(nextWorkspace); - setDataSource("live"); - setStatusTone("success"); - setStatusText(successMessage ?? "Live vNext workspace loaded."); - setActionLog((previous) => pushBoundedLog(successMessage ?? "Live workspace refreshed.", previous)); - } catch (error) { - setStatusTone("danger"); - setStatusText(`Unable to load live workspace: ${error instanceof Error ? error.message : "Request failed"}`); - setDataSource("live"); - } finally { - setIsRefreshing(false); - } - }, - [apiBaseUrl, liveModeReady, userId], - ); - - useEffect( - () => () => { - clearVNextOperatorAgentApiKey(); - }, - [], - ); - - useEffect(() => { - if (liveModeReady) { - void refreshWorkspace("Live vNext workspace loaded."); } - }, [liveModeReady, refreshWorkspace]); + } - useEffect(() => { + if ( + selectedSource !== previousSourceDraftInputs.selectedSource || + workspace.projects !== previousSourceDraftInputs.projects + ) { + setPreviousSourceDraftInputs({ selectedSource, projects: workspace.projects }); if (selectedSource) { const metadata = asRecord(selectedSource.metadata_json); setSelectedSourceId(selectedSource.id); @@ -278,9 +325,13 @@ export function VNextBrainWorkspace({ setSourceTitleDraft(""); setSourceProjectDraft(workspace.projects[0]?.id ?? ""); } - }, [selectedSource, workspace.projects]); + } - useEffect(() => { + if ( + selectedReview !== previousReviewDraftInputs.selectedReview || + workspace.projects !== previousReviewDraftInputs.projects + ) { + setPreviousReviewDraftInputs({ selectedReview, projects: workspace.projects }); if (selectedReview) { setSelectedReviewId(selectedReview.id); setDraftTitle(textValue(selectedReview.title) || memoryText(selectedReview)); @@ -296,38 +347,122 @@ export function VNextBrainWorkspace({ setDraftText(""); setSelectedProjectId(workspace.projects[0]?.id ?? ""); } - }, [selectedReview, workspace.projects]); + } - useEffect(() => { - if (workspace.openLoops.length > 0 && !workspace.openLoops.some((loop) => loop.id === selectedOpenLoopId)) { - setSelectedOpenLoopId(workspace.openLoops[0].id); - } - }, [selectedOpenLoopId, workspace.openLoops]); + if ( + workspace.openLoops.length > 0 && + !workspace.openLoops.some((loop) => loop.id === selectedOpenLoopId) + ) { + setSelectedOpenLoopId(workspace.openLoops[0].id); + } - useEffect(() => { + if (workspace.brainCharter !== previousBrainCharter) { + setPreviousBrainCharter(workspace.brainCharter); if (workspace.brainCharter) { setCharterText(workspace.brainCharter.content_markdown); setCharterSensitivity(asSensitivity(workspace.brainCharter.sensitivity)); } - }, [workspace.brainCharter]); + } - useEffect(() => { - const connector = INITIAL_CONNECTORS.find((item) => item.id === selectedConnectorId) ?? INITIAL_CONNECTORS[0]; - const health = workspace.connectorHealth.items.find((item) => item.connector_name === connector.id) ?? null; + if ( + selectedConnectorId !== previousConnectorDraftInputs.selectedConnectorId || + workspace.connectorHealth !== previousConnectorDraftInputs.connectorHealth + ) { + setPreviousConnectorDraftInputs({ + connectorHealth: workspace.connectorHealth, + selectedConnectorId, + }); + const connector = + INITIAL_CONNECTORS.find((item) => item.id === selectedConnectorId) ?? INITIAL_CONNECTORS[0]; + const health = + workspace.connectorHealth.items.find((item) => item.connector_name === connector.id) ?? null; setConnectorEnabled(Boolean(health?.enabled ?? false)); setConnectorDomain(asDomain(health?.default_domain ?? connector.defaultDomain)); - setConnectorSensitivity(asSensitivity(health?.default_sensitivity ?? connector.defaultSensitivity)); - if (connector.id === "browser_clipper") { - setConnectorSecretRef("browser.capture_token.default"); - } else { - setConnectorSecretRef(""); - } - }, [selectedConnectorId, workspace.connectorHealth]); + setConnectorSensitivity( + asSensitivity(health?.default_sensitivity ?? connector.defaultSensitivity), + ); + setConnectorSecretRef( + connector.id === "browser_clipper" ? "browser.capture_token.default" : "", + ); + } - useEffect(() => { + if ( + apiBaseUrl !== previousBrowserClipperInputs.apiBaseUrl || + browserClipUrl !== previousBrowserClipperInputs.browserClipUrl || + connectorDomain !== previousBrowserClipperInputs.connectorDomain || + connectorSensitivity !== previousBrowserClipperInputs.connectorSensitivity || + userId !== previousBrowserClipperInputs.userId + ) { + setPreviousBrowserClipperInputs({ + apiBaseUrl, + browserClipUrl, + connectorDomain, + connectorSensitivity, + userId, + }); setBrowserClipperPreparedOrigin(""); setBrowserClipperExpiresAt(""); - }, [apiBaseUrl, browserClipUrl, connectorDomain, connectorSensitivity, userId]); + } + + const dailyArtifact = latestArtifact(workspace.artifacts, "daily_brief"); + const weeklyArtifact = latestArtifact(workspace.artifacts, "weekly_synthesis"); + const projectUpdateArtifacts = workspace.artifacts.filter((artifact) => artifact.artifact_type === "project_update"); + + const loadWorkspace = useCallback( + (successMessage?: string) => { + if (!liveModeReady || !apiBaseUrl || !userId) { + return Promise.resolve(); + } + return getVNextWorkspace(apiBaseUrl, userId) + .then((payload) => { + const nextWorkspace = workspaceFromPayload(payload); + setWorkspace(nextWorkspace); + setDataSource("live"); + setStatusTone("success"); + setStatusText(successMessage ?? "Live vNext workspace loaded."); + setActionLog((previous) => + pushBoundedLog(successMessage ?? "Live workspace refreshed.", previous), + ); + }) + .catch((error: unknown) => { + setStatusTone("danger"); + setStatusText( + `Unable to load live workspace: ${error instanceof Error ? error.message : "Request failed"}`, + ); + setDataSource("live"); + }) + .finally(() => { + setIsRefreshing(false); + }); + }, + [apiBaseUrl, liveModeReady, userId], + ); + + const refreshWorkspace = useCallback( + async (successMessage?: string) => { + if (!liveModeReady || !apiBaseUrl || !userId) { + return; + } + setIsRefreshing(true); + setStatusTone("info"); + setStatusText("Refreshing live vNext workspace..."); + await loadWorkspace(successMessage); + }, + [apiBaseUrl, liveModeReady, loadWorkspace, userId], + ); + + useEffect( + () => () => { + clearVNextOperatorAgentApiKey(); + }, + [], + ); + + useEffect(() => { + if (liveModeReady) { + void loadWorkspace("Live vNext workspace loaded."); + } + }, [liveModeReady, loadWorkspace]); function handleOperatorAgentApiKeyChange(value: string) { clearVNextOperatorAgentApiKey(); diff --git a/apps/web/components/vnext-operator-auth.test.tsx b/apps/web/components/vnext-operator-auth.test.tsx index 80720f42..ec16c4d8 100644 --- a/apps/web/components/vnext-operator-auth.test.tsx +++ b/apps/web/components/vnext-operator-auth.test.tsx @@ -64,6 +64,49 @@ describe("VNextBrainWorkspace operator authentication", () => { vi.unstubAllEnvs(); }); + it.each([ + { + label: "success", + response: new Response(JSON.stringify(EMPTY_LIVE_WORKSPACE), { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + expectedStatus: "Live vNext workspace loaded.", + }, + { + label: "failure", + response: new Response(JSON.stringify({ detail: "Workspace offline" }), { + status: 503, + headers: { "Content-Type": "application/json" }, + }), + expectedStatus: "Unable to load live workspace: Workspace offline", + }, + ])("starts one immediate live request and reaches the $label state", async ({ response, expectedStatus }) => { + let resolveWorkspaceRequest: ((value: Response) => void) | undefined; + fetchMock.mockImplementation( + () => + new Promise((resolve) => { + resolveWorkspaceRequest = resolve; + }), + ); + + render( + , + ); + + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(screen.getByText("Loading live vNext workspace from the trusted API.")).toBeInTheDocument(); + expect(screen.queryByText("Refreshing live vNext workspace...")).not.toBeInTheDocument(); + + resolveWorkspaceRequest?.(response); + expect((await screen.findAllByText(expectedStatus)).length).toBeGreaterThan(0); + expect(fetchMock).toHaveBeenCalledTimes(1); + }); + it("loads the remote same-origin page with the entered key without forwarding it to an evil origin", async () => { const agentApiKey = "alice_sk_remote_operator_secret"; vi.stubEnv("NEXT_PUBLIC_ALICEBOT_API_BASE_URL", "https://alice.example.com"); @@ -230,6 +273,46 @@ describe("VNextBrainWorkspace operator authentication", () => { expect(window.sessionStorage.length).toBe(0); expect(window.location.href).not.toContain(capability); expect(document.documentElement.outerHTML).not.toContain(capability); - expect(screen.getByText(/Prepared for https:\/\/example.com/)).toBeInTheDocument(); + const preparedPattern = /Prepared for https:\/\/example.com/; + expect(screen.getByText(preparedPattern)).toBeInTheDocument(); + + fireEvent.change(screen.getByLabelText("Selected text"), { + target: { value: "Fact: Editing clip content does not invalidate the bound capability." }, + }); + expect(screen.getByText(preparedPattern)).toBeInTheDocument(); + + fireEvent.change(screen.getByLabelText("Page URL"), { + target: { value: "https://example.com/changed" }, + }); + expect(screen.queryByText(preparedPattern)).not.toBeInTheDocument(); + + fireEvent.change(screen.getByLabelText("Page URL"), { + target: { value: "https://example.com/article" }, + }); + fireEvent.click(screen.getByRole("button", { name: "Issue and copy one-time bookmarklet" })); + await waitFor(() => expect(writeText).toHaveBeenCalledTimes(2)); + expect(screen.getByText(preparedPattern)).toBeInTheDocument(); + + fireEvent.change( + screen.getByLabelText("Default domain", { selector: "#vnext-connector-domain" }), + { target: { value: "personal" } }, + ); + expect(screen.queryByText(preparedPattern)).not.toBeInTheDocument(); + + fireEvent.change( + screen.getByLabelText("Default domain", { selector: "#vnext-connector-domain" }), + { target: { value: "professional" } }, + ); + fireEvent.click(screen.getByRole("button", { name: "Issue and copy one-time bookmarklet" })); + await waitFor(() => expect(writeText).toHaveBeenCalledTimes(3)); + expect(screen.getByText(preparedPattern)).toBeInTheDocument(); + + fireEvent.change( + screen.getByLabelText("Default sensitivity", { + selector: "#vnext-connector-sensitivity", + }), + { target: { value: "confidential" } }, + ); + expect(screen.queryByText(preparedPattern)).not.toBeInTheDocument(); }); }); diff --git a/apps/web/components/workflow-memory-writeback-form.tsx b/apps/web/components/workflow-memory-writeback-form.tsx index 1ccc6f7b..ea63d081 100644 --- a/apps/web/components/workflow-memory-writeback-form.tsx +++ b/apps/web/components/workflow-memory-writeback-form.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useMemo, useState, type FormEvent } from "react"; +import { useMemo, useState, type FormEvent } from "react"; import { useRouter } from "next/navigation"; @@ -93,8 +93,25 @@ export function WorkflowMemoryWritebackForm({ hasPreview: Boolean(preview), }), ); - - useEffect(() => { + const [previousStatusInputs, setPreviousStatusInputs] = useState(() => ({ + evidenceEventCount: evidenceEventIds.length, + liveModeReady, + preview, + source, + })); + + if ( + evidenceEventIds.length !== previousStatusInputs.evidenceEventCount || + liveModeReady !== previousStatusInputs.liveModeReady || + preview !== previousStatusInputs.preview || + source !== previousStatusInputs.source + ) { + setPreviousStatusInputs({ + evidenceEventCount: evidenceEventIds.length, + liveModeReady, + preview, + source, + }); setStatusTone("info"); setStatusText( defaultStatusText({ @@ -104,7 +121,7 @@ export function WorkflowMemoryWritebackForm({ hasPreview: Boolean(preview), }), ); - }, [evidenceEventIds.length, liveModeReady, preview, source]); + } async function handleSubmit(event: FormEvent) { event.preventDefault(); diff --git a/apps/web/eslint.config.mjs b/apps/web/eslint.config.mjs index ec5cc8a5..e9c1cef7 100644 --- a/apps/web/eslint.config.mjs +++ b/apps/web/eslint.config.mjs @@ -1,21 +1,10 @@ -import js from "@eslint/js"; -import { FlatCompat } from "@eslint/eslintrc"; -import { dirname } from "node:path"; -import { fileURLToPath } from "node:url"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -const compat = new FlatCompat({ - baseDirectory: __dirname, - recommendedConfig: js.configs.recommended, -}); +import coreWebVitals from "eslint-config-next/core-web-vitals"; const eslintConfig = [ { ignores: [".next/**", "node_modules/**", "next-env.d.ts"], }, - ...compat.extends("next/core-web-vitals"), + ...coreWebVitals, ]; export default eslintConfig; diff --git a/apps/web/next-env.d.ts b/apps/web/next-env.d.ts index 830fb594..9edff1c7 100644 --- a/apps/web/next-env.d.ts +++ b/apps/web/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -/// +import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/web/package.json b/apps/web/package.json index c8f0ca32..9ede2dd4 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -26,7 +26,7 @@ "test:mvp:validation-matrix": "vitest run test/periphery-cut.test.ts lib/legacy-surfaces.server.test.ts components/shell-basics.test.tsx app/approvals/page.test.tsx app/tasks/page.test.tsx app/artifacts/page.test.tsx app/gmail/page.test.tsx app/calendar/page.test.tsx app/memories/page.test.tsx app/entities/page.test.tsx components/trace-list.test.tsx lib/api.test.ts" }, "dependencies": { - "next": "15.5.21", + "next": "16.2.12", "react": "19.2.7", "react-dom": "19.2.7" }, @@ -40,10 +40,10 @@ "@types/react-dom": "19.2.3", "@vitest/coverage-v8": "3.2.6", "eslint": "9.39.5", - "eslint-config-next": "15.5.21", + "eslint-config-next": "16.2.12", "jsdom": "26.1.0", "semver": "7.8.0", - "typescript": "5.8.2", + "typescript": "6.0.3", "vitest": "3.2.6" }, "pnpm": { diff --git a/apps/web/pnpm-lock.yaml b/apps/web/pnpm-lock.yaml index 7a5dda13..b7648da7 100644 --- a/apps/web/pnpm-lock.yaml +++ b/apps/web/pnpm-lock.yaml @@ -17,8 +17,8 @@ importers: .: dependencies: next: - specifier: 15.5.21 - version: 15.5.21(@playwright/test@1.55.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + specifier: 16.2.12 + version: 16.2.12(@babel/core@7.29.7)(@playwright/test@1.55.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: specifier: 19.2.7 version: 19.2.7 @@ -54,8 +54,8 @@ importers: specifier: 9.39.5 version: 9.39.5 eslint-config-next: - specifier: 15.5.21 - version: 15.5.21(eslint@9.39.5)(typescript@5.8.2) + specifier: 16.2.12 + version: 16.2.12(@typescript-eslint/parser@8.65.0(eslint@9.39.5)(typescript@6.0.3))(eslint@9.39.5)(typescript@6.0.3) jsdom: specifier: 26.1.0 version: 26.1.0 @@ -63,8 +63,8 @@ importers: specifier: 7.8.0 version: 7.8.0 typescript: - specifier: 5.8.2 - version: 5.8.2 + specifier: 6.0.3 + version: 6.0.3 vitest: specifier: 3.2.6 version: 3.2.6(@types/node@26.1.2)(jsdom@26.1.0) @@ -90,6 +90,36 @@ packages: resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.7': + resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@babel/helper-string-parser@7.29.7': resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} engines: {node: '>=6.9.0'} @@ -98,6 +128,14 @@ packages: resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + '@babel/parser@7.29.7': resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} engines: {node: '>=6.0.0'} @@ -107,6 +145,14 @@ packages: resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} engines: {node: '>=6.9.0'} + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.7': + resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} + engines: {node: '>=6.9.0'} + '@babel/types@7.29.7': resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} @@ -523,6 +569,9 @@ packages: '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -536,56 +585,56 @@ packages: '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} - '@next/env@15.5.21': - resolution: {integrity: sha512-hjJI/GfrjWHgNguRIBzItjRRu0m3Nrz17GhxsjuHfjIvg9hyg3239REd2dpI+bpMTFuVrVprHzEQ19m++cDtbw==} + '@next/env@16.2.12': + resolution: {integrity: sha512-d0Z5Bc13Fa4nR8pFAKx2jay2yhJM16vlfHbTzYnUQAxlNb6B6lmn4hjt69lYNt4kRtyYP6gEM49lPRHNbIyneg==} - '@next/eslint-plugin-next@15.5.21': - resolution: {integrity: sha512-5MoR9vO3dE1lU3LixogB54xrn7OhMGempTqk1q3WlvbKrT6cNt5mV2mMkX5sghAk0vZF6Nz1HxoTb9YmZv8wRg==} + '@next/eslint-plugin-next@16.2.12': + resolution: {integrity: sha512-uF2z/qAK2q7B5/6CpnFcBRX6jOq5iCO+Uqh1UkJhXljX1JwLarLYhhoJadO6dPb6moTprOKewMXheBcbIoSbug==} - '@next/swc-darwin-arm64@15.5.21': - resolution: {integrity: sha512-ZfjqPEdi6TRC/fWx7UDbwb1fbVgyh2uD5tVTRKIDZDlYM+UNuE/LafDG2fwuAoZilADpABh46OY/F5qf9JjqLQ==} + '@next/swc-darwin-arm64@16.2.12': + resolution: {integrity: sha512-0W1R0teHWJrqKX0FH20IzzIWAOuGtBxPGuObrxy1lE8hQvCFj49KE8a3WUg0D7sq6rn6zkM4c7YGUnhudBS6oA==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@15.5.21': - resolution: {integrity: sha512-TlCf1NpxgQLzTrexuev75xwmNCJMd1/qkJpTVP1GRRcih93hlIBn1P72hkh8T0gnRFr6BmWksQtbyG3jT6jnww==} + '@next/swc-darwin-x64@16.2.12': + resolution: {integrity: sha512-Hy5Ls099+aFUmOLmIgPfLqNi6iCwhL3uQCssz5rWk+5Nkc6TUKCE83DY5BbNylfm3+mfwcSFnLRfrZDJhVxdtw==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@15.5.21': - resolution: {integrity: sha512-LXRsq1p+HvHSi7ygwNcSEEcK0zuo5jS75ZlqFHtOH+LF7qntXAJVJxah+1Pi/GyBm7EpkwU7m4EgbvIKrMqm9A==} + '@next/swc-linux-arm64-gnu@16.2.12': + resolution: {integrity: sha512-+YqU2h1cQkHsGfvjAsrSmst8UIFBibBGm5x3Xgel8NLMiDQtNOM4sM2GOEMvG5YiOBNeN/Ykk8cQC2S0Xrqljg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@15.5.21': - resolution: {integrity: sha512-hyGixhFxpDKjqoev6l4KlcRBlt9AXWrGhDZwmwg49sMJM5tnKQPSi+SEj9+e5n+l/bthRGZUdh59GKIs6lQPRw==} + '@next/swc-linux-arm64-musl@16.2.12': + resolution: {integrity: sha512-0qjhiYBaKAqF63LA1ZWAAnKTzFUguAaZiRa5etMLGGPj/B6uEVjtIZldIzFEp3wHlB0koK6aTzqPtSdplTCjoA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@15.5.21': - resolution: {integrity: sha512-qfE+YfOba6S2+13e8qn1/UozDVNZ2clBlrs8UtDoax4s8ediu6sq93z66OEHUYlb69Tffh5JTNkgtsAKiSuugg==} + '@next/swc-linux-x64-gnu@16.2.12': + resolution: {integrity: sha512-7A3q26W+h7gnA15uqBToNuDqBEFZZcqh0mW2mn4AJh/G5pdg2RVE3n4slzLEliASZFG3NmsbEzng/x2Sh09mBg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@15.5.21': - resolution: {integrity: sha512-BXLGG+EvIwp/Rrgl6HY8sqvD6BOUOIRz8/naDbeLNX7mlA5H2XRcL6MW/0IGnJISfj5BA9gNhFyJj5yOoiIDJQ==} + '@next/swc-linux-x64-musl@16.2.12': + resolution: {integrity: sha512-qSjL/uppm+cbh21s72Ss8gkiOhQ4dExWHNGOWy6eZV7STj5WsKehgxT61beSsOj+YYQuTplL376lOCdMQU5T8w==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@15.5.21': - resolution: {integrity: sha512-tNGNOlT0Wn7E4IMsSnufjXN/l2L2/AGdLLpa2vzS89SYCBuihgLn3ngLsIrvndAnWo9nAkus+4gZHTI/Ijx9HA==} + '@next/swc-win32-arm64-msvc@16.2.12': + resolution: {integrity: sha512-X6hzsOUJac/e7AWSbn9gQ9nzHld1xWP5iyjHpYWvud8pufB679O1xg4JDyKr8Xd69Jvd+kM2Der6uftiZCmjYA==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@15.5.21': - resolution: {integrity: sha512-DmIdWmC9p4rdNIiQqo8ap0+Cnj6kKtTZnuSCxoYydSc8sgpDgAg9wFhxplunak9imLV0pTvc5WVCOHwm5eHLtQ==} + '@next/swc-win32-x64-msvc@16.2.12': + resolution: {integrity: sha512-F6fakeHuFTLOPt0bslQJdf+xtT+WIP9DVn/m4y1w1mRnVPyh3D/cNvzlRkxM444xfm+IvvYNSOrKiA2CDJ0Uxw==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -743,9 +792,6 @@ packages: '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} - '@rushstack/eslint-patch@1.16.1': - resolution: {integrity: sha512-TvZbIpeKqGQQ7X0zSCvPH9riMSFQFSggnfBjFZ1mEoILW+UuXCKwOoPcgjMwiUtRqFZ8jWhPJc4um14vC6I4ag==} - '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} @@ -1118,6 +1164,11 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} + baseline-browser-mapping@2.11.5: + resolution: {integrity: sha512-xJo6a6YZnwZfnyGmQKWMbVOcii7XRibjOskRh+WJ9UHQoX16xrQrcIgAMQOzfvs8XiLMx6ih/fsLPF73iY2D1A==} + engines: {node: '>=6.0.0'} + hasBin: true + brace-expansion@1.1.16: resolution: {integrity: sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==} @@ -1132,6 +1183,11 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + browserslist@4.28.7: + resolution: {integrity: sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -1184,6 +1240,9 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1277,6 +1336,9 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + electron-to-chromium@1.5.397: + resolution: {integrity: sha512-khGTy9U9x02KEtsKM8vx5A62BsRmcOsIgDpWr1ImE32Ax8GxHGPHZf+Eu9H8zOOyHJnB0jTbseyTHbq2XCT8yw==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -1327,14 +1389,18 @@ packages: engines: {node: '>=18'} hasBin: true + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - eslint-config-next@15.5.21: - resolution: {integrity: sha512-W8m8iFZis5SLqrHVXFv3IrRUpUu95VCHcXRMrMWtxpjV7JHfRB847qL2HJS5wIdLER+k17LWGnihBDRkFBRK8A==} + eslint-config-next@16.2.12: + resolution: {integrity: sha512-iaaf4vvKo5h2LBdGt0JuRv7t0Ysqr9FMCiFxbptDg8LqOE//mIKR80DdpOnSVM7qjLH3jT8P0aFiwXxBEGZRXw==} peerDependencies: - eslint: ^7.23.0 || ^8.0.0 || ^9.0.0 + eslint: '>=9.0.0' typescript: '>=3.3.1' peerDependenciesMeta: typescript: @@ -1393,11 +1459,11 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 - eslint-plugin-react-hooks@5.2.0: - resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==} - engines: {node: '>=10'} + eslint-plugin-react-hooks@7.1.1: + resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==} + engines: {node: '>=18'} peerDependencies: - eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 eslint-plugin-react@7.37.5: resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} @@ -1534,6 +1600,10 @@ packages: resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} engines: {node: '>= 0.4'} + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -1566,6 +1636,10 @@ packages: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} + globals@16.4.0: + resolution: {integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==} + engines: {node: '>=18'} + globalthis@1.0.4: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} @@ -1601,6 +1675,12 @@ packages: resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} engines: {node: '>= 0.4'} + hermes-estree@0.25.1: + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + + hermes-parser@0.25.1: + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + html-encoding-sniffer@4.0.0: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} @@ -1805,6 +1885,11 @@ packages: canvas: optional: true + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -1818,6 +1903,11 @@ packages: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + jsx-ast-utils@3.3.5: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} @@ -1856,6 +1946,9 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lz-string@1.5.0: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true @@ -1920,9 +2013,9 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - next@15.5.21: - resolution: {integrity: sha512-/TsdBtkWLhkl+NVL3Uqws2UphNd6IPzOtzSk1fHaf+0P7GQKLZDUytyhns/Ykbzdy9+YRjwG7ONvrHaaTDdFqQ==} - engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} + next@16.2.12: + resolution: {integrity: sha512-iD59eYQWmbFcEbX7v/acG5DRym9iw1DdaPoD0WTA920naWsE25wShzJW4+UvAs8MK9EC2kBfIH6vtto1H1PHGw==} + engines: {node: '>=20.9.0'} hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 @@ -1945,6 +2038,10 @@ packages: resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==} engines: {node: '>= 0.4'} + node-releases@2.0.51: + resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==} + engines: {node: '>=18'} + nwsapi@2.2.23: resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} @@ -2384,8 +2481,15 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} - typescript@5.8.2: - resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==} + typescript-eslint@8.65.0: + resolution: {integrity: sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} engines: {node: '>=14.17'} hasBin: true @@ -2399,6 +2503,12 @@ packages: unrs-resolver@1.11.1: resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -2553,10 +2663,22 @@ packages: xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zod-validation-error@4.0.2: + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + snapshots: '@adobe/css-tools@4.4.4': {} @@ -2585,16 +2707,97 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.7': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.7 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + '@babel/helper-string-parser@7.29.7': {} '@babel/helper-validator-identifier@7.29.7': {} + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + '@babel/parser@7.29.7': dependencies: '@babel/types': 7.29.7 '@babel/runtime@7.29.2': {} + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/traverse@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + '@babel/types@7.29.7': dependencies: '@babel/helper-string-parser': 7.29.7 @@ -2901,6 +3104,11 @@ snapshots: '@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/sourcemap-codec@1.5.5': {} @@ -2917,34 +3125,34 @@ snapshots: '@tybys/wasm-util': 0.10.2 optional: true - '@next/env@15.5.21': {} + '@next/env@16.2.12': {} - '@next/eslint-plugin-next@15.5.21': + '@next/eslint-plugin-next@16.2.12': dependencies: fast-glob: 3.3.1 - '@next/swc-darwin-arm64@15.5.21': + '@next/swc-darwin-arm64@16.2.12': optional: true - '@next/swc-darwin-x64@15.5.21': + '@next/swc-darwin-x64@16.2.12': optional: true - '@next/swc-linux-arm64-gnu@15.5.21': + '@next/swc-linux-arm64-gnu@16.2.12': optional: true - '@next/swc-linux-arm64-musl@15.5.21': + '@next/swc-linux-arm64-musl@16.2.12': optional: true - '@next/swc-linux-x64-gnu@15.5.21': + '@next/swc-linux-x64-gnu@16.2.12': optional: true - '@next/swc-linux-x64-musl@15.5.21': + '@next/swc-linux-x64-musl@16.2.12': optional: true - '@next/swc-win32-arm64-msvc@15.5.21': + '@next/swc-win32-arm64-msvc@16.2.12': optional: true - '@next/swc-win32-x64-msvc@15.5.21': + '@next/swc-win32-x64-msvc@16.2.12': optional: true '@nodelib/fs.scandir@2.1.5': @@ -3045,8 +3253,6 @@ snapshots: '@rtsao/scc@1.1.0': {} - '@rushstack/eslint-patch@1.16.1': {} - '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -3116,40 +3322,40 @@ snapshots: dependencies: csstype: 3.2.3 - '@typescript-eslint/eslint-plugin@8.65.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5)(typescript@5.8.2))(eslint@9.39.5)(typescript@5.8.2)': + '@typescript-eslint/eslint-plugin@8.65.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5)(typescript@6.0.3))(eslint@9.39.5)(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.65.0(eslint@9.39.5)(typescript@5.8.2) + '@typescript-eslint/parser': 8.65.0(eslint@9.39.5)(typescript@6.0.3) '@typescript-eslint/scope-manager': 8.65.0 - '@typescript-eslint/type-utils': 8.65.0(eslint@9.39.5)(typescript@5.8.2) - '@typescript-eslint/utils': 8.65.0(eslint@9.39.5)(typescript@5.8.2) + '@typescript-eslint/type-utils': 8.65.0(eslint@9.39.5)(typescript@6.0.3) + '@typescript-eslint/utils': 8.65.0(eslint@9.39.5)(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.65.0 eslint: 9.39.5 ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 2.5.0(typescript@5.8.2) - typescript: 5.8.2 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.65.0(eslint@9.39.5)(typescript@5.8.2)': + '@typescript-eslint/parser@8.65.0(eslint@9.39.5)(typescript@6.0.3)': dependencies: '@typescript-eslint/scope-manager': 8.65.0 '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/typescript-estree': 8.65.0(typescript@5.8.2) + '@typescript-eslint/typescript-estree': 8.65.0(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.65.0 debug: 4.4.3 eslint: 9.39.5 - typescript: 5.8.2 + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.65.0(typescript@5.8.2)': + '@typescript-eslint/project-service@8.65.0(typescript@6.0.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.65.0(typescript@5.8.2) + '@typescript-eslint/tsconfig-utils': 8.65.0(typescript@6.0.3) '@typescript-eslint/types': 8.65.0 debug: 4.4.3 - typescript: 5.8.2 + typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -3158,47 +3364,47 @@ snapshots: '@typescript-eslint/types': 8.65.0 '@typescript-eslint/visitor-keys': 8.65.0 - '@typescript-eslint/tsconfig-utils@8.65.0(typescript@5.8.2)': + '@typescript-eslint/tsconfig-utils@8.65.0(typescript@6.0.3)': dependencies: - typescript: 5.8.2 + typescript: 6.0.3 - '@typescript-eslint/type-utils@8.65.0(eslint@9.39.5)(typescript@5.8.2)': + '@typescript-eslint/type-utils@8.65.0(eslint@9.39.5)(typescript@6.0.3)': dependencies: '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/typescript-estree': 8.65.0(typescript@5.8.2) - '@typescript-eslint/utils': 8.65.0(eslint@9.39.5)(typescript@5.8.2) + '@typescript-eslint/typescript-estree': 8.65.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.65.0(eslint@9.39.5)(typescript@6.0.3) debug: 4.4.3 eslint: 9.39.5 - ts-api-utils: 2.5.0(typescript@5.8.2) - typescript: 5.8.2 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 transitivePeerDependencies: - supports-color '@typescript-eslint/types@8.65.0': {} - '@typescript-eslint/typescript-estree@8.65.0(typescript@5.8.2)': + '@typescript-eslint/typescript-estree@8.65.0(typescript@6.0.3)': dependencies: - '@typescript-eslint/project-service': 8.65.0(typescript@5.8.2) - '@typescript-eslint/tsconfig-utils': 8.65.0(typescript@5.8.2) + '@typescript-eslint/project-service': 8.65.0(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.65.0(typescript@6.0.3) '@typescript-eslint/types': 8.65.0 '@typescript-eslint/visitor-keys': 8.65.0 debug: 4.4.3 minimatch: 10.2.5 semver: 7.8.0 tinyglobby: 0.2.16 - ts-api-utils: 2.5.0(typescript@5.8.2) - typescript: 5.8.2 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.65.0(eslint@9.39.5)(typescript@5.8.2)': + '@typescript-eslint/utils@8.65.0(eslint@9.39.5)(typescript@6.0.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.5) '@typescript-eslint/scope-manager': 8.65.0 '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/typescript-estree': 8.65.0(typescript@5.8.2) + '@typescript-eslint/typescript-estree': 8.65.0(typescript@6.0.3) eslint: 9.39.5 - typescript: 5.8.2 + typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -3459,6 +3665,8 @@ snapshots: balanced-match@4.0.4: {} + baseline-browser-mapping@2.11.5: {} + brace-expansion@1.1.16: dependencies: balanced-match: 1.0.2 @@ -3476,6 +3684,14 @@ snapshots: dependencies: fill-range: 7.1.1 + browserslist@4.28.7: + dependencies: + baseline-browser-mapping: 2.11.5 + caniuse-lite: 1.0.30001806 + electron-to-chromium: 1.5.397 + node-releases: 2.0.51 + update-browserslist-db: 1.2.3(browserslist@4.28.7) + cac@6.7.14: {} call-bind-apply-helpers@1.0.2: @@ -3529,6 +3745,8 @@ snapshots: concat-map@0.0.1: {} + convert-source-map@2.0.0: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -3616,6 +3834,8 @@ snapshots: eastasianwidth@0.2.0: {} + electron-to-chromium@1.5.397: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -3754,24 +3974,26 @@ snapshots: '@esbuild/win32-ia32': 0.25.12 '@esbuild/win32-x64': 0.25.12 + escalade@3.2.0: {} + escape-string-regexp@4.0.0: {} - eslint-config-next@15.5.21(eslint@9.39.5)(typescript@5.8.2): + eslint-config-next@16.2.12(@typescript-eslint/parser@8.65.0(eslint@9.39.5)(typescript@6.0.3))(eslint@9.39.5)(typescript@6.0.3): dependencies: - '@next/eslint-plugin-next': 15.5.21 - '@rushstack/eslint-patch': 1.16.1 - '@typescript-eslint/eslint-plugin': 8.65.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5)(typescript@5.8.2))(eslint@9.39.5)(typescript@5.8.2) - '@typescript-eslint/parser': 8.65.0(eslint@9.39.5)(typescript@5.8.2) + '@next/eslint-plugin-next': 16.2.12 eslint: 9.39.5 eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5)(typescript@5.8.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5)(typescript@6.0.3))(eslint@9.39.5))(eslint@9.39.5) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5)(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5)(typescript@6.0.3))(eslint@9.39.5))(eslint@9.39.5))(eslint@9.39.5) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.5) eslint-plugin-react: 7.37.5(eslint@9.39.5) - eslint-plugin-react-hooks: 5.2.0(eslint@9.39.5) + eslint-plugin-react-hooks: 7.1.1(eslint@9.39.5) + globals: 16.4.0 + typescript-eslint: 8.65.0(eslint@9.39.5)(typescript@6.0.3) optionalDependencies: - typescript: 5.8.2 + typescript: 6.0.3 transitivePeerDependencies: + - '@typescript-eslint/parser' - eslint-import-resolver-webpack - eslint-plugin-import-x - supports-color @@ -3784,7 +4006,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5)(typescript@6.0.3))(eslint@9.39.5))(eslint@9.39.5): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -3795,22 +4017,22 @@ snapshots: tinyglobby: 0.2.16 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5)(typescript@5.8.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5)(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5)(typescript@6.0.3))(eslint@9.39.5))(eslint@9.39.5))(eslint@9.39.5) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.65.0(eslint@9.39.5)(typescript@5.8.2))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.65.0(eslint@9.39.5)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5)(typescript@6.0.3))(eslint@9.39.5))(eslint@9.39.5))(eslint@9.39.5): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.65.0(eslint@9.39.5)(typescript@5.8.2) + '@typescript-eslint/parser': 8.65.0(eslint@9.39.5)(typescript@6.0.3) eslint: 9.39.5 eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5)(typescript@6.0.3))(eslint@9.39.5))(eslint@9.39.5) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5)(typescript@5.8.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5)(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5)(typescript@6.0.3))(eslint@9.39.5))(eslint@9.39.5))(eslint@9.39.5): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -3821,7 +4043,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.5 eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.65.0(eslint@9.39.5)(typescript@5.8.2))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.65.0(eslint@9.39.5)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5)(typescript@6.0.3))(eslint@9.39.5))(eslint@9.39.5))(eslint@9.39.5) hasown: 2.0.3 is-core-module: 2.16.2 is-glob: 4.0.3 @@ -3833,7 +4055,7 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.65.0(eslint@9.39.5)(typescript@5.8.2) + '@typescript-eslint/parser': 8.65.0(eslint@9.39.5)(typescript@6.0.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -3858,9 +4080,16 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 - eslint-plugin-react-hooks@5.2.0(eslint@9.39.5): + eslint-plugin-react-hooks@7.1.1(eslint@9.39.5): dependencies: + '@babel/core': 7.29.7 + '@babel/parser': 7.29.7 eslint: 9.39.5 + hermes-parser: 0.25.1 + zod: 4.4.3 + zod-validation-error: 4.0.2(zod@4.4.3) + transitivePeerDependencies: + - supports-color eslint-plugin-react@7.37.5(eslint@9.39.5): dependencies: @@ -4030,6 +4259,8 @@ snapshots: generator-function@2.0.1: {} + gensync@1.0.0-beta.2: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -4077,6 +4308,8 @@ snapshots: globals@14.0.0: {} + globals@16.4.0: {} + globalthis@1.0.4: dependencies: define-properties: 1.2.1 @@ -4106,6 +4339,12 @@ snapshots: dependencies: function-bind: 1.1.2 + hermes-estree@0.25.1: {} + + hermes-parser@0.25.1: + dependencies: + hermes-estree: 0.25.1 + html-encoding-sniffer@4.0.0: dependencies: whatwg-encoding: 3.1.1 @@ -4342,6 +4581,8 @@ snapshots: - supports-color - utf-8-validate + jsesc@3.1.0: {} + json-buffer@3.0.1: {} json-schema-traverse@0.4.1: {} @@ -4352,6 +4593,8 @@ snapshots: dependencies: minimist: 1.2.8 + json5@2.2.3: {} + jsx-ast-utils@3.3.5: dependencies: array-includes: 3.1.9 @@ -4390,6 +4633,10 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + lz-string@1.5.0: {} magic-string@0.30.21: @@ -4441,24 +4688,25 @@ snapshots: natural-compare@1.4.0: {} - next@15.5.21(@playwright/test@1.55.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + next@16.2.12(@babel/core@7.29.7)(@playwright/test@1.55.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: - '@next/env': 15.5.21 + '@next/env': 16.2.12 '@swc/helpers': 0.5.15 + baseline-browser-mapping: 2.11.5 caniuse-lite: 1.0.30001806 postcss: 8.5.18 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - styled-jsx: 5.1.6(react@19.2.7) + styled-jsx: 5.1.6(@babel/core@7.29.7)(react@19.2.7) optionalDependencies: - '@next/swc-darwin-arm64': 15.5.21 - '@next/swc-darwin-x64': 15.5.21 - '@next/swc-linux-arm64-gnu': 15.5.21 - '@next/swc-linux-arm64-musl': 15.5.21 - '@next/swc-linux-x64-gnu': 15.5.21 - '@next/swc-linux-x64-musl': 15.5.21 - '@next/swc-win32-arm64-msvc': 15.5.21 - '@next/swc-win32-x64-msvc': 15.5.21 + '@next/swc-darwin-arm64': 16.2.12 + '@next/swc-darwin-x64': 16.2.12 + '@next/swc-linux-arm64-gnu': 16.2.12 + '@next/swc-linux-arm64-musl': 16.2.12 + '@next/swc-linux-x64-gnu': 16.2.12 + '@next/swc-linux-x64-musl': 16.2.12 + '@next/swc-win32-arm64-msvc': 16.2.12 + '@next/swc-win32-x64-msvc': 16.2.12 '@playwright/test': 1.55.1 sharp: 0.35.0 transitivePeerDependencies: @@ -4472,6 +4720,8 @@ snapshots: object.entries: 1.1.9 semver: 6.3.1 + node-releases@2.0.51: {} + nwsapi@2.2.23: {} object-assign@4.1.1: {} @@ -4914,10 +5164,12 @@ snapshots: dependencies: js-tokens: 9.0.1 - styled-jsx@5.1.6(react@19.2.7): + styled-jsx@5.1.6(@babel/core@7.29.7)(react@19.2.7): dependencies: client-only: 0.0.1 react: 19.2.7 + optionalDependencies: + '@babel/core': 7.29.7 supports-color@7.2.0: dependencies: @@ -4966,9 +5218,9 @@ snapshots: dependencies: punycode: 2.3.1 - ts-api-utils@2.5.0(typescript@5.8.2): + ts-api-utils@2.5.0(typescript@6.0.3): dependencies: - typescript: 5.8.2 + typescript: 6.0.3 tsconfig-paths@3.15.0: dependencies: @@ -5016,7 +5268,18 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript@5.8.2: {} + typescript-eslint@8.65.0(eslint@9.39.5)(typescript@6.0.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.65.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5)(typescript@6.0.3))(eslint@9.39.5)(typescript@6.0.3) + '@typescript-eslint/parser': 8.65.0(eslint@9.39.5)(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.65.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.65.0(eslint@9.39.5)(typescript@6.0.3) + eslint: 9.39.5 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + typescript@6.0.3: {} unbox-primitive@1.1.0: dependencies: @@ -5051,6 +5314,12 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 + update-browserslist-db@1.2.3(browserslist@4.28.7): + dependencies: + browserslist: 4.28.7 + escalade: 3.2.0 + picocolors: 1.1.1 + uri-js@4.4.1: dependencies: punycode: 2.3.1 @@ -5217,4 +5486,12 @@ snapshots: xmlchars@2.2.0: {} + yallist@3.1.1: {} + yocto-queue@0.1.0: {} + + zod-validation-error@4.0.2(zod@4.4.3): + dependencies: + zod: 4.4.3 + + zod@4.4.3: {} diff --git a/apps/web/scripts/check-bundle-budget.mjs b/apps/web/scripts/check-bundle-budget.mjs index 0b0be42f..2f92481b 100644 --- a/apps/web/scripts/check-bundle-budget.mjs +++ b/apps/web/scripts/check-bundle-budget.mjs @@ -1,21 +1,37 @@ import { readFile, stat } from "node:fs/promises"; import { gzipSync } from "node:zlib"; -const manifestPath = new URL("../.next/app-build-manifest.json", import.meta.url); +const legacyManifestPath = new URL("../.next/app-build-manifest.json", import.meta.url); +const routeStatsPath = new URL("../.next/diagnostics/route-bundle-stats.json", import.meta.url); const nextRoot = new URL("../.next/", import.meta.url); -const budgets = { - "/page": 120_000, - "/continuity/page": 130_000, - "/vnext/page": 155_000, -}; +const budgets = [ + { legacyRoute: "/page", route: "/", budget: 164_000 }, + { legacyRoute: "/continuity/page", route: "/continuity", budget: 174_000 }, + { legacyRoute: "/vnext/page", route: "/vnext", budget: 199_000 }, +]; -const manifest = JSON.parse(await readFile(manifestPath, "utf8")); +let legacyManifest = null; +try { + legacyManifest = JSON.parse(await readFile(legacyManifestPath, "utf8")); +} catch (error) { + if (!(error instanceof Error) || !Object.hasOwn(error, "code") || error.code !== "ENOENT") { + throw error; + } +} + +const routeStats = legacyManifest + ? null + : JSON.parse(await readFile(routeStatsPath, "utf8")); let failed = false; -for (const [route, budget] of Object.entries(budgets)) { - const assets = manifest.pages?.[route]; +for (const { legacyRoute, route, budget } of budgets) { + const assets = legacyManifest + ? legacyManifest.pages?.[legacyRoute] + : routeStats + .find((entry) => entry.route === route) + ?.firstLoadChunkPaths.map((asset) => asset.replace(/^\.next\//, "")); if (!Array.isArray(assets)) { - throw new Error(`Bundle manifest is missing expected app route ${route}`); + throw new Error(`Bundle metadata is missing expected app route ${route}`); } const javascriptAssets = [...new Set(assets.filter((asset) => asset.endsWith(".js")))]; diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index b5692538..cab78cfb 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -14,7 +14,7 @@ "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "preserve", + "jsx": "react-jsx", "incremental": true, "esModuleInterop": true, "plugins": [ @@ -27,7 +27,8 @@ "**/*.ts", "**/*.tsx", "next-env.d.ts", - ".next/types/**/*.ts" + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts" ], "exclude": [ "node_modules"