diff --git a/.gitignore b/.gitignore index 9a541d2b..2f788e2f 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,28 @@ scripts/ /scripts/* # worktrees .worktrees/ + +# ── GSD baseline (auto-generated) ── +Thumbs.db +*.swp +*.swo +*~ +.idea/ +.vscode/ +*.code-workspace +.env.* +!.env.example +node_modules/ +.next/ +dist/ +build/ +__pycache__/ +*.pyc +.venv/ +venv/ +target/ +vendor/ +*.log +coverage/ +.cache/ +tmp/ diff --git a/next.config.ts b/next.config.ts index 24954e4a..acc8db9c 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,4 +1,5 @@ import type { NextConfig } from "next"; +import bundleAnalyzer from "@next/bundle-analyzer"; const nextConfig: NextConfig = { output: "standalone", @@ -15,4 +16,8 @@ const nextConfig: NextConfig = { }, }; -export default nextConfig; +const withBundleAnalyzer = bundleAnalyzer({ + enabled: process.env.ANALYZE === "true", +}); + +export default withBundleAnalyzer(nextConfig); diff --git a/package.json b/package.json index 03df1173..bbd0ea1a 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "build": "next build", "start": "next start", "lint": "eslint", + "test": "vitest run", "postinstall": "prisma generate" }, "dependencies": { @@ -64,6 +65,7 @@ } }, "devDependencies": { + "@next/bundle-analyzer": "^16.2.1", "@tailwindcss/postcss": "^4", "@types/bcryptjs": "^3.0.0", "@types/dagre": "^0.7.54", @@ -80,6 +82,8 @@ "shadcn": "^3.8.5", "tailwindcss": "^4", "tw-animate-css": "^1.4.0", - "typescript": "^5" + "typescript": "^5", + "vitest": "^4.1.0", + "vitest-mock-extended": "^3.1.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 70f01709..398fbff2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -136,6 +136,9 @@ importers: specifier: ^5.0.11 version: 5.0.11(@types/react@19.2.14)(immer@11.1.4)(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3)) devDependencies: + '@next/bundle-analyzer': + specifier: ^16.2.1 + version: 16.2.1 '@tailwindcss/postcss': specifier: ^4 version: 4.2.1 @@ -187,6 +190,12 @@ importers: typescript: specifier: ^5 version: 5.9.3 + vitest: + specifier: ^4.1.0 + version: 4.1.0(@types/node@20.19.35)(msw@2.12.10(@types/node@20.19.35)(typescript@5.9.3))(vite@8.0.1(@types/node@20.19.35)(jiti@2.6.1)) + vitest-mock-extended: + specifier: ^3.1.0 + version: 3.1.0(typescript@5.9.3)(vitest@4.1.0(@types/node@20.19.35)(msw@2.12.10(@types/node@20.19.35)(typescript@5.9.3))(vite@8.0.1(@types/node@20.19.35)(jiti@2.6.1))) packages: @@ -382,6 +391,10 @@ packages: '@dagrejs/graphlib@3.0.4': resolution: {integrity: sha512-HxZ7fCvAwTLCWCO0WjDkzAFQze8LdC6iOpKbetDKHIuDfIgMlIzYzqZ4nxwLlclQX+3ZVeZ1K2OuaOE2WWcyOg==} + '@discoveryjs/json-ext@0.5.7': + resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} + engines: {node: '>=10.0.0'} + '@dotenvx/dotenvx@1.52.0': resolution: {integrity: sha512-CaQcc8JvtzQhUSm9877b6V4Tb7HCotkcyud9X2YwdqtQKwgljkMRwU96fVYKnzN3V0Hj74oP7Es+vZ0mS+Aa1w==} hasBin: true @@ -720,6 +733,12 @@ packages: '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + '@napi-rs/wasm-runtime@1.1.1': + resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} + + '@next/bundle-analyzer@16.2.1': + resolution: {integrity: sha512-fbj2WE6dnCyG8CvQnrBfpHyxdOIyZ4aEHJY0bSqAmamRiIXDqunFQPDvuSOPo24mJE9zQHw7TY6d+sGrXO98TQ==} + '@next/env@16.1.6': resolution: {integrity: sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==} @@ -815,9 +834,15 @@ packages: '@open-draft/until@2.1.0': resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@oxc-project/types@0.120.0': + resolution: {integrity: sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg==} + '@panva/hkdf@1.2.1': resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==} + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + '@prisma/adapter-pg@7.4.2': resolution: {integrity: sha512-oUo2Zhe9Tf6YwVL8kLPuOLTK1Z2pwi/Ua77t2PuGyBan2w7shRKqHvYK+3XXmRH9RWhPJ4SMtHZKpNo6Ax/4bQ==} @@ -1566,6 +1591,98 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + '@rolldown/binding-android-arm64@1.0.0-rc.10': + resolution: {integrity: sha512-jOHxwXhxmFKuXztiu1ORieJeTbx5vrTkcOkkkn2d35726+iwhrY1w/+nYY/AGgF12thg33qC3R1LMBF5tHTZHg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.10': + resolution: {integrity: sha512-gED05Teg/vtTZbIJBc4VNMAxAFDUPkuO/rAIyyxZjTj1a1/s6z5TII/5yMGZ0uLRCifEtwUQn8OlYzuYc0m70w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.10': + resolution: {integrity: sha512-rI15NcM1mA48lqrIxVkHfAqcyFLcQwyXWThy+BQ5+mkKKPvSO26ir+ZDp36AgYoYVkqvMcdS8zOE6SeBsR9e8A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.10': + resolution: {integrity: sha512-XZRXHdTa+4ME1MuDVp021+doQ+z6Ei4CCFmNc5/sKbqb8YmkiJdj8QKlV3rCI0AJtAeSB5n0WGPuJWNL9p/L2w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.10': + resolution: {integrity: sha512-R0SQMRluISSLzFE20sPWYHVmJdDQnRyc/FzSCN72BqQmh2SOZUFG+N3/vBZpR4C6WpEUVYJLrYUXaj43sJsNLA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.10': + resolution: {integrity: sha512-Y1reMrV/o+cwpduYhJuOE3OMKx32RMYCidf14y+HssARRmhDuWXJ4yVguDg2R/8SyyGNo+auzz64LnPK9Hq6jg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.10': + resolution: {integrity: sha512-vELN+HNb2IzuzSBUOD4NHmP9yrGwl1DVM29wlQvx1OLSclL0NgVWnVDKl/8tEks79EFek/kebQKnNJkIAA4W2g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.10': + resolution: {integrity: sha512-ZqrufYTgzxbHwpqOjzSsb0UV/aV2TFIY5rP8HdsiPTv/CuAgCRjM6s9cYFwQ4CNH+hf9Y4erHW1GjZuZ7WoI7w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.10': + resolution: {integrity: sha512-gSlmVS1FZJSRicA6IyjoRoKAFK7IIHBs7xJuHRSmjImqk3mPPWbR7RhbnfH2G6bcmMEllCt2vQ/7u9e6bBnByg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.10': + resolution: {integrity: sha512-eOCKUpluKgfObT2pHjztnaWEIbUabWzk3qPZ5PuacuPmr4+JtQG4k2vGTY0H15edaTnicgU428XW/IH6AimcQw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.10': + resolution: {integrity: sha512-Xdf2jQbfQowJnLcgYfD/m0Uu0Qj5OdxKallD78/IPPfzaiaI4KRAwZzHcKQ4ig1gtg1SuzC7jovNiM2TzQsBXA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.10': + resolution: {integrity: sha512-o1hYe8hLi1EY6jgPFyxQgQ1wcycX+qz8eEbVmot2hFkgUzPxy9+kF0u0NIQBeDq+Mko47AkaFFaChcvZa9UX9Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.10': + resolution: {integrity: sha512-Ugv9o7qYJudqQO5Y5y2N2SOo6S4WiqiNOpuQyoPInnhVzCY+wi/GHltcLHypG9DEUYMB0iTB/huJrpadiAcNcA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.10': + resolution: {integrity: sha512-7UODQb4fQUNT/vmgDZBl3XOBAIOutP5R3O/rkxg0aLfEGQ4opbCgU5vOw/scPe4xOqBwL9fw7/RP1vAMZ6QlAQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.10': + resolution: {integrity: sha512-PYxKHMVHOb5NJuDL53vBUl1VwUjymDcYI6rzpIni0C9+9mTiJedvUxSk7/RPp7OOAm3v+EjgMu9bIy3N6b408w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.10': + resolution: {integrity: sha512-UkVDEFk1w3mveXeKgaTuYfKWtPbvgck1dT8TUG3bnccrH0XtLTuAyfCoks4Q/M5ZGToSVJTIQYCzy2g/atAOeg==} + '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} @@ -1711,6 +1828,9 @@ packages: resolution: {integrity: sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg==} deprecated: This is a stub types definition. bcryptjs provides its own type definitions, so you do not need this installed. + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/d3-array@3.2.2': resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} @@ -1753,6 +1873,9 @@ packages: '@types/dagre@0.7.54': resolution: {integrity: sha512-QjcRY+adGbYvBFS7cwv5txhVIwX1XXIUswWl+kSQTbI6NjgZydrZkEKX/etzVd7i+bCsCb40Z/xlBY5eoFuvWQ==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -1948,6 +2071,35 @@ packages: cpu: [x64] os: [win32] + '@vitest/expect@4.1.0': + resolution: {integrity: sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==} + + '@vitest/mocker@4.1.0': + resolution: {integrity: sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.0': + resolution: {integrity: sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==} + + '@vitest/runner@4.1.0': + resolution: {integrity: sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==} + + '@vitest/snapshot@4.1.0': + resolution: {integrity: sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==} + + '@vitest/spy@4.1.0': + resolution: {integrity: sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==} + + '@vitest/utils@4.1.0': + resolution: {integrity: sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==} + '@xyflow/react@12.10.1': resolution: {integrity: sha512-5eSWtIK/+rkldOuFbOOz44CRgQRjtS9v5nufk77DV+XBnfCGL9HAQ8PG00o2ZYKqkEU/Ak6wrKC95Tu+2zuK3Q==} peerDependencies: @@ -1966,6 +2118,10 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + acorn-walk@8.3.5: + resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==} + engines: {node: '>=0.4.0'} + acorn@8.16.0: resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} engines: {node: '>=0.4.0'} @@ -2048,6 +2204,10 @@ packages: resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} engines: {node: '>= 0.4'} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + ast-types-flow@0.0.8: resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} @@ -2150,6 +2310,10 @@ packages: caniuse-lite@1.0.30001774: resolution: {integrity: sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -2227,6 +2391,10 @@ packages: resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -2374,6 +2542,9 @@ packages: resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} engines: {node: '>= 0.4'} + debounce@1.2.1: + resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} + debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -2487,6 +2658,9 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + duplexer@0.1.2: + resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + eciesjs@0.4.17: resolution: {integrity: sha512-TOOURki4G7sD1wDCjj7NfLaXZZ49dFOeEb5y39IXpb8p0hRzVvfvzZHOi5JcT+PpyAbi/Y+lxPb8eTag2WYH8w==} engines: {bun: '>=1', deno: '>=2', node: '>=16'} @@ -2544,6 +2718,9 @@ packages: resolution: {integrity: sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==} engines: {node: '>= 0.4'} + es-module-lexer@2.0.0: + resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -2692,6 +2869,9 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -2719,6 +2899,10 @@ packages: resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} engines: {node: ^18.19.0 || >=20.5.0} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + express-rate-limit@8.3.0: resolution: {integrity: sha512-KJzBawY6fB9FiZGdE/0aftepZ91YlaGIrV8vgblRM3J8X+dHx/aiowJWwkx6LIGyuqGiANsjSwwrbb8mifOJ4Q==} engines: {node: '>= 16'} @@ -2834,6 +3018,11 @@ packages: resolution: {integrity: sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==} engines: {node: '>=14.14'} + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} @@ -2944,6 +3133,10 @@ packages: resolution: {integrity: sha512-uSisMYERbaB9bkA9M4/4dnqyktaEkf1kMHNKq/7DHyxVeWqHQ2mBmVqm5u6/FVHwF3iCNalKcg82Zfl+tffWoA==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + gzip-size@6.0.0: + resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} + engines: {node: '>=10'} + has-bigints@1.1.0: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} @@ -2984,6 +3177,9 @@ packages: resolution: {integrity: sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw==} engines: {node: '>=16.9.0'} + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} @@ -3148,6 +3344,10 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} @@ -3313,70 +3513,140 @@ packages: cpu: [arm64] os: [android] + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + lightningcss-darwin-arm64@1.31.1: resolution: {integrity: sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [darwin] + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + lightningcss-darwin-x64@1.31.1: resolution: {integrity: sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [darwin] + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + lightningcss-freebsd-x64@1.31.1: resolution: {integrity: sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [freebsd] + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + lightningcss-linux-arm-gnueabihf@1.31.1: resolution: {integrity: sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==} engines: {node: '>= 12.0.0'} cpu: [arm] os: [linux] + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + lightningcss-linux-arm64-gnu@1.31.1: resolution: {integrity: sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + lightningcss-linux-arm64-musl@1.31.1: resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + lightningcss-linux-x64-gnu@1.31.1: resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + lightningcss-linux-x64-musl@1.31.1: resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + lightningcss-win32-arm64-msvc@1.31.1: resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [win32] + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + lightningcss-win32-x64-msvc@1.31.1: resolution: {integrity: sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [win32] + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + lightningcss@1.31.1: resolution: {integrity: sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==} engines: {node: '>= 12.0.0'} + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + lilconfig@2.1.0: resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} engines: {node: '>=10'} @@ -3481,6 +3751,10 @@ packages: monaco-editor@0.55.1: resolution: {integrity: sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==} + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -3650,6 +3924,9 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + ohash@2.0.11: resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} @@ -3672,6 +3949,10 @@ packages: resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} engines: {node: '>=20'} + opener@1.5.2: + resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} + hasBin: true + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -3831,6 +4112,10 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + engines: {node: ^10 || ^12 || >=14} + postgres-array@2.0.0: resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} engines: {node: '>=4'} @@ -4104,6 +4389,11 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rolldown@1.0.0-rc.10: + resolution: {integrity: sha512-q7j6vvarRFmKpgJUT8HCAUljkgzEp4LAhPlJUvQhA5LA1SUL36s5QCysMutErzL3EbNOZOkoziSx9iZC4FddKA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + router@2.2.0: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} @@ -4203,6 +4493,9 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -4213,6 +4506,10 @@ packages: simple-git@3.32.3: resolution: {integrity: sha512-56a5oxFdWlsGygOXHWrG+xjj5w9ZIt2uQbzqiIGdR/6i5iococ7WQ/bNPzWxCJdEUGUCmyMH0t9zMpRJTaKxmw==} + sirv@2.0.4: + resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} + engines: {node: '>= 10'} + sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -4241,6 +4538,9 @@ packages: stable-hash@0.0.5: resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + state-local@1.0.7: resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} @@ -4251,6 +4551,9 @@ packages: std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + std-env@4.0.0: + resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} + stdin-discarder@0.2.2: resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} engines: {node: '>=18'} @@ -4363,6 +4666,9 @@ packages: tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinyexec@1.0.2: resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} engines: {node: '>=18'} @@ -4371,6 +4677,10 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + tldts-core@7.0.23: resolution: {integrity: sha512-0g9vrtDQLrNIiCj22HSe9d4mLVG3g5ph5DZ8zCKBr4OtrspmNB6ss7hVyzArAeE88ceZocIEGkyW1Ime7fxPtQ==} @@ -4386,6 +4696,10 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + tough-cookie@6.0.0: resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} engines: {node: '>=16'} @@ -4396,6 +4710,14 @@ packages: peerDependencies: typescript: '>=4.8.4' + ts-essentials@10.1.1: + resolution: {integrity: sha512-4aTB7KLHKmUvkjNj8V+EdnmuVTiECzn3K+zIbRthumvHu+j44x3w63xpfs0JL3NGIzGXqoQ7AV591xHO+XrOTw==} + peerDependencies: + typescript: '>=4.5.0' + peerDependenciesMeta: + typescript: + optional: true + ts-morph@26.0.0: resolution: {integrity: sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug==} @@ -4533,10 +4855,99 @@ packages: victory-vendor@36.9.2: resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} + vite@8.0.1: + resolution: {integrity: sha512-wt+Z2qIhfFt85uiyRt5LPU4oVEJBXj8hZNWKeqFG4gRG/0RaRGJ7njQCwzFVjO+v4+Ipmf5CY7VdmZRAYYBPHw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.0 + esbuild: ^0.27.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest-mock-extended@3.1.0: + resolution: {integrity: sha512-vCM0VkuocOUBwwqwV7JB7YStw07pqeKvEIrZnR8l3PtwYi6rAAJAyJACeC1UYNfbQWi85nz7EdiXWBFI5hll2g==} + peerDependencies: + typescript: 3.x || 4.x || 5.x + vitest: '>=3.0.0' + + vitest@4.1.0: + resolution: {integrity: sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.0 + '@vitest/browser-preview': 4.1.0 + '@vitest/browser-webdriverio': 4.1.0 + '@vitest/ui': 4.1.0 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0-0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + web-streams-polyfill@3.3.3: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} + webpack-bundle-analyzer@4.10.1: + resolution: {integrity: sha512-s3P7pgexgT/HTUSYgxJyn28A+99mmLq4HsJepMPzu0R8ImJc52QNqaFYW1Z2z2uIb1/J3eYgaAWVpaC+v/1aAQ==} + engines: {node: '>= 10.13.0'} + hasBin: true + which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -4566,6 +4977,11 @@ packages: engines: {node: ^16.13.0 || >=18.0.0} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -4581,6 +4997,18 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@7.5.10: + resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + wsl-utils@0.3.1: resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==} engines: {node: '>=20'} @@ -4929,6 +5357,8 @@ snapshots: '@dagrejs/graphlib@3.0.4': {} + '@discoveryjs/json-ext@0.5.7': {} + '@dotenvx/dotenvx@1.52.0': dependencies: commander: 11.1.0 @@ -5260,6 +5690,20 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true + '@napi-rs/wasm-runtime@1.1.1': + dependencies: + '@emnapi/core': 1.8.1 + '@emnapi/runtime': 1.8.1 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@next/bundle-analyzer@16.2.1': + dependencies: + webpack-bundle-analyzer: 4.10.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@next/env@16.1.6': {} '@next/eslint-plugin-next@16.1.6': @@ -5323,8 +5767,12 @@ snapshots: '@open-draft/until@2.1.0': {} + '@oxc-project/types@0.120.0': {} + '@panva/hkdf@1.2.1': {} + '@polka/url@1.0.0-next.29': {} + '@prisma/adapter-pg@7.4.2': dependencies: '@prisma/driver-adapter-utils': 7.4.2 @@ -6159,6 +6607,55 @@ snapshots: '@radix-ui/rect@1.1.1': {} + '@rolldown/binding-android-arm64@1.0.0-rc.10': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.10': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.10': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.10': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.10': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.10': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.10': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.10': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.10': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.10': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.10': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.10': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.10': + dependencies: + '@napi-rs/wasm-runtime': 1.1.1 + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.10': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.10': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.10': {} + '@rtsao/scc@1.1.0': {} '@sec-ant/readable-stream@0.4.1': {} @@ -6281,6 +6778,11 @@ snapshots: dependencies: bcryptjs: 3.0.3 + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + '@types/d3-array@3.2.2': {} '@types/d3-color@3.1.3': {} @@ -6322,6 +6824,8 @@ snapshots: '@types/dagre@0.7.54': {} + '@types/deep-eql@4.0.2': {} + '@types/estree@1.0.8': {} '@types/js-yaml@4.0.9': {} @@ -6509,6 +7013,48 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true + '@vitest/expect@4.1.0': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.0 + '@vitest/utils': 4.1.0 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.0(msw@2.12.10(@types/node@20.19.35)(typescript@5.9.3))(vite@8.0.1(@types/node@20.19.35)(jiti@2.6.1))': + dependencies: + '@vitest/spy': 4.1.0 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + msw: 2.12.10(@types/node@20.19.35)(typescript@5.9.3) + vite: 8.0.1(@types/node@20.19.35)(jiti@2.6.1) + + '@vitest/pretty-format@4.1.0': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.0': + dependencies: + '@vitest/utils': 4.1.0 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.0': + dependencies: + '@vitest/pretty-format': 4.1.0 + '@vitest/utils': 4.1.0 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.0': {} + + '@vitest/utils@4.1.0': + dependencies: + '@vitest/pretty-format': 4.1.0 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + '@xyflow/react@12.10.1(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@xyflow/system': 0.0.75 @@ -6541,6 +7087,10 @@ snapshots: dependencies: acorn: 8.16.0 + acorn-walk@8.3.5: + dependencies: + acorn: 8.16.0 + acorn@8.16.0: {} agent-base@7.1.4: {} @@ -6648,6 +7198,8 @@ snapshots: get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 + assertion-error@2.0.1: {} + ast-types-flow@0.0.8: {} ast-types@0.16.1: @@ -6753,6 +7305,8 @@ snapshots: caniuse-lite@1.0.30001774: {} + chai@6.2.2: {} + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -6833,6 +7387,8 @@ snapshots: commander@14.0.3: {} + commander@7.2.0: {} + concat-map@0.0.1: {} confbox@0.2.4: {} @@ -6965,6 +7521,8 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.2 + debounce@1.2.1: {} + debug@3.2.7: dependencies: ms: 2.1.3 @@ -7045,6 +7603,8 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + duplexer@0.1.2: {} + eciesjs@0.4.17: dependencies: '@ecies/ciphers': 0.2.5(@noble/ciphers@1.3.0) @@ -7162,6 +7722,8 @@ snapshots: iterator.prototype: 1.1.5 safe-array-concat: 1.1.3 + es-module-lexer@2.0.0: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -7394,6 +7956,10 @@ snapshots: estraverse@5.3.0: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + esutils@2.0.3: {} etag@1.8.1: {} @@ -7433,6 +7999,8 @@ snapshots: strip-final-newline: 4.0.0 yoctocolors: 2.1.2 + expect-type@1.3.0: {} + express-rate-limit@8.3.0(express@5.2.1): dependencies: express: 5.2.1 @@ -7581,6 +8149,9 @@ snapshots: jsonfile: 6.2.0 universalify: 2.0.1 + fsevents@2.3.3: + optional: true + function-bind@1.1.2: {} function.prototype.name@1.1.8: @@ -7687,6 +8258,10 @@ snapshots: graphql@16.13.0: {} + gzip-size@6.0.0: + dependencies: + duplexer: 0.1.2 + has-bigints@1.1.0: {} has-flag@4.0.0: {} @@ -7719,6 +8294,8 @@ snapshots: hono@4.12.7: {} + html-escaper@2.0.2: {} + http-errors@2.0.1: dependencies: depd: 2.0.0 @@ -7865,6 +8442,8 @@ snapshots: is-plain-obj@4.1.0: {} + is-plain-object@5.0.0: {} + is-promise@4.0.0: {} is-property@1.0.2: {} @@ -8004,36 +8583,69 @@ snapshots: lightningcss-android-arm64@1.31.1: optional: true + lightningcss-android-arm64@1.32.0: + optional: true + lightningcss-darwin-arm64@1.31.1: optional: true + lightningcss-darwin-arm64@1.32.0: + optional: true + lightningcss-darwin-x64@1.31.1: optional: true + lightningcss-darwin-x64@1.32.0: + optional: true + lightningcss-freebsd-x64@1.31.1: optional: true + lightningcss-freebsd-x64@1.32.0: + optional: true + lightningcss-linux-arm-gnueabihf@1.31.1: optional: true + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + lightningcss-linux-arm64-gnu@1.31.1: optional: true + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + lightningcss-linux-arm64-musl@1.31.1: optional: true + lightningcss-linux-arm64-musl@1.32.0: + optional: true + lightningcss-linux-x64-gnu@1.31.1: optional: true + lightningcss-linux-x64-gnu@1.32.0: + optional: true + lightningcss-linux-x64-musl@1.31.1: optional: true + lightningcss-linux-x64-musl@1.32.0: + optional: true + lightningcss-win32-arm64-msvc@1.31.1: optional: true + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + lightningcss-win32-x64-msvc@1.31.1: optional: true + lightningcss-win32-x64-msvc@1.32.0: + optional: true + lightningcss@1.31.1: dependencies: detect-libc: 2.1.2 @@ -8050,6 +8662,22 @@ snapshots: lightningcss-win32-arm64-msvc: 1.31.1 lightningcss-win32-x64-msvc: 1.31.1 + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + lilconfig@2.1.0: {} lines-and-columns@1.2.4: {} @@ -8133,6 +8761,8 @@ snapshots: dompurify: 3.2.7 marked: 14.0.0 + mrmime@2.0.1: {} + ms@2.1.3: {} msw@2.12.10(@types/node@20.19.35)(typescript@5.9.3): @@ -8309,6 +8939,8 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + obug@2.1.1: {} + ohash@2.0.11: {} on-finished@2.4.1: @@ -8336,6 +8968,8 @@ snapshots: powershell-utils: 0.1.0 wsl-utils: 0.3.1 + opener@1.5.2: {} + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -8492,6 +9126,12 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postcss@8.5.8: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + postgres-array@2.0.0: {} postgres-array@3.0.4: {} @@ -8824,6 +9464,27 @@ snapshots: reusify@1.1.0: {} + rolldown@1.0.0-rc.10: + dependencies: + '@oxc-project/types': 0.120.0 + '@rolldown/pluginutils': 1.0.0-rc.10 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.10 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.10 + '@rolldown/binding-darwin-x64': 1.0.0-rc.10 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.10 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.10 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.10 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.10 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.10 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.10 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.10 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.10 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.10 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.10 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.10 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.10 + router@2.2.0: dependencies: debug: 4.4.3 @@ -9030,6 +9691,8 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} + signal-exit@3.0.7: {} signal-exit@4.1.0: {} @@ -9042,6 +9705,12 @@ snapshots: transitivePeerDependencies: - supports-color + sirv@2.0.4: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + sisteransi@1.0.5: {} sonner@2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3): @@ -9059,12 +9728,16 @@ snapshots: stable-hash@0.0.5: {} + stackback@0.0.2: {} + state-local@1.0.7: {} statuses@2.0.2: {} std-env@3.10.0: {} + std-env@4.0.0: {} + stdin-discarder@0.2.2: {} stop-iteration-iterator@1.1.0: @@ -9185,6 +9858,8 @@ snapshots: tiny-invariant@1.3.3: {} + tinybench@2.9.0: {} + tinyexec@1.0.2: {} tinyglobby@0.2.15: @@ -9192,6 +9867,8 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tinyrainbow@3.1.0: {} + tldts-core@7.0.23: {} tldts@7.0.23: @@ -9204,6 +9881,8 @@ snapshots: toidentifier@1.0.1: {} + totalist@3.0.1: {} + tough-cookie@6.0.0: dependencies: tldts: 7.0.23 @@ -9212,6 +9891,10 @@ snapshots: dependencies: typescript: 5.9.3 + ts-essentials@10.1.1(typescript@5.9.3): + optionalDependencies: + typescript: 5.9.3 + ts-morph@26.0.0: dependencies: '@ts-morph/common': 0.27.0 @@ -9391,8 +10074,72 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 + vite@8.0.1(@types/node@20.19.35)(jiti@2.6.1): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.3 + postcss: 8.5.8 + rolldown: 1.0.0-rc.10 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 20.19.35 + fsevents: 2.3.3 + jiti: 2.6.1 + + vitest-mock-extended@3.1.0(typescript@5.9.3)(vitest@4.1.0(@types/node@20.19.35)(msw@2.12.10(@types/node@20.19.35)(typescript@5.9.3))(vite@8.0.1(@types/node@20.19.35)(jiti@2.6.1))): + dependencies: + ts-essentials: 10.1.1(typescript@5.9.3) + typescript: 5.9.3 + vitest: 4.1.0(@types/node@20.19.35)(msw@2.12.10(@types/node@20.19.35)(typescript@5.9.3))(vite@8.0.1(@types/node@20.19.35)(jiti@2.6.1)) + + vitest@4.1.0(@types/node@20.19.35)(msw@2.12.10(@types/node@20.19.35)(typescript@5.9.3))(vite@8.0.1(@types/node@20.19.35)(jiti@2.6.1)): + dependencies: + '@vitest/expect': 4.1.0 + '@vitest/mocker': 4.1.0(msw@2.12.10(@types/node@20.19.35)(typescript@5.9.3))(vite@8.0.1(@types/node@20.19.35)(jiti@2.6.1)) + '@vitest/pretty-format': 4.1.0 + '@vitest/runner': 4.1.0 + '@vitest/snapshot': 4.1.0 + '@vitest/spy': 4.1.0 + '@vitest/utils': 4.1.0 + es-module-lexer: 2.0.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 4.0.0 + tinybench: 2.9.0 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.1.0 + vite: 8.0.1(@types/node@20.19.35)(jiti@2.6.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 20.19.35 + transitivePeerDependencies: + - msw + web-streams-polyfill@3.3.3: {} + webpack-bundle-analyzer@4.10.1: + dependencies: + '@discoveryjs/json-ext': 0.5.7 + acorn: 8.16.0 + acorn-walk: 8.3.5 + commander: 7.2.0 + debounce: 1.2.1 + escape-string-regexp: 4.0.0 + gzip-size: 6.0.0 + html-escaper: 2.0.2 + is-plain-object: 5.0.0 + opener: 1.5.2 + picocolors: 1.1.1 + sirv: 2.0.4 + ws: 7.5.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 @@ -9444,6 +10191,11 @@ snapshots: dependencies: isexe: 3.1.5 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + word-wrap@1.2.5: {} wrap-ansi@6.2.0: @@ -9460,6 +10212,8 @@ snapshots: wrappy@1.0.2: {} + ws@7.5.10: {} + wsl-utils@0.3.1: dependencies: is-wsl: 3.1.1 diff --git a/src/__mocks__/lib/prisma.ts b/src/__mocks__/lib/prisma.ts new file mode 100644 index 00000000..dd627113 --- /dev/null +++ b/src/__mocks__/lib/prisma.ts @@ -0,0 +1,27 @@ +/** + * Prisma mock helper for Vitest. + * + * Usage in test files: + * ```ts + * import { vi } from "vitest"; + * import { mockDeep, mockReset, type DeepMockProxy } from "vitest-mock-extended"; + * import type { PrismaClient } from "@/generated/prisma"; + * + * vi.mock("@/lib/prisma", () => ({ + * prisma: mockDeep(), + * })); + * + * import { prisma } from "@/lib/prisma"; + * const prismaMock = prisma as unknown as DeepMockProxy; + * + * beforeEach(() => { + * mockReset(prismaMock); + * }); + * ``` + * + * The vi.mock factory is hoisted above imports, so mockDeep creates a fresh + * mock that replaces the real PrismaClient. Importing `prisma` from the + * mocked module gives you the mock instance. Cast it to DeepMockProxy for + * full type-safe mock API access. + */ +export {}; diff --git a/src/app/(dashboard)/alerts/_components/alert-history-section.tsx b/src/app/(dashboard)/alerts/_components/alert-history-section.tsx new file mode 100644 index 00000000..86c81112 --- /dev/null +++ b/src/app/(dashboard)/alerts/_components/alert-history-section.tsx @@ -0,0 +1,174 @@ +"use client"; + +import { useState } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { useTRPC } from "@/trpc/client"; +import { + Loader2, + History, +} from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { Skeleton } from "@/components/ui/skeleton"; +import { StatusBadge } from "@/components/ui/status-badge"; +import { EmptyState } from "@/components/empty-state"; +import { QueryError } from "@/components/query-error"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; + +// ─── Alert History Section ────────────────────────────────────────────────────── + +export function AlertHistorySection({ environmentId }: { environmentId: string }) { + const trpc = useTRPC(); + const [cursor, setCursor] = useState(undefined); + const [allItems, setAllItems] = useState< + Array<{ + id: string; + status: string; + value: number; + message: string | null; + firedAt: Date; + resolvedAt: Date | null; + node: { id: string; host: string } | null; + alertRule: { + id: string; + name: string; + metric: string; + condition: string | null; + threshold: number | null; + pipeline: { id: string; name: string } | null; + }; + }> + >([]); + + const eventsQuery = useQuery( + trpc.alert.listEvents.queryOptions( + { environmentId, limit: 50, cursor }, + { enabled: !!environmentId }, + ), + ); + + // Merge newly fetched items when data changes + const items = eventsQuery.data?.items ?? []; + const nextCursor = eventsQuery.data?.nextCursor; + + // Build display list: first page directly from query, subsequent pages accumulated + const displayItems = cursor ? allItems : items; + + const loadMore = () => { + if (nextCursor) { + setAllItems((prev) => { + // Combine previous items with current items, dedup by id + const existing = new Set(prev.map((i) => i.id)); + const newItems = items.filter((i) => !existing.has(i.id)); + return [...prev, ...newItems]; + }); + setCursor(nextCursor); + } + }; + + const formatTimestamp = (date: Date | string) => { + const d = typeof date === "string" ? new Date(date) : date; + return d.toLocaleString(); + }; + + const isLoading = eventsQuery.isLoading; + const isFetchingMore = eventsQuery.isFetching && !!cursor; + + return ( +
+
+ +

