From 9f0ba74a9bfad72637403896118a68315811336b Mon Sep 17 00:00:00 2001 From: ablaszkiewicz Date: Wed, 28 Oct 2026 19:37:15 +0100 Subject: [PATCH 1/2] feat(error-tracking): Next.js 14/15/16 fixtures with scenario manifests --- .../.scenarios/next-16-app.json | 459 ++++++ .../.scenarios/no-posthog-next-14-app.json | 429 ++++++ .../.scenarios/no-posthog-next-15-pages.json | 322 +++++ .../.scenarios/no-posthog-next-16-app.json | 132 ++ apps/error-tracking/next-16-app/.gitignore | 5 + apps/error-tracking/next-16-app/README.md | 22 + .../next-16-app/app/api/cache/warm/route.ts | 13 + .../next-16-app/app/api/exports/route.ts | 14 + .../next-16-app/app/api/geo/route.ts | 14 + .../app/api/invoices/[id]/pdf/route.ts | 10 + .../app/api/invoices/[id]/remind/route.ts | 11 + .../app/api/webhooks/payment/route.ts | 11 + .../next-16-app/app/global-error.tsx | 17 + .../next-16-app/app/globals.css | 8 + .../app/invoices/[id]/invoice-actions.tsx | 49 + .../next-16-app/app/invoices/[id]/page.tsx | 44 + .../next-16-app/app/invoices/new/actions.ts | 20 + .../next-16-app/app/invoices/new/page.tsx | 22 + .../error-tracking/next-16-app/app/layout.tsx | 24 + apps/error-tracking/next-16-app/app/page.tsx | 33 + .../next-16-app/app/portal/[tenant]/page.tsx | 9 + .../next-16-app/app/reports/error.tsx | 10 + .../next-16-app/app/reports/page.tsx | 10 + .../next-16-app/app/reports/revenue-chart.tsx | 36 + .../next-16-app/app/settings/page.tsx | 10 + .../app/settings/preferences-panel.tsx | 36 + .../next-16-app/instrumentation-client.ts | 6 + apps/error-tracking/next-16-app/lib/errors.ts | 51 + .../next-16-app/lib/invoices.ts | 149 ++ .../next-16-app/lib/posthog-server.ts | 12 + .../error-tracking/next-16-app/next.config.ts | 5 + .../next-16-app/package-lock.json | 1230 +++++++++++++++++ apps/error-tracking/next-16-app/package.json | 23 + apps/error-tracking/next-16-app/proxy.ts | 16 + apps/error-tracking/next-16-app/tsconfig.json | 41 + .../no-posthog-next-14-app/.gitignore | 5 + .../no-posthog-next-14-app/README.md | 19 + .../app/api/cache/warm/route.ts | 13 + .../app/api/exports/route.ts | 14 + .../app/api/geo/route.ts | 14 + .../app/api/invoices/[id]/pdf/route.ts | 10 + .../app/api/invoices/[id]/remind/route.ts | 9 + .../no-posthog-next-14-app/app/globals.css | 8 + .../app/invoices/[id]/invoice-actions.tsx | 47 + .../app/invoices/[id]/page.tsx | 44 + .../app/invoices/new/actions.ts | 18 + .../app/invoices/new/page.tsx | 22 + .../no-posthog-next-14-app/app/layout.tsx | 24 + .../no-posthog-next-14-app/app/page.tsx | 33 + .../app/portal/[tenant]/page.tsx | 9 + .../app/reports/error.tsx | 10 + .../app/reports/page.tsx | 10 + .../app/reports/revenue-chart.tsx | 36 + .../app/settings/page.tsx | 10 + .../app/settings/preferences-panel.tsx | 36 + .../no-posthog-next-14-app/lib/errors.ts | 51 + .../no-posthog-next-14-app/lib/invoices.ts | 149 ++ .../no-posthog-next-14-app/middleware.ts | 16 + .../no-posthog-next-14-app/next.config.mjs | 4 + .../no-posthog-next-14-app/package-lock.json | 510 +++++++ .../no-posthog-next-14-app/package.json | 21 + .../no-posthog-next-14-app/tsconfig.json | 41 + .../no-posthog-next-15-pages/.gitignore | 5 + .../no-posthog-next-15-pages/README.md | 19 + .../components/invoice-actions.tsx | 45 + .../components/revenue-chart.tsx | 34 + .../no-posthog-next-15-pages/lib/errors.ts | 51 + .../no-posthog-next-15-pages/lib/invoices.ts | 149 ++ .../no-posthog-next-15-pages/middleware.ts | 16 + .../no-posthog-next-15-pages/next.config.ts | 5 + .../package-lock.json | 1063 ++++++++++++++ .../no-posthog-next-15-pages/package.json | 21 + .../no-posthog-next-15-pages/pages/_app.tsx | 17 + .../pages/api/cache/warm.ts | 14 + .../pages/api/exports.ts | 15 + .../no-posthog-next-15-pages/pages/api/geo.ts | 15 + .../pages/api/invoices/[id]/pdf.ts | 11 + .../pages/api/invoices/[id]/remind.ts | 10 + .../no-posthog-next-15-pages/pages/index.tsx | 25 + .../pages/invoices/[id].tsx | 41 + .../pages/portal/[tenant].tsx | 11 + .../pages/reports.tsx | 10 + .../styles/globals.css | 8 + .../no-posthog-next-15-pages/tsconfig.json | 41 + .../no-posthog-next-16-app/.gitignore | 5 + .../no-posthog-next-16-app/README.md | 20 + .../app/api/cache/warm/route.ts | 13 + .../app/api/exports/route.ts | 14 + .../app/api/geo/route.ts | 14 + .../app/api/invoices/[id]/pdf/route.ts | 10 + .../app/api/invoices/[id]/remind/route.ts | 9 + .../app/api/webhooks/payment/route.ts | 11 + .../no-posthog-next-16-app/app/globals.css | 8 + .../app/invoices/[id]/invoice-actions.tsx | 47 + .../app/invoices/[id]/page.tsx | 44 + .../app/invoices/new/actions.ts | 18 + .../app/invoices/new/page.tsx | 22 + .../no-posthog-next-16-app/app/layout.tsx | 24 + .../no-posthog-next-16-app/app/page.tsx | 33 + .../app/portal/[tenant]/page.tsx | 9 + .../app/reports/error.tsx | 10 + .../app/reports/page.tsx | 10 + .../app/reports/revenue-chart.tsx | 36 + .../app/settings/page.tsx | 10 + .../app/settings/preferences-panel.tsx | 36 + .../no-posthog-next-16-app/lib/errors.ts | 51 + .../no-posthog-next-16-app/lib/invoices.ts | 149 ++ .../no-posthog-next-16-app/next.config.ts | 5 + .../no-posthog-next-16-app/package-lock.json | 1076 ++++++++++++++ .../no-posthog-next-16-app/package.json | 21 + .../no-posthog-next-16-app/proxy.ts | 16 + .../no-posthog-next-16-app/tsconfig.json | 41 + 112 files changed, 7874 insertions(+) create mode 100644 apps/error-tracking/.scenarios/next-16-app.json create mode 100644 apps/error-tracking/.scenarios/no-posthog-next-14-app.json create mode 100644 apps/error-tracking/.scenarios/no-posthog-next-15-pages.json create mode 100644 apps/error-tracking/.scenarios/no-posthog-next-16-app.json create mode 100644 apps/error-tracking/next-16-app/.gitignore create mode 100644 apps/error-tracking/next-16-app/README.md create mode 100644 apps/error-tracking/next-16-app/app/api/cache/warm/route.ts create mode 100644 apps/error-tracking/next-16-app/app/api/exports/route.ts create mode 100644 apps/error-tracking/next-16-app/app/api/geo/route.ts create mode 100644 apps/error-tracking/next-16-app/app/api/invoices/[id]/pdf/route.ts create mode 100644 apps/error-tracking/next-16-app/app/api/invoices/[id]/remind/route.ts create mode 100644 apps/error-tracking/next-16-app/app/api/webhooks/payment/route.ts create mode 100644 apps/error-tracking/next-16-app/app/global-error.tsx create mode 100644 apps/error-tracking/next-16-app/app/globals.css create mode 100644 apps/error-tracking/next-16-app/app/invoices/[id]/invoice-actions.tsx create mode 100644 apps/error-tracking/next-16-app/app/invoices/[id]/page.tsx create mode 100644 apps/error-tracking/next-16-app/app/invoices/new/actions.ts create mode 100644 apps/error-tracking/next-16-app/app/invoices/new/page.tsx create mode 100644 apps/error-tracking/next-16-app/app/layout.tsx create mode 100644 apps/error-tracking/next-16-app/app/page.tsx create mode 100644 apps/error-tracking/next-16-app/app/portal/[tenant]/page.tsx create mode 100644 apps/error-tracking/next-16-app/app/reports/error.tsx create mode 100644 apps/error-tracking/next-16-app/app/reports/page.tsx create mode 100644 apps/error-tracking/next-16-app/app/reports/revenue-chart.tsx create mode 100644 apps/error-tracking/next-16-app/app/settings/page.tsx create mode 100644 apps/error-tracking/next-16-app/app/settings/preferences-panel.tsx create mode 100644 apps/error-tracking/next-16-app/instrumentation-client.ts create mode 100644 apps/error-tracking/next-16-app/lib/errors.ts create mode 100644 apps/error-tracking/next-16-app/lib/invoices.ts create mode 100644 apps/error-tracking/next-16-app/lib/posthog-server.ts create mode 100644 apps/error-tracking/next-16-app/next.config.ts create mode 100644 apps/error-tracking/next-16-app/package-lock.json create mode 100644 apps/error-tracking/next-16-app/package.json create mode 100644 apps/error-tracking/next-16-app/proxy.ts create mode 100644 apps/error-tracking/next-16-app/tsconfig.json create mode 100644 apps/error-tracking/no-posthog-next-14-app/.gitignore create mode 100644 apps/error-tracking/no-posthog-next-14-app/README.md create mode 100644 apps/error-tracking/no-posthog-next-14-app/app/api/cache/warm/route.ts create mode 100644 apps/error-tracking/no-posthog-next-14-app/app/api/exports/route.ts create mode 100644 apps/error-tracking/no-posthog-next-14-app/app/api/geo/route.ts create mode 100644 apps/error-tracking/no-posthog-next-14-app/app/api/invoices/[id]/pdf/route.ts create mode 100644 apps/error-tracking/no-posthog-next-14-app/app/api/invoices/[id]/remind/route.ts create mode 100644 apps/error-tracking/no-posthog-next-14-app/app/globals.css create mode 100644 apps/error-tracking/no-posthog-next-14-app/app/invoices/[id]/invoice-actions.tsx create mode 100644 apps/error-tracking/no-posthog-next-14-app/app/invoices/[id]/page.tsx create mode 100644 apps/error-tracking/no-posthog-next-14-app/app/invoices/new/actions.ts create mode 100644 apps/error-tracking/no-posthog-next-14-app/app/invoices/new/page.tsx create mode 100644 apps/error-tracking/no-posthog-next-14-app/app/layout.tsx create mode 100644 apps/error-tracking/no-posthog-next-14-app/app/page.tsx create mode 100644 apps/error-tracking/no-posthog-next-14-app/app/portal/[tenant]/page.tsx create mode 100644 apps/error-tracking/no-posthog-next-14-app/app/reports/error.tsx create mode 100644 apps/error-tracking/no-posthog-next-14-app/app/reports/page.tsx create mode 100644 apps/error-tracking/no-posthog-next-14-app/app/reports/revenue-chart.tsx create mode 100644 apps/error-tracking/no-posthog-next-14-app/app/settings/page.tsx create mode 100644 apps/error-tracking/no-posthog-next-14-app/app/settings/preferences-panel.tsx create mode 100644 apps/error-tracking/no-posthog-next-14-app/lib/errors.ts create mode 100644 apps/error-tracking/no-posthog-next-14-app/lib/invoices.ts create mode 100644 apps/error-tracking/no-posthog-next-14-app/middleware.ts create mode 100644 apps/error-tracking/no-posthog-next-14-app/next.config.mjs create mode 100644 apps/error-tracking/no-posthog-next-14-app/package-lock.json create mode 100644 apps/error-tracking/no-posthog-next-14-app/package.json create mode 100644 apps/error-tracking/no-posthog-next-14-app/tsconfig.json create mode 100644 apps/error-tracking/no-posthog-next-15-pages/.gitignore create mode 100644 apps/error-tracking/no-posthog-next-15-pages/README.md create mode 100644 apps/error-tracking/no-posthog-next-15-pages/components/invoice-actions.tsx create mode 100644 apps/error-tracking/no-posthog-next-15-pages/components/revenue-chart.tsx create mode 100644 apps/error-tracking/no-posthog-next-15-pages/lib/errors.ts create mode 100644 apps/error-tracking/no-posthog-next-15-pages/lib/invoices.ts create mode 100644 apps/error-tracking/no-posthog-next-15-pages/middleware.ts create mode 100644 apps/error-tracking/no-posthog-next-15-pages/next.config.ts create mode 100644 apps/error-tracking/no-posthog-next-15-pages/package-lock.json create mode 100644 apps/error-tracking/no-posthog-next-15-pages/package.json create mode 100644 apps/error-tracking/no-posthog-next-15-pages/pages/_app.tsx create mode 100644 apps/error-tracking/no-posthog-next-15-pages/pages/api/cache/warm.ts create mode 100644 apps/error-tracking/no-posthog-next-15-pages/pages/api/exports.ts create mode 100644 apps/error-tracking/no-posthog-next-15-pages/pages/api/geo.ts create mode 100644 apps/error-tracking/no-posthog-next-15-pages/pages/api/invoices/[id]/pdf.ts create mode 100644 apps/error-tracking/no-posthog-next-15-pages/pages/api/invoices/[id]/remind.ts create mode 100644 apps/error-tracking/no-posthog-next-15-pages/pages/index.tsx create mode 100644 apps/error-tracking/no-posthog-next-15-pages/pages/invoices/[id].tsx create mode 100644 apps/error-tracking/no-posthog-next-15-pages/pages/portal/[tenant].tsx create mode 100644 apps/error-tracking/no-posthog-next-15-pages/pages/reports.tsx create mode 100644 apps/error-tracking/no-posthog-next-15-pages/styles/globals.css create mode 100644 apps/error-tracking/no-posthog-next-15-pages/tsconfig.json create mode 100644 apps/error-tracking/no-posthog-next-16-app/.gitignore create mode 100644 apps/error-tracking/no-posthog-next-16-app/README.md create mode 100644 apps/error-tracking/no-posthog-next-16-app/app/api/cache/warm/route.ts create mode 100644 apps/error-tracking/no-posthog-next-16-app/app/api/exports/route.ts create mode 100644 apps/error-tracking/no-posthog-next-16-app/app/api/geo/route.ts create mode 100644 apps/error-tracking/no-posthog-next-16-app/app/api/invoices/[id]/pdf/route.ts create mode 100644 apps/error-tracking/no-posthog-next-16-app/app/api/invoices/[id]/remind/route.ts create mode 100644 apps/error-tracking/no-posthog-next-16-app/app/api/webhooks/payment/route.ts create mode 100644 apps/error-tracking/no-posthog-next-16-app/app/globals.css create mode 100644 apps/error-tracking/no-posthog-next-16-app/app/invoices/[id]/invoice-actions.tsx create mode 100644 apps/error-tracking/no-posthog-next-16-app/app/invoices/[id]/page.tsx create mode 100644 apps/error-tracking/no-posthog-next-16-app/app/invoices/new/actions.ts create mode 100644 apps/error-tracking/no-posthog-next-16-app/app/invoices/new/page.tsx create mode 100644 apps/error-tracking/no-posthog-next-16-app/app/layout.tsx create mode 100644 apps/error-tracking/no-posthog-next-16-app/app/page.tsx create mode 100644 apps/error-tracking/no-posthog-next-16-app/app/portal/[tenant]/page.tsx create mode 100644 apps/error-tracking/no-posthog-next-16-app/app/reports/error.tsx create mode 100644 apps/error-tracking/no-posthog-next-16-app/app/reports/page.tsx create mode 100644 apps/error-tracking/no-posthog-next-16-app/app/reports/revenue-chart.tsx create mode 100644 apps/error-tracking/no-posthog-next-16-app/app/settings/page.tsx create mode 100644 apps/error-tracking/no-posthog-next-16-app/app/settings/preferences-panel.tsx create mode 100644 apps/error-tracking/no-posthog-next-16-app/lib/errors.ts create mode 100644 apps/error-tracking/no-posthog-next-16-app/lib/invoices.ts create mode 100644 apps/error-tracking/no-posthog-next-16-app/next.config.ts create mode 100644 apps/error-tracking/no-posthog-next-16-app/package-lock.json create mode 100644 apps/error-tracking/no-posthog-next-16-app/package.json create mode 100644 apps/error-tracking/no-posthog-next-16-app/proxy.ts create mode 100644 apps/error-tracking/no-posthog-next-16-app/tsconfig.json diff --git a/apps/error-tracking/.scenarios/next-16-app.json b/apps/error-tracking/.scenarios/next-16-app.json new file mode 100644 index 000000000..642292af2 --- /dev/null +++ b/apps/error-tracking/.scenarios/next-16-app.json @@ -0,0 +1,459 @@ +{ + "app": "next-16-app", + "lifecycle": { + "install": { + "argv": [ + "npm", + "ci", + "--no-audit", + "--no-fund" + ] + }, + "build": { + "argv": [ + "npm", + "run", + "build" + ] + }, + "start": { + "argv": [ + "npm", + "run", + "start", + "--", + "-p", + "{port}" + ], + "ready": { + "path": "/", + "timeoutMs": 60000 + } + }, + "env": {}, + "flushWaitMs": 15000 + }, + "scenarios": [ + { + "id": "client-render-segment", + "surface": "Client Component render error caught by a segment error.tsx (surfaces/nextjs.md §2)", + "requirement": "COV-2", + "trigger": { + "kind": "browser", + "page": "/reports?range=q5", + "expectText": "load this report" + }, + "throws": { + "match": "RX20", + "file": "app/reports/revenue-chart.tsx" + }, + "expect": { + "count": 1, + "lib": "web", + "linked": true, + "release": true, + "topFrame": { + "resolved": true, + "sourceEndsWith": "app/reports/revenue-chart.tsx" + } + } + }, + { + "id": "client-render-root", + "surface": "Client render error caught by a custom global-error.tsx (explicit boundary: no reportError on >= 15.2)", + "requirement": "COV-2", + "trigger": { + "kind": "browser", + "page": "/settings?import=legacy", + "expectText": "Ledgerly hit a snag" + }, + "throws": { + "match": "PF88", + "file": "app/settings/preferences-panel.tsx" + }, + "expect": { + "count": 1, + "lib": "web", + "linked": true, + "release": true, + "topFrame": { + "resolved": true, + "sourceEndsWith": "app/settings/preferences-panel.tsx" + } + } + }, + { + "id": "client-onclick", + "surface": "Client event handler throw", + "requirement": "COV-1", + "trigger": { + "kind": "browser", + "page": "/invoices/INV-3003", + "actions": [ + { + "click": "button:has-text('Download CSV')" + } + ], + "expectPageError": "CE03" + }, + "throws": { + "match": "CE03", + "file": "app/invoices/[id]/invoice-actions.tsx" + }, + "expect": { + "count": 1, + "lib": "web", + "linked": true, + "handled": false, + "release": true, + "topFrame": { + "resolved": true, + "sourceEndsWith": "app/invoices/[id]/invoice-actions.tsx" + } + } + }, + { + "id": "rsc-render-navigation", + "surface": "Server Component render error on a document navigation (cookie only)", + "requirement": "COV-2", + "trigger": { + "kind": "browser", + "page": "/", + "actions": [ + { + "wait": 2500 + }, + { + "goto": "/invoices/INV-4471" + } + ] + }, + "throws": { + "match": "TM71", + "file": "lib/invoices.ts" + }, + "expect": { + "count": 1, + "lib": "posthog-node", + "linked": true, + "request": true, + "release": true, + "topFrame": { + "resolved": true, + "sourceEndsWith": "lib/invoices.ts" + } + } + }, + { + "id": "metadata-cookie", + "surface": "generateMetadata throws, page request carrying only the PostHog cookie", + "requirement": "COV-2", + "trigger": { + "kind": "http", + "path": "/invoices/INV-0913", + "identity": "cookie" + }, + "throws": { + "match": "MX13", + "file": "lib/invoices.ts" + }, + "expect": { + "count": 1, + "lib": "posthog-node", + "linked": true, + "request": true, + "release": true, + "topFrame": { + "resolved": true, + "sourceEndsWith": "lib/invoices.ts" + } + } + }, + { + "id": "route-handler-headers", + "surface": "Route Handler throws, request with X-POSTHOG-* tracing headers", + "requirement": "CTX-1", + "trigger": { + "kind": "http", + "path": "/api/invoices/INV-5150/pdf", + "identity": "headers", + "expectStatus": 500 + }, + "throws": { + "match": "PD50", + "file": "lib/invoices.ts" + }, + "expect": { + "count": 1, + "lib": "posthog-node", + "linked": true, + "request": true, + "release": true, + "topFrame": { + "resolved": true, + "sourceEndsWith": "lib/invoices.ts" + } + } + }, + { + "id": "route-handler-browser", + "surface": "Route Handler throws, called by fetch from the page", + "requirement": "COV-2", + "trigger": { + "kind": "browser", + "page": "/invoices/INV-2202", + "actions": [ + { + "wait": 1500 + }, + { + "click": "button:has-text('Send reminder')" + } + ], + "expectText": "Could not send reminder" + }, + "throws": { + "match": "RQ02", + "file": "lib/invoices.ts" + }, + "expect": { + "count": 1, + "lib": "posthog-node", + "linked": true, + "request": true, + "release": true, + "topFrame": { + "resolved": true, + "sourceEndsWith": "lib/invoices.ts" + } + } + }, + { + "id": "server-action-form", + "surface": "Server Action throws, submitted from a form", + "requirement": "COV-2", + "trigger": { + "kind": "browser", + "page": "/invoices/new", + "actions": [ + { + "wait": 1500 + }, + { + "fill": { + "selector": "input[name=customer]", + "value": "ACME-0042" + } + }, + { + "click": "button[type=submit]" + } + ] + }, + "throws": { + "match": "CL42", + "file": "lib/invoices.ts" + }, + "expect": { + "count": 1, + "lib": "posthog-node", + "linked": true, + "request": true, + "release": true, + "topFrame": { + "resolved": true, + "sourceEndsWith": "lib/invoices.ts" + } + } + }, + { + "id": "proxy-cookie", + "surface": "proxy.ts throws (Next 16, Node.js runtime)", + "requirement": "COV-2", + "trigger": { + "kind": "http", + "path": "/portal/~bad", + "identity": "cookie", + "expectStatus": 500 + }, + "throws": { + "match": "TN09", + "file": "proxy.ts" + }, + "expect": { + "count": 1, + "lib": "posthog-", + "linked": true, + "request": true, + "release": true, + "topFrame": { + "resolved": true, + "sourceEndsWith": "proxy.ts" + } + } + }, + { + "id": "edge-route-headers", + "surface": "Route Handler on the edge runtime throws", + "requirement": "COV-3", + "trigger": { + "kind": "http", + "path": "/api/geo?country=ZZ", + "identity": "headers", + "expectStatus": 500 + }, + "throws": { + "match": "GX01", + "file": "app/api/geo/route.ts" + }, + "expect": { + "count": 1, + "lib": "posthog-", + "linked": true, + "request": true, + "release": true, + "topFrame": { + "resolved": true, + "sourceEndsWith": "app/api/geo/route.ts" + } + } + }, + { + "id": "after-callback", + "surface": "after() callback throws (onRequestError is not called)", + "requirement": "COV-2", + "trigger": { + "kind": "http", + "method": "POST", + "path": "/api/webhooks/payment", + "body": { + "id": "PAY-3321", + "invoice": "INV-2202" + }, + "identity": "none", + "expectStatus": 200 + }, + "throws": { + "match": "RC21", + "file": "lib/invoices.ts" + }, + "expect": { + "count": 1, + "lib": "posthog-node", + "release": true, + "topFrame": { + "resolved": true, + "sourceEndsWith": "lib/invoices.ts" + } + } + }, + { + "id": "background-rejection", + "surface": "Fire-and-forget promise in a Route Handler rejects (process unhandledRejection)", + "requirement": "COV-1", + "trigger": { + "kind": "http", + "method": "POST", + "path": "/api/exports", + "body": { + "batch": "EX-77" + }, + "identity": "headers", + "expectStatus": 202 + }, + "throws": { + "match": "EX77", + "file": "app/api/exports/route.ts" + }, + "expect": { + "count": 1, + "lib": "posthog-node", + "handled": false, + "release": true, + "topFrame": { + "resolved": true, + "sourceEndsWith": "app/api/exports/route.ts" + } + } + }, + { + "id": "uncaught-timer", + "surface": "Timer callback throws on the Node server (process uncaughtException)", + "requirement": "COV-1", + "trigger": { + "kind": "http", + "method": "POST", + "path": "/api/cache/warm", + "body": { + "region": "eu-2" + }, + "identity": "none", + "expectStatus": 202 + }, + "throws": { + "match": "CW02", + "file": "app/api/cache/warm/route.ts" + }, + "expect": { + "count": 1, + "lib": "posthog-node", + "handled": false, + "release": true, + "topFrame": { + "resolved": true, + "sourceEndsWith": "app/api/cache/warm/route.ts" + } + } + }, + { + "id": "not-found", + "surface": "notFound() control flow", + "requirement": "PRE-2", + "trigger": { + "kind": "http", + "path": "/invoices/INV-9999", + "identity": "cookie", + "expectStatus": 404 + }, + "throws": { + "match": "NEXT_HTTP_ERROR_FALLBACK" + }, + "expect": { + "count": 0 + } + }, + { + "id": "action-redirect", + "surface": "redirect() inside a Server Action", + "requirement": "PRE-2", + "trigger": { + "kind": "browser", + "page": "/invoices/new", + "actions": [ + { + "wait": 1500 + }, + { + "fill": { + "selector": "input[name=customer]", + "value": "ACME-0007" + } + }, + { + "click": "button[type=submit]" + }, + { + "wait": 1500 + } + ], + "expectText": "Annual support plan" + }, + "throws": { + "match": "NEXT_REDIRECT" + }, + "expect": { + "count": 0 + } + } + ] +} diff --git a/apps/error-tracking/.scenarios/no-posthog-next-14-app.json b/apps/error-tracking/.scenarios/no-posthog-next-14-app.json new file mode 100644 index 000000000..cb7b83ed0 --- /dev/null +++ b/apps/error-tracking/.scenarios/no-posthog-next-14-app.json @@ -0,0 +1,429 @@ +{ + "app": "no-posthog-next-14-app", + "lifecycle": { + "install": { + "argv": [ + "npm", + "ci", + "--no-audit", + "--no-fund" + ] + }, + "build": { + "argv": [ + "npm", + "run", + "build" + ] + }, + "start": { + "argv": [ + "npm", + "run", + "start", + "--", + "-p", + "{port}" + ], + "ready": { + "path": "/", + "timeoutMs": 60000 + } + }, + "env": {}, + "flushWaitMs": 15000 + }, + "scenarios": [ + { + "id": "client-render-segment", + "surface": "Client Component render error caught by a segment error.tsx (surfaces/nextjs.md §2)", + "requirement": "COV-2", + "trigger": { + "kind": "browser", + "page": "/reports?range=q5", + "expectText": "load this report" + }, + "throws": { + "match": "RX20", + "file": "app/reports/revenue-chart.tsx" + }, + "expect": { + "count": 1, + "lib": "web", + "linked": true, + "release": true, + "topFrame": { + "resolved": true, + "sourceEndsWith": "app/reports/revenue-chart.tsx" + } + } + }, + { + "id": "client-render-root", + "surface": "Client render error with no user boundary (13.x-15.1: implicit boundary, console.error only)", + "requirement": "COV-2", + "trigger": { + "kind": "browser", + "page": "/settings?import=legacy" + }, + "throws": { + "match": "PF88", + "file": "app/settings/preferences-panel.tsx" + }, + "expect": { + "count": 1, + "lib": "web", + "linked": true, + "release": true, + "topFrame": { + "resolved": true, + "sourceEndsWith": "app/settings/preferences-panel.tsx" + } + } + }, + { + "id": "client-onclick", + "surface": "Client event handler throw", + "requirement": "COV-1", + "trigger": { + "kind": "browser", + "page": "/invoices/INV-3003", + "actions": [ + { + "click": "button:has-text('Download CSV')" + } + ], + "expectPageError": "CE03" + }, + "throws": { + "match": "CE03", + "file": "app/invoices/[id]/invoice-actions.tsx" + }, + "expect": { + "count": 1, + "lib": "web", + "linked": true, + "handled": false, + "release": true, + "topFrame": { + "resolved": true, + "sourceEndsWith": "app/invoices/[id]/invoice-actions.tsx" + } + } + }, + { + "id": "rsc-render-navigation", + "surface": "Server Component render error on a document navigation (cookie only)", + "requirement": "COV-2", + "trigger": { + "kind": "browser", + "page": "/", + "actions": [ + { + "wait": 2500 + }, + { + "goto": "/invoices/INV-4471" + } + ] + }, + "throws": { + "match": "TM71", + "file": "lib/invoices.ts" + }, + "expect": { + "count": 1, + "lib": "posthog-node", + "linked": true, + "request": true, + "release": true, + "topFrame": { + "resolved": true, + "sourceEndsWith": "lib/invoices.ts" + } + } + }, + { + "id": "metadata-cookie", + "surface": "generateMetadata throws, page request carrying only the PostHog cookie", + "requirement": "COV-2", + "trigger": { + "kind": "http", + "path": "/invoices/INV-0913", + "identity": "cookie" + }, + "throws": { + "match": "MX13", + "file": "lib/invoices.ts" + }, + "expect": { + "count": 1, + "lib": "posthog-node", + "linked": true, + "request": true, + "release": true, + "topFrame": { + "resolved": true, + "sourceEndsWith": "lib/invoices.ts" + } + } + }, + { + "id": "route-handler-headers", + "surface": "Route Handler throws, request with X-POSTHOG-* tracing headers", + "requirement": "CTX-1", + "trigger": { + "kind": "http", + "path": "/api/invoices/INV-5150/pdf", + "identity": "headers", + "expectStatus": 500 + }, + "throws": { + "match": "PD50", + "file": "lib/invoices.ts" + }, + "expect": { + "count": 1, + "lib": "posthog-node", + "linked": true, + "request": true, + "release": true, + "topFrame": { + "resolved": true, + "sourceEndsWith": "lib/invoices.ts" + } + } + }, + { + "id": "route-handler-browser", + "surface": "Route Handler throws, called by fetch from the page", + "requirement": "COV-2", + "trigger": { + "kind": "browser", + "page": "/invoices/INV-2202", + "actions": [ + { + "wait": 1500 + }, + { + "click": "button:has-text('Send reminder')" + } + ], + "expectText": "Could not send reminder" + }, + "throws": { + "match": "RQ02", + "file": "lib/invoices.ts" + }, + "expect": { + "count": 1, + "lib": "posthog-node", + "linked": true, + "request": true, + "release": true, + "topFrame": { + "resolved": true, + "sourceEndsWith": "lib/invoices.ts" + } + } + }, + { + "id": "server-action-form", + "surface": "Server Action throws, submitted from a form", + "requirement": "COV-2", + "trigger": { + "kind": "browser", + "page": "/invoices/new", + "actions": [ + { + "wait": 1500 + }, + { + "fill": { + "selector": "input[name=customer]", + "value": "ACME-0042" + } + }, + { + "click": "button[type=submit]" + } + ] + }, + "throws": { + "match": "CL42", + "file": "lib/invoices.ts" + }, + "expect": { + "count": 1, + "lib": "posthog-node", + "linked": true, + "request": true, + "release": true, + "topFrame": { + "resolved": true, + "sourceEndsWith": "lib/invoices.ts" + } + } + }, + { + "id": "middleware-cookie", + "surface": "middleware.ts throws (edge runtime)", + "requirement": "COV-3", + "trigger": { + "kind": "http", + "path": "/portal/~bad", + "identity": "cookie", + "expectStatus": 500 + }, + "throws": { + "match": "TN09", + "file": "middleware.ts" + }, + "expect": { + "count": 1, + "lib": "posthog-", + "linked": true, + "request": true, + "release": true, + "topFrame": { + "resolved": true, + "sourceEndsWith": "middleware.ts" + } + } + }, + { + "id": "edge-route-headers", + "surface": "Route Handler on the edge runtime throws", + "requirement": "COV-3", + "trigger": { + "kind": "http", + "path": "/api/geo?country=ZZ", + "identity": "headers", + "expectStatus": 500 + }, + "throws": { + "match": "GX01", + "file": "app/api/geo/route.ts" + }, + "expect": { + "count": 1, + "lib": "posthog-", + "linked": true, + "request": true, + "release": true, + "topFrame": { + "resolved": true, + "sourceEndsWith": "app/api/geo/route.ts" + } + } + }, + { + "id": "background-rejection", + "surface": "Fire-and-forget promise in a Route Handler rejects (process unhandledRejection)", + "requirement": "COV-1", + "trigger": { + "kind": "http", + "method": "POST", + "path": "/api/exports", + "body": { + "batch": "EX-77" + }, + "identity": "headers", + "expectStatus": 202 + }, + "throws": { + "match": "EX77", + "file": "app/api/exports/route.ts" + }, + "expect": { + "count": 1, + "lib": "posthog-node", + "handled": false, + "release": true, + "topFrame": { + "resolved": true, + "sourceEndsWith": "app/api/exports/route.ts" + } + } + }, + { + "id": "uncaught-timer", + "surface": "Timer callback throws on the Node server (process uncaughtException)", + "requirement": "COV-1", + "trigger": { + "kind": "http", + "method": "POST", + "path": "/api/cache/warm", + "body": { + "region": "eu-2" + }, + "identity": "none", + "expectStatus": 202 + }, + "throws": { + "match": "CW02", + "file": "app/api/cache/warm/route.ts" + }, + "expect": { + "count": 1, + "lib": "posthog-node", + "handled": false, + "release": true, + "topFrame": { + "resolved": true, + "sourceEndsWith": "app/api/cache/warm/route.ts" + } + } + }, + { + "id": "not-found", + "surface": "notFound() control flow", + "requirement": "PRE-2", + "trigger": { + "kind": "http", + "path": "/invoices/INV-9999", + "identity": "cookie", + "expectStatus": 404 + }, + "throws": { + "match": "NEXT_NOT_FOUND" + }, + "expect": { + "count": 0 + } + }, + { + "id": "action-redirect", + "surface": "redirect() inside a Server Action", + "requirement": "PRE-2", + "trigger": { + "kind": "browser", + "page": "/invoices/new", + "actions": [ + { + "wait": 1500 + }, + { + "fill": { + "selector": "input[name=customer]", + "value": "ACME-0007" + } + }, + { + "click": "button[type=submit]" + }, + { + "wait": 1500 + } + ], + "expectText": "Annual support plan" + }, + "throws": { + "match": "NEXT_REDIRECT" + }, + "expect": { + "count": 0 + } + } + ] +} diff --git a/apps/error-tracking/.scenarios/no-posthog-next-15-pages.json b/apps/error-tracking/.scenarios/no-posthog-next-15-pages.json new file mode 100644 index 000000000..b5e7f64f3 --- /dev/null +++ b/apps/error-tracking/.scenarios/no-posthog-next-15-pages.json @@ -0,0 +1,322 @@ +{ + "app": "no-posthog-next-15-pages", + "lifecycle": { + "install": { + "argv": [ + "npm", + "ci", + "--no-audit", + "--no-fund" + ] + }, + "build": { + "argv": [ + "npm", + "run", + "build" + ] + }, + "start": { + "argv": [ + "npm", + "run", + "start", + "--", + "-p", + "{port}" + ], + "ready": { + "path": "/", + "timeoutMs": 60000 + } + }, + "env": {}, + "flushWaitMs": 15000 + }, + "scenarios": [ + { + "id": "client-render", + "surface": "Client render error in a page (Pages Router Container boundary: console.error only)", + "requirement": "COV-2", + "trigger": { + "kind": "browser", + "page": "/reports?range=q5" + }, + "throws": { + "match": "RX20", + "file": "components/revenue-chart.tsx" + }, + "expect": { + "count": 1, + "lib": "web", + "linked": true, + "release": true, + "topFrame": { + "resolved": true, + "sourceEndsWith": "components/revenue-chart.tsx" + } + } + }, + { + "id": "client-onclick", + "surface": "Client event handler throw", + "requirement": "COV-1", + "trigger": { + "kind": "browser", + "page": "/invoices/INV-3003", + "actions": [ + { + "click": "button:has-text('Download CSV')" + } + ], + "expectPageError": "CE03" + }, + "throws": { + "match": "CE03", + "file": "components/invoice-actions.tsx" + }, + "expect": { + "count": 1, + "lib": "web", + "linked": true, + "handled": false, + "release": true, + "topFrame": { + "resolved": true, + "sourceEndsWith": "components/invoice-actions.tsx" + } + } + }, + { + "id": "gssp-navigation", + "surface": "getServerSideProps throws on a document navigation (cookie only)", + "requirement": "COV-2", + "trigger": { + "kind": "browser", + "page": "/", + "actions": [ + { + "wait": 2500 + }, + { + "goto": "/invoices/INV-4471" + } + ] + }, + "throws": { + "match": "TM71", + "file": "lib/invoices.ts" + }, + "expect": { + "count": 1, + "lib": "posthog-node", + "linked": true, + "request": true, + "release": true, + "topFrame": { + "resolved": true, + "sourceEndsWith": "lib/invoices.ts" + } + } + }, + { + "id": "api-route-headers", + "surface": "API route throws, request with X-POSTHOG-* tracing headers", + "requirement": "CTX-1", + "trigger": { + "kind": "http", + "path": "/api/invoices/INV-5150/pdf", + "identity": "headers", + "expectStatus": 500 + }, + "throws": { + "match": "PD50", + "file": "lib/invoices.ts" + }, + "expect": { + "count": 1, + "lib": "posthog-node", + "linked": true, + "request": true, + "release": true, + "topFrame": { + "resolved": true, + "sourceEndsWith": "lib/invoices.ts" + } + } + }, + { + "id": "api-route-browser", + "surface": "API route throws, called by fetch from the page", + "requirement": "COV-2", + "trigger": { + "kind": "browser", + "page": "/invoices/INV-2202", + "actions": [ + { + "wait": 1500 + }, + { + "click": "button:has-text('Send reminder')" + } + ], + "expectText": "Could not send reminder" + }, + "throws": { + "match": "RQ02", + "file": "lib/invoices.ts" + }, + "expect": { + "count": 1, + "lib": "posthog-node", + "linked": true, + "request": true, + "release": true, + "topFrame": { + "resolved": true, + "sourceEndsWith": "lib/invoices.ts" + } + } + }, + { + "id": "middleware-cookie", + "surface": "middleware.ts throws (edge runtime)", + "requirement": "COV-3", + "trigger": { + "kind": "http", + "path": "/portal/~bad", + "identity": "cookie", + "expectStatus": 500 + }, + "throws": { + "match": "TN09", + "file": "middleware.ts" + }, + "expect": { + "count": 1, + "lib": "posthog-", + "linked": true, + "request": true, + "release": true, + "topFrame": { + "resolved": true, + "sourceEndsWith": "middleware.ts" + } + } + }, + { + "id": "edge-api-headers", + "surface": "API route on the edge runtime throws", + "requirement": "COV-3", + "trigger": { + "kind": "http", + "path": "/api/geo?country=ZZ", + "identity": "headers", + "expectStatus": 500 + }, + "throws": { + "match": "GX01", + "file": "pages/api/geo.ts" + }, + "expect": { + "count": 1, + "lib": "posthog-", + "linked": true, + "request": true, + "release": true, + "topFrame": { + "resolved": true, + "sourceEndsWith": "pages/api/geo.ts" + } + } + }, + { + "id": "background-rejection", + "surface": "Fire-and-forget promise in a Route Handler rejects (process unhandledRejection)", + "requirement": "COV-1", + "trigger": { + "kind": "http", + "method": "POST", + "path": "/api/exports", + "body": { + "batch": "EX-77" + }, + "identity": "headers", + "expectStatus": 202 + }, + "throws": { + "match": "EX77", + "file": "pages/api/exports.ts" + }, + "expect": { + "count": 1, + "lib": "posthog-node", + "handled": false, + "release": true, + "topFrame": { + "resolved": true, + "sourceEndsWith": "pages/api/exports.ts" + } + } + }, + { + "id": "uncaught-timer", + "surface": "Timer callback throws on the Node server (process uncaughtException)", + "requirement": "COV-1", + "trigger": { + "kind": "http", + "method": "POST", + "path": "/api/cache/warm", + "body": { + "region": "eu-2" + }, + "identity": "none", + "expectStatus": 202 + }, + "throws": { + "match": "CW02", + "file": "pages/api/cache/warm.ts" + }, + "expect": { + "count": 1, + "lib": "posthog-node", + "handled": false, + "release": true, + "topFrame": { + "resolved": true, + "sourceEndsWith": "pages/api/cache/warm.ts" + } + } + }, + { + "id": "gssp-not-found", + "surface": "getServerSideProps returns { notFound: true }", + "requirement": "PRE-2", + "trigger": { + "kind": "browser", + "page": "/invoices/INV-9999" + }, + "throws": { + "urlMatch": "/invoices/INV-9999" + }, + "expect": { + "count": 0 + } + }, + { + "id": "missing-page", + "surface": "Unknown page renders the default _error with 404", + "requirement": "PRE-2", + "trigger": { + "kind": "browser", + "page": "/no-such-page-9q" + }, + "throws": { + "urlMatch": "/no-such-page-9q" + }, + "expect": { + "count": 0 + } + } + ] +} diff --git a/apps/error-tracking/.scenarios/no-posthog-next-16-app.json b/apps/error-tracking/.scenarios/no-posthog-next-16-app.json new file mode 100644 index 000000000..7aad04e5a --- /dev/null +++ b/apps/error-tracking/.scenarios/no-posthog-next-16-app.json @@ -0,0 +1,132 @@ +{ + "app": "no-posthog-next-16-app", + "lifecycle": { + "install": { "argv": ["npm", "ci", "--no-audit", "--no-fund"] }, + "build": { "argv": ["npm", "run", "build"] }, + "start": { "argv": ["npm", "run", "start", "--", "-p", "{port}"], "ready": { "path": "/", "timeoutMs": 60000 } }, + "env": {}, + "flushWaitMs": 15000 + }, + "scenarios": [ + { + "id": "client-render-segment", + "surface": "Client Component render error caught by a segment error.tsx (surfaces/nextjs.md §2)", + "requirement": "COV-2", + "trigger": { "kind": "browser", "page": "/reports?range=q5", "expectText": "load this report" }, + "throws": { "match": "RX20", "file": "app/reports/revenue-chart.tsx" }, + "expect": { "count": 1, "lib": "web", "linked": true, "release": true, "topFrame": { "resolved": true, "sourceEndsWith": "app/reports/revenue-chart.tsx" } } + }, + { + "id": "client-render-root", + "surface": "Client render error with no user boundary on the path (implicit default global error, >= 15.2 reportError)", + "requirement": "COV-2", + "trigger": { "kind": "browser", "page": "/settings?import=legacy" }, + "throws": { "match": "PF88", "file": "app/settings/preferences-panel.tsx" }, + "expect": { "count": 1, "lib": "web", "linked": true, "release": true, "topFrame": { "resolved": true, "sourceEndsWith": "app/settings/preferences-panel.tsx" } } + }, + { + "id": "client-onclick", + "surface": "Client event handler throw", + "requirement": "COV-1", + "trigger": { "kind": "browser", "page": "/invoices/INV-3003", "actions": [{ "click": "button:has-text('Download CSV')" }], "expectPageError": "CE03" }, + "throws": { "match": "CE03", "file": "app/invoices/[id]/invoice-actions.tsx" }, + "expect": { "count": 1, "lib": "web", "linked": true, "handled": false, "release": true, "topFrame": { "resolved": true, "sourceEndsWith": "app/invoices/[id]/invoice-actions.tsx" } } + }, + { + "id": "rsc-render-navigation", + "surface": "Server Component render error on a document navigation (cookie only)", + "requirement": "COV-2", + "trigger": { "kind": "browser", "page": "/", "actions": [{ "wait": 2500 }, { "goto": "/invoices/INV-4471" }] }, + "throws": { "match": "TM71", "file": "lib/invoices.ts" }, + "expect": { "count": 1, "lib": "posthog-node", "linked": true, "request": true, "release": true, "topFrame": { "resolved": true, "sourceEndsWith": "lib/invoices.ts" } } + }, + { + "id": "metadata-cookie", + "surface": "generateMetadata throws, page request carrying only the PostHog cookie", + "requirement": "COV-2", + "trigger": { "kind": "http", "path": "/invoices/INV-0913", "identity": "cookie" }, + "throws": { "match": "MX13", "file": "lib/invoices.ts" }, + "expect": { "count": 1, "lib": "posthog-node", "linked": true, "request": true, "release": true, "topFrame": { "resolved": true, "sourceEndsWith": "lib/invoices.ts" } } + }, + { + "id": "route-handler-headers", + "surface": "Route Handler throws, request with X-POSTHOG-* tracing headers", + "requirement": "CTX-1", + "trigger": { "kind": "http", "path": "/api/invoices/INV-5150/pdf", "identity": "headers", "expectStatus": 500 }, + "throws": { "match": "PD50", "file": "lib/invoices.ts" }, + "expect": { "count": 1, "lib": "posthog-node", "linked": true, "request": true, "release": true, "topFrame": { "resolved": true, "sourceEndsWith": "lib/invoices.ts" } } + }, + { + "id": "route-handler-browser", + "surface": "Route Handler throws, called by fetch from the page", + "requirement": "COV-2", + "trigger": { "kind": "browser", "page": "/invoices/INV-2202", "actions": [{ "wait": 1500 }, { "click": "button:has-text('Send reminder')" }], "expectText": "Could not send reminder" }, + "throws": { "match": "RQ02", "file": "lib/invoices.ts" }, + "expect": { "count": 1, "lib": "posthog-node", "linked": true, "request": true, "release": true, "topFrame": { "resolved": true, "sourceEndsWith": "lib/invoices.ts" } } + }, + { + "id": "server-action-form", + "surface": "Server Action throws, submitted from a form", + "requirement": "COV-2", + "trigger": { "kind": "browser", "page": "/invoices/new", "actions": [{ "wait": 1500 }, { "fill": { "selector": "input[name=customer]", "value": "ACME-0042" } }, { "click": "button[type=submit]" }] }, + "throws": { "match": "CL42", "file": "lib/invoices.ts" }, + "expect": { "count": 1, "lib": "posthog-node", "linked": true, "request": true, "release": true, "topFrame": { "resolved": true, "sourceEndsWith": "lib/invoices.ts" } } + }, + { + "id": "proxy-cookie", + "surface": "proxy.ts throws (Next 16, Node.js runtime)", + "requirement": "COV-2", + "trigger": { "kind": "http", "path": "/portal/~bad", "identity": "cookie", "expectStatus": 500 }, + "throws": { "match": "TN09", "file": "proxy.ts" }, + "expect": { "count": 1, "lib": "posthog-", "linked": true, "request": true, "release": true, "topFrame": { "resolved": true, "sourceEndsWith": "proxy.ts" } } + }, + { + "id": "edge-route-headers", + "surface": "Route Handler on the edge runtime throws", + "requirement": "COV-3", + "trigger": { "kind": "http", "path": "/api/geo?country=ZZ", "identity": "headers", "expectStatus": 500 }, + "throws": { "match": "GX01", "file": "app/api/geo/route.ts" }, + "expect": { "count": 1, "lib": "posthog-", "linked": true, "request": true, "release": true, "topFrame": { "resolved": true, "sourceEndsWith": "app/api/geo/route.ts" } } + }, + { + "id": "after-callback", + "surface": "after() callback throws (onRequestError is not called)", + "requirement": "COV-2", + "trigger": { "kind": "http", "method": "POST", "path": "/api/webhooks/payment", "body": { "id": "PAY-3321", "invoice": "INV-2202" }, "identity": "none", "expectStatus": 200 }, + "throws": { "match": "RC21", "file": "lib/invoices.ts" }, + "expect": { "count": 1, "lib": "posthog-node", "release": true, "topFrame": { "resolved": true, "sourceEndsWith": "lib/invoices.ts" } } + }, + { + "id": "background-rejection", + "surface": "Fire-and-forget promise in a Route Handler rejects (process unhandledRejection)", + "requirement": "COV-1", + "trigger": { "kind": "http", "method": "POST", "path": "/api/exports", "body": { "batch": "EX-77" }, "identity": "headers", "expectStatus": 202 }, + "throws": { "match": "EX77", "file": "app/api/exports/route.ts" }, + "expect": { "count": 1, "lib": "posthog-node", "handled": false, "release": true, "topFrame": { "resolved": true, "sourceEndsWith": "app/api/exports/route.ts" } } + }, + { + "id": "uncaught-timer", + "surface": "Timer callback throws on the Node server (process uncaughtException)", + "requirement": "COV-1", + "trigger": { "kind": "http", "method": "POST", "path": "/api/cache/warm", "body": { "region": "eu-2" }, "identity": "none", "expectStatus": 202 }, + "throws": { "match": "CW02", "file": "app/api/cache/warm/route.ts" }, + "expect": { "count": 1, "lib": "posthog-node", "handled": false, "release": true, "topFrame": { "resolved": true, "sourceEndsWith": "app/api/cache/warm/route.ts" } } + }, + { + "id": "not-found", + "surface": "notFound() control flow", + "requirement": "PRE-2", + "trigger": { "kind": "http", "path": "/invoices/INV-9999", "identity": "cookie", "expectStatus": 404 }, + "throws": { "match": "NEXT_HTTP_ERROR_FALLBACK" }, + "expect": { "count": 0 } + }, + { + "id": "action-redirect", + "surface": "redirect() inside a Server Action", + "requirement": "PRE-2", + "trigger": { "kind": "browser", "page": "/invoices/new", "actions": [{ "wait": 1500 }, { "fill": { "selector": "input[name=customer]", "value": "ACME-0007" } }, { "click": "button[type=submit]" }, { "wait": 1500 }], "expectText": "Annual support plan" }, + "throws": { "match": "NEXT_REDIRECT" }, + "expect": { "count": 0 } + } + ] +} diff --git a/apps/error-tracking/next-16-app/.gitignore b/apps/error-tracking/next-16-app/.gitignore new file mode 100644 index 000000000..16bd2039d --- /dev/null +++ b/apps/error-tracking/next-16-app/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +.env +.next/ +next-env.d.ts diff --git a/apps/error-tracking/next-16-app/README.md b/apps/error-tracking/next-16-app/README.md new file mode 100644 index 000000000..aa0f81401 --- /dev/null +++ b/apps/error-tracking/next-16-app/README.md @@ -0,0 +1,22 @@ +# Ledgerly + +A small invoicing app: invoices, a revenue report, account settings and a +customer portal. Next.js 16 with the App Router. + +```bash +npm ci +npm run dev # http://localhost:3000 +npm run build && npm start +``` + +- `app/invoices` — invoice list, invoice pages (PDF download, reminders, CSV + export) and the new-invoice form (server action) +- `app/reports` — revenue report with a selectable range +- `app/settings` — preferences, including an import of settings saved by the + old app +- `app/portal/[tenant]` — customer portal, resolved per tenant in `proxy.ts` +- `app/api` — PDF rendering, reminders, payment webhooks, CSV exports, cache + warm-up and a tax-region lookup that runs on the edge +- `lib/` — invoice data and billing rules; `lib/posthog-server.ts` is the + server-side PostHog client +- `instrumentation-client.ts` — PostHog product analytics in the browser diff --git a/apps/error-tracking/next-16-app/app/api/cache/warm/route.ts b/apps/error-tracking/next-16-app/app/api/cache/warm/route.ts new file mode 100644 index 000000000..ba87bfc29 --- /dev/null +++ b/apps/error-tracking/next-16-app/app/api/cache/warm/route.ts @@ -0,0 +1,13 @@ +import { CacheWarmError } from '@/lib/errors'; + +const REGIONS = ['us-1', 'us-2', 'eu-1']; + +export async function POST(request: Request) { + const { region } = (await request.json()) as { region: string }; + setTimeout(() => { + if (!REGIONS.includes(region)) { + throw new CacheWarmError(`Cache warm failed for region ${region} (CW02)`); + } + }, 20); + return Response.json({ region, status: 'warming' }, { status: 202 }); +} diff --git a/apps/error-tracking/next-16-app/app/api/exports/route.ts b/apps/error-tracking/next-16-app/app/api/exports/route.ts new file mode 100644 index 000000000..fc1bcc7dc --- /dev/null +++ b/apps/error-tracking/next-16-app/app/api/exports/route.ts @@ -0,0 +1,14 @@ +import { ExportWriteError } from '@/lib/errors'; + +async function writeExport(batch: string): Promise { + await new Promise((resolve) => setTimeout(resolve, 50)); + if (batch.startsWith('EX-7')) { + throw new ExportWriteError(`CSV export batch ${batch} failed to write (EX77)`); + } +} + +export async function POST(request: Request) { + const { batch } = (await request.json()) as { batch: string }; + void writeExport(batch); + return Response.json({ batch, status: 'started' }, { status: 202 }); +} diff --git a/apps/error-tracking/next-16-app/app/api/geo/route.ts b/apps/error-tracking/next-16-app/app/api/geo/route.ts new file mode 100644 index 000000000..11cbac022 --- /dev/null +++ b/apps/error-tracking/next-16-app/app/api/geo/route.ts @@ -0,0 +1,14 @@ +import { TaxRegionError } from '@/lib/errors'; + +export const runtime = 'edge'; + +const TAX_REGIONS: Record = { US: 'us-sales-tax', DE: 'eu-vat', GB: 'uk-vat', PL: 'eu-vat' }; + +export async function GET(request: Request) { + const country = new URL(request.url).searchParams.get('country') ?? 'US'; + const region = TAX_REGIONS[country]; + if (!region) { + throw new TaxRegionError(`No tax region for country ${country} (GX01)`); + } + return Response.json({ country, region }); +} diff --git a/apps/error-tracking/next-16-app/app/api/invoices/[id]/pdf/route.ts b/apps/error-tracking/next-16-app/app/api/invoices/[id]/pdf/route.ts new file mode 100644 index 000000000..5d079e056 --- /dev/null +++ b/apps/error-tracking/next-16-app/app/api/invoices/[id]/pdf/route.ts @@ -0,0 +1,10 @@ +import { findInvoice, renderPdf } from '@/lib/invoices'; + +export async function GET(_request: Request, { params }: { params: Promise<{ id: string }> }) { + const { id } = await params; + const invoice = findInvoice(id); + if (!invoice) return Response.json({ error: 'not_found' }, { status: 404 }); + return new Response(renderPdf(invoice), { + headers: { 'Content-Type': 'application/pdf', 'Content-Disposition': `attachment; filename="${id}.pdf"` }, + }); +} diff --git a/apps/error-tracking/next-16-app/app/api/invoices/[id]/remind/route.ts b/apps/error-tracking/next-16-app/app/api/invoices/[id]/remind/route.ts new file mode 100644 index 000000000..51053f6a8 --- /dev/null +++ b/apps/error-tracking/next-16-app/app/api/invoices/[id]/remind/route.ts @@ -0,0 +1,11 @@ +import { findInvoice, queueReminder } from '@/lib/invoices'; +import { posthogServer } from '@/lib/posthog-server'; + +export async function POST(_request: Request, { params }: { params: Promise<{ id: string }> }) { + const { id } = await params; + const invoice = findInvoice(id); + if (!invoice) return Response.json({ error: 'not_found' }, { status: 404 }); + await queueReminder(invoice); + posthogServer().capture({ distinctId: invoice.customerId, event: 'reminder_queued', properties: { invoice: invoice.id } }); + return Response.json({ queued: true }); +} diff --git a/apps/error-tracking/next-16-app/app/api/webhooks/payment/route.ts b/apps/error-tracking/next-16-app/app/api/webhooks/payment/route.ts new file mode 100644 index 000000000..f3a275f07 --- /dev/null +++ b/apps/error-tracking/next-16-app/app/api/webhooks/payment/route.ts @@ -0,0 +1,11 @@ +import { after } from 'next/server'; +import { reconcilePayment } from '@/lib/invoices'; + +type PaymentEvent = { id: string; invoice: string }; + +export async function POST(request: Request) { + const event = (await request.json()) as PaymentEvent; + // Acknowledge the provider right away; reconciliation runs after the response. + after(() => reconcilePayment(event.id, event.invoice)); + return Response.json({ received: true }); +} diff --git a/apps/error-tracking/next-16-app/app/global-error.tsx b/apps/error-tracking/next-16-app/app/global-error.tsx new file mode 100644 index 000000000..6b8e7443c --- /dev/null +++ b/apps/error-tracking/next-16-app/app/global-error.tsx @@ -0,0 +1,17 @@ +'use client'; + +import './globals.css'; + +export default function GlobalError({ reset }: { error: Error & { digest?: string }; reset: () => void }) { + return ( + + +
+