Alert History

+
+ + {eventsQuery.isError ? ( + eventsQuery.refetch()} /> + ) : isLoading && !cursor ? ( +
+ {Array.from({ length: 3 }).map((_, i) => ( + + ))} +
+ ) : displayItems.length === 0 && items.length === 0 ? ( + + ) : ( + <> + + + + Timestamp + Rule Name + Node + Pipeline + Status + Value + Message + + + + {(cursor ? displayItems : items).map((event) => ( + + + {formatTimestamp(event.firedAt)} + + + {event.alertRule.name} + + + {event.node?.host ?? "-"} + + + {event.alertRule.pipeline?.name ?? "-"} + + + + {event.status === "firing" ? "Firing" : "Resolved"} + + + + {typeof event.value === "number" + ? event.value.toFixed(2) + : event.value} + + + {event.message || "-"} + + + ))} + +
+ + {nextCursor && ( +
+ +
+ )} + + )} +
+ ); +} diff --git a/src/app/(dashboard)/alerts/_components/alert-rules-section.tsx b/src/app/(dashboard)/alerts/_components/alert-rules-section.tsx new file mode 100644 index 00000000..65146cc7 --- /dev/null +++ b/src/app/(dashboard)/alerts/_components/alert-rules-section.tsx @@ -0,0 +1,568 @@ +"use client"; + +import { useState, useCallback } from "react"; +import { + useQuery, + useMutation, + useQueryClient, +} from "@tanstack/react-query"; +import { useTRPC } from "@/trpc/client"; +import { useTeamStore } from "@/stores/team-store"; +import { toast } from "sonner"; +import { + Plus, + Pencil, + Trash2, + Loader2, +} from "lucide-react"; +import type { AlertMetric, AlertCondition } from "@/generated/prisma"; + +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Badge } from "@/components/ui/badge"; +import { Switch } from "@/components/ui/switch"; +import { Skeleton } from "@/components/ui/skeleton"; +import { EmptyState } from "@/components/empty-state"; +import { QueryError } from "@/components/query-error"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { ConfirmDialog } from "@/components/confirm-dialog"; +import { isEventMetric } from "@/lib/alert-metrics"; + +import { + METRIC_LABELS, + CONDITION_LABELS, + BINARY_METRICS, + GLOBAL_METRICS, + CHANNEL_TYPE_LABELS, +} from "./constants"; + +// ─── Alert Rules Section ──────────────────────────────────────────────────────── + +interface RuleFormState { + name: string; + pipelineId: string; + metric: string; + condition: string; + threshold: string; + durationSeconds: string; + channelIds: string[]; +} + +const EMPTY_RULE_FORM: RuleFormState = { + name: "", + pipelineId: "", + metric: "", + condition: "", + threshold: "", + durationSeconds: "60", + channelIds: [], +}; + +export function AlertRulesSection({ environmentId }: { environmentId: string }) { + const trpc = useTRPC(); + const queryClient = useQueryClient(); + const selectedTeamId = useTeamStore((s) => s.selectedTeamId); + + const [dialogOpen, setDialogOpen] = useState(false); + const [editingRuleId, setEditingRuleId] = useState(null); + const [form, setForm] = useState(EMPTY_RULE_FORM); + const [deleteTarget, setDeleteTarget] = useState<{ + id: string; + name: string; + } | null>(null); + + const rulesQuery = useQuery( + trpc.alert.listRules.queryOptions( + { environmentId }, + { enabled: !!environmentId }, + ), + ); + + const pipelinesQuery = useQuery( + trpc.pipeline.list.queryOptions( + { environmentId }, + { enabled: !!environmentId }, + ), + ); + + const channelsQuery = useQuery( + trpc.alert.listChannels.queryOptions( + { environmentId }, + { enabled: !!environmentId }, + ), + ); + + const invalidateRules = useCallback(() => { + queryClient.invalidateQueries({ + queryKey: trpc.alert.listRules.queryKey({ environmentId }), + }); + }, [queryClient, trpc, environmentId]); + + const createMutation = useMutation( + trpc.alert.createRule.mutationOptions({ + onSuccess: () => { + toast.success("Alert rule created"); + invalidateRules(); + setDialogOpen(false); + }, + onError: (error) => { + toast.error(error.message || "Failed to create alert rule"); + }, + }), + ); + + const updateMutation = useMutation( + trpc.alert.updateRule.mutationOptions({ + onSuccess: () => { + toast.success("Alert rule updated"); + invalidateRules(); + setDialogOpen(false); + setEditingRuleId(null); + }, + onError: (error) => { + toast.error(error.message || "Failed to update alert rule"); + }, + }), + ); + + const toggleMutation = useMutation( + trpc.alert.updateRule.mutationOptions({ + onSuccess: () => { + invalidateRules(); + }, + onError: (error) => { + toast.error(error.message || "Failed to toggle alert rule"); + }, + }), + ); + + const deleteMutation = useMutation( + trpc.alert.deleteRule.mutationOptions({ + onSuccess: () => { + toast.success("Alert rule deleted"); + invalidateRules(); + setDeleteTarget(null); + }, + onError: (error) => { + toast.error(error.message || "Failed to delete alert rule"); + }, + }), + ); + + const rules = rulesQuery.data ?? []; + const pipelines = pipelinesQuery.data ?? []; + const channels = channelsQuery.data ?? []; + + const openCreate = () => { + setEditingRuleId(null); + setForm(EMPTY_RULE_FORM); + setDialogOpen(true); + }; + + const openEdit = (rule: (typeof rules)[0]) => { + setEditingRuleId(rule.id); + const skipThreshold = isEventMetric(rule.metric) || BINARY_METRICS.has(rule.metric); + setForm({ + name: rule.name, + pipelineId: rule.pipelineId ?? "", + metric: rule.metric, + condition: skipThreshold ? "" : (rule.condition ?? "gt"), + threshold: skipThreshold ? "" : String(rule.threshold ?? ""), + durationSeconds: skipThreshold ? "" : String(rule.durationSeconds ?? ""), + channelIds: rule.channels?.map((c) => c.channelId) ?? [], + }); + setDialogOpen(true); + }; + + const toggleChannel = (channelId: string) => { + setForm((f) => ({ + ...f, + channelIds: f.channelIds.includes(channelId) + ? f.channelIds.filter((id) => id !== channelId) + : [...f.channelIds, channelId], + })); + }; + + const handleSubmit = () => { + const isBinary = BINARY_METRICS.has(form.metric); + const isEvent = isEventMetric(form.metric); + if (!form.name || !form.metric || (!isBinary && !isEvent && !form.threshold)) { + toast.error("Please fill in all required fields"); + return; + } + + const skipThreshold = isEvent || isBinary; + + if (editingRuleId) { + updateMutation.mutate({ + id: editingRuleId, + name: form.name, + ...(skipThreshold + ? {} + : { + threshold: parseFloat(form.threshold), + durationSeconds: parseInt(form.durationSeconds, 10) || 60, + }), + channelIds: form.channelIds, + }); + } else { + createMutation.mutate({ + name: form.name, + environmentId, + pipelineId: form.pipelineId || undefined, + metric: form.metric as AlertMetric, + condition: skipThreshold ? null : (form.condition as AlertCondition), + threshold: skipThreshold ? null : parseFloat(form.threshold), + durationSeconds: skipThreshold ? null : (parseInt(form.durationSeconds, 10) || 60), + teamId: selectedTeamId!, + channelIds: form.channelIds.length > 0 ? form.channelIds : undefined, + }); + } + }; + + const isSaving = createMutation.isPending || updateMutation.isPending; + + return ( +
+
+ +
+ + {rulesQuery.isError ? ( + rulesQuery.refetch()} /> + ) : rulesQuery.isLoading ? ( +
+ {Array.from({ length: 3 }).map((_, i) => ( + + ))} +
+ ) : rules.length === 0 ? ( + + ) : ( + + + + Name + Metric + Condition + Threshold + Duration + Pipeline + Enabled + Actions + + + + {rules.map((rule) => ( + + {rule.name} + + + {METRIC_LABELS[rule.metric] ?? rule.metric} + + + + {BINARY_METRICS.has(rule.metric) || !rule.condition ? "—" : (CONDITION_LABELS[rule.condition] ?? rule.condition)} + + + {BINARY_METRICS.has(rule.metric) ? "—" : (rule.threshold ?? "—")} + + + {BINARY_METRICS.has(rule.metric) || rule.durationSeconds == null ? "—" : `${rule.durationSeconds}s`} + + + {GLOBAL_METRICS.has(rule.metric) ? ( + + ) : rule.pipeline ? ( + {rule.pipeline.name} + ) : ( + All + )} + + + + toggleMutation.mutate({ id: rule.id, enabled: checked }) + } + /> + + +
+ + +
+
+
+ ))} +
+
+ )} + + {/* Create / Edit Dialog */} + { + setDialogOpen(open); + if (!open) setEditingRuleId(null); + }} + > + + + + {editingRuleId ? "Edit Alert Rule" : "Create Alert Rule"} + + + {editingRuleId + ? "Update the alert rule configuration." + : "Define a new alert rule for this environment."} + + + +
+
+ + + setForm((f) => ({ ...f, name: e.target.value })) + } + /> +
+ + {!editingRuleId && ( + <> +
+ + +
+ + {!GLOBAL_METRICS.has(form.metric) && ( +
+ + +
+ )} + + )} + + {isEventMetric(form.metric) || BINARY_METRICS.has(form.metric) ? ( +

+ Notifications will be sent when this event occurs. +

+ ) : ( + <> +
+ + + setForm((f) => ({ ...f, threshold: e.target.value })) + } + /> +
+ +
+ + + setForm((f) => ({ ...f, durationSeconds: e.target.value })) + } + /> +
+ + )} + + {channels.length > 0 && ( +
+ +

+ Select channels for this rule. If none are selected, all + enabled channels will be used. +

+
+ {channels.map((ch) => { + const selected = form.channelIds.includes(ch.id); + return ( + toggleChannel(ch.id)} + > + {CHANNEL_TYPE_LABELS[ch.type] ?? ch.type}: {ch.name} + + ); + })} +
+
+ )} +
+ + + + + +
+
+ + {/* Delete Confirmation */} + { + if (!open) setDeleteTarget(null); + }} + title="Delete Alert Rule" + description={ + <> + Are you sure you want to delete{" "} + {deleteTarget?.name}? This action cannot be undone. + + } + confirmLabel="Delete" + isPending={deleteMutation.isPending} + pendingLabel="Deleting..." + onConfirm={() => { + if (deleteTarget) deleteMutation.mutate({ id: deleteTarget.id }); + }} + /> +
+ ); +} diff --git a/src/app/(dashboard)/alerts/_components/constants.ts b/src/app/(dashboard)/alerts/_components/constants.ts new file mode 100644 index 00000000..a99be549 --- /dev/null +++ b/src/app/(dashboard)/alerts/_components/constants.ts @@ -0,0 +1,63 @@ +import { + MessageSquare, + Mail, + AlertTriangle, + Webhook, +} from "lucide-react"; + +// ─── Constants shared across alert sections ───────────────────────────────────── + +export const METRIC_LABELS: Record = { + // Infrastructure (threshold-based) + node_unreachable: "Node Unreachable", + cpu_usage: "CPU Usage", + memory_usage: "Memory Usage", + disk_usage: "Disk Usage", + error_rate: "Error Rate", + discarded_rate: "Discarded Rate", + pipeline_crashed: "Pipeline Crashed", + // Events (fire on occurrence) + deploy_requested: "Deploy Requested", + deploy_completed: "Deploy Completed", + deploy_rejected: "Deploy Rejected", + deploy_cancelled: "Deploy Cancelled", + new_version_available: "New Version Available", + scim_sync_failed: "SCIM Sync Failed", + backup_failed: "Backup Failed", + certificate_expiring: "Certificate Expiring", + node_joined: "Node Joined", + node_left: "Node Left", +}; + +export const CONDITION_LABELS: Record = { + gt: ">", + lt: "<", + eq: "=", +}; + +export const BINARY_METRICS = new Set(["node_unreachable", "pipeline_crashed"]); + +/** Metrics that cannot be scoped to a specific pipeline. */ +export const GLOBAL_METRICS = new Set([ + "node_unreachable", + "new_version_available", + "scim_sync_failed", + "backup_failed", + "certificate_expiring", + "node_joined", + "node_left", +]); + +export const CHANNEL_TYPE_LABELS: Record = { + slack: "Slack", + email: "Email", + pagerduty: "PagerDuty", + webhook: "Webhook", +}; + +export const CHANNEL_TYPE_ICONS: Record = { + slack: MessageSquare, + email: Mail, + pagerduty: AlertTriangle, + webhook: Webhook, +}; diff --git a/src/app/(dashboard)/alerts/_components/notification-channels-section.tsx b/src/app/(dashboard)/alerts/_components/notification-channels-section.tsx new file mode 100644 index 00000000..ee1ca98a --- /dev/null +++ b/src/app/(dashboard)/alerts/_components/notification-channels-section.tsx @@ -0,0 +1,748 @@ +"use client"; + +import { useState, useCallback } from "react"; +import { + useQuery, + useMutation, + useQueryClient, +} from "@tanstack/react-query"; +import { useTRPC } from "@/trpc/client"; +import { toast } from "sonner"; +import { + Plus, + Pencil, + Trash2, + Loader2, + Send, + Webhook, + BellRing, +} from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Badge } from "@/components/ui/badge"; +import { Switch } from "@/components/ui/switch"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Textarea } from "@/components/ui/textarea"; +import { EmptyState } from "@/components/empty-state"; +import { QueryError } from "@/components/query-error"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { ConfirmDialog } from "@/components/confirm-dialog"; + +import { + CHANNEL_TYPE_LABELS, + CHANNEL_TYPE_ICONS, +} from "./constants"; + +// ─── Notification Channels Section ─────────────────────────────────────────────── + +type ChannelType = "slack" | "email" | "pagerduty" | "webhook"; + +interface ChannelFormState { + name: string; + type: ChannelType; + // Slack + webhookUrl: string; + // Email + smtpHost: string; + smtpPort: string; + smtpUser: string; + smtpPass: string; + emailFrom: string; + recipients: string; + // PagerDuty + integrationKey: string; + // Webhook + url: string; + headers: string; + hmacSecret: string; +} + +const EMPTY_CHANNEL_FORM: ChannelFormState = { + name: "", + type: "slack", + webhookUrl: "", + smtpHost: "", + smtpPort: "587", + smtpUser: "", + smtpPass: "", + emailFrom: "", + recipients: "", + integrationKey: "", + url: "", + headers: "", + hmacSecret: "", +}; + +function buildConfigFromForm(form: ChannelFormState): Record { + switch (form.type) { + case "slack": + return { webhookUrl: form.webhookUrl }; + case "email": + return { + smtpHost: form.smtpHost, + smtpPort: parseInt(form.smtpPort, 10) || 587, + smtpUser: form.smtpUser || undefined, + smtpPass: form.smtpPass || undefined, + from: form.emailFrom, + recipients: form.recipients + .split(",") + .map((e) => e.trim()) + .filter(Boolean), + }; + case "pagerduty": + return { integrationKey: form.integrationKey }; + case "webhook": { + const config: Record = { url: form.url }; + if (form.headers.trim()) { + try { + config.headers = JSON.parse(form.headers); + } catch { + // Will be caught by validation + } + } + if (form.hmacSecret) config.hmacSecret = form.hmacSecret; + return config; + } + } +} + +function formFromConfig( + type: string, + name: string, + config: Record, +): ChannelFormState { + const base = { ...EMPTY_CHANNEL_FORM, name, type: type as ChannelType }; + + switch (type) { + case "slack": + return { ...base, webhookUrl: (config.webhookUrl as string) ?? "" }; + case "email": + return { + ...base, + smtpHost: (config.smtpHost as string) ?? "", + smtpPort: String(config.smtpPort ?? 587), + smtpUser: (config.smtpUser as string) ?? "", + smtpPass: "", + emailFrom: (config.from as string) ?? "", + recipients: Array.isArray(config.recipients) + ? (config.recipients as string[]).join(", ") + : "", + }; + case "pagerduty": + return { ...base, integrationKey: "" }; + case "webhook": + return { + ...base, + url: (config.url as string) ?? "", + headers: config.headers + ? JSON.stringify(config.headers, null, 2) + : "", + hmacSecret: "", + }; + default: + return base; + } +} + +export function NotificationChannelsSection({ + environmentId, +}: { + environmentId: string; +}) { + const trpc = useTRPC(); + const queryClient = useQueryClient(); + + const [dialogOpen, setDialogOpen] = useState(false); + const [editingChannelId, setEditingChannelId] = useState(null); + const [form, setForm] = useState(EMPTY_CHANNEL_FORM); + const [deleteTarget, setDeleteTarget] = useState<{ + id: string; + name: string; + } | null>(null); + + const channelsQuery = useQuery( + trpc.alert.listChannels.queryOptions( + { environmentId }, + { enabled: !!environmentId }, + ), + ); + + const invalidateChannels = useCallback(() => { + queryClient.invalidateQueries({ + queryKey: trpc.alert.listChannels.queryKey({ environmentId }), + }); + }, [queryClient, trpc, environmentId]); + + const createMutation = useMutation( + trpc.alert.createChannel.mutationOptions({ + onSuccess: () => { + toast.success("Notification channel created"); + invalidateChannels(); + setDialogOpen(false); + }, + onError: (error) => { + toast.error(error.message || "Failed to create channel"); + }, + }), + ); + + const updateMutation = useMutation( + trpc.alert.updateChannel.mutationOptions({ + onSuccess: () => { + toast.success("Notification channel updated"); + invalidateChannels(); + setDialogOpen(false); + setEditingChannelId(null); + }, + onError: (error) => { + toast.error(error.message || "Failed to update channel"); + }, + }), + ); + + const toggleMutation = useMutation( + trpc.alert.updateChannel.mutationOptions({ + onSuccess: () => { + invalidateChannels(); + }, + onError: (error) => { + toast.error(error.message || "Failed to toggle channel"); + }, + }), + ); + + const deleteMutation = useMutation( + trpc.alert.deleteChannel.mutationOptions({ + onSuccess: () => { + toast.success("Notification channel deleted"); + invalidateChannels(); + setDeleteTarget(null); + }, + onError: (error) => { + toast.error(error.message || "Failed to delete channel"); + }, + }), + ); + + const testMutation = useMutation( + trpc.alert.testChannel.mutationOptions({ + onSuccess: (result) => { + if (result.success) { + toast.success("Channel test successful"); + } else { + toast.error(`Channel test failed: ${result.error ?? "Unknown error"}`); + } + }, + onError: (error) => { + toast.error(error.message || "Failed to test channel"); + }, + }), + ); + + const channels = channelsQuery.data ?? []; + + const openCreate = () => { + setEditingChannelId(null); + setForm(EMPTY_CHANNEL_FORM); + setDialogOpen(true); + }; + + const openEdit = (channel: (typeof channels)[0]) => { + setEditingChannelId(channel.id); + setForm( + formFromConfig( + channel.type, + channel.name, + channel.config as Record, + ), + ); + setDialogOpen(true); + }; + + const validateForm = (): boolean => { + if (!form.name.trim()) { + toast.error("Name is required"); + return false; + } + + switch (form.type) { + case "slack": + if (!form.webhookUrl.trim()) { + toast.error("Webhook URL is required"); + return false; + } + break; + case "email": + if (!form.smtpHost.trim() || !form.emailFrom.trim() || !form.recipients.trim()) { + toast.error("SMTP host, from address, and recipients are required"); + return false; + } + break; + case "pagerduty": + if (!editingChannelId && !form.integrationKey.trim()) { + toast.error("Integration key is required"); + return false; + } + break; + case "webhook": + if (!form.url.trim()) { + toast.error("URL is required"); + return false; + } + if (form.headers.trim()) { + try { + const parsed = JSON.parse(form.headers); + if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) { + toast.error("Headers must be a JSON object"); + return false; + } + } catch { + toast.error("Invalid JSON in headers field"); + return false; + } + } + break; + } + + return true; + }; + + const handleSubmit = () => { + if (!validateForm()) return; + + const config = buildConfigFromForm(form); + + if (editingChannelId) { + updateMutation.mutate({ + id: editingChannelId, + name: form.name, + config, + }); + } else { + createMutation.mutate({ + environmentId, + name: form.name, + type: form.type, + config, + }); + } + }; + + const isSaving = createMutation.isPending || updateMutation.isPending; + + return ( +
+
+
+ +