Ledgerly hit a snag

+

Something went wrong on our side. Please try again.

+ +
+ + + ); +} diff --git a/apps/error-tracking/next-16-app/app/globals.css b/apps/error-tracking/next-16-app/app/globals.css new file mode 100644 index 000000000..8149faedf --- /dev/null +++ b/apps/error-tracking/next-16-app/app/globals.css @@ -0,0 +1,8 @@ +body { font-family: system-ui, sans-serif; margin: 0; color: #1f2933; } +header { display: flex; gap: 1rem; padding: 1rem 2rem; background: #102a43; } +header a { color: #f0f4f8; text-decoration: none; } +main { padding: 2rem; max-width: 56rem; } +table { border-collapse: collapse; width: 100%; } +td, th { border-bottom: 1px solid #d9e2ec; padding: 0.4rem; text-align: left; } +button { padding: 0.4rem 0.8rem; margin-right: 0.5rem; } +.notice { padding: 0.6rem; background: #fff3c4; } diff --git a/apps/error-tracking/next-16-app/app/invoices/[id]/invoice-actions.tsx b/apps/error-tracking/next-16-app/app/invoices/[id]/invoice-actions.tsx new file mode 100644 index 000000000..90ac31225 --- /dev/null +++ b/apps/error-tracking/next-16-app/app/invoices/[id]/invoice-actions.tsx @@ -0,0 +1,49 @@ +'use client'; + +import posthog from 'posthog-js'; +import { useState } from 'react'; +import { CsvExportError } from '@/lib/errors'; +import type { Invoice } from '@/lib/invoices'; + +const COLUMN_MAPS: Record = { + 'ACME-0007': ['description', 'amount'], + 'GLBX-0101': ['description', 'amount'], + 'UMBR-0471': ['description', 'amount'], + 'HOOL-0515': ['description', 'amount'], +}; + +function toCsv(invoice: Invoice): string { + const columns = COLUMN_MAPS[invoice.customerId]; + if (!columns) { + throw new CsvExportError(`CSV encoder has no column map for ${invoice.id} (CE03)`); + } + return [columns.join(','), ...invoice.lines.map((l) => `"${l.description}",${l.amountCents}`)].join('\n'); +} + +export function InvoiceActions({ invoice }: { invoice: Invoice }) { + const [status, setStatus] = useState(null); + + async function sendReminder() { + setStatus('Sending…'); + const res = await fetch(`/api/invoices/${invoice.id}/remind`, { method: 'POST' }); + if (res.ok) posthog.capture('reminder_sent', { invoice: invoice.id }); + setStatus(res.ok ? 'Reminder sent' : 'Could not send reminder'); + } + + function downloadCsv() { + const blob = new Blob([toCsv(invoice)], { type: 'text/csv' }); + const link = document.createElement('a'); + link.href = URL.createObjectURL(blob); + link.download = `${invoice.id}.csv`; + link.click(); + } + + return ( +

+ Download PDF{' '} + + + {status && {status}} +

+ ); +} diff --git a/apps/error-tracking/next-16-app/app/invoices/[id]/page.tsx b/apps/error-tracking/next-16-app/app/invoices/[id]/page.tsx new file mode 100644 index 000000000..14ec8236b --- /dev/null +++ b/apps/error-tracking/next-16-app/app/invoices/[id]/page.tsx @@ -0,0 +1,44 @@ +import type { Metadata } from 'next'; +import { notFound } from 'next/navigation'; +import { customerName, findInvoice, formatMoney, reconcileTotals } from '@/lib/invoices'; +import { InvoiceActions } from './invoice-actions'; + +type Props = { params: Promise<{ id: string }> }; + +export async function generateMetadata({ params }: Props): Promise { + const { id } = await params; + const invoice = findInvoice(id); + if (!invoice) return { title: 'Invoice not found' }; + return { title: `${invoice.id} · ${customerName(invoice)}` }; +} + +export default async function InvoicePage({ params }: Props) { + const { id } = await params; + const invoice = findInvoice(id); + if (!invoice) notFound(); + const total = reconcileTotals(invoice); + + return ( + <> +

{invoice.id}

+

+ Customer {invoice.customerId} · issued {invoice.issuedOn} · {invoice.status} +

+ + + {invoice.lines.map((line, i) => ( + + + + + ))} + + + + + +
{line.description}{formatMoney(line.amountCents)}
Total{formatMoney(total)}
+ + + ); +} diff --git a/apps/error-tracking/next-16-app/app/invoices/new/actions.ts b/apps/error-tracking/next-16-app/app/invoices/new/actions.ts new file mode 100644 index 000000000..ab18f388d --- /dev/null +++ b/apps/error-tracking/next-16-app/app/invoices/new/actions.ts @@ -0,0 +1,20 @@ +'use server'; + +import { redirect } from 'next/navigation'; +import { checkCredit, listInvoices } from '@/lib/invoices'; +import { posthogServer } from '@/lib/posthog-server'; + +export async function createInvoice(formData: FormData) { + const customerId = String(formData.get('customer') ?? '').trim().toUpperCase(); + const amountCents = Math.round(Number(formData.get('amount') ?? 0) * 100); + + const open = listInvoices().find((i) => i.customerId === customerId && i.status !== 'draft'); + if (open) { + // Customers with an open invoice get their existing invoice instead of a duplicate. + redirect(`/invoices/${open.id}`); + } + + checkCredit(customerId, amountCents); + posthogServer().capture({ distinctId: customerId, event: 'invoice_created', properties: { amount_cents: amountCents } }); + redirect('/?created=1'); +} diff --git a/apps/error-tracking/next-16-app/app/invoices/new/page.tsx b/apps/error-tracking/next-16-app/app/invoices/new/page.tsx new file mode 100644 index 000000000..94f72a25b --- /dev/null +++ b/apps/error-tracking/next-16-app/app/invoices/new/page.tsx @@ -0,0 +1,22 @@ +import { createInvoice } from './actions'; + +export default function NewInvoicePage() { + return ( + <> +

New invoice

+
+

+ +

+

+ +

+ +
+ + ); +} diff --git a/apps/error-tracking/next-16-app/app/layout.tsx b/apps/error-tracking/next-16-app/app/layout.tsx new file mode 100644 index 000000000..8877f1ca2 --- /dev/null +++ b/apps/error-tracking/next-16-app/app/layout.tsx @@ -0,0 +1,24 @@ +import type { Metadata } from 'next'; +import Link from 'next/link'; +import './globals.css'; + +export const metadata: Metadata = { + title: 'Ledgerly', + description: 'Invoices for small teams', +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + +
+ Invoices + New invoice + Reports + Settings +
+
{children}
+ + + ); +} diff --git a/apps/error-tracking/next-16-app/app/page.tsx b/apps/error-tracking/next-16-app/app/page.tsx new file mode 100644 index 000000000..be6f6e894 --- /dev/null +++ b/apps/error-tracking/next-16-app/app/page.tsx @@ -0,0 +1,33 @@ +import Link from 'next/link'; +import { formatMoney, listInvoices } from '@/lib/invoices'; + +export default function InvoicesPage() { + const invoices = listInvoices(); + return ( + <> +

Invoices

+ + + + + + + + + + + {invoices.map((invoice) => ( + + + + + + + ))} + +
InvoiceIssuedStatusTotal
+ {invoice.id} + {invoice.issuedOn}{invoice.status}{formatMoney(invoice.totalCents)}
+ + ); +} diff --git a/apps/error-tracking/next-16-app/app/portal/[tenant]/page.tsx b/apps/error-tracking/next-16-app/app/portal/[tenant]/page.tsx new file mode 100644 index 000000000..be80b2723 --- /dev/null +++ b/apps/error-tracking/next-16-app/app/portal/[tenant]/page.tsx @@ -0,0 +1,9 @@ +export default async function PortalPage({ params }: { params: Promise<{ tenant: string }> }) { + const { tenant } = await params; + return ( + <> +

Customer portal

+

Signed in to {tenant}.

+ + ); +} diff --git a/apps/error-tracking/next-16-app/app/reports/error.tsx b/apps/error-tracking/next-16-app/app/reports/error.tsx new file mode 100644 index 000000000..e057ad9dc --- /dev/null +++ b/apps/error-tracking/next-16-app/app/reports/error.tsx @@ -0,0 +1,10 @@ +'use client'; + +export default function ReportsError({ reset }: { error: Error & { digest?: string }; reset: () => void }) { + return ( +
+

We couldn't load this report.

+ +
+ ); +} diff --git a/apps/error-tracking/next-16-app/app/reports/page.tsx b/apps/error-tracking/next-16-app/app/reports/page.tsx new file mode 100644 index 000000000..8e5635b91 --- /dev/null +++ b/apps/error-tracking/next-16-app/app/reports/page.tsx @@ -0,0 +1,10 @@ +import { RevenueChart } from './revenue-chart'; + +export default function ReportsPage() { + return ( + <> +

Revenue

+ + + ); +} diff --git a/apps/error-tracking/next-16-app/app/reports/revenue-chart.tsx b/apps/error-tracking/next-16-app/app/reports/revenue-chart.tsx new file mode 100644 index 000000000..98713701f --- /dev/null +++ b/apps/error-tracking/next-16-app/app/reports/revenue-chart.tsx @@ -0,0 +1,36 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { ReportRangeError } from '@/lib/errors'; +import { formatMoney, listInvoices } from '@/lib/invoices'; + +const RANGES: Record = { + q3: ['2026-07-01', '2026-09-30'], + q4: ['2026-10-01', '2026-12-31'], + ytd: ['2026-01-01', '2026-12-31'], +}; + +function revenueFor(range: string): number { + const bounds = RANGES[range]; + if (!bounds) { + throw new ReportRangeError(`Unsupported report range "${range}" (RX20)`); + } + return listInvoices() + .filter((i) => i.issuedOn >= bounds[0] && i.issuedOn <= bounds[1]) + .reduce((acc, i) => acc + i.totalCents, 0); +} + +export function RevenueChart() { + const [range, setRange] = useState(null); + + useEffect(() => { + setRange(new URLSearchParams(window.location.search).get('range') ?? 'q4'); + }, []); + + if (!range) return

Loading…

; + return ( +

+ Revenue for {range}: {formatMoney(revenueFor(range))} +

+ ); +} diff --git a/apps/error-tracking/next-16-app/app/settings/page.tsx b/apps/error-tracking/next-16-app/app/settings/page.tsx new file mode 100644 index 000000000..794d8f811 --- /dev/null +++ b/apps/error-tracking/next-16-app/app/settings/page.tsx @@ -0,0 +1,10 @@ +import { PreferencesPanel } from './preferences-panel'; + +export default function SettingsPage() { + return ( + <> +

Settings

+ + + ); +} diff --git a/apps/error-tracking/next-16-app/app/settings/preferences-panel.tsx b/apps/error-tracking/next-16-app/app/settings/preferences-panel.tsx new file mode 100644 index 000000000..cfd54fdd9 --- /dev/null +++ b/apps/error-tracking/next-16-app/app/settings/preferences-panel.tsx @@ -0,0 +1,36 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { PreferencesParseError } from '@/lib/errors'; + +type Preferences = { currency: string; reminderDays: number }; + +// Settings exported by the previous version of the app: "currency|reminderDays". +const LEGACY_EXPORT = 'USD;14'; + +function parseLegacy(raw: string): Preferences { + const [currency, days] = raw.split('|'); + if (!days) { + throw new PreferencesParseError('Saved preferences are corrupt (PF88)'); + } + return { currency, reminderDays: Number(days) }; +} + +export function PreferencesPanel() { + const [legacy, setLegacy] = useState(null); + + useEffect(() => { + if (new URLSearchParams(window.location.search).get('import') === 'legacy') setLegacy(LEGACY_EXPORT); + }, []); + + const prefs: Preferences = legacy ? parseLegacy(legacy) : { currency: 'USD', reminderDays: 7 }; + return ( + + ); +} diff --git a/apps/error-tracking/next-16-app/instrumentation-client.ts b/apps/error-tracking/next-16-app/instrumentation-client.ts new file mode 100644 index 000000000..b6ae0a154 --- /dev/null +++ b/apps/error-tracking/next-16-app/instrumentation-client.ts @@ -0,0 +1,6 @@ +import posthog from 'posthog-js'; + +posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, { + api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST ?? 'https://us.i.posthog.com', + defaults: '2026-01-30', +}); diff --git a/apps/error-tracking/next-16-app/lib/errors.ts b/apps/error-tracking/next-16-app/lib/errors.ts new file mode 100644 index 000000000..05294a457 --- /dev/null +++ b/apps/error-tracking/next-16-app/lib/errors.ts @@ -0,0 +1,51 @@ +export class InvoiceTotalMismatchError extends Error { + override name = 'InvoiceTotalMismatchError'; +} + +export class CustomerLookupError extends Error { + override name = 'CustomerLookupError'; +} + +export class ReportRangeError extends Error { + override name = 'ReportRangeError'; +} + +export class PreferencesParseError extends Error { + override name = 'PreferencesParseError'; +} + +export class CsvExportError extends Error { + override name = 'CsvExportError'; +} + +export class PdfLayoutError extends Error { + override name = 'PdfLayoutError'; +} + +export class ReminderQueueError extends Error { + override name = 'ReminderQueueError'; +} + +export class CreditLimitError extends Error { + override name = 'CreditLimitError'; +} + +export class TenantResolutionError extends Error { + override name = 'TenantResolutionError'; +} + +export class TaxRegionError extends Error { + override name = 'TaxRegionError'; +} + +export class PaymentReconcileError extends Error { + override name = 'PaymentReconcileError'; +} + +export class ExportWriteError extends Error { + override name = 'ExportWriteError'; +} + +export class CacheWarmError extends Error { + override name = 'CacheWarmError'; +} diff --git a/apps/error-tracking/next-16-app/lib/invoices.ts b/apps/error-tracking/next-16-app/lib/invoices.ts new file mode 100644 index 000000000..0153b6795 --- /dev/null +++ b/apps/error-tracking/next-16-app/lib/invoices.ts @@ -0,0 +1,149 @@ +import { + CreditLimitError, + CustomerLookupError, + InvoiceTotalMismatchError, + PaymentReconcileError, + PdfLayoutError, + ReminderQueueError, +} from './errors'; + +export type InvoiceLine = { description: string; amountCents: number }; + +export type Invoice = { + id: string; + customerId: string; + issuedOn: string; + status: 'draft' | 'sent' | 'paid'; + lines: InvoiceLine[]; + totalCents: number; +}; + +const CUSTOMERS: Record = { + 'ACME-0007': { name: 'Acme Corp', creditLimitCents: 5_000_000 }, + 'ACME-0042': { name: 'Acme Logistics', creditLimitCents: 0 }, + 'GLBX-0101': { name: 'Globex', creditLimitCents: 2_000_000 }, + 'INIT-0300': { name: 'Initech', creditLimitCents: 1_000_000 }, + 'UMBR-0471': { name: 'Umbrella Health', creditLimitCents: 3_000_000 }, + 'HOOL-0515': { name: 'Hooli', creditLimitCents: 9_000_000 }, +}; + +const INVOICES: Invoice[] = [ + { + id: 'INV-1001', + customerId: 'ACME-0007', + issuedOn: '2026-09-02', + status: 'paid', + lines: [{ description: 'Annual support plan', amountCents: 480_000 }], + totalCents: 480_000, + }, + { + id: 'INV-2202', + customerId: 'GLBX-0101', + issuedOn: '2026-09-18', + status: 'sent', + lines: [ + { description: 'Onboarding workshop', amountCents: 150_000 }, + { description: 'Travel', amountCents: 32_500 }, + ], + totalCents: 182_500, + }, + { + id: 'INV-3003', + customerId: 'INIT-0300', + issuedOn: '2026-10-01', + status: 'draft', + lines: [{ description: 'TPS report automation', amountCents: 99_000 }], + totalCents: 99_000, + }, + { + id: 'INV-4471', + customerId: 'UMBR-0471', + issuedOn: '2026-10-04', + status: 'sent', + lines: [ + { description: 'Data migration', amountCents: 240_000 }, + { description: 'Discount', amountCents: -24_000 }, + ], + totalCents: 240_000, + }, + { + id: 'INV-5150', + customerId: 'HOOL-0515', + issuedOn: '2026-10-09', + status: 'sent', + lines: Array.from({ length: 64 }, (_, i) => ({ + description: `Compute reservation block ${i + 1}`, + amountCents: 12_000, + })), + totalCents: 64 * 12_000, + }, + { + id: 'INV-0913', + customerId: 'CUST-0913', + issuedOn: '2026-10-12', + status: 'draft', + lines: [{ description: 'Consulting', amountCents: 75_000 }], + totalCents: 75_000, + }, +]; + +const LINES_PER_PDF_PAGE = 48; +const BILLING_DATE = '2026-10-28'; + +export function listInvoices(): Invoice[] { + return INVOICES; +} + +export function findInvoice(id: string): Invoice | undefined { + return INVOICES.find((invoice) => invoice.id === id); +} + +export function customerName(invoice: Invoice): string { + const customer = CUSTOMERS[invoice.customerId]; + if (!customer) { + throw new CustomerLookupError(`Customer record missing for ${invoice.id} (MX13)`); + } + return customer.name; +} + +export function reconcileTotals(invoice: Invoice): number { + const sum = invoice.lines.reduce((acc, line) => acc + line.amountCents, 0); + if (sum !== invoice.totalCents) { + throw new InvoiceTotalMismatchError(`Invoice total mismatch for ${invoice.id} (TM71)`); + } + return sum; +} + +export function renderPdf(invoice: Invoice): string { + if (invoice.lines.length > LINES_PER_PDF_PAGE) { + throw new PdfLayoutError(`PDF layout overflow on ${invoice.id} (PD50)`); + } + const body = invoice.lines.map((l) => `${l.description}\t${formatMoney(l.amountCents)}`).join('\n'); + return `INVOICE ${invoice.id}\n${body}\nTOTAL ${formatMoney(invoice.totalCents)}\n`; +} + +export async function queueReminder(invoice: Invoice): Promise { + // The reminder queue only accepts invoices that were sent less than 30 days ago. + const ageDays = (Date.parse(BILLING_DATE) - Date.parse(invoice.issuedOn)) / 86_400_000; + if (invoice.status === 'sent' && ageDays > 30) { + throw new ReminderQueueError(`Reminder queue rejected ${invoice.id} (RQ02)`); + } +} + +export function checkCredit(customerId: string, amountCents: number): void { + const customer = CUSTOMERS[customerId]; + if (customer && amountCents > customer.creditLimitCents) { + throw new CreditLimitError(`Credit limit check failed for ${customerId} (CL42)`); + } +} + +export async function reconcilePayment(paymentId: string, invoiceId: string): Promise { + const invoice = findInvoice(invoiceId); + if (!invoice || invoice.status !== 'paid') { + throw new PaymentReconcileError(`Payment reconcile failed for ${paymentId} (RC21)`); + } +} + +export function formatMoney(cents: number): string { + return `$${(cents / 100).toFixed(2)}`; +} diff --git a/apps/error-tracking/next-16-app/lib/posthog-server.ts b/apps/error-tracking/next-16-app/lib/posthog-server.ts new file mode 100644 index 000000000..bdcc35d34 --- /dev/null +++ b/apps/error-tracking/next-16-app/lib/posthog-server.ts @@ -0,0 +1,12 @@ +import { PostHog } from 'posthog-node'; + +let client: PostHog | undefined; + +export function posthogServer(): PostHog { + client ??= new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY!, { + host: process.env.NEXT_PUBLIC_POSTHOG_HOST ?? 'https://us.i.posthog.com', + flushAt: 1, + flushInterval: 0, + }); + return client; +} diff --git a/apps/error-tracking/next-16-app/next.config.ts b/apps/error-tracking/next-16-app/next.config.ts new file mode 100644 index 000000000..b22af960a --- /dev/null +++ b/apps/error-tracking/next-16-app/next.config.ts @@ -0,0 +1,5 @@ +import type { NextConfig } from 'next'; + +const nextConfig: NextConfig = {}; + +export default nextConfig; diff --git a/apps/error-tracking/next-16-app/package-lock.json b/apps/error-tracking/next-16-app/package-lock.json new file mode 100644 index 000000000..6b4098313 --- /dev/null +++ b/apps/error-tracking/next-16-app/package-lock.json @@ -0,0 +1,1230 @@ +{ + "name": "ledgerly", + "version": "0.4.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ledgerly", + "version": "0.4.0", + "dependencies": { + "next": "16.4.2", + "posthog-js": "^1.438.12", + "posthog-node": "^5.56.2", + "react": "19.3.1", + "react-dom": "19.3.1" + }, + "devDependencies": { + "@types/node": "22.10.0", + "@types/react": "19.1.0", + "@types/react-dom": "19.1.0", + "typescript": "5.7.2" + } + }, + "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/@next/env": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.4.2.tgz", + "integrity": "sha512-nTUTFH7G5/J/fmpkjVLegOkqyKOP0CEsJcMsyO4UCYeN1su+YMW/MfaSErnJskL7XM+UDK6aTvslxsRCyya/sA==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.4.2.tgz", + "integrity": "sha512-ONkhFvZNFWlsEHOM/81AtxdyuJP2KRYKYR0JxPwvcu1AuPMMMgcL+wW2luldJvMtZwKtUxoSkCychW5wIeoYSw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.4.2.tgz", + "integrity": "sha512-luPCMbOsCoaF+kXJEUjmjmOZg1lC3BflhmUu+ZmW4tBu2vW/4AXak8JBFvePizGkxVmNiAw/r9ra+6UviCWtWQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.4.2.tgz", + "integrity": "sha512-xhwFTAncJyaCNy5KqKKuKaIIdCy1/1ZV1s5S1G0hhT7wGMkDTlfJ6aQbfRFnIG/s3/kDrxQ8aycVe+c5QnP8ow==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.4.2.tgz", + "integrity": "sha512-T1PNmfbPcxr5ndtWUL/3wQtLXsz808lq6sc2GrNT5k2oquF7UyqZddHSuAewHVsWsGXe6nsl9HoN1YNriVjuSg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.4.2.tgz", + "integrity": "sha512-N9cDUOCGtkJG5PNKDFZUF3xXndrJJcpLp121uAGjIGvO/U2p9tfdNOM9htj7+5f72l0m8rMw/cvYacUJBQoudg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.4.2.tgz", + "integrity": "sha512-yY2uh1YhX6ufuYy2FCbw1dqblbhI57GLYAJfCkqD9auTv6pLrH67Uccjc3QMKBeCYAG6lkgu08ExmX1bbjejjQ==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.4.2.tgz", + "integrity": "sha512-j/ArWRIHpMMB6wYLMhr2puTguBt70lNU/Cmjl9p6dTDE8DGknYJMP9PdwPk2dqpCNL/KIX9yfngDvIesxJPmsg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.4.2.tgz", + "integrity": "sha512-AA4edyWHDko3NCSo0x6GomsdgFb0oR1J3MXU1b5xt6FRFg4AucrMPW3S0hCe/9onU0O1BbpGQBsqo1r0Gneu1g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@posthog/browser-common": { + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/@posthog/browser-common/-/browser-common-0.9.5.tgz", + "integrity": "sha512-oXnBgZvM8UQ84IcBLfGP0WSGd9rA0PCGR37H8Pw0NszJRwxwbJnFXIUih0pvI/ze2eGmzkIEC9i3SwC+bMAgjQ==", + "license": "MIT", + "dependencies": { + "@posthog/core": "^1.59.1", + "@posthog/types": "^1.416.0" + } + }, + "node_modules/@posthog/core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.59.1.tgz", + "integrity": "sha512-P39YgzQVDh6oz6wAzQXEnoHgTYpM7gifonmVKq1scAJco79h31XmmNVurRq9tTmmyIrr1uX8FHT0riCsyfV9AQ==", + "license": "MIT", + "dependencies": { + "@posthog/types": "^1.416.0" + } + }, + "node_modules/@posthog/types": { + "version": "1.416.0", + "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.416.0.tgz", + "integrity": "sha512-0/9+02s/0uDyrH4G/qUg1yyl6gXUg8Uuxw/PlDad3KLzo9FFS9zS/kAQaEshx/saVGupj1aAy/4EUBxWisBogw==", + "license": "MIT" + }, + "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/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/react": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.0.tgz", + "integrity": "sha512-UaicktuQI+9UKyA4njtDOGBD/67t8YEBt2xdfqu8+gP9hqPUPsiXlNPcpS2gVdjmis5GKPG3fCxbQLVgxsQZ8w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-21E2zejNNRtjG4hKIyJz4aWswGEcNFTgttA0bZIRGjj1HA/tbSUxIJnIcYbn98pwJck0cS1bsQhn6eaKqbcFWw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.0.0" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.30", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.30.tgz", + "integrity": "sha512-YJs6Pz17RMNv8HoyKREtCY38E3BV1nPkWVmIb+XN1uHC1Vqt3KZCH/uJH9ZKRoiKb/icZ6tvehyee6RZNbFfRA==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "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/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/core-js": { + "version": "3.51.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.51.0.tgz", + "integrity": "sha512-7bDKx/5jm5MxzsXxc93g6Z2+4ZsuT/MddrfRmTDJNAkWsYWHU8EVNEsBiIEWnMAMCHMmRfUEjkuo3jeNBOIHRw==", + "hasInstallScript": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "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==", + "devOptional": true, + "license": "MIT" + }, + "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==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dompurify": { + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.17.tgz", + "integrity": "sha512-B/pSlYVHQ58kqISD/yDjuJ1yfzI34m1gF4lAod2bwSRhiN4K5Cl3H8Af7hqB2PmTL911tdEudnnXn5FnhRoz0w==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/fflate": { + "version": "0.4.9", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.9.tgz", + "integrity": "sha512-zdxgIEddhfsyCaWpJ2SdXEP8ZMrKJ6+5jl4OupODcywU0IhRk6gdXuVGcPICyfx2H97hVK7xmJtRLPjkxAX8Vw==", + "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.4.2", + "resolved": "https://registry.npmjs.org/next/-/next-16.4.2.tgz", + "integrity": "sha512-r+oBJojfefUjOiWkBhXgLS+igxX/7BWyK59uHnyOyplMSLtBZ+QLP3okrdYTbrfrbrmNTVWLBPaHDX9KRaIt3w==", + "license": "MIT", + "dependencies": { + "@next/env": "16.4.2", + "@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.4.2", + "@next/swc-darwin-x64": "16.4.2", + "@next/swc-linux-arm64-gnu": "16.4.2", + "@next/swc-linux-arm64-musl": "16.4.2", + "@next/swc-linux-x64-gnu": "16.4.2", + "@next/swc-linux-x64-musl": "16.4.2", + "@next/swc-win32-arm64-msvc": "16.4.2", + "@next/swc-win32-x64-msvc": "16.4.2", + "sharp": "^0.35.5" + }, + "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/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/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/posthog-js": { + "version": "1.438.12", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.438.12.tgz", + "integrity": "sha512-hjhvrtqbk6pFlbtNEGk0FrWKKFdeXe/WWA/LppN0f+uDoHKR9/SZJOxUHSVCGYbGTJvNRoDjjQvZuTcq4oKSlg==", + "license": "(Apache-2.0 AND MIT)", + "dependencies": { + "@posthog/browser-common": "^0.9.5", + "@posthog/core": "^1.59.1", + "@posthog/types": "^1.416.0", + "core-js": "^3.49.0", + "dompurify": "^3.4.13", + "fflate": "^0.4.8", + "preact": "^10.29.3", + "query-selector-shadow-dom": "^1.0.1", + "web-vitals": "^6.2.1", + "web-vitals-soft-navs": "npm:web-vitals@6.2.1" + }, + "peerDependencies": { + "@types/react": ">=16.8.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/posthog-node": { + "version": "5.56.2", + "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.56.2.tgz", + "integrity": "sha512-1/qYFNuHkDJJuDHjAvozN/fH/+XdqzlIZ0WCsCYrhUgGNxDBw5M3kZaJaRX0YSPi9tn7PnZFD5eTXg2Z7m5LeQ==", + "license": "MIT", + "dependencies": { + "@posthog/core": "^1.59.1" + }, + "engines": { + "node": "^20.20.0 || >=22.22.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.2.0", + "rxjs": "^7.0.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "rxjs": { + "optional": true + } + } + }, + "node_modules/preact": { + "version": "10.29.8", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.8.tgz", + "integrity": "sha512-ej2aVZ+vZ8WO7tvlQWRM9N63A0KzF9q4mWJfDUHgYaIofWY9hu74QdnQrjoPMmZi2/nZ5gN0bJCQF49xQqx09Q==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + }, + "peerDependencies": { + "preact-render-to-string": ">=5" + }, + "peerDependenciesMeta": { + "preact-render-to-string": { + "optional": true + } + } + }, + "node_modules/query-selector-shadow-dom": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.1.tgz", + "integrity": "sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==", + "license": "MIT" + }, + "node_modules/react": { + "version": "19.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.1.tgz", + "integrity": "sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.1.tgz", + "integrity": "sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.1" + } + }, + "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/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/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.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/web-vitals": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-6.2.3.tgz", + "integrity": "sha512-Dmy+vdO1UPMqXiAtZSNayhl4f794Bxfd94W4HIj2HlYtczEQfEOon969memlcpcHrTuOKLa5baFjX7WbgmOq+A==", + "license": "Apache-2.0" + }, + "node_modules/web-vitals-soft-navs": { + "name": "web-vitals", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-6.2.1.tgz", + "integrity": "sha512-rLcLXA2sx6+9dE88NHFubwTtGxpK4yYBLj6qHPdFoCaLr0cXGb4efOqtKLlm4loGA4OEKHIQKMKzZkKyOh5ctw==", + "license": "Apache-2.0" + } + } +} diff --git a/apps/error-tracking/next-16-app/package.json b/apps/error-tracking/next-16-app/package.json new file mode 100644 index 000000000..32147dfe3 --- /dev/null +++ b/apps/error-tracking/next-16-app/package.json @@ -0,0 +1,23 @@ +{ + "name": "ledgerly", + "version": "0.4.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "next": "16.4.2", + "posthog-js": "^1.438.12", + "posthog-node": "^5.56.2", + "react": "19.3.1", + "react-dom": "19.3.1" + }, + "devDependencies": { + "@types/node": "22.10.0", + "@types/react": "19.1.0", + "@types/react-dom": "19.1.0", + "typescript": "5.7.2" + } +} diff --git a/apps/error-tracking/next-16-app/proxy.ts b/apps/error-tracking/next-16-app/proxy.ts new file mode 100644 index 000000000..4641c08df --- /dev/null +++ b/apps/error-tracking/next-16-app/proxy.ts @@ -0,0 +1,16 @@ +import { NextResponse, type NextRequest } from 'next/server'; +import { TenantResolutionError } from '@/lib/errors'; + +const TENANT_SLUG = /^[a-z0-9-]+$/; + +export function proxy(request: NextRequest) { + const slug = request.nextUrl.pathname.split('/')[2] ?? ''; + if (!TENANT_SLUG.test(slug)) { + throw new TenantResolutionError(`Tenant slug is not valid: ${slug} (TN09)`); + } + const response = NextResponse.next(); + response.headers.set('x-tenant', slug); + return response; +} + +export const config = { matcher: '/portal/:path*' }; diff --git a/apps/error-tracking/next-16-app/tsconfig.json b/apps/error-tracking/next-16-app/tsconfig.json new file mode 100644 index 000000000..e7ff3a26b --- /dev/null +++ b/apps/error-tracking/next-16-app/tsconfig.json @@ -0,0 +1,41 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "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/apps/error-tracking/no-posthog-next-14-app/.gitignore b/apps/error-tracking/no-posthog-next-14-app/.gitignore new file mode 100644 index 000000000..16bd2039d --- /dev/null +++ b/apps/error-tracking/no-posthog-next-14-app/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +.env +.next/ +next-env.d.ts diff --git a/apps/error-tracking/no-posthog-next-14-app/README.md b/apps/error-tracking/no-posthog-next-14-app/README.md new file mode 100644 index 000000000..a15cb118b --- /dev/null +++ b/apps/error-tracking/no-posthog-next-14-app/README.md @@ -0,0 +1,19 @@ +# Ledgerly + +A small invoicing app: invoices, a revenue report, account settings and a +customer portal. Next.js 14 with the App Router. + +```bash +npm ci +npm run dev # http://localhost:3000 +npm run build && npm start +``` + +- `app/invoices` — invoice list, invoice pages (PDF download, reminders, CSV + export) and the new-invoice form (server action) +- `app/reports` — revenue report with a selectable range +- `app/settings` — preferences, including an import of settings saved by the + old app +- `app/portal/[tenant]` — customer portal, resolved per tenant in `middleware.ts` +- `app/api` — PDF rendering, reminders, CSV exports, cache warm-up and a tax-region lookup that runs on the edge +- `lib/` — invoice data and billing rules diff --git a/apps/error-tracking/no-posthog-next-14-app/app/api/cache/warm/route.ts b/apps/error-tracking/no-posthog-next-14-app/app/api/cache/warm/route.ts new file mode 100644 index 000000000..ba87bfc29 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-14-app/app/api/cache/warm/route.ts @@ -0,0 +1,13 @@ +import { CacheWarmError } from '@/lib/errors'; + +const REGIONS = ['us-1', 'us-2', 'eu-1']; + +export async function POST(request: Request) { + const { region } = (await request.json()) as { region: string }; + setTimeout(() => { + if (!REGIONS.includes(region)) { + throw new CacheWarmError(`Cache warm failed for region ${region} (CW02)`); + } + }, 20); + return Response.json({ region, status: 'warming' }, { status: 202 }); +} diff --git a/apps/error-tracking/no-posthog-next-14-app/app/api/exports/route.ts b/apps/error-tracking/no-posthog-next-14-app/app/api/exports/route.ts new file mode 100644 index 000000000..fc1bcc7dc --- /dev/null +++ b/apps/error-tracking/no-posthog-next-14-app/app/api/exports/route.ts @@ -0,0 +1,14 @@ +import { ExportWriteError } from '@/lib/errors'; + +async function writeExport(batch: string): Promise { + await new Promise((resolve) => setTimeout(resolve, 50)); + if (batch.startsWith('EX-7')) { + throw new ExportWriteError(`CSV export batch ${batch} failed to write (EX77)`); + } +} + +export async function POST(request: Request) { + const { batch } = (await request.json()) as { batch: string }; + void writeExport(batch); + return Response.json({ batch, status: 'started' }, { status: 202 }); +} diff --git a/apps/error-tracking/no-posthog-next-14-app/app/api/geo/route.ts b/apps/error-tracking/no-posthog-next-14-app/app/api/geo/route.ts new file mode 100644 index 000000000..11cbac022 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-14-app/app/api/geo/route.ts @@ -0,0 +1,14 @@ +import { TaxRegionError } from '@/lib/errors'; + +export const runtime = 'edge'; + +const TAX_REGIONS: Record = { US: 'us-sales-tax', DE: 'eu-vat', GB: 'uk-vat', PL: 'eu-vat' }; + +export async function GET(request: Request) { + const country = new URL(request.url).searchParams.get('country') ?? 'US'; + const region = TAX_REGIONS[country]; + if (!region) { + throw new TaxRegionError(`No tax region for country ${country} (GX01)`); + } + return Response.json({ country, region }); +} diff --git a/apps/error-tracking/no-posthog-next-14-app/app/api/invoices/[id]/pdf/route.ts b/apps/error-tracking/no-posthog-next-14-app/app/api/invoices/[id]/pdf/route.ts new file mode 100644 index 000000000..1d3f274c0 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-14-app/app/api/invoices/[id]/pdf/route.ts @@ -0,0 +1,10 @@ +import { findInvoice, renderPdf } from '@/lib/invoices'; + +export async function GET(_request: Request, { params }: { params: { id: string } }) { + const { id } = params; + const invoice = findInvoice(id); + if (!invoice) return Response.json({ error: 'not_found' }, { status: 404 }); + return new Response(renderPdf(invoice), { + headers: { 'Content-Type': 'application/pdf', 'Content-Disposition': `attachment; filename="${id}.pdf"` }, + }); +} diff --git a/apps/error-tracking/no-posthog-next-14-app/app/api/invoices/[id]/remind/route.ts b/apps/error-tracking/no-posthog-next-14-app/app/api/invoices/[id]/remind/route.ts new file mode 100644 index 000000000..ca1eaa68f --- /dev/null +++ b/apps/error-tracking/no-posthog-next-14-app/app/api/invoices/[id]/remind/route.ts @@ -0,0 +1,9 @@ +import { findInvoice, queueReminder } from '@/lib/invoices'; + +export async function POST(_request: Request, { params }: { params: { id: string } }) { + const { id } = params; + const invoice = findInvoice(id); + if (!invoice) return Response.json({ error: 'not_found' }, { status: 404 }); + await queueReminder(invoice); + return Response.json({ queued: true }); +} diff --git a/apps/error-tracking/no-posthog-next-14-app/app/globals.css b/apps/error-tracking/no-posthog-next-14-app/app/globals.css new file mode 100644 index 000000000..8149faedf --- /dev/null +++ b/apps/error-tracking/no-posthog-next-14-app/app/globals.css @@ -0,0 +1,8 @@ +body { font-family: system-ui, sans-serif; margin: 0; color: #1f2933; } +header { display: flex; gap: 1rem; padding: 1rem 2rem; background: #102a43; } +header a { color: #f0f4f8; text-decoration: none; } +main { padding: 2rem; max-width: 56rem; } +table { border-collapse: collapse; width: 100%; } +td, th { border-bottom: 1px solid #d9e2ec; padding: 0.4rem; text-align: left; } +button { padding: 0.4rem 0.8rem; margin-right: 0.5rem; } +.notice { padding: 0.6rem; background: #fff3c4; } diff --git a/apps/error-tracking/no-posthog-next-14-app/app/invoices/[id]/invoice-actions.tsx b/apps/error-tracking/no-posthog-next-14-app/app/invoices/[id]/invoice-actions.tsx new file mode 100644 index 000000000..52be2eb66 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-14-app/app/invoices/[id]/invoice-actions.tsx @@ -0,0 +1,47 @@ +'use client'; + +import { useState } from 'react'; +import { CsvExportError } from '@/lib/errors'; +import type { Invoice } from '@/lib/invoices'; + +const COLUMN_MAPS: Record = { + 'ACME-0007': ['description', 'amount'], + 'GLBX-0101': ['description', 'amount'], + 'UMBR-0471': ['description', 'amount'], + 'HOOL-0515': ['description', 'amount'], +}; + +function toCsv(invoice: Invoice): string { + const columns = COLUMN_MAPS[invoice.customerId]; + if (!columns) { + throw new CsvExportError(`CSV encoder has no column map for ${invoice.id} (CE03)`); + } + return [columns.join(','), ...invoice.lines.map((l) => `"${l.description}",${l.amountCents}`)].join('\n'); +} + +export function InvoiceActions({ invoice }: { invoice: Invoice }) { + const [status, setStatus] = useState(null); + + async function sendReminder() { + setStatus('Sending…'); + const res = await fetch(`/api/invoices/${invoice.id}/remind`, { method: 'POST' }); + setStatus(res.ok ? 'Reminder sent' : 'Could not send reminder'); + } + + function downloadCsv() { + const blob = new Blob([toCsv(invoice)], { type: 'text/csv' }); + const link = document.createElement('a'); + link.href = URL.createObjectURL(blob); + link.download = `${invoice.id}.csv`; + link.click(); + } + + return ( +

+ Download PDF{' '} + + + {status && {status}} +

+ ); +} diff --git a/apps/error-tracking/no-posthog-next-14-app/app/invoices/[id]/page.tsx b/apps/error-tracking/no-posthog-next-14-app/app/invoices/[id]/page.tsx new file mode 100644 index 000000000..71bbdd6c8 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-14-app/app/invoices/[id]/page.tsx @@ -0,0 +1,44 @@ +import type { Metadata } from 'next'; +import { notFound } from 'next/navigation'; +import { customerName, findInvoice, formatMoney, reconcileTotals } from '@/lib/invoices'; +import { InvoiceActions } from './invoice-actions'; + +type Props = { params: { id: string } }; + +export async function generateMetadata({ params }: Props): Promise { + const { id } = params; + const invoice = findInvoice(id); + if (!invoice) return { title: 'Invoice not found' }; + return { title: `${invoice.id} · ${customerName(invoice)}` }; +} + +export default async function InvoicePage({ params }: Props) { + const { id } = params; + const invoice = findInvoice(id); + if (!invoice) notFound(); + const total = reconcileTotals(invoice); + + return ( + <> +

{invoice.id}

+

+ Customer {invoice.customerId} · issued {invoice.issuedOn} · {invoice.status} +

+ + + {invoice.lines.map((line, i) => ( + + + + + ))} + + + + + +
{line.description}{formatMoney(line.amountCents)}
Total{formatMoney(total)}
+ + + ); +} diff --git a/apps/error-tracking/no-posthog-next-14-app/app/invoices/new/actions.ts b/apps/error-tracking/no-posthog-next-14-app/app/invoices/new/actions.ts new file mode 100644 index 000000000..d7896bd0b --- /dev/null +++ b/apps/error-tracking/no-posthog-next-14-app/app/invoices/new/actions.ts @@ -0,0 +1,18 @@ +'use server'; + +import { redirect } from 'next/navigation'; +import { checkCredit, listInvoices } from '@/lib/invoices'; + +export async function createInvoice(formData: FormData) { + const customerId = String(formData.get('customer') ?? '').trim().toUpperCase(); + const amountCents = Math.round(Number(formData.get('amount') ?? 0) * 100); + + const open = listInvoices().find((i) => i.customerId === customerId && i.status !== 'draft'); + if (open) { + // Customers with an open invoice get their existing invoice instead of a duplicate. + redirect(`/invoices/${open.id}`); + } + + checkCredit(customerId, amountCents); + redirect('/?created=1'); +} diff --git a/apps/error-tracking/no-posthog-next-14-app/app/invoices/new/page.tsx b/apps/error-tracking/no-posthog-next-14-app/app/invoices/new/page.tsx new file mode 100644 index 000000000..94f72a25b --- /dev/null +++ b/apps/error-tracking/no-posthog-next-14-app/app/invoices/new/page.tsx @@ -0,0 +1,22 @@ +import { createInvoice } from './actions'; + +export default function NewInvoicePage() { + return ( + <> +

New invoice

+
+

+ +

+

+ +

+ +
+ + ); +} diff --git a/apps/error-tracking/no-posthog-next-14-app/app/layout.tsx b/apps/error-tracking/no-posthog-next-14-app/app/layout.tsx new file mode 100644 index 000000000..8877f1ca2 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-14-app/app/layout.tsx @@ -0,0 +1,24 @@ +import type { Metadata } from 'next'; +import Link from 'next/link'; +import './globals.css'; + +export const metadata: Metadata = { + title: 'Ledgerly', + description: 'Invoices for small teams', +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + +
+ Invoices + New invoice + Reports + Settings +
+
{children}
+ + + ); +} diff --git a/apps/error-tracking/no-posthog-next-14-app/app/page.tsx b/apps/error-tracking/no-posthog-next-14-app/app/page.tsx new file mode 100644 index 000000000..be6f6e894 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-14-app/app/page.tsx @@ -0,0 +1,33 @@ +import Link from 'next/link'; +import { formatMoney, listInvoices } from '@/lib/invoices'; + +export default function InvoicesPage() { + const invoices = listInvoices(); + return ( + <> +

Invoices

+ + + + + + + + + + + {invoices.map((invoice) => ( + + + + + + + ))} + +
InvoiceIssuedStatusTotal
+ {invoice.id} + {invoice.issuedOn}{invoice.status}{formatMoney(invoice.totalCents)}
+ + ); +} diff --git a/apps/error-tracking/no-posthog-next-14-app/app/portal/[tenant]/page.tsx b/apps/error-tracking/no-posthog-next-14-app/app/portal/[tenant]/page.tsx new file mode 100644 index 000000000..18b9df41a --- /dev/null +++ b/apps/error-tracking/no-posthog-next-14-app/app/portal/[tenant]/page.tsx @@ -0,0 +1,9 @@ +export default function PortalPage({ params }: { params: { tenant: string } }) { + const { tenant } = params; + return ( + <> +

Customer portal

+

Signed in to {tenant}.

+ + ); +} diff --git a/apps/error-tracking/no-posthog-next-14-app/app/reports/error.tsx b/apps/error-tracking/no-posthog-next-14-app/app/reports/error.tsx new file mode 100644 index 000000000..e057ad9dc --- /dev/null +++ b/apps/error-tracking/no-posthog-next-14-app/app/reports/error.tsx @@ -0,0 +1,10 @@ +'use client'; + +export default function ReportsError({ reset }: { error: Error & { digest?: string }; reset: () => void }) { + return ( +
+

We couldn't load this report.

+ +
+ ); +} diff --git a/apps/error-tracking/no-posthog-next-14-app/app/reports/page.tsx b/apps/error-tracking/no-posthog-next-14-app/app/reports/page.tsx new file mode 100644 index 000000000..8e5635b91 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-14-app/app/reports/page.tsx @@ -0,0 +1,10 @@ +import { RevenueChart } from './revenue-chart'; + +export default function ReportsPage() { + return ( + <> +

Revenue

+ + + ); +} diff --git a/apps/error-tracking/no-posthog-next-14-app/app/reports/revenue-chart.tsx b/apps/error-tracking/no-posthog-next-14-app/app/reports/revenue-chart.tsx new file mode 100644 index 000000000..98713701f --- /dev/null +++ b/apps/error-tracking/no-posthog-next-14-app/app/reports/revenue-chart.tsx @@ -0,0 +1,36 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { ReportRangeError } from '@/lib/errors'; +import { formatMoney, listInvoices } from '@/lib/invoices'; + +const RANGES: Record = { + q3: ['2026-07-01', '2026-09-30'], + q4: ['2026-10-01', '2026-12-31'], + ytd: ['2026-01-01', '2026-12-31'], +}; + +function revenueFor(range: string): number { + const bounds = RANGES[range]; + if (!bounds) { + throw new ReportRangeError(`Unsupported report range "${range}" (RX20)`); + } + return listInvoices() + .filter((i) => i.issuedOn >= bounds[0] && i.issuedOn <= bounds[1]) + .reduce((acc, i) => acc + i.totalCents, 0); +} + +export function RevenueChart() { + const [range, setRange] = useState(null); + + useEffect(() => { + setRange(new URLSearchParams(window.location.search).get('range') ?? 'q4'); + }, []); + + if (!range) return

Loading…

; + return ( +

+ Revenue for {range}: {formatMoney(revenueFor(range))} +

+ ); +} diff --git a/apps/error-tracking/no-posthog-next-14-app/app/settings/page.tsx b/apps/error-tracking/no-posthog-next-14-app/app/settings/page.tsx new file mode 100644 index 000000000..794d8f811 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-14-app/app/settings/page.tsx @@ -0,0 +1,10 @@ +import { PreferencesPanel } from './preferences-panel'; + +export default function SettingsPage() { + return ( + <> +

Settings

+ + + ); +} diff --git a/apps/error-tracking/no-posthog-next-14-app/app/settings/preferences-panel.tsx b/apps/error-tracking/no-posthog-next-14-app/app/settings/preferences-panel.tsx new file mode 100644 index 000000000..cfd54fdd9 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-14-app/app/settings/preferences-panel.tsx @@ -0,0 +1,36 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { PreferencesParseError } from '@/lib/errors'; + +type Preferences = { currency: string; reminderDays: number }; + +// Settings exported by the previous version of the app: "currency|reminderDays". +const LEGACY_EXPORT = 'USD;14'; + +function parseLegacy(raw: string): Preferences { + const [currency, days] = raw.split('|'); + if (!days) { + throw new PreferencesParseError('Saved preferences are corrupt (PF88)'); + } + return { currency, reminderDays: Number(days) }; +} + +export function PreferencesPanel() { + const [legacy, setLegacy] = useState(null); + + useEffect(() => { + if (new URLSearchParams(window.location.search).get('import') === 'legacy') setLegacy(LEGACY_EXPORT); + }, []); + + const prefs: Preferences = legacy ? parseLegacy(legacy) : { currency: 'USD', reminderDays: 7 }; + return ( + + ); +} diff --git a/apps/error-tracking/no-posthog-next-14-app/lib/errors.ts b/apps/error-tracking/no-posthog-next-14-app/lib/errors.ts new file mode 100644 index 000000000..05294a457 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-14-app/lib/errors.ts @@ -0,0 +1,51 @@ +export class InvoiceTotalMismatchError extends Error { + override name = 'InvoiceTotalMismatchError'; +} + +export class CustomerLookupError extends Error { + override name = 'CustomerLookupError'; +} + +export class ReportRangeError extends Error { + override name = 'ReportRangeError'; +} + +export class PreferencesParseError extends Error { + override name = 'PreferencesParseError'; +} + +export class CsvExportError extends Error { + override name = 'CsvExportError'; +} + +export class PdfLayoutError extends Error { + override name = 'PdfLayoutError'; +} + +export class ReminderQueueError extends Error { + override name = 'ReminderQueueError'; +} + +export class CreditLimitError extends Error { + override name = 'CreditLimitError'; +} + +export class TenantResolutionError extends Error { + override name = 'TenantResolutionError'; +} + +export class TaxRegionError extends Error { + override name = 'TaxRegionError'; +} + +export class PaymentReconcileError extends Error { + override name = 'PaymentReconcileError'; +} + +export class ExportWriteError extends Error { + override name = 'ExportWriteError'; +} + +export class CacheWarmError extends Error { + override name = 'CacheWarmError'; +} diff --git a/apps/error-tracking/no-posthog-next-14-app/lib/invoices.ts b/apps/error-tracking/no-posthog-next-14-app/lib/invoices.ts new file mode 100644 index 000000000..0153b6795 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-14-app/lib/invoices.ts @@ -0,0 +1,149 @@ +import { + CreditLimitError, + CustomerLookupError, + InvoiceTotalMismatchError, + PaymentReconcileError, + PdfLayoutError, + ReminderQueueError, +} from './errors'; + +export type InvoiceLine = { description: string; amountCents: number }; + +export type Invoice = { + id: string; + customerId: string; + issuedOn: string; + status: 'draft' | 'sent' | 'paid'; + lines: InvoiceLine[]; + totalCents: number; +}; + +const CUSTOMERS: Record = { + 'ACME-0007': { name: 'Acme Corp', creditLimitCents: 5_000_000 }, + 'ACME-0042': { name: 'Acme Logistics', creditLimitCents: 0 }, + 'GLBX-0101': { name: 'Globex', creditLimitCents: 2_000_000 }, + 'INIT-0300': { name: 'Initech', creditLimitCents: 1_000_000 }, + 'UMBR-0471': { name: 'Umbrella Health', creditLimitCents: 3_000_000 }, + 'HOOL-0515': { name: 'Hooli', creditLimitCents: 9_000_000 }, +}; + +const INVOICES: Invoice[] = [ + { + id: 'INV-1001', + customerId: 'ACME-0007', + issuedOn: '2026-09-02', + status: 'paid', + lines: [{ description: 'Annual support plan', amountCents: 480_000 }], + totalCents: 480_000, + }, + { + id: 'INV-2202', + customerId: 'GLBX-0101', + issuedOn: '2026-09-18', + status: 'sent', + lines: [ + { description: 'Onboarding workshop', amountCents: 150_000 }, + { description: 'Travel', amountCents: 32_500 }, + ], + totalCents: 182_500, + }, + { + id: 'INV-3003', + customerId: 'INIT-0300', + issuedOn: '2026-10-01', + status: 'draft', + lines: [{ description: 'TPS report automation', amountCents: 99_000 }], + totalCents: 99_000, + }, + { + id: 'INV-4471', + customerId: 'UMBR-0471', + issuedOn: '2026-10-04', + status: 'sent', + lines: [ + { description: 'Data migration', amountCents: 240_000 }, + { description: 'Discount', amountCents: -24_000 }, + ], + totalCents: 240_000, + }, + { + id: 'INV-5150', + customerId: 'HOOL-0515', + issuedOn: '2026-10-09', + status: 'sent', + lines: Array.from({ length: 64 }, (_, i) => ({ + description: `Compute reservation block ${i + 1}`, + amountCents: 12_000, + })), + totalCents: 64 * 12_000, + }, + { + id: 'INV-0913', + customerId: 'CUST-0913', + issuedOn: '2026-10-12', + status: 'draft', + lines: [{ description: 'Consulting', amountCents: 75_000 }], + totalCents: 75_000, + }, +]; + +const LINES_PER_PDF_PAGE = 48; +const BILLING_DATE = '2026-10-28'; + +export function listInvoices(): Invoice[] { + return INVOICES; +} + +export function findInvoice(id: string): Invoice | undefined { + return INVOICES.find((invoice) => invoice.id === id); +} + +export function customerName(invoice: Invoice): string { + const customer = CUSTOMERS[invoice.customerId]; + if (!customer) { + throw new CustomerLookupError(`Customer record missing for ${invoice.id} (MX13)`); + } + return customer.name; +} + +export function reconcileTotals(invoice: Invoice): number { + const sum = invoice.lines.reduce((acc, line) => acc + line.amountCents, 0); + if (sum !== invoice.totalCents) { + throw new InvoiceTotalMismatchError(`Invoice total mismatch for ${invoice.id} (TM71)`); + } + return sum; +} + +export function renderPdf(invoice: Invoice): string { + if (invoice.lines.length > LINES_PER_PDF_PAGE) { + throw new PdfLayoutError(`PDF layout overflow on ${invoice.id} (PD50)`); + } + const body = invoice.lines.map((l) => `${l.description}\t${formatMoney(l.amountCents)}`).join('\n'); + return `INVOICE ${invoice.id}\n${body}\nTOTAL ${formatMoney(invoice.totalCents)}\n`; +} + +export async function queueReminder(invoice: Invoice): Promise { + // The reminder queue only accepts invoices that were sent less than 30 days ago. + const ageDays = (Date.parse(BILLING_DATE) - Date.parse(invoice.issuedOn)) / 86_400_000; + if (invoice.status === 'sent' && ageDays > 30) { + throw new ReminderQueueError(`Reminder queue rejected ${invoice.id} (RQ02)`); + } +} + +export function checkCredit(customerId: string, amountCents: number): void { + const customer = CUSTOMERS[customerId]; + if (customer && amountCents > customer.creditLimitCents) { + throw new CreditLimitError(`Credit limit check failed for ${customerId} (CL42)`); + } +} + +export async function reconcilePayment(paymentId: string, invoiceId: string): Promise { + const invoice = findInvoice(invoiceId); + if (!invoice || invoice.status !== 'paid') { + throw new PaymentReconcileError(`Payment reconcile failed for ${paymentId} (RC21)`); + } +} + +export function formatMoney(cents: number): string { + return `$${(cents / 100).toFixed(2)}`; +} diff --git a/apps/error-tracking/no-posthog-next-14-app/middleware.ts b/apps/error-tracking/no-posthog-next-14-app/middleware.ts new file mode 100644 index 000000000..06ab5cbad --- /dev/null +++ b/apps/error-tracking/no-posthog-next-14-app/middleware.ts @@ -0,0 +1,16 @@ +import { NextResponse, type NextRequest } from 'next/server'; +import { TenantResolutionError } from '@/lib/errors'; + +const TENANT_SLUG = /^[a-z0-9-]+$/; + +export function middleware(request: NextRequest) { + const slug = request.nextUrl.pathname.split('/')[2] ?? ''; + if (!TENANT_SLUG.test(slug)) { + throw new TenantResolutionError(`Tenant slug is not valid: ${slug} (TN09)`); + } + const response = NextResponse.next(); + response.headers.set('x-tenant', slug); + return response; +} + +export const config = { matcher: '/portal/:path*' }; diff --git a/apps/error-tracking/no-posthog-next-14-app/next.config.mjs b/apps/error-tracking/no-posthog-next-14-app/next.config.mjs new file mode 100644 index 000000000..4678774e6 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-14-app/next.config.mjs @@ -0,0 +1,4 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = {}; + +export default nextConfig; diff --git a/apps/error-tracking/no-posthog-next-14-app/package-lock.json b/apps/error-tracking/no-posthog-next-14-app/package-lock.json new file mode 100644 index 000000000..5feb7b034 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-14-app/package-lock.json @@ -0,0 +1,510 @@ +{ + "name": "ledgerly", + "version": "0.4.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ledgerly", + "version": "0.4.0", + "dependencies": { + "next": "14.2.35", + "react": "18.3.1", + "react-dom": "18.3.1" + }, + "devDependencies": { + "@types/node": "22.10.0", + "@types/react": "18.3.12", + "@types/react-dom": "18.3.1", + "typescript": "5.7.2" + } + }, + "node_modules/@next/env": { + "version": "14.2.35", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.35.tgz", + "integrity": "sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.33.tgz", + "integrity": "sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.33.tgz", + "integrity": "sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.33.tgz", + "integrity": "sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.33.tgz", + "integrity": "sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.33.tgz", + "integrity": "sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.33.tgz", + "integrity": "sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.33.tgz", + "integrity": "sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.33.tgz", + "integrity": "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.33.tgz", + "integrity": "sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, + "node_modules/@swc/helpers": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", + "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "tslib": "^2.4.0" + } + }, + "node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.12", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz", + "integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "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/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/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/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "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==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "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": "14.2.35", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.35.tgz", + "integrity": "sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig==", + "license": "MIT", + "dependencies": { + "@next/env": "14.2.35", + "@swc/helpers": "0.5.5", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001579", + "graceful-fs": "^4.2.11", + "postcss": "8.4.31", + "styled-jsx": "5.1.1" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=18.17.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "14.2.33", + "@next/swc-darwin-x64": "14.2.33", + "@next/swc-linux-arm64-gnu": "14.2.33", + "@next/swc-linux-arm64-musl": "14.2.33", + "@next/swc-linux-x64-gnu": "14.2.33", + "@next/swc-linux-x64-musl": "14.2.33", + "@next/swc-win32-arm64-msvc": "14.2.33", + "@next/swc-win32-ia32-msvc": "14.2.33", + "@next/swc-win32-x64-msvc": "14.2.33" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "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/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "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.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "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/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", + "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", + "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" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "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.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/apps/error-tracking/no-posthog-next-14-app/package.json b/apps/error-tracking/no-posthog-next-14-app/package.json new file mode 100644 index 000000000..bee20c5d0 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-14-app/package.json @@ -0,0 +1,21 @@ +{ + "name": "ledgerly", + "version": "0.4.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "next": "14.2.35", + "react": "18.3.1", + "react-dom": "18.3.1" + }, + "devDependencies": { + "@types/node": "22.10.0", + "@types/react": "18.3.12", + "@types/react-dom": "18.3.1", + "typescript": "5.7.2" + } +} diff --git a/apps/error-tracking/no-posthog-next-14-app/tsconfig.json b/apps/error-tracking/no-posthog-next-14-app/tsconfig.json new file mode 100644 index 000000000..e7ff3a26b --- /dev/null +++ b/apps/error-tracking/no-posthog-next-14-app/tsconfig.json @@ -0,0 +1,41 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "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/apps/error-tracking/no-posthog-next-15-pages/.gitignore b/apps/error-tracking/no-posthog-next-15-pages/.gitignore new file mode 100644 index 000000000..16bd2039d --- /dev/null +++ b/apps/error-tracking/no-posthog-next-15-pages/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +.env +.next/ +next-env.d.ts diff --git a/apps/error-tracking/no-posthog-next-15-pages/README.md b/apps/error-tracking/no-posthog-next-15-pages/README.md new file mode 100644 index 000000000..685cfa11c --- /dev/null +++ b/apps/error-tracking/no-posthog-next-15-pages/README.md @@ -0,0 +1,19 @@ +# Ledgerly + +A small invoicing app: invoices, a revenue report and a customer portal. +Next.js 15 with the Pages Router. + +```bash +npm ci +npm run dev # http://localhost:3000 +npm run build && npm start +``` + +- `pages/invoices/[id].tsx` — invoice pages (server-rendered), with PDF + download, reminders and CSV export +- `pages/reports.tsx` — revenue report with a selectable range +- `pages/portal/[tenant].tsx` — customer portal, resolved per tenant in + `middleware.ts` +- `pages/api` — PDF rendering, reminders, CSV exports, cache warm-up and a + tax-region lookup that runs on the edge +- `lib/` — invoice data and billing rules diff --git a/apps/error-tracking/no-posthog-next-15-pages/components/invoice-actions.tsx b/apps/error-tracking/no-posthog-next-15-pages/components/invoice-actions.tsx new file mode 100644 index 000000000..a02863995 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-15-pages/components/invoice-actions.tsx @@ -0,0 +1,45 @@ +import { useState } from 'react'; +import { CsvExportError } from '@/lib/errors'; +import type { Invoice } from '@/lib/invoices'; + +const COLUMN_MAPS: Record = { + 'ACME-0007': ['description', 'amount'], + 'GLBX-0101': ['description', 'amount'], + 'UMBR-0471': ['description', 'amount'], + 'HOOL-0515': ['description', 'amount'], +}; + +function toCsv(invoice: Invoice): string { + const columns = COLUMN_MAPS[invoice.customerId]; + if (!columns) { + throw new CsvExportError(`CSV encoder has no column map for ${invoice.id} (CE03)`); + } + return [columns.join(','), ...invoice.lines.map((l) => `"${l.description}",${l.amountCents}`)].join('\n'); +} + +export function InvoiceActions({ invoice }: { invoice: Invoice }) { + const [status, setStatus] = useState(null); + + async function sendReminder() { + setStatus('Sending…'); + const res = await fetch(`/api/invoices/${invoice.id}/remind`, { method: 'POST' }); + setStatus(res.ok ? 'Reminder sent' : 'Could not send reminder'); + } + + function downloadCsv() { + const blob = new Blob([toCsv(invoice)], { type: 'text/csv' }); + const link = document.createElement('a'); + link.href = URL.createObjectURL(blob); + link.download = `${invoice.id}.csv`; + link.click(); + } + + return ( +