Notification Channels

+
+ +
+ + {channelsQuery.isError ? ( + channelsQuery.refetch()} /> + ) : channelsQuery.isLoading ? ( +
+ {Array.from({ length: 2 }).map((_, i) => ( + + ))} +
+ ) : channels.length === 0 ? ( + + ) : ( + + + + Name + Type + Enabled + Actions + + + + {channels.map((channel) => { + const Icon = + CHANNEL_TYPE_ICONS[channel.type] ?? Webhook; + return ( + + {channel.name} + + + + {CHANNEL_TYPE_LABELS[channel.type] ?? channel.type} + + + + + toggleMutation.mutate({ + id: channel.id, + enabled: checked, + }) + } + /> + + +
+ + + +
+
+
+ ); + })} +
+
+ )} + + {/* Create / Edit Dialog */} + { + setDialogOpen(open); + if (!open) setEditingChannelId(null); + }} + > + + + + {editingChannelId + ? "Edit Notification Channel" + : "Add Notification Channel"} + + + {editingChannelId + ? "Update the channel configuration." + : "Configure a new notification channel for alert delivery."} + + + +
+
+ + + setForm((f) => ({ ...f, name: e.target.value })) + } + /> +
+ + {!editingChannelId && ( +
+ + +
+ )} + + {/* Type-specific config forms */} + {form.type === "slack" && ( +
+ + + setForm((f) => ({ ...f, webhookUrl: e.target.value })) + } + /> +

+ Create an Incoming Webhook in your Slack workspace settings. +

+
+ )} + + {form.type === "email" && ( + <> +
+
+ + + setForm((f) => ({ ...f, smtpHost: e.target.value })) + } + /> +
+
+ + + setForm((f) => ({ ...f, smtpPort: e.target.value })) + } + /> +
+
+
+
+ + + setForm((f) => ({ ...f, smtpUser: e.target.value })) + } + /> +
+
+ + + setForm((f) => ({ ...f, smtpPass: e.target.value })) + } + /> +
+
+
+ + + setForm((f) => ({ ...f, emailFrom: e.target.value })) + } + /> +
+
+ + + setForm((f) => ({ ...f, recipients: e.target.value })) + } + /> +

+ Comma-separated list of email addresses. +

+
+ + )} + + {form.type === "pagerduty" && ( +
+ + + setForm((f) => ({ + ...f, + integrationKey: e.target.value, + })) + } + /> +

+ {editingChannelId + ? "Leave blank to keep the existing key, or enter a new one to replace it." + : "Found in PagerDuty under Service > Integrations > Events API v2."} +

+
+ )} + + {form.type === "webhook" && ( + <> +
+ + + setForm((f) => ({ ...f, url: e.target.value })) + } + /> +
+
+ +