+ Download PDF{' '} + + + {status && {status}} +

+ ); +} diff --git a/apps/error-tracking/no-posthog-next-15-pages/components/revenue-chart.tsx b/apps/error-tracking/no-posthog-next-15-pages/components/revenue-chart.tsx new file mode 100644 index 000000000..fd4a8e4a3 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-15-pages/components/revenue-chart.tsx @@ -0,0 +1,34 @@ +import { useEffect, useState } from 'react'; +import { ReportRangeError } from '@/lib/errors'; +import { formatMoney, listInvoices } from '@/lib/invoices'; + +const RANGES: Record = { + q3: ['2026-07-01', '2026-09-30'], + q4: ['2026-10-01', '2026-12-31'], + ytd: ['2026-01-01', '2026-12-31'], +}; + +function revenueFor(range: string): number { + const bounds = RANGES[range]; + if (!bounds) { + throw new ReportRangeError(`Unsupported report range "${range}" (RX20)`); + } + return listInvoices() + .filter((i) => i.issuedOn >= bounds[0] && i.issuedOn <= bounds[1]) + .reduce((acc, i) => acc + i.totalCents, 0); +} + +export function RevenueChart() { + const [range, setRange] = useState(null); + + useEffect(() => { + setRange(new URLSearchParams(window.location.search).get('range') ?? 'q4'); + }, []); + + if (!range) return

Loading…

; + return ( +

+ Revenue for {range}: {formatMoney(revenueFor(range))} +

+ ); +} diff --git a/apps/error-tracking/no-posthog-next-15-pages/lib/errors.ts b/apps/error-tracking/no-posthog-next-15-pages/lib/errors.ts new file mode 100644 index 000000000..05294a457 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-15-pages/lib/errors.ts @@ -0,0 +1,51 @@ +export class InvoiceTotalMismatchError extends Error { + override name = 'InvoiceTotalMismatchError'; +} + +export class CustomerLookupError extends Error { + override name = 'CustomerLookupError'; +} + +export class ReportRangeError extends Error { + override name = 'ReportRangeError'; +} + +export class PreferencesParseError extends Error { + override name = 'PreferencesParseError'; +} + +export class CsvExportError extends Error { + override name = 'CsvExportError'; +} + +export class PdfLayoutError extends Error { + override name = 'PdfLayoutError'; +} + +export class ReminderQueueError extends Error { + override name = 'ReminderQueueError'; +} + +export class CreditLimitError extends Error { + override name = 'CreditLimitError'; +} + +export class TenantResolutionError extends Error { + override name = 'TenantResolutionError'; +} + +export class TaxRegionError extends Error { + override name = 'TaxRegionError'; +} + +export class PaymentReconcileError extends Error { + override name = 'PaymentReconcileError'; +} + +export class ExportWriteError extends Error { + override name = 'ExportWriteError'; +} + +export class CacheWarmError extends Error { + override name = 'CacheWarmError'; +} diff --git a/apps/error-tracking/no-posthog-next-15-pages/lib/invoices.ts b/apps/error-tracking/no-posthog-next-15-pages/lib/invoices.ts new file mode 100644 index 000000000..0153b6795 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-15-pages/lib/invoices.ts @@ -0,0 +1,149 @@ +import { + CreditLimitError, + CustomerLookupError, + InvoiceTotalMismatchError, + PaymentReconcileError, + PdfLayoutError, + ReminderQueueError, +} from './errors'; + +export type InvoiceLine = { description: string; amountCents: number }; + +export type Invoice = { + id: string; + customerId: string; + issuedOn: string; + status: 'draft' | 'sent' | 'paid'; + lines: InvoiceLine[]; + totalCents: number; +}; + +const CUSTOMERS: Record = { + 'ACME-0007': { name: 'Acme Corp', creditLimitCents: 5_000_000 }, + 'ACME-0042': { name: 'Acme Logistics', creditLimitCents: 0 }, + 'GLBX-0101': { name: 'Globex', creditLimitCents: 2_000_000 }, + 'INIT-0300': { name: 'Initech', creditLimitCents: 1_000_000 }, + 'UMBR-0471': { name: 'Umbrella Health', creditLimitCents: 3_000_000 }, + 'HOOL-0515': { name: 'Hooli', creditLimitCents: 9_000_000 }, +}; + +const INVOICES: Invoice[] = [ + { + id: 'INV-1001', + customerId: 'ACME-0007', + issuedOn: '2026-09-02', + status: 'paid', + lines: [{ description: 'Annual support plan', amountCents: 480_000 }], + totalCents: 480_000, + }, + { + id: 'INV-2202', + customerId: 'GLBX-0101', + issuedOn: '2026-09-18', + status: 'sent', + lines: [ + { description: 'Onboarding workshop', amountCents: 150_000 }, + { description: 'Travel', amountCents: 32_500 }, + ], + totalCents: 182_500, + }, + { + id: 'INV-3003', + customerId: 'INIT-0300', + issuedOn: '2026-10-01', + status: 'draft', + lines: [{ description: 'TPS report automation', amountCents: 99_000 }], + totalCents: 99_000, + }, + { + id: 'INV-4471', + customerId: 'UMBR-0471', + issuedOn: '2026-10-04', + status: 'sent', + lines: [ + { description: 'Data migration', amountCents: 240_000 }, + { description: 'Discount', amountCents: -24_000 }, + ], + totalCents: 240_000, + }, + { + id: 'INV-5150', + customerId: 'HOOL-0515', + issuedOn: '2026-10-09', + status: 'sent', + lines: Array.from({ length: 64 }, (_, i) => ({ + description: `Compute reservation block ${i + 1}`, + amountCents: 12_000, + })), + totalCents: 64 * 12_000, + }, + { + id: 'INV-0913', + customerId: 'CUST-0913', + issuedOn: '2026-10-12', + status: 'draft', + lines: [{ description: 'Consulting', amountCents: 75_000 }], + totalCents: 75_000, + }, +]; + +const LINES_PER_PDF_PAGE = 48; +const BILLING_DATE = '2026-10-28'; + +export function listInvoices(): Invoice[] { + return INVOICES; +} + +export function findInvoice(id: string): Invoice | undefined { + return INVOICES.find((invoice) => invoice.id === id); +} + +export function customerName(invoice: Invoice): string { + const customer = CUSTOMERS[invoice.customerId]; + if (!customer) { + throw new CustomerLookupError(`Customer record missing for ${invoice.id} (MX13)`); + } + return customer.name; +} + +export function reconcileTotals(invoice: Invoice): number { + const sum = invoice.lines.reduce((acc, line) => acc + line.amountCents, 0); + if (sum !== invoice.totalCents) { + throw new InvoiceTotalMismatchError(`Invoice total mismatch for ${invoice.id} (TM71)`); + } + return sum; +} + +export function renderPdf(invoice: Invoice): string { + if (invoice.lines.length > LINES_PER_PDF_PAGE) { + throw new PdfLayoutError(`PDF layout overflow on ${invoice.id} (PD50)`); + } + const body = invoice.lines.map((l) => `${l.description}\t${formatMoney(l.amountCents)}`).join('\n'); + return `INVOICE ${invoice.id}\n${body}\nTOTAL ${formatMoney(invoice.totalCents)}\n`; +} + +export async function queueReminder(invoice: Invoice): Promise { + // The reminder queue only accepts invoices that were sent less than 30 days ago. + const ageDays = (Date.parse(BILLING_DATE) - Date.parse(invoice.issuedOn)) / 86_400_000; + if (invoice.status === 'sent' && ageDays > 30) { + throw new ReminderQueueError(`Reminder queue rejected ${invoice.id} (RQ02)`); + } +} + +export function checkCredit(customerId: string, amountCents: number): void { + const customer = CUSTOMERS[customerId]; + if (customer && amountCents > customer.creditLimitCents) { + throw new CreditLimitError(`Credit limit check failed for ${customerId} (CL42)`); + } +} + +export async function reconcilePayment(paymentId: string, invoiceId: string): Promise { + const invoice = findInvoice(invoiceId); + if (!invoice || invoice.status !== 'paid') { + throw new PaymentReconcileError(`Payment reconcile failed for ${paymentId} (RC21)`); + } +} + +export function formatMoney(cents: number): string { + return `$${(cents / 100).toFixed(2)}`; +} diff --git a/apps/error-tracking/no-posthog-next-15-pages/middleware.ts b/apps/error-tracking/no-posthog-next-15-pages/middleware.ts new file mode 100644 index 000000000..06ab5cbad --- /dev/null +++ b/apps/error-tracking/no-posthog-next-15-pages/middleware.ts @@ -0,0 +1,16 @@ +import { NextResponse, type NextRequest } from 'next/server'; +import { TenantResolutionError } from '@/lib/errors'; + +const TENANT_SLUG = /^[a-z0-9-]+$/; + +export function middleware(request: NextRequest) { + const slug = request.nextUrl.pathname.split('/')[2] ?? ''; + if (!TENANT_SLUG.test(slug)) { + throw new TenantResolutionError(`Tenant slug is not valid: ${slug} (TN09)`); + } + const response = NextResponse.next(); + response.headers.set('x-tenant', slug); + return response; +} + +export const config = { matcher: '/portal/:path*' }; diff --git a/apps/error-tracking/no-posthog-next-15-pages/next.config.ts b/apps/error-tracking/no-posthog-next-15-pages/next.config.ts new file mode 100644 index 000000000..b22af960a --- /dev/null +++ b/apps/error-tracking/no-posthog-next-15-pages/next.config.ts @@ -0,0 +1,5 @@ +import type { NextConfig } from 'next'; + +const nextConfig: NextConfig = {}; + +export default nextConfig; diff --git a/apps/error-tracking/no-posthog-next-15-pages/package-lock.json b/apps/error-tracking/no-posthog-next-15-pages/package-lock.json new file mode 100644 index 000000000..6bb8710ee --- /dev/null +++ b/apps/error-tracking/no-posthog-next-15-pages/package-lock.json @@ -0,0 +1,1063 @@ +{ + "name": "ledgerly", + "version": "0.4.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ledgerly", + "version": "0.4.0", + "dependencies": { + "next": "15.5.28", + "react": "19.3.1", + "react-dom": "19.3.1" + }, + "devDependencies": { + "@types/node": "22.10.0", + "@types/react": "19.1.0", + "@types/react-dom": "19.1.0", + "typescript": "5.7.2" + } + }, + "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/@next/env": { + "version": "15.5.28", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.28.tgz", + "integrity": "sha512-xxW5EBI8fMPuVTPBW7LL980IRbuDXlbkUAwp9DDhtb4Lnich3nJ13uYpvrX4VE74hWPf6AvHHAMmGCidcHjbfg==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.5.28", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.28.tgz", + "integrity": "sha512-dPoQYXcfSIqx5o+TOXVXO7kONK5qbtNRDeiTfII0bT3puGIT6VF5e0QwmxNZfEZ5Zety5f3m9P/p4xqwfiLlPg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.5.28", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.28.tgz", + "integrity": "sha512-RgCJ7buBivTH+6sddqaVm+/v8kwMHlFkobvdt3DfETutws5HrxnToMIWSN9X3MZ3aAi7BbKrGZZWUDQLEycXqw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.5.28", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.28.tgz", + "integrity": "sha512-OHXQe13olE59aKe++zHavGcJPh5SLHXi6Anret3BBeq1+qhSuOJDNAli/EDDfxs6UTqA4Qddo4uyYry2slyjiQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.5.28", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.28.tgz", + "integrity": "sha512-nTB7bB6vfn4ezCJNYP43ukt2o8L3+WxKF/sTcTxepdXsJh3SM9RhlU7wrZYbH7L4HO5M87dqDTwm8587vAkCiw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.5.28", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.28.tgz", + "integrity": "sha512-WXCfVI32rW3xCusoUX1RpZKtyChWU2Zwe27JUDmM3KrXwcDNc9vV4zEq0GKFAURnuQ/Ouo/XqPvB0yh445XZCg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.5.28", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.28.tgz", + "integrity": "sha512-Miq+NDb/HWsXuxseYxMM6fuEOQXgTziarkKIRpVKCn8W69cEfhQaQr+90pO+u4q2CbJR2wzXmUuCFyZaJs/Nng==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.5.28", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.28.tgz", + "integrity": "sha512-5LR79lemgDlgxvG7MQJ+zHCkVcrUoQuO7CiIvq3I58vNW4goSOE2b+19Z6FujRWMouvni4gvudYFuFuNxolGWA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.5.28", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.28.tgz", + "integrity": "sha512-GNipvXUYROSTf4PXEyH95FrWImIB5zdaK6uEOg0RP1/X2drnvoMwiBkJ0VWdzX7w3iiBm3y5g2TxkKDGVTOQcg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/react": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.0.tgz", + "integrity": "sha512-UaicktuQI+9UKyA4njtDOGBD/67t8YEBt2xdfqu8+gP9hqPUPsiXlNPcpS2gVdjmis5GKPG3fCxbQLVgxsQZ8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-21E2zejNNRtjG4hKIyJz4aWswGEcNFTgttA0bZIRGjj1HA/tbSUxIJnIcYbn98pwJck0cS1bsQhn6eaKqbcFWw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.0.0" + } + }, + "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/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/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/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "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": "15.5.28", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.28.tgz", + "integrity": "sha512-JjhLBBY1PNnuwPtcZ7t8yxVeJVYvD2Jyp633J+gRCNbzSt7N0E+xqCf6uQiff3+3qmUkwcF/ksfSsK1xsZ76Tw==", + "license": "MIT", + "dependencies": { + "@next/env": "15.5.28", + "@swc/helpers": "0.5.15", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.5.28", + "@next/swc-darwin-x64": "15.5.28", + "@next/swc-linux-arm64-gnu": "15.5.28", + "@next/swc-linux-arm64-musl": "15.5.28", + "@next/swc-linux-x64-gnu": "15.5.28", + "@next/swc-linux-x64-musl": "15.5.28", + "@next/swc-win32-arm64-msvc": "15.5.28", + "@next/swc-win32-x64-msvc": "15.5.28", + "sharp": "^0.34.3 || ^0.35.5" + }, + "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/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/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "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.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.1.tgz", + "integrity": "sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.1.tgz", + "integrity": "sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.1" + } + }, + "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/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/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.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/apps/error-tracking/no-posthog-next-15-pages/package.json b/apps/error-tracking/no-posthog-next-15-pages/package.json new file mode 100644 index 000000000..ceb04346d --- /dev/null +++ b/apps/error-tracking/no-posthog-next-15-pages/package.json @@ -0,0 +1,21 @@ +{ + "name": "ledgerly", + "version": "0.4.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "next": "15.5.28", + "react": "19.3.1", + "react-dom": "19.3.1" + }, + "devDependencies": { + "@types/node": "22.10.0", + "@types/react": "19.1.0", + "@types/react-dom": "19.1.0", + "typescript": "5.7.2" + } +} diff --git a/apps/error-tracking/no-posthog-next-15-pages/pages/_app.tsx b/apps/error-tracking/no-posthog-next-15-pages/pages/_app.tsx new file mode 100644 index 000000000..a2994fc3d --- /dev/null +++ b/apps/error-tracking/no-posthog-next-15-pages/pages/_app.tsx @@ -0,0 +1,17 @@ +import type { AppProps } from 'next/app'; +import Link from 'next/link'; +import '../styles/globals.css'; + +export default function App({ Component, pageProps }: AppProps) { + return ( + <> +
+ Invoices + Reports +
+
+ +
+ + ); +} diff --git a/apps/error-tracking/no-posthog-next-15-pages/pages/api/cache/warm.ts b/apps/error-tracking/no-posthog-next-15-pages/pages/api/cache/warm.ts new file mode 100644 index 000000000..30a66bdc4 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-15-pages/pages/api/cache/warm.ts @@ -0,0 +1,14 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { CacheWarmError } from '@/lib/errors'; + +const REGIONS = ['us-1', 'us-2', 'eu-1']; + +export default function handler(req: NextApiRequest, res: NextApiResponse) { + const { region } = req.body as { region: string }; + setTimeout(() => { + if (!REGIONS.includes(region)) { + throw new CacheWarmError(`Cache warm failed for region ${region} (CW02)`); + } + }, 20); + res.status(202).json({ region, status: 'warming' }); +} diff --git a/apps/error-tracking/no-posthog-next-15-pages/pages/api/exports.ts b/apps/error-tracking/no-posthog-next-15-pages/pages/api/exports.ts new file mode 100644 index 000000000..009e342ea --- /dev/null +++ b/apps/error-tracking/no-posthog-next-15-pages/pages/api/exports.ts @@ -0,0 +1,15 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { ExportWriteError } from '@/lib/errors'; + +async function writeExport(batch: string): Promise { + await new Promise((resolve) => setTimeout(resolve, 50)); + if (batch.startsWith('EX-7')) { + throw new ExportWriteError(`CSV export batch ${batch} failed to write (EX77)`); + } +} + +export default function handler(req: NextApiRequest, res: NextApiResponse) { + const { batch } = req.body as { batch: string }; + void writeExport(batch); + res.status(202).json({ batch, status: 'started' }); +} diff --git a/apps/error-tracking/no-posthog-next-15-pages/pages/api/geo.ts b/apps/error-tracking/no-posthog-next-15-pages/pages/api/geo.ts new file mode 100644 index 000000000..d0995f5cc --- /dev/null +++ b/apps/error-tracking/no-posthog-next-15-pages/pages/api/geo.ts @@ -0,0 +1,15 @@ +import type { NextRequest } from 'next/server'; +import { TaxRegionError } from '@/lib/errors'; + +export const config = { runtime: 'edge' }; + +const TAX_REGIONS: Record = { US: 'us-sales-tax', DE: 'eu-vat', GB: 'uk-vat', PL: 'eu-vat' }; + +export default async function handler(request: NextRequest) { + const country = request.nextUrl.searchParams.get('country') ?? 'US'; + const region = TAX_REGIONS[country]; + if (!region) { + throw new TaxRegionError(`No tax region for country ${country} (GX01)`); + } + return Response.json({ country, region }); +} diff --git a/apps/error-tracking/no-posthog-next-15-pages/pages/api/invoices/[id]/pdf.ts b/apps/error-tracking/no-posthog-next-15-pages/pages/api/invoices/[id]/pdf.ts new file mode 100644 index 000000000..c77592ee9 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-15-pages/pages/api/invoices/[id]/pdf.ts @@ -0,0 +1,11 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { findInvoice, renderPdf } from '@/lib/invoices'; + +export default function handler(req: NextApiRequest, res: NextApiResponse) { + const id = String(req.query.id); + const invoice = findInvoice(id); + if (!invoice) return res.status(404).json({ error: 'not_found' }); + res.setHeader('Content-Type', 'application/pdf'); + res.setHeader('Content-Disposition', `attachment; filename="${id}.pdf"`); + res.send(renderPdf(invoice)); +} diff --git a/apps/error-tracking/no-posthog-next-15-pages/pages/api/invoices/[id]/remind.ts b/apps/error-tracking/no-posthog-next-15-pages/pages/api/invoices/[id]/remind.ts new file mode 100644 index 000000000..2a47e80a8 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-15-pages/pages/api/invoices/[id]/remind.ts @@ -0,0 +1,10 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { findInvoice, queueReminder } from '@/lib/invoices'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== 'POST') return res.status(405).end(); + const invoice = findInvoice(String(req.query.id)); + if (!invoice) return res.status(404).json({ error: 'not_found' }); + await queueReminder(invoice); + res.json({ queued: true }); +} diff --git a/apps/error-tracking/no-posthog-next-15-pages/pages/index.tsx b/apps/error-tracking/no-posthog-next-15-pages/pages/index.tsx new file mode 100644 index 000000000..35975204b --- /dev/null +++ b/apps/error-tracking/no-posthog-next-15-pages/pages/index.tsx @@ -0,0 +1,25 @@ +import Link from 'next/link'; +import { formatMoney, listInvoices } from '@/lib/invoices'; + +export default function InvoicesPage() { + const invoices = listInvoices(); + return ( + <> +

Invoices

+ + + {invoices.map((invoice) => ( + + + + + + + ))} + +
+ {invoice.id} + {invoice.issuedOn}{invoice.status}{formatMoney(invoice.totalCents)}
+ + ); +} diff --git a/apps/error-tracking/no-posthog-next-15-pages/pages/invoices/[id].tsx b/apps/error-tracking/no-posthog-next-15-pages/pages/invoices/[id].tsx new file mode 100644 index 000000000..fde88c0b4 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-15-pages/pages/invoices/[id].tsx @@ -0,0 +1,41 @@ +import type { GetServerSideProps } from 'next'; +import Head from 'next/head'; +import { InvoiceActions } from '@/components/invoice-actions'; +import { findInvoice, formatMoney, reconcileTotals, type Invoice } from '@/lib/invoices'; + +type Props = { invoice: Invoice; total: number }; + +export const getServerSideProps: GetServerSideProps = async ({ params }) => { + const invoice = findInvoice(String(params?.id)); + if (!invoice) return { notFound: true }; + return { props: { invoice, total: reconcileTotals(invoice) } }; +}; + +export default function InvoicePage({ invoice, total }: Props) { + return ( + <> + + {`${invoice.id} · Ledgerly`} + +

{invoice.id}

+

+ Customer {invoice.customerId} · issued {invoice.issuedOn} · {invoice.status} +

+ + + {invoice.lines.map((line, i) => ( + + + + + ))} + + + + + +
{line.description}{formatMoney(line.amountCents)}
Total{formatMoney(total)}
+ + + ); +} diff --git a/apps/error-tracking/no-posthog-next-15-pages/pages/portal/[tenant].tsx b/apps/error-tracking/no-posthog-next-15-pages/pages/portal/[tenant].tsx new file mode 100644 index 000000000..9cf7e9e3a --- /dev/null +++ b/apps/error-tracking/no-posthog-next-15-pages/pages/portal/[tenant].tsx @@ -0,0 +1,11 @@ +import { useRouter } from 'next/router'; + +export default function PortalPage() { + const { query } = useRouter(); + return ( + <> +

Customer portal

+

Signed in to {query.tenant}.

+ + ); +} diff --git a/apps/error-tracking/no-posthog-next-15-pages/pages/reports.tsx b/apps/error-tracking/no-posthog-next-15-pages/pages/reports.tsx new file mode 100644 index 000000000..cfe31be1c --- /dev/null +++ b/apps/error-tracking/no-posthog-next-15-pages/pages/reports.tsx @@ -0,0 +1,10 @@ +import { RevenueChart } from '@/components/revenue-chart'; + +export default function ReportsPage() { + return ( + <> +

Revenue

+ + + ); +} diff --git a/apps/error-tracking/no-posthog-next-15-pages/styles/globals.css b/apps/error-tracking/no-posthog-next-15-pages/styles/globals.css new file mode 100644 index 000000000..8149faedf --- /dev/null +++ b/apps/error-tracking/no-posthog-next-15-pages/styles/globals.css @@ -0,0 +1,8 @@ +body { font-family: system-ui, sans-serif; margin: 0; color: #1f2933; } +header { display: flex; gap: 1rem; padding: 1rem 2rem; background: #102a43; } +header a { color: #f0f4f8; text-decoration: none; } +main { padding: 2rem; max-width: 56rem; } +table { border-collapse: collapse; width: 100%; } +td, th { border-bottom: 1px solid #d9e2ec; padding: 0.4rem; text-align: left; } +button { padding: 0.4rem 0.8rem; margin-right: 0.5rem; } +.notice { padding: 0.6rem; background: #fff3c4; } diff --git a/apps/error-tracking/no-posthog-next-15-pages/tsconfig.json b/apps/error-tracking/no-posthog-next-15-pages/tsconfig.json new file mode 100644 index 000000000..e7ff3a26b --- /dev/null +++ b/apps/error-tracking/no-posthog-next-15-pages/tsconfig.json @@ -0,0 +1,41 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "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/apps/error-tracking/no-posthog-next-16-app/.gitignore b/apps/error-tracking/no-posthog-next-16-app/.gitignore new file mode 100644 index 000000000..16bd2039d --- /dev/null +++ b/apps/error-tracking/no-posthog-next-16-app/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +.env +.next/ +next-env.d.ts diff --git a/apps/error-tracking/no-posthog-next-16-app/README.md b/apps/error-tracking/no-posthog-next-16-app/README.md new file mode 100644 index 000000000..1fc764b17 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-16-app/README.md @@ -0,0 +1,20 @@ +# Ledgerly + +A small invoicing app: invoices, a revenue report, account settings and a +customer portal. Next.js 16 with the App Router. + +```bash +npm ci +npm run dev # http://localhost:3000 +npm run build && npm start +``` + +- `app/invoices` — invoice list, invoice pages (PDF download, reminders, CSV + export) and the new-invoice form (server action) +- `app/reports` — revenue report with a selectable range +- `app/settings` — preferences, including an import of settings saved by the + old app +- `app/portal/[tenant]` — customer portal, resolved per tenant in `proxy.ts` +- `app/api` — PDF rendering, reminders, payment webhooks, CSV exports, cache + warm-up and a tax-region lookup that runs on the edge +- `lib/` — invoice data and billing rules diff --git a/apps/error-tracking/no-posthog-next-16-app/app/api/cache/warm/route.ts b/apps/error-tracking/no-posthog-next-16-app/app/api/cache/warm/route.ts new file mode 100644 index 000000000..ba87bfc29 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-16-app/app/api/cache/warm/route.ts @@ -0,0 +1,13 @@ +import { CacheWarmError } from '@/lib/errors'; + +const REGIONS = ['us-1', 'us-2', 'eu-1']; + +export async function POST(request: Request) { + const { region } = (await request.json()) as { region: string }; + setTimeout(() => { + if (!REGIONS.includes(region)) { + throw new CacheWarmError(`Cache warm failed for region ${region} (CW02)`); + } + }, 20); + return Response.json({ region, status: 'warming' }, { status: 202 }); +} diff --git a/apps/error-tracking/no-posthog-next-16-app/app/api/exports/route.ts b/apps/error-tracking/no-posthog-next-16-app/app/api/exports/route.ts new file mode 100644 index 000000000..fc1bcc7dc --- /dev/null +++ b/apps/error-tracking/no-posthog-next-16-app/app/api/exports/route.ts @@ -0,0 +1,14 @@ +import { ExportWriteError } from '@/lib/errors'; + +async function writeExport(batch: string): Promise { + await new Promise((resolve) => setTimeout(resolve, 50)); + if (batch.startsWith('EX-7')) { + throw new ExportWriteError(`CSV export batch ${batch} failed to write (EX77)`); + } +} + +export async function POST(request: Request) { + const { batch } = (await request.json()) as { batch: string }; + void writeExport(batch); + return Response.json({ batch, status: 'started' }, { status: 202 }); +} diff --git a/apps/error-tracking/no-posthog-next-16-app/app/api/geo/route.ts b/apps/error-tracking/no-posthog-next-16-app/app/api/geo/route.ts new file mode 100644 index 000000000..11cbac022 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-16-app/app/api/geo/route.ts @@ -0,0 +1,14 @@ +import { TaxRegionError } from '@/lib/errors'; + +export const runtime = 'edge'; + +const TAX_REGIONS: Record = { US: 'us-sales-tax', DE: 'eu-vat', GB: 'uk-vat', PL: 'eu-vat' }; + +export async function GET(request: Request) { + const country = new URL(request.url).searchParams.get('country') ?? 'US'; + const region = TAX_REGIONS[country]; + if (!region) { + throw new TaxRegionError(`No tax region for country ${country} (GX01)`); + } + return Response.json({ country, region }); +} diff --git a/apps/error-tracking/no-posthog-next-16-app/app/api/invoices/[id]/pdf/route.ts b/apps/error-tracking/no-posthog-next-16-app/app/api/invoices/[id]/pdf/route.ts new file mode 100644 index 000000000..5d079e056 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-16-app/app/api/invoices/[id]/pdf/route.ts @@ -0,0 +1,10 @@ +import { findInvoice, renderPdf } from '@/lib/invoices'; + +export async function GET(_request: Request, { params }: { params: Promise<{ id: string }> }) { + const { id } = await params; + const invoice = findInvoice(id); + if (!invoice) return Response.json({ error: 'not_found' }, { status: 404 }); + return new Response(renderPdf(invoice), { + headers: { 'Content-Type': 'application/pdf', 'Content-Disposition': `attachment; filename="${id}.pdf"` }, + }); +} diff --git a/apps/error-tracking/no-posthog-next-16-app/app/api/invoices/[id]/remind/route.ts b/apps/error-tracking/no-posthog-next-16-app/app/api/invoices/[id]/remind/route.ts new file mode 100644 index 000000000..206cd0dfd --- /dev/null +++ b/apps/error-tracking/no-posthog-next-16-app/app/api/invoices/[id]/remind/route.ts @@ -0,0 +1,9 @@ +import { findInvoice, queueReminder } from '@/lib/invoices'; + +export async function POST(_request: Request, { params }: { params: Promise<{ id: string }> }) { + const { id } = await params; + const invoice = findInvoice(id); + if (!invoice) return Response.json({ error: 'not_found' }, { status: 404 }); + await queueReminder(invoice); + return Response.json({ queued: true }); +} diff --git a/apps/error-tracking/no-posthog-next-16-app/app/api/webhooks/payment/route.ts b/apps/error-tracking/no-posthog-next-16-app/app/api/webhooks/payment/route.ts new file mode 100644 index 000000000..f3a275f07 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-16-app/app/api/webhooks/payment/route.ts @@ -0,0 +1,11 @@ +import { after } from 'next/server'; +import { reconcilePayment } from '@/lib/invoices'; + +type PaymentEvent = { id: string; invoice: string }; + +export async function POST(request: Request) { + const event = (await request.json()) as PaymentEvent; + // Acknowledge the provider right away; reconciliation runs after the response. + after(() => reconcilePayment(event.id, event.invoice)); + return Response.json({ received: true }); +} diff --git a/apps/error-tracking/no-posthog-next-16-app/app/globals.css b/apps/error-tracking/no-posthog-next-16-app/app/globals.css new file mode 100644 index 000000000..8149faedf --- /dev/null +++ b/apps/error-tracking/no-posthog-next-16-app/app/globals.css @@ -0,0 +1,8 @@ +body { font-family: system-ui, sans-serif; margin: 0; color: #1f2933; } +header { display: flex; gap: 1rem; padding: 1rem 2rem; background: #102a43; } +header a { color: #f0f4f8; text-decoration: none; } +main { padding: 2rem; max-width: 56rem; } +table { border-collapse: collapse; width: 100%; } +td, th { border-bottom: 1px solid #d9e2ec; padding: 0.4rem; text-align: left; } +button { padding: 0.4rem 0.8rem; margin-right: 0.5rem; } +.notice { padding: 0.6rem; background: #fff3c4; } diff --git a/apps/error-tracking/no-posthog-next-16-app/app/invoices/[id]/invoice-actions.tsx b/apps/error-tracking/no-posthog-next-16-app/app/invoices/[id]/invoice-actions.tsx new file mode 100644 index 000000000..52be2eb66 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-16-app/app/invoices/[id]/invoice-actions.tsx @@ -0,0 +1,47 @@ +'use client'; + +import { useState } from 'react'; +import { CsvExportError } from '@/lib/errors'; +import type { Invoice } from '@/lib/invoices'; + +const COLUMN_MAPS: Record = { + 'ACME-0007': ['description', 'amount'], + 'GLBX-0101': ['description', 'amount'], + 'UMBR-0471': ['description', 'amount'], + 'HOOL-0515': ['description', 'amount'], +}; + +function toCsv(invoice: Invoice): string { + const columns = COLUMN_MAPS[invoice.customerId]; + if (!columns) { + throw new CsvExportError(`CSV encoder has no column map for ${invoice.id} (CE03)`); + } + return [columns.join(','), ...invoice.lines.map((l) => `"${l.description}",${l.amountCents}`)].join('\n'); +} + +export function InvoiceActions({ invoice }: { invoice: Invoice }) { + const [status, setStatus] = useState(null); + + async function sendReminder() { + setStatus('Sending…'); + const res = await fetch(`/api/invoices/${invoice.id}/remind`, { method: 'POST' }); + setStatus(res.ok ? 'Reminder sent' : 'Could not send reminder'); + } + + function downloadCsv() { + const blob = new Blob([toCsv(invoice)], { type: 'text/csv' }); + const link = document.createElement('a'); + link.href = URL.createObjectURL(blob); + link.download = `${invoice.id}.csv`; + link.click(); + } + + return ( +

+ Download PDF{' '} + + + {status && {status}} +

+ ); +} diff --git a/apps/error-tracking/no-posthog-next-16-app/app/invoices/[id]/page.tsx b/apps/error-tracking/no-posthog-next-16-app/app/invoices/[id]/page.tsx new file mode 100644 index 000000000..14ec8236b --- /dev/null +++ b/apps/error-tracking/no-posthog-next-16-app/app/invoices/[id]/page.tsx @@ -0,0 +1,44 @@ +import type { Metadata } from 'next'; +import { notFound } from 'next/navigation'; +import { customerName, findInvoice, formatMoney, reconcileTotals } from '@/lib/invoices'; +import { InvoiceActions } from './invoice-actions'; + +type Props = { params: Promise<{ id: string }> }; + +export async function generateMetadata({ params }: Props): Promise { + const { id } = await params; + const invoice = findInvoice(id); + if (!invoice) return { title: 'Invoice not found' }; + return { title: `${invoice.id} · ${customerName(invoice)}` }; +} + +export default async function InvoicePage({ params }: Props) { + const { id } = await params; + const invoice = findInvoice(id); + if (!invoice) notFound(); + const total = reconcileTotals(invoice); + + return ( + <> +

{invoice.id}

+

+ Customer {invoice.customerId} · issued {invoice.issuedOn} · {invoice.status} +

+ + + {invoice.lines.map((line, i) => ( + + + + + ))} + + + + + +
{line.description}{formatMoney(line.amountCents)}
Total{formatMoney(total)}
+ + + ); +} diff --git a/apps/error-tracking/no-posthog-next-16-app/app/invoices/new/actions.ts b/apps/error-tracking/no-posthog-next-16-app/app/invoices/new/actions.ts new file mode 100644 index 000000000..d7896bd0b --- /dev/null +++ b/apps/error-tracking/no-posthog-next-16-app/app/invoices/new/actions.ts @@ -0,0 +1,18 @@ +'use server'; + +import { redirect } from 'next/navigation'; +import { checkCredit, listInvoices } from '@/lib/invoices'; + +export async function createInvoice(formData: FormData) { + const customerId = String(formData.get('customer') ?? '').trim().toUpperCase(); + const amountCents = Math.round(Number(formData.get('amount') ?? 0) * 100); + + const open = listInvoices().find((i) => i.customerId === customerId && i.status !== 'draft'); + if (open) { + // Customers with an open invoice get their existing invoice instead of a duplicate. + redirect(`/invoices/${open.id}`); + } + + checkCredit(customerId, amountCents); + redirect('/?created=1'); +} diff --git a/apps/error-tracking/no-posthog-next-16-app/app/invoices/new/page.tsx b/apps/error-tracking/no-posthog-next-16-app/app/invoices/new/page.tsx new file mode 100644 index 000000000..94f72a25b --- /dev/null +++ b/apps/error-tracking/no-posthog-next-16-app/app/invoices/new/page.tsx @@ -0,0 +1,22 @@ +import { createInvoice } from './actions'; + +export default function NewInvoicePage() { + return ( + <> +

New invoice

+
+

+ +

+

+ +

+ +
+ + ); +} diff --git a/apps/error-tracking/no-posthog-next-16-app/app/layout.tsx b/apps/error-tracking/no-posthog-next-16-app/app/layout.tsx new file mode 100644 index 000000000..8877f1ca2 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-16-app/app/layout.tsx @@ -0,0 +1,24 @@ +import type { Metadata } from 'next'; +import Link from 'next/link'; +import './globals.css'; + +export const metadata: Metadata = { + title: 'Ledgerly', + description: 'Invoices for small teams', +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + +
+ Invoices + New invoice + Reports + Settings +
+
{children}
+ + + ); +} diff --git a/apps/error-tracking/no-posthog-next-16-app/app/page.tsx b/apps/error-tracking/no-posthog-next-16-app/app/page.tsx new file mode 100644 index 000000000..be6f6e894 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-16-app/app/page.tsx @@ -0,0 +1,33 @@ +import Link from 'next/link'; +import { formatMoney, listInvoices } from '@/lib/invoices'; + +export default function InvoicesPage() { + const invoices = listInvoices(); + return ( + <> +

Invoices

+ + + + + + + + + + + {invoices.map((invoice) => ( + + + + + + + ))} + +
InvoiceIssuedStatusTotal
+ {invoice.id} + {invoice.issuedOn}{invoice.status}{formatMoney(invoice.totalCents)}
+ + ); +} diff --git a/apps/error-tracking/no-posthog-next-16-app/app/portal/[tenant]/page.tsx b/apps/error-tracking/no-posthog-next-16-app/app/portal/[tenant]/page.tsx new file mode 100644 index 000000000..be80b2723 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-16-app/app/portal/[tenant]/page.tsx @@ -0,0 +1,9 @@ +export default async function PortalPage({ params }: { params: Promise<{ tenant: string }> }) { + const { tenant } = await params; + return ( + <> +

Customer portal

+

Signed in to {tenant}.

+ + ); +} diff --git a/apps/error-tracking/no-posthog-next-16-app/app/reports/error.tsx b/apps/error-tracking/no-posthog-next-16-app/app/reports/error.tsx new file mode 100644 index 000000000..e057ad9dc --- /dev/null +++ b/apps/error-tracking/no-posthog-next-16-app/app/reports/error.tsx @@ -0,0 +1,10 @@ +'use client'; + +export default function ReportsError({ reset }: { error: Error & { digest?: string }; reset: () => void }) { + return ( +
+

We couldn't load this report.

+ +
+ ); +} diff --git a/apps/error-tracking/no-posthog-next-16-app/app/reports/page.tsx b/apps/error-tracking/no-posthog-next-16-app/app/reports/page.tsx new file mode 100644 index 000000000..8e5635b91 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-16-app/app/reports/page.tsx @@ -0,0 +1,10 @@ +import { RevenueChart } from './revenue-chart'; + +export default function ReportsPage() { + return ( + <> +

Revenue

+ + + ); +} diff --git a/apps/error-tracking/no-posthog-next-16-app/app/reports/revenue-chart.tsx b/apps/error-tracking/no-posthog-next-16-app/app/reports/revenue-chart.tsx new file mode 100644 index 000000000..98713701f --- /dev/null +++ b/apps/error-tracking/no-posthog-next-16-app/app/reports/revenue-chart.tsx @@ -0,0 +1,36 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { ReportRangeError } from '@/lib/errors'; +import { formatMoney, listInvoices } from '@/lib/invoices'; + +const RANGES: Record = { + q3: ['2026-07-01', '2026-09-30'], + q4: ['2026-10-01', '2026-12-31'], + ytd: ['2026-01-01', '2026-12-31'], +}; + +function revenueFor(range: string): number { + const bounds = RANGES[range]; + if (!bounds) { + throw new ReportRangeError(`Unsupported report range "${range}" (RX20)`); + } + return listInvoices() + .filter((i) => i.issuedOn >= bounds[0] && i.issuedOn <= bounds[1]) + .reduce((acc, i) => acc + i.totalCents, 0); +} + +export function RevenueChart() { + const [range, setRange] = useState(null); + + useEffect(() => { + setRange(new URLSearchParams(window.location.search).get('range') ?? 'q4'); + }, []); + + if (!range) return

Loading…

; + return ( +

+ Revenue for {range}: {formatMoney(revenueFor(range))} +

+ ); +} diff --git a/apps/error-tracking/no-posthog-next-16-app/app/settings/page.tsx b/apps/error-tracking/no-posthog-next-16-app/app/settings/page.tsx new file mode 100644 index 000000000..794d8f811 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-16-app/app/settings/page.tsx @@ -0,0 +1,10 @@ +import { PreferencesPanel } from './preferences-panel'; + +export default function SettingsPage() { + return ( + <> +

Settings

+ + + ); +} diff --git a/apps/error-tracking/no-posthog-next-16-app/app/settings/preferences-panel.tsx b/apps/error-tracking/no-posthog-next-16-app/app/settings/preferences-panel.tsx new file mode 100644 index 000000000..cfd54fdd9 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-16-app/app/settings/preferences-panel.tsx @@ -0,0 +1,36 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { PreferencesParseError } from '@/lib/errors'; + +type Preferences = { currency: string; reminderDays: number }; + +// Settings exported by the previous version of the app: "currency|reminderDays". +const LEGACY_EXPORT = 'USD;14'; + +function parseLegacy(raw: string): Preferences { + const [currency, days] = raw.split('|'); + if (!days) { + throw new PreferencesParseError('Saved preferences are corrupt (PF88)'); + } + return { currency, reminderDays: Number(days) }; +} + +export function PreferencesPanel() { + const [legacy, setLegacy] = useState(null); + + useEffect(() => { + if (new URLSearchParams(window.location.search).get('import') === 'legacy') setLegacy(LEGACY_EXPORT); + }, []); + + const prefs: Preferences = legacy ? parseLegacy(legacy) : { currency: 'USD', reminderDays: 7 }; + return ( + + ); +} diff --git a/apps/error-tracking/no-posthog-next-16-app/lib/errors.ts b/apps/error-tracking/no-posthog-next-16-app/lib/errors.ts new file mode 100644 index 000000000..05294a457 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-16-app/lib/errors.ts @@ -0,0 +1,51 @@ +export class InvoiceTotalMismatchError extends Error { + override name = 'InvoiceTotalMismatchError'; +} + +export class CustomerLookupError extends Error { + override name = 'CustomerLookupError'; +} + +export class ReportRangeError extends Error { + override name = 'ReportRangeError'; +} + +export class PreferencesParseError extends Error { + override name = 'PreferencesParseError'; +} + +export class CsvExportError extends Error { + override name = 'CsvExportError'; +} + +export class PdfLayoutError extends Error { + override name = 'PdfLayoutError'; +} + +export class ReminderQueueError extends Error { + override name = 'ReminderQueueError'; +} + +export class CreditLimitError extends Error { + override name = 'CreditLimitError'; +} + +export class TenantResolutionError extends Error { + override name = 'TenantResolutionError'; +} + +export class TaxRegionError extends Error { + override name = 'TaxRegionError'; +} + +export class PaymentReconcileError extends Error { + override name = 'PaymentReconcileError'; +} + +export class ExportWriteError extends Error { + override name = 'ExportWriteError'; +} + +export class CacheWarmError extends Error { + override name = 'CacheWarmError'; +} diff --git a/apps/error-tracking/no-posthog-next-16-app/lib/invoices.ts b/apps/error-tracking/no-posthog-next-16-app/lib/invoices.ts new file mode 100644 index 000000000..0153b6795 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-16-app/lib/invoices.ts @@ -0,0 +1,149 @@ +import { + CreditLimitError, + CustomerLookupError, + InvoiceTotalMismatchError, + PaymentReconcileError, + PdfLayoutError, + ReminderQueueError, +} from './errors'; + +export type InvoiceLine = { description: string; amountCents: number }; + +export type Invoice = { + id: string; + customerId: string; + issuedOn: string; + status: 'draft' | 'sent' | 'paid'; + lines: InvoiceLine[]; + totalCents: number; +}; + +const CUSTOMERS: Record = { + 'ACME-0007': { name: 'Acme Corp', creditLimitCents: 5_000_000 }, + 'ACME-0042': { name: 'Acme Logistics', creditLimitCents: 0 }, + 'GLBX-0101': { name: 'Globex', creditLimitCents: 2_000_000 }, + 'INIT-0300': { name: 'Initech', creditLimitCents: 1_000_000 }, + 'UMBR-0471': { name: 'Umbrella Health', creditLimitCents: 3_000_000 }, + 'HOOL-0515': { name: 'Hooli', creditLimitCents: 9_000_000 }, +}; + +const INVOICES: Invoice[] = [ + { + id: 'INV-1001', + customerId: 'ACME-0007', + issuedOn: '2026-09-02', + status: 'paid', + lines: [{ description: 'Annual support plan', amountCents: 480_000 }], + totalCents: 480_000, + }, + { + id: 'INV-2202', + customerId: 'GLBX-0101', + issuedOn: '2026-09-18', + status: 'sent', + lines: [ + { description: 'Onboarding workshop', amountCents: 150_000 }, + { description: 'Travel', amountCents: 32_500 }, + ], + totalCents: 182_500, + }, + { + id: 'INV-3003', + customerId: 'INIT-0300', + issuedOn: '2026-10-01', + status: 'draft', + lines: [{ description: 'TPS report automation', amountCents: 99_000 }], + totalCents: 99_000, + }, + { + id: 'INV-4471', + customerId: 'UMBR-0471', + issuedOn: '2026-10-04', + status: 'sent', + lines: [ + { description: 'Data migration', amountCents: 240_000 }, + { description: 'Discount', amountCents: -24_000 }, + ], + totalCents: 240_000, + }, + { + id: 'INV-5150', + customerId: 'HOOL-0515', + issuedOn: '2026-10-09', + status: 'sent', + lines: Array.from({ length: 64 }, (_, i) => ({ + description: `Compute reservation block ${i + 1}`, + amountCents: 12_000, + })), + totalCents: 64 * 12_000, + }, + { + id: 'INV-0913', + customerId: 'CUST-0913', + issuedOn: '2026-10-12', + status: 'draft', + lines: [{ description: 'Consulting', amountCents: 75_000 }], + totalCents: 75_000, + }, +]; + +const LINES_PER_PDF_PAGE = 48; +const BILLING_DATE = '2026-10-28'; + +export function listInvoices(): Invoice[] { + return INVOICES; +} + +export function findInvoice(id: string): Invoice | undefined { + return INVOICES.find((invoice) => invoice.id === id); +} + +export function customerName(invoice: Invoice): string { + const customer = CUSTOMERS[invoice.customerId]; + if (!customer) { + throw new CustomerLookupError(`Customer record missing for ${invoice.id} (MX13)`); + } + return customer.name; +} + +export function reconcileTotals(invoice: Invoice): number { + const sum = invoice.lines.reduce((acc, line) => acc + line.amountCents, 0); + if (sum !== invoice.totalCents) { + throw new InvoiceTotalMismatchError(`Invoice total mismatch for ${invoice.id} (TM71)`); + } + return sum; +} + +export function renderPdf(invoice: Invoice): string { + if (invoice.lines.length > LINES_PER_PDF_PAGE) { + throw new PdfLayoutError(`PDF layout overflow on ${invoice.id} (PD50)`); + } + const body = invoice.lines.map((l) => `${l.description}\t${formatMoney(l.amountCents)}`).join('\n'); + return `INVOICE ${invoice.id}\n${body}\nTOTAL ${formatMoney(invoice.totalCents)}\n`; +} + +export async function queueReminder(invoice: Invoice): Promise { + // The reminder queue only accepts invoices that were sent less than 30 days ago. + const ageDays = (Date.parse(BILLING_DATE) - Date.parse(invoice.issuedOn)) / 86_400_000; + if (invoice.status === 'sent' && ageDays > 30) { + throw new ReminderQueueError(`Reminder queue rejected ${invoice.id} (RQ02)`); + } +} + +export function checkCredit(customerId: string, amountCents: number): void { + const customer = CUSTOMERS[customerId]; + if (customer && amountCents > customer.creditLimitCents) { + throw new CreditLimitError(`Credit limit check failed for ${customerId} (CL42)`); + } +} + +export async function reconcilePayment(paymentId: string, invoiceId: string): Promise { + const invoice = findInvoice(invoiceId); + if (!invoice || invoice.status !== 'paid') { + throw new PaymentReconcileError(`Payment reconcile failed for ${paymentId} (RC21)`); + } +} + +export function formatMoney(cents: number): string { + return `$${(cents / 100).toFixed(2)}`; +} diff --git a/apps/error-tracking/no-posthog-next-16-app/next.config.ts b/apps/error-tracking/no-posthog-next-16-app/next.config.ts new file mode 100644 index 000000000..b22af960a --- /dev/null +++ b/apps/error-tracking/no-posthog-next-16-app/next.config.ts @@ -0,0 +1,5 @@ +import type { NextConfig } from 'next'; + +const nextConfig: NextConfig = {}; + +export default nextConfig; diff --git a/apps/error-tracking/no-posthog-next-16-app/package-lock.json b/apps/error-tracking/no-posthog-next-16-app/package-lock.json new file mode 100644 index 000000000..b9644f409 --- /dev/null +++ b/apps/error-tracking/no-posthog-next-16-app/package-lock.json @@ -0,0 +1,1076 @@ +{ + "name": "ledgerly", + "version": "0.4.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ledgerly", + "version": "0.4.0", + "dependencies": { + "next": "16.4.2", + "react": "19.3.1", + "react-dom": "19.3.1" + }, + "devDependencies": { + "@types/node": "22.10.0", + "@types/react": "19.1.0", + "@types/react-dom": "19.1.0", + "typescript": "5.7.2" + } + }, + "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/@next/env": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.4.2.tgz", + "integrity": "sha512-nTUTFH7G5/J/fmpkjVLegOkqyKOP0CEsJcMsyO4UCYeN1su+YMW/MfaSErnJskL7XM+UDK6aTvslxsRCyya/sA==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.4.2.tgz", + "integrity": "sha512-ONkhFvZNFWlsEHOM/81AtxdyuJP2KRYKYR0JxPwvcu1AuPMMMgcL+wW2luldJvMtZwKtUxoSkCychW5wIeoYSw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.4.2.tgz", + "integrity": "sha512-luPCMbOsCoaF+kXJEUjmjmOZg1lC3BflhmUu+ZmW4tBu2vW/4AXak8JBFvePizGkxVmNiAw/r9ra+6UviCWtWQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.4.2.tgz", + "integrity": "sha512-xhwFTAncJyaCNy5KqKKuKaIIdCy1/1ZV1s5S1G0hhT7wGMkDTlfJ6aQbfRFnIG/s3/kDrxQ8aycVe+c5QnP8ow==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.4.2.tgz", + "integrity": "sha512-T1PNmfbPcxr5ndtWUL/3wQtLXsz808lq6sc2GrNT5k2oquF7UyqZddHSuAewHVsWsGXe6nsl9HoN1YNriVjuSg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.4.2.tgz", + "integrity": "sha512-N9cDUOCGtkJG5PNKDFZUF3xXndrJJcpLp121uAGjIGvO/U2p9tfdNOM9htj7+5f72l0m8rMw/cvYacUJBQoudg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.4.2.tgz", + "integrity": "sha512-yY2uh1YhX6ufuYy2FCbw1dqblbhI57GLYAJfCkqD9auTv6pLrH67Uccjc3QMKBeCYAG6lkgu08ExmX1bbjejjQ==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.4.2.tgz", + "integrity": "sha512-j/ArWRIHpMMB6wYLMhr2puTguBt70lNU/Cmjl9p6dTDE8DGknYJMP9PdwPk2dqpCNL/KIX9yfngDvIesxJPmsg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.4.2.tgz", + "integrity": "sha512-AA4edyWHDko3NCSo0x6GomsdgFb0oR1J3MXU1b5xt6FRFg4AucrMPW3S0hCe/9onU0O1BbpGQBsqo1r0Gneu1g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "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/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/react": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.0.tgz", + "integrity": "sha512-UaicktuQI+9UKyA4njtDOGBD/67t8YEBt2xdfqu8+gP9hqPUPsiXlNPcpS2gVdjmis5GKPG3fCxbQLVgxsQZ8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-21E2zejNNRtjG4hKIyJz4aWswGEcNFTgttA0bZIRGjj1HA/tbSUxIJnIcYbn98pwJck0cS1bsQhn6eaKqbcFWw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.30", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.30.tgz", + "integrity": "sha512-YJs6Pz17RMNv8HoyKREtCY38E3BV1nPkWVmIb+XN1uHC1Vqt3KZCH/uJH9ZKRoiKb/icZ6tvehyee6RZNbFfRA==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "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/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/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/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "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.4.2", + "resolved": "https://registry.npmjs.org/next/-/next-16.4.2.tgz", + "integrity": "sha512-r+oBJojfefUjOiWkBhXgLS+igxX/7BWyK59uHnyOyplMSLtBZ+QLP3okrdYTbrfrbrmNTVWLBPaHDX9KRaIt3w==", + "license": "MIT", + "dependencies": { + "@next/env": "16.4.2", + "@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.4.2", + "@next/swc-darwin-x64": "16.4.2", + "@next/swc-linux-arm64-gnu": "16.4.2", + "@next/swc-linux-arm64-musl": "16.4.2", + "@next/swc-linux-x64-gnu": "16.4.2", + "@next/swc-linux-x64-musl": "16.4.2", + "@next/swc-win32-arm64-msvc": "16.4.2", + "@next/swc-win32-x64-msvc": "16.4.2", + "sharp": "^0.35.5" + }, + "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/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/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/react": { + "version": "19.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.1.tgz", + "integrity": "sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.1.tgz", + "integrity": "sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.1" + } + }, + "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/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/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.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/apps/error-tracking/no-posthog-next-16-app/package.json b/apps/error-tracking/no-posthog-next-16-app/package.json new file mode 100644 index 000000000..4dbcd44ca --- /dev/null +++ b/apps/error-tracking/no-posthog-next-16-app/package.json @@ -0,0 +1,21 @@ +{ + "name": "ledgerly", + "version": "0.4.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "next": "16.4.2", + "react": "19.3.1", + "react-dom": "19.3.1" + }, + "devDependencies": { + "@types/node": "22.10.0", + "@types/react": "19.1.0", + "@types/react-dom": "19.1.0", + "typescript": "5.7.2" + } +} diff --git a/apps/error-tracking/no-posthog-next-16-app/proxy.ts b/apps/error-tracking/no-posthog-next-16-app/proxy.ts new file mode 100644 index 000000000..4641c08df --- /dev/null +++ b/apps/error-tracking/no-posthog-next-16-app/proxy.ts @@ -0,0 +1,16 @@ +import { NextResponse, type NextRequest } from 'next/server'; +import { TenantResolutionError } from '@/lib/errors'; + +const TENANT_SLUG = /^[a-z0-9-]+$/; + +export function proxy(request: NextRequest) { + const slug = request.nextUrl.pathname.split('/')[2] ?? ''; + if (!TENANT_SLUG.test(slug)) { + throw new TenantResolutionError(`Tenant slug is not valid: ${slug} (TN09)`); + } + const response = NextResponse.next(); + response.headers.set('x-tenant', slug); + return response; +} + +export const config = { matcher: '/portal/:path*' }; diff --git a/apps/error-tracking/no-posthog-next-16-app/tsconfig.json b/apps/error-tracking/no-posthog-next-16-app/tsconfig.json new file mode 100644 index 000000000..e7ff3a26b --- /dev/null +++ b/apps/error-tracking/no-posthog-next-16-app/tsconfig.json @@ -0,0 +1,41 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "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" + ] +} From f44b38b973c66b461fe5a6b73a9a21a0a58576d2 Mon Sep 17 00:00:00 2001 From: ablaszkiewicz Date: Wed, 28 Oct 2026 21:53:53 +0100 Subject: [PATCH 2/2] docs(error-tracking): Next.js fixture expectations and scenario manifest schema Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FkuJAD3n8f6Ra7L9p2UmPw --- apps/error-tracking/.scenarios/README.md | 63 ++++++++++++++++++++++++ apps/error-tracking/README.md | 10 ++++ 2 files changed, 73 insertions(+) create mode 100644 apps/error-tracking/.scenarios/README.md diff --git a/apps/error-tracking/.scenarios/README.md b/apps/error-tracking/.scenarios/README.md new file mode 100644 index 000000000..7a4a71a62 --- /dev/null +++ b/apps/error-tracking/.scenarios/README.md @@ -0,0 +1,63 @@ +# Scenario manifests + +One file per error-tracking fixture, `.json`, named after the app +directory. This directory sits outside every app, so the wizard's agent never +sees the expectations: `run.sh` copies only files inside the app, and the app +finders skip hidden directories. + +A manifest says how to build and start the app, how to trigger each error, and +what PostHog must receive for it. The QA scripts that read it +(`exercise.mjs`, `verify.mjs`) live in the `make-framework-great-again` skill. + +```json +{ + "app": "no-posthog-next-16-app", + "lifecycle": { + "install": { "argv": ["npm", "ci"] }, + "build": { "argv": ["npm", "run", "build"] }, + "start": { "argv": ["npm", "run", "start", "--", "-p", "{port}"], "ready": { "path": "/", "timeoutMs": 60000 } }, + "env": {}, + "flushWaitMs": 15000 + }, + "scenarios": [ + { + "id": "route-handler-headers", + "surface": "Route Handler throws, request with X-POSTHOG-* tracing headers", + "requirement": "CTX-1", + "trigger": { "kind": "http", "path": "/api/invoices/INV-5150/pdf", "identity": "headers", "expectStatus": 500 }, + "throws": { "match": "PD50", "file": "lib/invoices.ts" }, + "expect": { "count": 1, "lib": "posthog-node", "linked": true, "request": true, + "topFrame": { "resolved": true, "sourceEndsWith": "lib/invoices.ts" } } + } + ] +} +``` + +## Fields + +- `lifecycle`: commands run in the app copy with a clean environment (`PATH`, + `HOME`, `CI=true`, `PORT`) plus `env`; `{port}` and `{base}` are + substituted. `flushWaitMs` keeps the server up after the last trigger so + server SDKs flush. +- `trigger.kind`: + - `http`: `method`, `path`, `body`, `headers`, `expectStatus`, and + `identity`: `headers` (synthetic `X-POSTHOG-DISTINCT-ID` and + `X-POSTHOG-SESSION-ID`), `cookie` (a fresh `ph__posthog` cookie with + the same synthetic identity), or `none`. + - `browser`: `page`, `actions` (`{click}`, `{fill: {selector, value}}`, + `{goto}`, `{evaluate}`, `{wait}`), `waitMs`, `expectText` or + `expectPageError`. The identity is read from posthog-js's cookie or + localStorage. + - `process`: `argv`, `env`, `expectExit`. + - `gapMs` on any trigger: pause after it (default 1.5 s). +- `throws.match`: a substring of the error message, unique across the fixture. + `throws.urlMatch`: a page or request path, for negatives where nothing is + thrown (a 404 page). `throws.file`: where the code throws. +- `expect`: `count` (0 for errors that must not be captured), `lib` (`$lib` + prefix), `linked` (distinct ID and session ID equal the trigger's identity), + `request` (`$current_url`, `$request_path` or `$pathname` present), + `release`, `topFrame` (`resolved`, `sourceEndsWith`), `handled`, `level`. + +Expectations encode the error-tracking instrumentation standard, not what the +wizard does today. Fixture code never names them: errors carry opaque codes +(`PD50`), and the manifest maps each code to the surface it tests. diff --git a/apps/error-tracking/README.md b/apps/error-tracking/README.md index 735bb12a5..db941e143 100644 --- a/apps/error-tracking/README.md +++ b/apps/error-tracking/README.md @@ -101,6 +101,16 @@ Each of these apps has a setup that a generic integration gets wrong. As with th - `posthog-js` kept as an exact pin, upgraded only when the install fails on it - no `posthog-node` in the edge runtime (`middleware.ts`, `app/api/edge/route.ts`) +**`no-posthog-next-16-app`**, **`next-16-app`**, **`no-posthog-next-14-app`**, **`no-posthog-next-15-pages`**: one invoicing app (Ledgerly) on Next.js 16 App Router (Turbopack build), the same app with PostHog already used for analytics and a branded `global-error.tsx`, Next.js 14 App Router, and Next.js 15 Pages Router. Each has a scenario manifest in `.scenarios/` that names every error surface it exercises. Expected: + +- `capture_exceptions: true` in the browser init, and capture added to every existing `error.tsx` and custom `global-error.tsx` (a custom `global-error.tsx` turns off root autocapture on Next.js ≥ 15.2; on 14 and the Pages Router, every client render error needs a boundary capture: `_app` boundary or `_error`) +- server errors captured on 15 and 16 through `instrumentation.ts` `onRequestError`, in both the Node.js and the edge runtime, with the browser's distinct ID and session ID read from the `X-POSTHOG-*` headers first and the PostHog cookie second, and `tracing_headers: [window.location.hostname]` in the browser init +- on 14, which has no `onRequestError`: no `onRequestError` export that never runs; server errors captured where the app handles them (route handlers, server actions), or the report says server-side capture needs Next.js 15; `experimental.instrumentationHook` set when `instrumentation.ts` is added +- posthog-node exception autocapture started from `register()`, not lazily from the first request error +- the existing `posthog-js` and `posthog-node` clients in `next-16-app` kept as they were, including their host fallback +- `notFound()`, `redirect()` and 404 pages produce no `$exception` +- source maps uploaded only when the upload credentials are present, so a build without them still passes + **`vite-yarn-pnp`**: Vite and React on Yarn 4 Plug'n'Play, with no `node_modules` and a seven-day `npmMinimalAgeGate`. `posthog-js` runs through `PostHogProvider` with `VITE_PH_PROJECT_TOKEN`. The build emits hidden source maps and `.gz` and `.br` copies, and the GitHub Pages workflow publishes all of `dist/`. Expected: - packages added with `yarn add`, so `.pnp.cjs` and `yarn.lock` change together, never with npm or a switch to `nodeLinker: node-modules`