diff --git a/.gitignore b/.gitignore index 35b3279..1e7ad08 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,9 @@ db.sqlite data/settings.db +# prisma generated client +/prisma/generated/ + # ssh keys (sensitive) data/ssh-keys/ diff --git a/eslint.config.js b/eslint.config.js index 80d5aba..c9c0470 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,20 +1,23 @@ +import eslintPluginNext from "@next/eslint-plugin-next"; import tseslint from "typescript-eslint"; -import { createRequire } from "module"; -import { fileURLToPath } from "url"; -import path from "path"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); -const require = createRequire(import.meta.url); - -// Import Next.js config directly (it's already in flat config format) -const nextConfig = require("eslint-config-next/core-web-vitals"); +import reactPlugin from "eslint-plugin-react"; +import reactHooksPlugin from "eslint-plugin-react-hooks"; export default tseslint.config( { - ignores: [".next", "node_modules"], + ignores: [".next", "next-env.d.ts", "postcss.config.js", "prettier.config.js"], + }, + { + plugins: { + "@next/next": eslintPluginNext, + "react": reactPlugin, + "react-hooks": reactHooksPlugin, + }, + rules: { + ...eslintPluginNext.configs.recommended.rules, + ...eslintPluginNext.configs["core-web-vitals"].rules, + }, }, - ...nextConfig, { files: ["**/*.ts", "**/*.tsx"], extends: [ diff --git a/next.config.js b/next.config.js index 2cb9c44..df68f48 100644 --- a/next.config.js +++ b/next.config.js @@ -18,30 +18,20 @@ const config = { }, ], }, - // Allow cross-origin requests from local network ranges - allowedDevOrigins: [ - 'http://localhost:3000', - 'http://127.0.0.1:3000', - 'http://[::1]:3000', - 'http://10.*', - 'http://172.16.*', - 'http://172.17.*', - 'http://172.18.*', - 'http://172.19.*', - 'http://172.20.*', - 'http://172.21.*', - 'http://172.22.*', - 'http://172.23.*', - 'http://172.24.*', - 'http://172.25.*', - 'http://172.26.*', - 'http://172.27.*', - 'http://172.28.*', - 'http://172.29.*', - 'http://172.30.*', - 'http://172.31.*', - 'http://192.168.*', - ], + // Allow cross-origin requests from local network in dev mode + // Note: In Next.js 16, we disable this check entirely for dev + async headers() { + return [ + { + source: '/:path*', + headers: [ + { key: 'Access-Control-Allow-Origin', value: '*' }, + { key: 'Access-Control-Allow-Methods', value: 'GET,POST,PUT,DELETE,OPTIONS' }, + { key: 'Access-Control-Allow-Headers', value: 'Content-Type, Authorization' }, + ], + }, + ]; + }, turbopack: { // Disable Turbopack and use Webpack instead for compatibility diff --git a/package-lock.json b/package-lock.json index d2cd548..ad7f30b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,72 +7,75 @@ "": { "name": "pve-scripts-local", "version": "0.1.0", + "hasInstallScript": true, "dependencies": { - "@prisma/client": "^6.19.0", + "@prisma/adapter-better-sqlite3": "^7.0.1", + "@prisma/client": "^7.0.1", "@radix-ui/react-dropdown-menu": "^2.1.16", - "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-slot": "^1.2.4", "@t3-oss/env-nextjs": "^0.13.8", "@tailwindcss/typography": "^0.5.19", - "@tanstack/react-query": "^5.90.5", - "@trpc/client": "^11.6.0", - "@trpc/react-query": "^11.6.0", - "@trpc/server": "^11.6.0", + "@tanstack/react-query": "^5.90.11", + "@trpc/client": "^11.7.2", + "@trpc/react-query": "^11.7.2", + "@trpc/server": "^11.7.2", "@types/react-syntax-highlighter": "^15.5.13", "@types/ws": "^8.18.1", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-web-links": "^0.11.0", "@xterm/xterm": "^5.5.0", - "axios": "^1.7.9", - "bcryptjs": "^3.0.2", + "axios": "^1.13.2", + "bcryptjs": "^3.0.3", + "better-sqlite3": "^12.4.6", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "cron-validator": "^1.2.0", + "cron-validator": "^1.4.0", "dotenv": "^17.2.3", "jsonwebtoken": "^9.0.2", - "lucide-react": "^0.554.0", - "next": "^16.0.4", + "lucide-react": "^0.555.0", + "next": "^16.0.5", "node-cron": "^4.2.1", "node-pty": "^1.0.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", "react-markdown": "^10.1.0", "react-syntax-highlighter": "^16.1.0", "refractor": "^5.0.0", "remark-gfm": "^4.0.1", "server-only": "^0.0.1", "strip-ansi": "^7.1.2", - "superjson": "^2.2.3", - "tailwind-merge": "^3.3.1", + "superjson": "^2.2.6", + "tailwind-merge": "^3.4.0", "ws": "^8.18.3", - "zod": "^4.1.12" + "zod": "^4.1.13" }, "devDependencies": { - "@eslint/eslintrc": "^3.3.1", - "@tailwindcss/postcss": "^4.1.16", + "@tailwindcss/postcss": "^4.1.17", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", "@types/bcryptjs": "^3.0.0", - "@types/better-sqlite3": "^7.6.8", + "@types/better-sqlite3": "^7.6.13", "@types/jsonwebtoken": "^9.0.10", "@types/node": "^24.10.1", "@types/node-cron": "^3.0.11", - "@types/react": "^19.2.4", - "@types/react-dom": "^19.2.2", - "@vitejs/plugin-react": "^5.1.0", - "@vitest/coverage-v8": "^4.0.13", - "@vitest/ui": "^4.0.13", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "@vitest/coverage-v8": "^4.0.14", + "@vitest/ui": "^4.0.14", "eslint": "^9.39.1", - "eslint-config-next": "^16.0.4", + "eslint-config-next": "^16.0.5", "jsdom": "^27.2.0", - "postcss": "^8.5.3", - "prettier": "^3.5.3", + "postcss": "^8.5.6", + "prettier": "^3.7.1", "prettier-plugin-tailwindcss": "^0.7.1", - "prisma": "^6.19.0", + "prisma": "^7.0.1", "tailwindcss": "^4.1.17", - "typescript": "^5.8.2", - "typescript-eslint": "^8.46.2", - "vitest": "^4.0.13" + "tsx": "^4.19.4", + "typescript": "^5.9.3", + "typescript-eslint": "^8.48.0", + "vitest": "^4.0.14" }, "engines": { "node": ">=24.0.0" @@ -462,6 +465,43 @@ "node": ">=18" } }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-10.5.0.tgz", + "integrity": "sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/gast": "10.5.0", + "@chevrotain/types": "10.5.0", + "lodash": "4.17.21" + } + }, + "node_modules/@chevrotain/gast": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-10.5.0.tgz", + "integrity": "sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "10.5.0", + "lodash": "4.17.21" + } + }, + "node_modules/@chevrotain/types": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-10.5.0.tgz", + "integrity": "sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/utils": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-10.5.0.tgz", + "integrity": "sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ==", + "devOptional": true, + "license": "Apache-2.0" + }, "node_modules/@csstools/color-helpers": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", @@ -599,6 +639,37 @@ "node": ">=18" } }, + "node_modules/@electric-sql/pglite": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.3.2.tgz", + "integrity": "sha512-zfWWa+V2ViDCY/cmUfRqeWY1yLto+EpxjXnZzenB1TyxsTiXaTWeZFIZw6mac52BsuQm0RjCnisjBtdBaXOI6w==", + "devOptional": true, + "license": "Apache-2.0", + "peer": true + }, + "node_modules/@electric-sql/pglite-socket": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.0.6.tgz", + "integrity": "sha512-6RjmgzphIHIBA4NrMGJsjNWK4pu+bCWJlEWlwcxFTVY3WT86dFpKwbZaGWZV6C5Rd7sCk1Z0CI76QEfukLAUXw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "pglite-server": "dist/scripts/server.js" + }, + "peerDependencies": { + "@electric-sql/pglite": "0.3.2" + } + }, + "node_modules/@electric-sql/pglite-tools": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.2.7.tgz", + "integrity": "sha512-9dAccClqxx4cZB+Ar9B+FZ5WgxDc/Xvl9DPrTWv+dYTf0YNubLzi4wHHRGRGhrJv15XwnyKcGOZAP1VXSneSUg==", + "devOptional": true, + "license": "Apache-2.0", + "peerDependencies": { + "@electric-sql/pglite": "0.3.2" + } + }, "node_modules/@emnapi/core": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", @@ -1256,6 +1327,19 @@ "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", "license": "MIT" }, + "node_modules/@hono/node-server": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.14.2.tgz", + "integrity": "sha512-GHjpOeHYbr9d1vkID2sNUYkl5IxumyhDrUJB7wBp7jvqYwPFt+oNKsAPBRcdSbV7kIrXhouLE199ks1QcK4r7A==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1824,6 +1908,20 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mrleebo/prisma-ast": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@mrleebo/prisma-ast/-/prisma-ast-0.12.1.tgz", + "integrity": "sha512-JwqeCQ1U3fvccttHZq7Tk0m/TMC6WcFAQZdukypW3AzlJYKYTGNVd1ANU2GuhKnv4UQuOFj3oAl0LLG/gxFN1w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chevrotain": "^10.5.0", + "lilconfig": "^2.1.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -1838,15 +1936,15 @@ } }, "node_modules/@next/env": { - "version": "16.0.4", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.4.tgz", - "integrity": "sha512-FDPaVoB1kYhtOz6Le0Jn2QV7RZJ3Ngxzqri7YX4yu3Ini+l5lciR7nA9eNDpKTmDm7LWZtxSju+/CQnwRBn2pA==", + "version": "16.0.5", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.5.tgz", + "integrity": "sha512-jRLOw822AE6aaIm9oh0NrauZEM0Vtx5xhYPgqx89txUmv/UmcRwpcXmGeQOvYNT/1bakUwA+nG5CA74upYVVDw==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { - "version": "16.0.4", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.0.4.tgz", - "integrity": "sha512-0emoVyL4Z5NEkRNb63ko/BqLC9OFULcY7mJ3lSerBCqgh/UFcjnvodyikV2bTl7XygwcamJxJAfxCo1oAVfH6g==", + "version": "16.0.5", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.0.5.tgz", + "integrity": "sha512-m1zPz6hsBvQt1CMRz7rTga8OXpRE9rVW4JHCSjW+tswTxiEU+6ev+GTlgm7ZzcCiMEVQAHTNhpEGFzDtVha9qg==", "dev": true, "license": "MIT", "dependencies": { @@ -1854,9 +1952,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.0.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.4.tgz", - "integrity": "sha512-TN0cfB4HT2YyEio9fLwZY33J+s+vMIgC84gQCOLZOYusW7ptgjIn8RwxQt0BUpoo9XRRVVWEHLld0uhyux1ZcA==", + "version": "16.0.5", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.5.tgz", + "integrity": "sha512-65Mfo1rD+mVbJuBTlXbNelNOJ5ef+5pskifpFHsUt3cnOWjDNKctHBwwSz9tJlPp7qADZtiN/sdcG7mnc0El8Q==", "cpu": [ "arm64" ], @@ -1870,9 +1968,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.0.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.4.tgz", - "integrity": "sha512-XsfI23jvimCaA7e+9f3yMCoVjrny2D11G6H8NCcgv+Ina/TQhKPXB9P4q0WjTuEoyZmcNvPdrZ+XtTh3uPfH7Q==", + "version": "16.0.5", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.5.tgz", + "integrity": "sha512-2fDzXD/JpEjY500VUF0uuGq3YZcpC6XxmGabePPLyHCKbw/YXRugv3MRHH7MxE2hVHtryXeSYYnxcESb/3OUIQ==", "cpu": [ "x64" ], @@ -1886,9 +1984,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.0.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.4.tgz", - "integrity": "sha512-uo8X7qHDy4YdJUhaoJDMAbL8VT5Ed3lijip2DdBHIB4tfKAvB1XBih6INH2L4qIi4jA0Qq1J0ErxcOocBmUSwg==", + "version": "16.0.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.5.tgz", + "integrity": "sha512-meSLB52fw4tgDpPnyuhwA280EWLwwIntrxLYjzKU3e3730ur2WJAmmqoZ1LPIZ2l3eDfh9SBHnJGTczbgPeNeA==", "cpu": [ "arm64" ], @@ -1902,9 +2000,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.0.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.4.tgz", - "integrity": "sha512-pvR/AjNIAxsIz0PCNcZYpH+WmNIKNLcL4XYEfo+ArDi7GsxKWFO5BvVBLXbhti8Coyv3DE983NsitzUsGH5yTw==", + "version": "16.0.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.5.tgz", + "integrity": "sha512-aAJtQkvUzz5t0xVAmK931SIhWnSQAaEoTyG/sKPCYq2u835K/E4a14A+WRPd4dkhxIHNudE8dI+FpHekgdrA4g==", "cpu": [ "arm64" ], @@ -1918,9 +2016,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.0.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.4.tgz", - "integrity": "sha512-2hebpsd5MRRtgqmT7Jj/Wze+wG+ZEXUK2KFFL4IlZ0amEEFADo4ywsifJNeFTQGsamH3/aXkKWymDvgEi+pc2Q==", + "version": "16.0.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.5.tgz", + "integrity": "sha512-bYwbjBwooMWRhy6vRxenaYdguTM2hlxFt1QBnUF235zTnU2DhGpETm5WU93UvtAy0uhC5Kgqsl8RyNXlprFJ6Q==", "cpu": [ "x64" ], @@ -1934,9 +2032,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.0.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.4.tgz", - "integrity": "sha512-pzRXf0LZZ8zMljH78j8SeLncg9ifIOp3ugAFka+Bq8qMzw6hPXOc7wydY7ardIELlczzzreahyTpwsim/WL3Sg==", + "version": "16.0.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.5.tgz", + "integrity": "sha512-iGv2K/4gW3mkzh+VcZTf2gEGX5o9xdb5oPqHjgZvHdVzCw0iSAJ7n9vKzl3SIEIIHZmqRsgNasgoLd0cxaD+tg==", "cpu": [ "x64" ], @@ -1950,9 +2048,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.0.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.4.tgz", - "integrity": "sha512-7G/yJVzum52B5HOqqbQYX9bJHkN+c4YyZ2AIvEssMHQlbAWOn3iIJjD4sM6ihWsBxuljiTKJovEYlD1K8lCUHw==", + "version": "16.0.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.5.tgz", + "integrity": "sha512-6xf52Hp4SH9+4jbYmfUleqkuxvdB9JJRwwFlVG38UDuEGPqpIA+0KiJEU9lxvb0RGNo2i2ZUhc5LHajij9H9+A==", "cpu": [ "arm64" ], @@ -1966,9 +2064,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.0.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.4.tgz", - "integrity": "sha512-0Vy4g8SSeVkuU89g2OFHqGKM4rxsQtihGfenjx2tRckPrge5+gtFnRWGAAwvGXr0ty3twQvcnYjEyOrLHJ4JWA==", + "version": "16.0.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.5.tgz", + "integrity": "sha512-06kTaOh+Qy/kguN+MMK+/VtKmRkQJrPlGQMvCUbABk1UxI5SKTgJhbmMj9Hf0qWwrS6g9JM6/Zk+etqeMyvHAw==", "cpu": [ "x64" ], @@ -2036,18 +2134,30 @@ "dev": true, "license": "MIT" }, - "node_modules/@prisma/client": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.0.tgz", - "integrity": "sha512-QXFT+N/bva/QI2qoXmjBzL7D6aliPffIwP+81AdTGq0FXDoLxLkWivGMawG8iM5B9BKfxLIXxfWWAF6wbuJU6g==", - "hasInstallScript": true, + "node_modules/@prisma/adapter-better-sqlite3": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@prisma/adapter-better-sqlite3/-/adapter-better-sqlite3-7.0.1.tgz", + "integrity": "sha512-fgnn+lkUV/7pUvPnQSI/ZHONwGU+eisWqU6tvBFGK2ovgBUAJN8zG0fbt8v8Brphyo4h4AA3+jyG0TFFb+qVxQ==", "license": "Apache-2.0", + "dependencies": { + "@prisma/driver-adapter-utils": "7.0.1", + "better-sqlite3": "^12.4.5" + } + }, + "node_modules/@prisma/client": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.0.1.tgz", + "integrity": "sha512-O74T6xcfaGAq5gXwCAvfTLvI6fmC3and2g5yLRMkNjri1K8mSpEgclDNuUWs9xj5AwNEMQ88NeD3asI+sovm1g==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/client-runtime-utils": "7.0.1" + }, "engines": { - "node": ">=18.18" + "node": "^20.19 || ^22.12 || >=24.0" }, "peerDependencies": { "prisma": "*", - "typescript": ">=5.1.0" + "typescript": ">=5.4.0" }, "peerDependenciesMeta": { "prisma": { @@ -2058,10 +2168,16 @@ } } }, + "node_modules/@prisma/client-runtime-utils": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.0.1.tgz", + "integrity": "sha512-R26BVX9D/iw4toUmZKZf3jniM/9pMGHHdZN5LVP2L7HNiCQKNQQx/9LuMtjepbgRqSqQO3oHN0yzojHLnKTGEw==", + "license": "Apache-2.0" + }, "node_modules/@prisma/config": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.0.tgz", - "integrity": "sha512-zwCayme+NzI/WfrvFEtkFhhOaZb/hI+X8TTjzjJ252VbPxAl2hWHK5NMczmnG9sXck2lsXrxIZuK524E25UNmg==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.0.1.tgz", + "integrity": "sha512-MacIjXdo+hNKxPvtMzDXykIIc8HCRWoyjQ2nguJTFqLDzJBD5L6QRaANGTLOqbGtJ3sFvLRmfXhrFg3pWoK1BA==", "devOptional": true, "license": "Apache-2.0", "dependencies": { @@ -2114,53 +2230,140 @@ } }, "node_modules/@prisma/debug": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.0.tgz", - "integrity": "sha512-8hAdGG7JmxrzFcTzXZajlQCidX0XNkMJkpqtfbLV54wC6LSSX6Vni25W/G+nAANwLnZ2TmwkfIuWetA7jJxJFA==", - "devOptional": true, + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.0.1.tgz", + "integrity": "sha512-5+25XokVeAK2Z2C9W457AFw7Hk032Q3QI3G58KYKXPlpgxy+9FvV1+S1jqfJ2d4Nmq9LP/uACrM6OVhpJMSr8w==", "license": "Apache-2.0" }, + "node_modules/@prisma/dev": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.13.0.tgz", + "integrity": "sha512-QMmF6zFeUF78yv1HYbHvod83AQnl7u6NtKyDhTRZOJup3h1icWs8R7RUVxBJZvM2tBXNAMpLQYYM/8kPlOPegA==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "@electric-sql/pglite": "0.3.2", + "@electric-sql/pglite-socket": "0.0.6", + "@electric-sql/pglite-tools": "0.2.7", + "@hono/node-server": "1.14.2", + "@mrleebo/prisma-ast": "0.12.1", + "@prisma/get-platform": "6.8.2", + "@prisma/query-plan-executor": "6.18.0", + "foreground-child": "3.3.1", + "get-port-please": "3.1.2", + "hono": "4.7.10", + "http-status-codes": "2.3.0", + "pathe": "2.0.3", + "proper-lockfile": "4.1.2", + "remeda": "2.21.3", + "std-env": "3.9.0", + "valibot": "1.1.0", + "zeptomatch": "2.0.2" + } + }, + "node_modules/@prisma/dev/node_modules/std-env": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", + "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@prisma/driver-adapter-utils": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.0.1.tgz", + "integrity": "sha512-sBbxm/yysHLLF2iMAB+qcX/nn3WFgsiC4DQNz0uM6BwGSIs8lIvgo0u8nR9nxe5gvFgKiIH8f4z2fgOEMeXc8w==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.0.1" + } + }, "node_modules/@prisma/engines": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.0.tgz", - "integrity": "sha512-pMRJ+1S6NVdXoB8QJAPIGpKZevFjxhKt0paCkRDTZiczKb7F4yTgRP8M4JdVkpQwmaD4EoJf6qA+p61godDokw==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.0.1.tgz", + "integrity": "sha512-f+D/vdKeImqUHysd5Bgv8LQ1whl4sbLepHyYMQQMK61cp4WjwJVryophleLUrfEJRpBLGTBI/7fnLVENxxMFPQ==", "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "6.19.0", - "@prisma/engines-version": "6.19.0-26.2ba551f319ab1df4bc874a89965d8b3641056773", - "@prisma/fetch-engine": "6.19.0", - "@prisma/get-platform": "6.19.0" + "@prisma/debug": "7.0.1", + "@prisma/engines-version": "7.1.0-2.f09f2815f091dbba658cdcd2264306d88bb5bda6", + "@prisma/fetch-engine": "7.0.1", + "@prisma/get-platform": "7.0.1" } }, "node_modules/@prisma/engines-version": { - "version": "6.19.0-26.2ba551f319ab1df4bc874a89965d8b3641056773", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.19.0-26.2ba551f319ab1df4bc874a89965d8b3641056773.tgz", - "integrity": "sha512-gV7uOBQfAFlWDvPJdQxMT1aSRur3a0EkU/6cfbAC5isV67tKDWUrPauyaHNpB+wN1ebM4A9jn/f4gH+3iHSYSQ==", + "version": "7.1.0-2.f09f2815f091dbba658cdcd2264306d88bb5bda6", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.0-2.f09f2815f091dbba658cdcd2264306d88bb5bda6.tgz", + "integrity": "sha512-RA7pShKvijHib4USRB3YuLTQamHKJPkTRDc45AwxfahUQngiGVMlIj4ix4emUxkrum4o/jwn82WIwlG57EtgiQ==", "devOptional": true, "license": "Apache-2.0" }, - "node_modules/@prisma/fetch-engine": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.0.tgz", - "integrity": "sha512-OOx2Lda0DGrZ1rodADT06ZGqHzr7HY7LNMaFE2Vp8dp146uJld58sRuasdX0OiwpHgl8SqDTUKHNUyzEq7pDdQ==", + "node_modules/@prisma/engines/node_modules/@prisma/get-platform": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.0.1.tgz", + "integrity": "sha512-DrsGnZOsF7PlAE7UtqmJenWti87RQtg7v9qW9alS71Pj0P6ZQV0RuzRQaql9dCWoo6qKAaF5U/L4kI826MmiZg==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "6.19.0", - "@prisma/engines-version": "6.19.0-26.2ba551f319ab1df4bc874a89965d8b3641056773", - "@prisma/get-platform": "6.19.0" + "@prisma/debug": "7.0.1" + } + }, + "node_modules/@prisma/fetch-engine": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.0.1.tgz", + "integrity": "sha512-5DnSairYIYU7dcv/9pb1KCwIRHZfhVOd34855d01lUI5QdF9rdCkMywPQbBM67YP7iCgQoEZO0/COtOMpR4i9A==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.0.1", + "@prisma/engines-version": "7.1.0-2.f09f2815f091dbba658cdcd2264306d88bb5bda6", + "@prisma/get-platform": "7.0.1" + } + }, + "node_modules/@prisma/fetch-engine/node_modules/@prisma/get-platform": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.0.1.tgz", + "integrity": "sha512-DrsGnZOsF7PlAE7UtqmJenWti87RQtg7v9qW9alS71Pj0P6ZQV0RuzRQaql9dCWoo6qKAaF5U/L4kI826MmiZg==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.0.1" } }, "node_modules/@prisma/get-platform": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.0.tgz", - "integrity": "sha512-ym85WDO2yDhC3fIXHWYpG3kVMBA49cL1XD2GCsCF8xbwoy2OkDQY44gEbAt2X46IQ4Apq9H6g0Ex1iFfPqEkHA==", + "version": "6.8.2", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.8.2.tgz", + "integrity": "sha512-vXSxyUgX3vm1Q70QwzwkjeYfRryIvKno1SXbIqwSptKwqKzskINnDUcx85oX+ys6ooN2ATGSD0xN2UTfg6Zcow==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "6.19.0" + "@prisma/debug": "6.8.2" + } + }, + "node_modules/@prisma/get-platform/node_modules/@prisma/debug": { + "version": "6.8.2", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.8.2.tgz", + "integrity": "sha512-4muBSSUwJJ9BYth5N8tqts8JtiLT8QI/RSAzEogwEfpbYGFo9mYsInsVo8dqXdPO2+Rm5OG5q0qWDDE3nyUbVg==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/query-plan-executor": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/@prisma/query-plan-executor/-/query-plan-executor-6.18.0.tgz", + "integrity": "sha512-jZ8cfzFgL0jReE1R10gT8JLHtQxjWYLiQ//wHmVYZ2rVkFHoh0DT8IXsxcKcFlfKN7ak7k6j0XMNn2xVNyr5cA==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/studio-core": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/@prisma/studio-core/-/studio-core-0.8.2.tgz", + "integrity": "sha512-/iAEWEUpTja+7gVMu1LtR2pPlvDmveAwMHdTWbDeGlT7yiv0ZTCPpmeAGdq/Y9aJ9Zj1cEGBXGRbmmNPj022PQ==", + "devOptional": true, + "license": "UNLICENSED", + "peerDependencies": { + "@types/react": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" } }, "node_modules/@radix-ui/primitive": { @@ -3427,9 +3630,9 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.90.10", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.10.tgz", - "integrity": "sha512-EhZVFu9rl7GfRNuJLJ3Y7wtbTnENsvzp+YpcAV7kCYiXni1v8qZh++lpw4ch4rrwC0u/EZRnBHIehzCGzwXDSQ==", + "version": "5.90.11", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.11.tgz", + "integrity": "sha512-f9z/nXhCgWDF4lHqgIE30jxLe4sYv15QodfdPDKYAk7nAEjNcndy4dHz3ezhdUaR23BpWa4I2EH4/DZ0//Uf8A==", "license": "MIT", "funding": { "type": "github", @@ -3437,13 +3640,13 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.90.10", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.10.tgz", - "integrity": "sha512-BKLss9Y8PQ9IUjPYQiv3/Zmlx92uxffUOX8ZZNoQlCIZBJPT5M+GOMQj7xislvVQ6l1BstBjcX0XB/aHfFYVNw==", + "version": "5.90.11", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.11.tgz", + "integrity": "sha512-3uyzz01D1fkTLXuxF3JfoJoHQMU2fxsfJwE+6N5hHy0dVNoZOvwKP8Z2k7k1KDeD54N20apcJnG75TBAStIrBA==", "license": "MIT", "peer": true, "dependencies": { - "@tanstack/query-core": "5.90.10" + "@tanstack/query-core": "5.90.11" }, "funding": { "type": "github", @@ -4397,21 +4600,21 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.13.tgz", - "integrity": "sha512-w77N6bmtJ3CFnL/YHiYotwW/JI3oDlR3K38WEIqegRfdMSScaYxwYKB/0jSNpOTZzUjQkG8HHEz4sdWQMWpQ5g==", + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.14.tgz", + "integrity": "sha512-EYHLqN/BY6b47qHH7gtMxAg++saoGmsjWmAq9MlXxAz4M0NcHh9iOyKhBZyU4yxZqOd8Xnqp80/5saeitz4Cng==", "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.0.13", + "@vitest/utils": "4.0.14", "ast-v8-to-istanbul": "^0.3.8", - "debug": "^4.4.3", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^5.0.6", "istanbul-reports": "^3.2.0", "magicast": "^0.5.1", + "obug": "^2.1.1", "std-env": "^3.10.0", "tinyrainbow": "^3.0.3" }, @@ -4419,8 +4622,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "4.0.13", - "vitest": "4.0.13" + "@vitest/browser": "4.0.14", + "vitest": "4.0.14" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -4429,16 +4632,16 @@ } }, "node_modules/@vitest/expect": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.13.tgz", - "integrity": "sha512-zYtcnNIBm6yS7Gpr7nFTmq8ncowlMdOJkWLqYvhr/zweY6tFbDkDi8BPPOeHxEtK1rSI69H7Fd4+1sqvEGli6w==", + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.14.tgz", + "integrity": "sha512-RHk63V3zvRiYOWAV0rGEBRO820ce17hz7cI2kDmEdfQsBjT2luEKB5tCOc91u1oSQoUOZkSv3ZyzkdkSLD7lKw==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.0.13", - "@vitest/utils": "4.0.13", + "@vitest/spy": "4.0.14", + "@vitest/utils": "4.0.14", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" }, @@ -4447,13 +4650,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.13.tgz", - "integrity": "sha512-eNCwzrI5djoauklwP1fuslHBjrbR8rqIVbvNlAnkq1OTa6XT+lX68mrtPirNM9TnR69XUPt4puBCx2Wexseylg==", + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.14.tgz", + "integrity": "sha512-RzS5NujlCzeRPF1MK7MXLiEFpkIXeMdQ+rN3Kk3tDI9j0mtbr7Nmuq67tpkOJQpgyClbOltCXMjLZicJHsH5Cg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.0.13", + "@vitest/spy": "4.0.14", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -4474,9 +4677,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.13.tgz", - "integrity": "sha512-ooqfze8URWbI2ozOeLDMh8YZxWDpGXoeY3VOgcDnsUxN0jPyPWSUvjPQWqDGCBks+opWlN1E4oP1UYl3C/2EQA==", + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.14.tgz", + "integrity": "sha512-SOYPgujB6TITcJxgd3wmsLl+wZv+fy3av2PpiPpsWPZ6J1ySUYfScfpIt2Yv56ShJXR2MOA6q2KjKHN4EpdyRQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4487,13 +4690,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.13.tgz", - "integrity": "sha512-9IKlAru58wcVaWy7hz6qWPb2QzJTKt+IOVKjAx5vb5rzEFPTL6H4/R9BMvjZ2ppkxKgTrFONEJFtzvnyEpiT+A==", + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.14.tgz", + "integrity": "sha512-BsAIk3FAqxICqREbX8SetIteT8PiaUL/tgJjmhxJhCsigmzzH8xeadtp7LRnTpCVzvf0ib9BgAfKJHuhNllKLw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.0.13", + "@vitest/utils": "4.0.14", "pathe": "^2.0.3" }, "funding": { @@ -4501,13 +4704,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.13.tgz", - "integrity": "sha512-hb7Usvyika1huG6G6l191qu1urNPsq1iFc2hmdzQY3F5/rTgqQnwwplyf8zoYHkpt7H6rw5UfIw6i/3qf9oSxQ==", + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.14.tgz", + "integrity": "sha512-aQVBfT1PMzDSA16Y3Fp45a0q8nKexx6N5Amw3MX55BeTeZpoC08fGqEZqVmPcqN0ueZsuUQ9rriPMhZ3Mu19Ag==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.13", + "@vitest/pretty-format": "4.0.14", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -4516,9 +4719,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.13.tgz", - "integrity": "sha512-hSu+m4se0lDV5yVIcNWqjuncrmBgwaXa2utFLIrBkQCQkt+pSwyZTPFQAZiiF/63j8jYa8uAeUZ3RSfcdWaYWw==", + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.14.tgz", + "integrity": "sha512-JmAZT1UtZooO0tpY3GRyiC/8W7dCs05UOq9rfsUUgEZEdq+DuHLmWhPsrTt0TiW7WYeL/hXpaE07AZ2RCk44hg==", "dev": true, "license": "MIT", "funding": { @@ -4526,14 +4729,14 @@ } }, "node_modules/@vitest/ui": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.0.13.tgz", - "integrity": "sha512-MFV6GhTflgBj194+vowTB2iLI5niMZhqiW7/NV7U4AfWbX/IAtsq4zA+gzCLyGzpsQUdJlX26hrQ1vuWShq2BQ==", + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.0.14.tgz", + "integrity": "sha512-fvDz8o7SQpFLoSBo6Cudv+fE85/fPCkwTnLAN85M+Jv7k59w2mSIjT9Q5px7XwGrmYqqKBEYxh/09IBGd1E7AQ==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@vitest/utils": "4.0.13", + "@vitest/utils": "4.0.14", "fflate": "^0.8.2", "flatted": "^3.3.3", "pathe": "^2.0.3", @@ -4545,17 +4748,17 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "4.0.13" + "vitest": "4.0.14" } }, "node_modules/@vitest/utils": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.13.tgz", - "integrity": "sha512-ydozWyQ4LZuu8rLp47xFUWis5VOKMdHjXCWhs1LuJsTNKww+pTHQNK4e0assIB9K80TxFyskENL6vCu3j34EYA==", + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.14.tgz", + "integrity": "sha512-hLqXZKAWNg8pI+SQXyXxWCTOpA3MvsqcbVeNgSi8x/CSN2wi26dSzn1wrOhmCmFjEvN9p8/kLFRHa6PI8jHazw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.13", + "@vitest/pretty-format": "4.0.14", "tinyrainbow": "^3.0.3" }, "funding": { @@ -4921,6 +5124,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/axe-core": { "version": "4.11.0", "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.0.tgz", @@ -4969,6 +5182,26 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/baseline-browser-mapping": { "version": "2.8.31", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.31.tgz", @@ -4988,6 +5221,20 @@ "bcrypt": "bin/bcrypt" } }, + "node_modules/better-sqlite3": { + "version": "12.4.6", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.4.6.tgz", + "integrity": "sha512-gaYt9yqTbQ1iOxLpJA8FPR5PiaHP+jlg8I5EX0Rs2KFwNzhBsF40KzMZS5FwelY7RG0wzaucWdqSAJM3uNCPCg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x" + } + }, "node_modules/bidi-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", @@ -4998,6 +5245,26 @@ "require-from-string": "^2.0.2" } }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -5057,6 +5324,30 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -5219,6 +5510,21 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chevrotain": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-10.5.0.tgz", + "integrity": "sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/cst-dts-gen": "10.5.0", + "@chevrotain/gast": "10.5.0", + "@chevrotain/types": "10.5.0", + "@chevrotain/utils": "10.5.0", + "lodash": "4.17.21", + "regexp-to-ast": "0.5.0" + } + }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -5235,6 +5541,12 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, "node_modules/citty": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", @@ -5370,7 +5682,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -5547,6 +5859,30 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -5616,6 +5952,16 @@ "node": ">=0.4.0" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -5636,7 +5982,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -5751,6 +6096,15 @@ "node": ">=14" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.18.3", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", @@ -6085,13 +6439,13 @@ } }, "node_modules/eslint-config-next": { - "version": "16.0.4", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.0.4.tgz", - "integrity": "sha512-FknAsm/uexYriO6UXzV2QEm4Yz/5DVQCtMUHx0FRYAKqqf5ia8xPqdyoqXzoCc45nRF5brkFpBYMvtciavzD4g==", + "version": "16.0.5", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.0.5.tgz", + "integrity": "sha512-9rBjZ/biSpolkIUiqvx/iwJJaz8sxJ6pKWSPptJenpj01HlWbCDeaA1v0yG3a71IIPMplxVCSXhmtP27SXqMdg==", "dev": true, "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "16.0.4", + "@next/eslint-plugin-next": "16.0.5", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", @@ -6479,6 +6833,15 @@ "node": ">=0.10.0" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/expect-type": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", @@ -6619,6 +6982,12 @@ "node": ">=16.0.0" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -6706,6 +7075,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", @@ -6730,6 +7116,12 @@ "node": ">=0.4.x" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -6785,6 +7177,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, "node_modules/generator-function": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", @@ -6838,6 +7240,13 @@ "node": ">=6" } }, + "node_modules/get-port-please": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.1.2.tgz", + "integrity": "sha512-Gxc29eLs1fbn6LQ4jSU4vXjlwyZhF5HsGuMAa7gqBP4Rw4yxxltyDUuF5MBclFzDTXO+ACchGQoeela4DSfzdQ==", + "devOptional": true, + "license": "MIT" + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -6900,6 +7309,12 @@ "giget": "dist/cli.mjs" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -6959,9 +7374,16 @@ "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==", - "dev": true, + "devOptional": true, "license": "ISC" }, + "node_modules/grammex": { + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.11.tgz", + "integrity": "sha512-HNwLkgRg9SqTAd1N3Uh/MnKwTBTzwBxTOPbXQ8pb0tpwydjk90k4zRE8JUn9fMUiRwKtXFZ1TWFmms3dZHN+Fg==", + "devOptional": true, + "license": "MIT" + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -7162,6 +7584,17 @@ "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==", "license": "CC0-1.0" }, + "node_modules/hono": { + "version": "4.7.10", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.7.10.tgz", + "integrity": "sha512-QkACju9MiN59CKSY5JsGZCYmPZkA6sIW6OFCUp7qDjZu6S6KHtJHhAc9Uy9mV9F8PJ1/HQ3ybZF2yjCa/73fvQ==", + "devOptional": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/html-encoding-sniffer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", @@ -7206,6 +7639,13 @@ "node": ">= 14" } }, + "node_modules/http-status-codes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", + "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==", + "devOptional": true, + "license": "MIT" + }, "node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", @@ -7233,6 +7673,26 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -7280,6 +7740,18 @@ "node": ">=8" } }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, "node_modules/inline-style-parser": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", @@ -7634,6 +8106,13 @@ "dev": true, "license": "MIT" }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "devOptional": true, + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -7802,7 +8281,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/istanbul-lib-coverage": { @@ -8371,6 +8850,16 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -8387,6 +8876,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "devOptional": true, + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -8436,6 +8932,13 @@ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "license": "MIT" }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "devOptional": true, + "license": "Apache-2.0" + }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -8483,10 +8986,26 @@ "yallist": "^3.0.2" } }, + "node_modules/lru.min": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.3.tgz", + "integrity": "sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q==", + "devOptional": true, + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, "node_modules/lucide-react": { - "version": "0.554.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.554.0.tgz", - "integrity": "sha512-St+z29uthEJVx0Is7ellNkgTEhaeSoA42I7JjOCBCrc5X6LYMGSv0P/2uS5HDLTExP5tpiqRD2PyUEOS6s9UXA==", + "version": "0.555.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.555.0.tgz", + "integrity": "sha512-D8FvHUGbxWBRQM90NZeIyhAvkFfsh3u9ekrMvJ30Z6gnpBHS6HC6ldLg7tL45hwiIz/u66eKDtdA23gwwGsAHA==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -9469,6 +9988,18 @@ "node": ">= 0.6" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -9496,12 +10027,17 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "node_modules/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", @@ -9518,6 +10054,67 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/mysql2": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz", + "integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.0", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/mysql2/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/named-placeholders/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "devOptional": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/nan": { "version": "2.23.1", "resolved": "https://registry.npmjs.org/nan/-/nan-2.23.1.tgz", @@ -9542,6 +10139,12 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, "node_modules/napi-postinstall": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", @@ -9566,12 +10169,12 @@ "license": "MIT" }, "node_modules/next": { - "version": "16.0.4", - "resolved": "https://registry.npmjs.org/next/-/next-16.0.4.tgz", - "integrity": "sha512-vICcxKusY8qW7QFOzTvnRL1ejz2ClTqDKtm1AcUjm2mPv/lVAdgpGNsftsPRIDJOXOjRQO68i1dM8Lp8GZnqoA==", + "version": "16.0.5", + "resolved": "https://registry.npmjs.org/next/-/next-16.0.5.tgz", + "integrity": "sha512-XUPsFqSqu/NDdPfn/cju9yfIedkDI7ytDoALD9todaSMxk1Z5e3WcbUjfI9xsanFTys7xz62lnRWNFqJordzkQ==", "license": "MIT", "dependencies": { - "@next/env": "16.0.4", + "@next/env": "16.0.5", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", @@ -9584,14 +10187,14 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.0.4", - "@next/swc-darwin-x64": "16.0.4", - "@next/swc-linux-arm64-gnu": "16.0.4", - "@next/swc-linux-arm64-musl": "16.0.4", - "@next/swc-linux-x64-gnu": "16.0.4", - "@next/swc-linux-x64-musl": "16.0.4", - "@next/swc-win32-arm64-msvc": "16.0.4", - "@next/swc-win32-x64-msvc": "16.0.4", + "@next/swc-darwin-arm64": "16.0.5", + "@next/swc-darwin-x64": "16.0.5", + "@next/swc-linux-arm64-gnu": "16.0.5", + "@next/swc-linux-arm64-musl": "16.0.5", + "@next/swc-linux-x64-gnu": "16.0.5", + "@next/swc-linux-x64-musl": "16.0.5", + "@next/swc-win32-arm64-msvc": "16.0.5", + "@next/swc-win32-x64-msvc": "16.0.5", "sharp": "^0.34.4" }, "peerDependencies": { @@ -9645,6 +10248,30 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/node-abi": { + "version": "3.85.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.85.0.tgz", + "integrity": "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-cron": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz", @@ -9831,6 +10458,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, "node_modules/ohash": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", @@ -9838,6 +10476,15 @@ "devOptional": true, "license": "MIT" }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -9971,7 +10618,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -10081,6 +10728,46 @@ "node": ">=4" } }, + "node_modules/postgres": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz", + "integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==", + "devOptional": true, + "license": "Unlicense", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/porsager" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -10092,9 +10779,9 @@ } }, "node_modules/prettier": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", - "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.1.tgz", + "integrity": "sha512-RWKXE4qB3u5Z6yz7omJkjWwmTfLdcbv44jUVHC5NpfXwFGzvpQM798FGv/6WNK879tc+Cn0AAyherCl1KjbyZQ==", "dev": true, "license": "MIT", "peer": true, @@ -10216,27 +10903,35 @@ } }, "node_modules/prisma": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.0.tgz", - "integrity": "sha512-F3eX7K+tWpkbhl3l4+VkFtrwJlLXbAM+f9jolgoUZbFcm1DgHZ4cq9AgVEgUym2au5Ad/TDLN8lg83D+M10ycw==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.0.1.tgz", + "integrity": "sha512-zp93MdFMSU1IHPEXbUHVUuD8wauh2BUm14OVxhxGrWJQQpXpda0rW4VSST2bci4raoldX64/wQxHKkl/wqDskQ==", "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "peer": true, "dependencies": { - "@prisma/config": "6.19.0", - "@prisma/engines": "6.19.0" + "@prisma/config": "7.0.1", + "@prisma/dev": "0.13.0", + "@prisma/engines": "7.0.1", + "@prisma/studio-core": "0.8.2", + "mysql2": "3.15.3", + "postgres": "3.4.7" }, "bin": { "prisma": "build/index.js" }, "engines": { - "node": ">=18.18" + "node": "^20.19 || ^22.12 || >=24.0" }, "peerDependencies": { - "typescript": ">=5.1.0" + "better-sqlite3": ">=9.0.0", + "typescript": ">=5.4.0" }, "peerDependenciesMeta": { + "better-sqlite3": { + "optional": true + }, "typescript": { "optional": true } @@ -10270,6 +10965,25 @@ "dev": true, "license": "MIT" }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proper-lockfile/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "devOptional": true, + "license": "ISC" + }, "node_modules/property-information": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", @@ -10286,6 +11000,16 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -10334,6 +11058,30 @@ ], "license": "MIT" }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/rc9": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", @@ -10501,6 +11249,20 @@ "react": ">= 0.14.0" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -10568,6 +11330,13 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/regexp-to-ast": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz", + "integrity": "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==", + "devOptional": true, + "license": "MIT" + }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -10655,6 +11424,16 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remeda": { + "version": "2.21.3", + "resolved": "https://registry.npmjs.org/remeda/-/remeda-2.21.3.tgz", + "integrity": "sha512-XXrZdLA10oEOQhLLzEJEiFFSKi21REGAkHdImIb4rt/XXy8ORGXh5HCcpUOsElfPNDb+X6TA/+wkh+p2KffYmg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "type-fest": "^4.39.1" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -10706,6 +11485,16 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -10862,7 +11651,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/saxes": { @@ -10894,6 +11683,12 @@ "semver": "bin/semver.js" } }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==", + "devOptional": true + }, "node_modules/server-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz", @@ -11011,7 +11806,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -11024,7 +11819,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -11113,6 +11908,64 @@ "dev": true, "license": "ISC" }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "devOptional": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/sirv": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", @@ -11147,6 +12000,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", @@ -11182,6 +12045,15 @@ "node": ">= 0.4" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -11414,9 +12286,9 @@ } }, "node_modules/superjson": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.5.tgz", - "integrity": "sha512-zWPTX96LVsA/eVYnqOM2+ofcdPqdS1dAF1LN4TS2/MWuUpfitd9ctTa87wt4xrYnZnkLtS69xpBdSxVBP5Rm6w==", + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", + "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", "license": "MIT", "dependencies": { "copy-anything": "^4" @@ -11489,6 +12361,34 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -11696,6 +12596,39 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.20.6", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", + "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -11709,6 +12642,19 @@ "node": ">= 0.8.0" } }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "devOptional": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -12062,6 +13008,22 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/valibot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.1.0.tgz", + "integrity": "sha512-Nk8lX30Qhu+9txPYTwM0cFlWLdPFsFr6LblzqIySfbZph9+BFsAHsNvHOymEviUepeIW6KFHzpX8TKhbptBXXw==", + "devOptional": true, + "license": "MIT", + "peer": true, + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", @@ -12199,24 +13161,24 @@ } }, "node_modules/vitest": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.13.tgz", - "integrity": "sha512-QSD4I0fN6uZQfftryIXuqvqgBxTvJ3ZNkF6RWECd82YGAYAfhcppBLFXzXJHQAAhVFyYEuFTrq6h0hQqjB7jIQ==", + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.14.tgz", + "integrity": "sha512-d9B2J9Cm9dN9+6nxMnnNJKJCtcyKfnHj15N6YNJfaFHRLua/d3sRKU9RuKmO9mB0XdFtUizlxfz/VPbd3OxGhw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@vitest/expect": "4.0.13", - "@vitest/mocker": "4.0.13", - "@vitest/pretty-format": "4.0.13", - "@vitest/runner": "4.0.13", - "@vitest/snapshot": "4.0.13", - "@vitest/spy": "4.0.13", - "@vitest/utils": "4.0.13", - "debug": "^4.4.3", + "@vitest/expect": "4.0.14", + "@vitest/mocker": "4.0.14", + "@vitest/pretty-format": "4.0.14", + "@vitest/runner": "4.0.14", + "@vitest/snapshot": "4.0.14", + "@vitest/spy": "4.0.14", + "@vitest/utils": "4.0.14", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", + "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", @@ -12239,12 +13201,11 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", - "@types/debug": "^4.1.12", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.0.13", - "@vitest/browser-preview": "4.0.13", - "@vitest/browser-webdriverio": "4.0.13", - "@vitest/ui": "4.0.13", + "@vitest/browser-playwright": "4.0.14", + "@vitest/browser-preview": "4.0.14", + "@vitest/browser-webdriverio": "4.0.14", + "@vitest/ui": "4.0.14", "happy-dom": "*", "jsdom": "*" }, @@ -12255,9 +13216,6 @@ "@opentelemetry/api": { "optional": true }, - "@types/debug": { - "optional": true - }, "@types/node": { "optional": true }, @@ -12358,7 +13316,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -12486,6 +13444,12 @@ "node": ">=0.10.0" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, "node_modules/ws": { "version": "8.18.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", @@ -12544,6 +13508,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zeptomatch": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.0.2.tgz", + "integrity": "sha512-H33jtSKf8Ijtb5BW6wua3G5DhnFjbFML36eFu+VdOoVY4HD9e7ggjqdM6639B+L87rjnR6Y+XeRzBXZdy52B/g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "grammex": "^3.1.10" + } + }, "node_modules/zod": { "version": "4.1.13", "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", diff --git a/package.json b/package.json index 9454b13..79d5a6e 100644 --- a/package.json +++ b/package.json @@ -4,17 +4,20 @@ "private": true, "type": "module", "scripts": { - "build": "next build --webpack", - "check": "npm run lint && tsc --noEmit", + "build": "prisma generate && next build --webpack", + "check": "eslint . && tsc --noEmit", "dev": "next dev --webpack", - "dev:server": "node server.js", + "dev:server": "node --import tsx server.js", "dev:next": "next dev --webpack", "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,mdx}\" --cache", "format:write": "prettier --write \"**/*.{ts,tsx,js,jsx,mdx}\" --cache", - "lint": "eslint . --ext .ts,.tsx,.js,.jsx", - "lint:fix": "eslint . --ext .ts,.tsx,.js,.jsx --fix", + "generate": "prisma generate", + "lint": "eslint .", + "lint:fix": "eslint --fix .", + "migrate": "prisma migrate dev", "preview": "next build && next start", - "start": "node server.js", + "postinstall": "prisma generate", + "start": "node --import tsx server.js", "test": "vitest", "test:ui": "vitest --ui", "test:run": "vitest run", @@ -22,71 +25,73 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@prisma/client": "^6.19.0", + "@prisma/adapter-better-sqlite3": "^7.0.1", + "@prisma/client": "^7.0.1", + "better-sqlite3": "^12.4.6", "@radix-ui/react-dropdown-menu": "^2.1.16", - "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-slot": "^1.2.4", "@t3-oss/env-nextjs": "^0.13.8", "@tailwindcss/typography": "^0.5.19", - "@tanstack/react-query": "^5.90.5", - "@trpc/client": "^11.6.0", - "@trpc/react-query": "^11.6.0", - "@trpc/server": "^11.6.0", + "@tanstack/react-query": "^5.90.11", + "@trpc/client": "^11.7.2", + "@trpc/react-query": "^11.7.2", + "@trpc/server": "^11.7.2", "@types/react-syntax-highlighter": "^15.5.13", "@types/ws": "^8.18.1", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-web-links": "^0.11.0", "@xterm/xterm": "^5.5.0", - "axios": "^1.7.9", - "bcryptjs": "^3.0.2", + "axios": "^1.13.2", + "bcryptjs": "^3.0.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "cron-validator": "^1.2.0", + "cron-validator": "^1.4.0", "dotenv": "^17.2.3", "jsonwebtoken": "^9.0.2", - "lucide-react": "^0.554.0", - "next": "^16.0.4", + "lucide-react": "^0.555.0", + "next": "^16.0.5", "node-cron": "^4.2.1", "node-pty": "^1.0.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", "react-markdown": "^10.1.0", "react-syntax-highlighter": "^16.1.0", "refractor": "^5.0.0", "remark-gfm": "^4.0.1", "server-only": "^0.0.1", "strip-ansi": "^7.1.2", - "superjson": "^2.2.3", - "tailwind-merge": "^3.3.1", + "superjson": "^2.2.6", + "tailwind-merge": "^3.4.0", "ws": "^8.18.3", - "zod": "^4.1.12" + "zod": "^4.1.13" }, "devDependencies": { - "@eslint/eslintrc": "^3.3.1", - "@tailwindcss/postcss": "^4.1.16", + "@tailwindcss/postcss": "^4.1.17", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", "@types/bcryptjs": "^3.0.0", - "@types/better-sqlite3": "^7.6.8", + "@types/better-sqlite3": "^7.6.13", "@types/jsonwebtoken": "^9.0.10", "@types/node": "^24.10.1", "@types/node-cron": "^3.0.11", - "@types/react": "^19.2.4", - "@types/react-dom": "^19.2.2", - "@vitejs/plugin-react": "^5.1.0", - "@vitest/coverage-v8": "^4.0.13", - "@vitest/ui": "^4.0.13", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "@vitest/coverage-v8": "^4.0.14", + "@vitest/ui": "^4.0.14", "eslint": "^9.39.1", - "eslint-config-next": "^16.0.4", + "eslint-config-next": "^16.0.5", "jsdom": "^27.2.0", - "postcss": "^8.5.3", - "prettier": "^3.5.3", + "postcss": "^8.5.6", + "prettier": "^3.7.1", "prettier-plugin-tailwindcss": "^0.7.1", - "prisma": "^6.19.0", + "prisma": "^7.0.1", "tailwindcss": "^4.1.17", - "typescript": "^5.8.2", - "typescript-eslint": "^8.46.2", - "vitest": "^4.0.13" + "typescript": "^5.9.3", + "typescript-eslint": "^8.48.0", + "tsx": "^4.19.4", + "vitest": "^4.0.14" }, "ct3aMetadata": { "initVersion": "7.39.3" diff --git a/prisma.config.ts b/prisma.config.ts new file mode 100644 index 0000000..ab58e42 --- /dev/null +++ b/prisma.config.ts @@ -0,0 +1,20 @@ +import 'dotenv/config' +import path from 'path' +import { defineConfig } from 'prisma/config' + +// Resolve database path +const dbPath = process.env.DATABASE_URL ?? `file:${path.join(process.cwd(), 'data', 'pve-scripts.db')}` + +export default defineConfig({ + schema: 'prisma/schema.prisma', + datasource: { + url: dbPath, + }, + // @ts-expect-error - Prisma 7 config types are incomplete + studio: { + adapter: async () => { + const { PrismaBetterSqlite3 } = await import('@prisma/adapter-better-sqlite3') + return new PrismaBetterSqlite3({ url: dbPath }) + }, + }, +}) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 535f82b..de112ad 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1,10 +1,10 @@ generator client { - provider = "prisma-client-js" + provider = "prisma-client" + output = "./generated/prisma" } datasource db { provider = "sqlite" - url = env("DATABASE_URL") } model InstalledScript { diff --git a/server.js b/server.js index 9b3b48f..90b586c 100644 --- a/server.js +++ b/server.js @@ -2,14 +2,21 @@ import { createServer } from 'http'; import { parse } from 'url'; import next from 'next'; import { WebSocketServer } from 'ws'; +import { spawn } from 'child_process'; import { join, resolve } from 'path'; +import stripAnsi from 'strip-ansi'; import { spawn as ptySpawn } from 'node-pty'; import { getSSHExecutionService } from './src/server/ssh-execution-service.js'; import { getDatabase } from './src/server/database-prisma.js'; -import { initializeAutoSync, initializeRepositories, setupGracefulShutdown } from './src/server/lib/autoSyncInit.js'; import dotenv from 'dotenv'; +// Dynamic import for auto sync init to avoid tsx caching issues +/** @type {any} */ +let autoSyncModule = null; + +// Load environment variables from .env file dotenv.config(); +// Fallback minimal global error handlers for Node runtime (avoid TS import) function registerGlobalErrorHandlers() { if (registerGlobalErrorHandlers._registered) return; registerGlobalErrorHandlers._registered = true; @@ -27,9 +34,11 @@ const hostname = '0.0.0.0'; const port = parseInt(process.env.PORT || '3000', 10); const app = next({ dev, hostname, port }); +// Register global handlers once at bootstrap registerGlobalErrorHandlers(); const handle = app.getRequestHandler(); +// WebSocket handler for script execution /** * @typedef {import('ws').WebSocket & {connectionTime?: number, clientIP?: string}} ExtendedWebSocket */ @@ -76,6 +85,8 @@ class ScriptExecutionHandler { * @param {import('http').Server} server */ constructor(server) { + // Create WebSocketServer without attaching to server + // We'll handle upgrades manually to avoid interfering with Next.js HMR this.wss = new WebSocketServer({ noServer: true }); @@ -85,6 +96,7 @@ class ScriptExecutionHandler { } /** + * Handle WebSocket upgrade for our endpoint * @param {import('http').IncomingMessage} request * @param {import('stream').Duplex} socket * @param {Buffer} head @@ -96,33 +108,48 @@ class ScriptExecutionHandler { } /** - * @param {string} output - * @returns {string|null} + * Parse Container ID from terminal output + * @param {string} output - Terminal output to parse + * @returns {string|null} - Container ID if found, null otherwise */ parseContainerId(output) { + // First, strip ANSI color codes to make pattern matching more reliable const cleanOutput = output.replace(/\x1b\[[0-9;]*m/g, ''); + // Look for various patterns that Proxmox scripts might use const patterns = [ + // Primary pattern - the exact format from the output /🆔\s+Container\s+ID:\s+(\d+)/i, + + // Standard patterns with flexible spacing /🆔\s*Container\s*ID:\s*(\d+)/i, /Container\s*ID:\s*(\d+)/i, /CT\s*ID:\s*(\d+)/i, /Container\s*(\d+)/i, + + // Alternative patterns /CT\s*(\d+)/i, /Container\s*created\s*with\s*ID\s*(\d+)/i, /Created\s*container\s*(\d+)/i, /Container\s*(\d+)\s*created/i, /ID:\s*(\d+)/i, + + // Patterns with different spacing and punctuation /Container\s*ID\s*:\s*(\d+)/i, /CT\s*ID\s*:\s*(\d+)/i, /Container\s*#\s*(\d+)/i, /CT\s*#\s*(\d+)/i, + + // Patterns that might appear in success messages /Successfully\s*created\s*container\s*(\d+)/i, /Container\s*(\d+)\s*is\s*ready/i, /Container\s*(\d+)\s*started/i, + + // Generic number patterns that might be container IDs (3-4 digits) /(?:^|\s)(\d{3,4})(?:\s|$)/m, ]; + // Try patterns on both original and cleaned output const outputsToTry = [output, cleanOutput]; for (const testOutput of outputsToTry) { @@ -130,6 +157,7 @@ class ScriptExecutionHandler { const match = testOutput.match(pattern); if (match && match[1]) { const containerId = match[1]; + // Additional validation: container IDs are typically 3-4 digits if (containerId.length >= 3 && containerId.length <= 4) { return containerId; } @@ -137,24 +165,34 @@ class ScriptExecutionHandler { } } + return null; } /** - * @param {string} output - * @returns {{ip: string, port: number}|null} + * Parse Web UI URL from terminal output + * @param {string} output - Terminal output to parse + * @returns {{ip: string, port: number}|null} - Object with ip and port if found, null otherwise */ parseWebUIUrl(output) { + // First, strip ANSI color codes to make pattern matching more reliable const cleanOutput = output.replace(/\x1b\[[0-9;]*m/g, ''); + // Look for URL patterns with any valid IP address (private or public) const patterns = [ + // HTTP/HTTPS URLs with IP and port /https?:\/\/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):(\d+)/gi, + // URLs without explicit port (assume default ports) /https?:\/\/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(?:\/|$|\s)/gi, + // URLs with trailing slash and port /https?:\/\/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):(\d+)\//gi, + // URLs with just IP and port (no protocol) /(?:^|\s)(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):(\d+)(?:\s|$)/gi, + // URLs with just IP (no protocol, no port) /(?:^|\s)(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(?:\s|$)/gi, ]; + // Try patterns on both original and cleaned output const outputsToTry = [output, cleanOutput]; for (const testOutput of outputsToTry) { @@ -165,6 +203,7 @@ class ScriptExecutionHandler { const ip = match[1]; const port = match[2] || (match[0].startsWith('https') ? '443' : '80'); + // Validate IP address format if (ip.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) { return { ip: ip, @@ -180,11 +219,12 @@ class ScriptExecutionHandler { } /** - * @param {string} scriptName - * @param {string} scriptPath - * @param {string} executionMode - * @param {number|null} serverId - * @returns {Promise} + * Create installation record + * @param {string} scriptName - Name of the script + * @param {string} scriptPath - Path to the script + * @param {string} executionMode - 'local' or 'ssh' + * @param {number|null} serverId - Server ID for SSH executions + * @returns {Promise} - Installation record ID */ async createInstallationRecord(scriptName, scriptPath, executionMode, serverId = null) { try { @@ -205,8 +245,9 @@ class ScriptExecutionHandler { } /** - * @param {number} installationId - * @param {Object} updateData + * Update installation record + * @param {number} installationId - Installation record ID + * @param {Object} updateData - Data to update */ async updateInstallationRecord(installationId, updateData) { try { @@ -218,6 +259,8 @@ class ScriptExecutionHandler { setupWebSocket() { this.wss.on('connection', (ws, request) => { + + // Set connection metadata /** @type {ExtendedWebSocket} */ (ws).connectionTime = Date.now(); /** @type {ExtendedWebSocket} */ (ws).clientIP = request.socket.remoteAddress || 'unknown'; @@ -308,6 +351,8 @@ class ScriptExecutionHandler { let installationId = null; try { + + // Check if execution is already running if (this.activeExecutions.has(executionId)) { this.sendMessage(ws, { type: 'error', @@ -317,7 +362,10 @@ class ScriptExecutionHandler { return; } + // Extract script name from path const scriptName = scriptPath.split('/').pop() ?? scriptPath.split('\\').pop() ?? 'Unknown Script'; + + // Create installation record const serverId = server ? (server.id ?? null) : null; installationId = await this.createInstallationRecord(scriptName, scriptPath, mode, serverId); @@ -325,14 +373,17 @@ class ScriptExecutionHandler { console.error('Failed to create installation record'); } + // Handle SSH execution if (mode === 'ssh' && server) { await this.startSSHScriptExecution(ws, scriptPath, executionId, server, installationId); return; } if (mode === 'ssh' && !server) { + // SSH mode requested but no server provided, falling back to local execution } + // Basic validation for local execution const scriptsDir = join(process.cwd(), 'scripts'); const resolvedPath = resolve(scriptPath); @@ -343,12 +394,14 @@ class ScriptExecutionHandler { timestamp: Date.now() }); + // Update installation record with failure if (installationId) { await this.updateInstallationRecord(installationId, { status: 'failed' }); } return; } + // Start script execution with pty for proper TTY support const childProcess = ptySpawn('bash', [resolvedPath], { cwd: scriptsDir, name: 'xterm-256color', @@ -356,13 +409,16 @@ class ScriptExecutionHandler { rows: 24, env: { ...process.env, - TERM: 'xterm-256color', - FORCE_ANSI: 'true', - COLUMNS: '80', - LINES: '24' + TERM: 'xterm-256color', // Enable proper terminal support + FORCE_ANSI: 'true', // Allow ANSI codes for proper display + COLUMNS: '80', // Set terminal width + LINES: '24' // Set terminal height } }); + // pty handles encoding automatically + + // Store the execution with installation ID this.activeExecutions.set(executionId, { process: childProcess, ws, @@ -370,28 +426,34 @@ class ScriptExecutionHandler { outputBuffer: '' }); + // Send start message this.sendMessage(ws, { type: 'start', data: `Starting execution of ${scriptPath}`, timestamp: Date.now() }); - childProcess.onData(/** @param {string} data */ async (data) => { + // Handle pty data (both stdout and stderr combined) + childProcess.onData(async (data) => { const output = data.toString(); + // Store output in buffer for logging const execution = this.activeExecutions.get(executionId); if (execution) { execution.outputBuffer += output; + // Keep only last 1000 characters to avoid memory issues if (execution.outputBuffer.length > 1000) { execution.outputBuffer = execution.outputBuffer.slice(-1000); } } + // Parse for Container ID const containerId = this.parseContainerId(output); if (containerId && installationId) { await this.updateInstallationRecord(installationId, { container_id: containerId }); } + // Parse for Web UI URL const webUIUrl = this.parseWebUIUrl(output); if (webUIUrl && installationId) { const { ip, port } = webUIUrl; @@ -410,10 +472,12 @@ class ScriptExecutionHandler { }); }); + // Handle process exit childProcess.onExit((e) => { const execution = this.activeExecutions.get(executionId); const isSuccess = e.exitCode === 0; + // Update installation record with final status and output if (installationId && execution) { this.updateInstallationRecord(installationId, { status: isSuccess ? 'success' : 'failed', @@ -427,6 +491,7 @@ class ScriptExecutionHandler { timestamp: Date.now() }); + // Clean up this.activeExecutions.delete(executionId); }); @@ -437,6 +502,7 @@ class ScriptExecutionHandler { timestamp: Date.now() }); + // Update installation record with failure if (installationId) { await this.updateInstallationRecord(installationId, { status: 'failed' }); } @@ -444,6 +510,7 @@ class ScriptExecutionHandler { } /** + * Start SSH script execution * @param {ExtendedWebSocket} ws * @param {string} scriptPath * @param {string} executionId @@ -453,6 +520,7 @@ class ScriptExecutionHandler { async startSSHScriptExecution(ws, scriptPath, executionId, server, installationId = null) { const sshService = getSSHExecutionService(); + // Send start message this.sendMessage(ws, { type: 'start', data: `Starting SSH execution of ${scriptPath} on ${server.name} (${server.ip})`, @@ -460,23 +528,27 @@ class ScriptExecutionHandler { }); try { - const execution = await sshService.executeScript( + const execution = /** @type {ExecutionResult} */ (await sshService.executeScript( server, scriptPath, /** @param {string} data */ async (data) => { + // Store output in buffer for logging const exec = this.activeExecutions.get(executionId); if (exec) { exec.outputBuffer += data; + // Keep only last 1000 characters to avoid memory issues if (exec.outputBuffer.length > 1000) { exec.outputBuffer = exec.outputBuffer.slice(-1000); } } + // Parse for Container ID const containerId = this.parseContainerId(data); if (containerId && installationId) { await this.updateInstallationRecord(installationId, { container_id: containerId }); } + // Parse for Web UI URL const webUIUrl = this.parseWebUIUrl(data); if (webUIUrl && installationId) { const { ip, port } = webUIUrl; @@ -488,6 +560,7 @@ class ScriptExecutionHandler { } } + // Handle data output this.sendMessage(ws, { type: 'output', data: data, @@ -495,14 +568,17 @@ class ScriptExecutionHandler { }); }, /** @param {string} error */ (error) => { + // Store error in buffer for logging const exec = this.activeExecutions.get(executionId); if (exec) { exec.outputBuffer += error; + // Keep only last 1000 characters to avoid memory issues if (exec.outputBuffer.length > 1000) { exec.outputBuffer = exec.outputBuffer.slice(-1000); } } + // Handle errors this.sendMessage(ws, { type: 'error', data: error, @@ -513,6 +589,7 @@ class ScriptExecutionHandler { const exec = this.activeExecutions.get(executionId); const isSuccess = code === 0; + // Update installation record with final status and output if (installationId && exec) { await this.updateInstallationRecord(installationId, { status: isSuccess ? 'success' : 'failed', @@ -520,18 +597,21 @@ class ScriptExecutionHandler { }); } + // Handle process exit this.sendMessage(ws, { type: 'end', data: `SSH script execution finished with code: ${code}`, timestamp: Date.now() }); + // Clean up this.activeExecutions.delete(executionId); } - ); + )); + // Store the execution with installation ID this.activeExecutions.set(executionId, { - process: /** @type {ExecutionResult} */ (execution).process, + process: execution.process, ws, installationId, outputBuffer: '' @@ -544,6 +624,7 @@ class ScriptExecutionHandler { timestamp: Date.now() }); + // Update installation record with failure if (installationId) { await this.updateInstallationRecord(installationId, { status: 'failed' }); } @@ -583,7 +664,7 @@ class ScriptExecutionHandler { * @param {any} message */ sendMessage(ws, message) { - if (ws.readyState === 1) { + if (ws.readyState === 1) { // WebSocket.OPEN ws.send(JSON.stringify(message)); } } @@ -601,6 +682,7 @@ class ScriptExecutionHandler { } /** + * Start backup execution * @param {ExtendedWebSocket} ws * @param {string} containerId * @param {string} executionId @@ -610,6 +692,7 @@ class ScriptExecutionHandler { */ async startBackupExecution(ws, containerId, executionId, storage, mode = 'local', server = null) { try { + // Send start message this.sendMessage(ws, { type: 'start', data: `Starting backup for container ${containerId} to storage ${storage}...`, @@ -635,19 +718,22 @@ class ScriptExecutionHandler { } /** + * Start SSH backup execution * @param {ExtendedWebSocket} ws * @param {string} containerId * @param {string} executionId * @param {string} storage * @param {ServerInfo} server - * @param {Function|null} [onComplete] + * @param {Function} [onComplete] - Optional callback when backup completes */ - startSSHBackupExecution(ws, containerId, executionId, storage, server, onComplete = null) { + startSSHBackupExecution(ws, containerId, executionId, storage, server, onComplete = undefined) { const sshService = getSSHExecutionService(); return new Promise((resolve, reject) => { try { const backupCommand = `vzdump ${containerId} --storage ${storage} --mode snapshot`; + + // Wrap the onExit callback to resolve our promise let promiseResolved = false; sshService.executeCommand( @@ -671,6 +757,8 @@ class ScriptExecutionHandler { }, /** @param {number} code */ (code) => { + // Don't send 'end' message here if this is part of a backup+update flow + // The update flow will handle completion messages const success = code === 0; if (!success) { @@ -681,6 +769,7 @@ class ScriptExecutionHandler { }); } + // Send a completion message (but not 'end' type to avoid stopping terminal) this.sendMessage(ws, { type: 'output', data: `\n[Backup ${success ? 'completed' : 'failed'} with exit code: ${code}]\n`, @@ -689,10 +778,14 @@ class ScriptExecutionHandler { if (onComplete) onComplete(success); + // Resolve the promise when backup completes + // Use setImmediate to ensure resolution happens in the right execution context if (!promiseResolved) { promiseResolved = true; const result = { success, code }; + // Use setImmediate to ensure promise resolution happens in the next tick + // This ensures the await in startUpdateExecution can properly resume setImmediate(() => { try { resolve(result); @@ -706,10 +799,12 @@ class ScriptExecutionHandler { this.activeExecutions.delete(executionId); } ).then((execution) => { + // Store the execution this.activeExecutions.set(executionId, { process: /** @type {any} */ (execution).process, ws }); + // Note: Don't resolve here - wait for onExit callback }).catch((error) => { console.error('Error starting backup execution:', error); this.sendMessage(ws, { @@ -738,15 +833,17 @@ class ScriptExecutionHandler { } /** + * Start update execution (pct enter + update command) * @param {ExtendedWebSocket} ws * @param {string} containerId * @param {string} executionId * @param {string} mode - * @param {ServerInfo|null} server - * @param {string|null} [backupStorage] + * @param {ServerInfo|undefined} server + * @param {string} [backupStorage] - Optional storage to backup to before update */ - async startUpdateExecution(ws, containerId, executionId, mode = 'local', server = null, backupStorage = null) { + async startUpdateExecution(ws, containerId, executionId, mode = 'local', server = undefined, backupStorage = undefined) { try { + // If backup storage is provided, run backup first if (backupStorage && mode === 'ssh' && server) { this.sendMessage(ws, { type: 'start', @@ -754,8 +851,10 @@ class ScriptExecutionHandler { timestamp: Date.now() }); + // Create a separate execution ID for backup const backupExecutionId = `backup_${executionId}`; + // Run backup and wait for it to complete try { const backupResult = await this.startSSHBackupExecution( ws, @@ -765,13 +864,16 @@ class ScriptExecutionHandler { server ); + // Backup completed (successfully or not) if (!backupResult || !backupResult.success) { + // Backup failed, but we'll still allow update (per requirement 1b) this.sendMessage(ws, { type: 'output', data: '\n⚠️ Backup failed, but proceeding with update as requested...\n', timestamp: Date.now() }); } else { + // Backup succeeded this.sendMessage(ws, { type: 'output', data: '\n✅ Backup completed successfully. Starting update...\n', @@ -780,6 +882,7 @@ class ScriptExecutionHandler { } } catch (error) { console.error('Backup error before update:', error); + // Backup failed to start, but allow update to proceed this.sendMessage(ws, { type: 'output', data: `\n⚠️ Backup error: ${error instanceof Error ? error.message : String(error)}. Proceeding with update...\n`, @@ -787,9 +890,11 @@ class ScriptExecutionHandler { }); } + // Small delay before starting update await new Promise(resolve => setTimeout(resolve, 1000)); } + // Send start message for update (only if we're actually starting an update) this.sendMessage(ws, { type: 'start', data: `Starting update for container ${containerId}...`, @@ -812,6 +917,7 @@ class ScriptExecutionHandler { } /** + * Start local update execution * @param {ExtendedWebSocket} ws * @param {string} containerId * @param {string} executionId @@ -819,6 +925,7 @@ class ScriptExecutionHandler { async startLocalUpdateExecution(ws, containerId, executionId) { const { spawn } = await import('node-pty'); + // Create a shell process that will run pct enter and then update const childProcess = spawn('bash', ['-c', `pct enter ${containerId}`], { name: 'xterm-color', cols: 80, @@ -827,11 +934,13 @@ class ScriptExecutionHandler { env: process.env }); + // Store the execution this.activeExecutions.set(executionId, { process: childProcess, ws }); + // Handle pty data childProcess.onData((data) => { this.sendMessage(ws, { type: 'output', @@ -840,10 +949,12 @@ class ScriptExecutionHandler { }); }); + // Send the update command after a delay to ensure we're in the container setTimeout(() => { childProcess.write('update\n'); }, 4000); + // Handle process exit childProcess.onExit((e) => { this.sendMessage(ws, { type: 'end', @@ -856,6 +967,7 @@ class ScriptExecutionHandler { } /** + * Start SSH update execution * @param {ExtendedWebSocket} ws * @param {string} containerId * @param {string} executionId @@ -896,11 +1008,13 @@ class ScriptExecutionHandler { } ); + // Store the execution this.activeExecutions.set(executionId, { process: /** @type {any} */ (execution).process, ws }); + // Send the update command after a delay to ensure we're in the container setTimeout(() => { /** @type {any} */ (execution).process.write('update\n'); }, 4000); @@ -915,6 +1029,7 @@ class ScriptExecutionHandler { } /** + * Start shell execution * @param {ExtendedWebSocket} ws * @param {string} containerId * @param {string} executionId @@ -923,6 +1038,8 @@ class ScriptExecutionHandler { */ async startShellExecution(ws, containerId, executionId, mode = 'local', server = null) { try { + + // Send start message this.sendMessage(ws, { type: 'start', data: `Starting shell session for container ${containerId}...`, @@ -945,6 +1062,7 @@ class ScriptExecutionHandler { } /** + * Start local shell execution * @param {ExtendedWebSocket} ws * @param {string} containerId * @param {string} executionId @@ -952,6 +1070,7 @@ class ScriptExecutionHandler { async startLocalShellExecution(ws, containerId, executionId) { const { spawn } = await import('node-pty'); + // Create a shell process that will run pct enter const childProcess = spawn('bash', ['-c', `pct enter ${containerId}`], { name: 'xterm-color', cols: 80, @@ -960,11 +1079,13 @@ class ScriptExecutionHandler { env: process.env }); + // Store the execution this.activeExecutions.set(executionId, { process: childProcess, ws }); + // Handle pty data childProcess.onData((data) => { this.sendMessage(ws, { type: 'output', @@ -973,6 +1094,9 @@ class ScriptExecutionHandler { }); }); + // Note: No automatic command is sent - user can type commands interactively + + // Handle process exit childProcess.onExit((e) => { this.sendMessage(ws, { type: 'end', @@ -985,6 +1109,7 @@ class ScriptExecutionHandler { } /** + * Start SSH shell execution * @param {ExtendedWebSocket} ws * @param {string} containerId * @param {string} executionId @@ -1025,11 +1150,14 @@ class ScriptExecutionHandler { } ); + // Store the execution this.activeExecutions.set(executionId, { process: /** @type {any} */ (execution).process, ws }); + // Note: No automatic command is sent - user can type commands interactively + } catch (error) { this.sendMessage(ws, { type: 'error', @@ -1040,24 +1168,32 @@ class ScriptExecutionHandler { } } - +// TerminalHandler removed - not used by current application app.prepare().then(() => { const httpServer = createServer(async (req, res) => { try { + // Be sure to pass `true` as the second argument to `url.parse`. + // This tells it to parse the query portion of the URL. const parsedUrl = parse(req.url || '', true); const { pathname, query } = parsedUrl; + // Check if this is a WebSocket upgrade request const isWebSocketUpgrade = req.headers.upgrade === 'websocket'; + // Only intercept WebSocket upgrades for /ws/script-execution + // Let Next.js handle all other WebSocket upgrades (like HMR) and all HTTP requests if (isWebSocketUpgrade && pathname === '/ws/script-execution') { - return; - } - - if (isWebSocketUpgrade) { + // WebSocket upgrade will be handled by the WebSocket server + // Don't call handle() for this path - let WebSocketServer handle it return; } + // Let Next.js handle all other requests including: + // - HTTP requests to /ws/script-execution (non-WebSocket) + // - WebSocket upgrades to other paths (like /_next/webpack-hmr) + // - All static assets (_next routes) + // - All other routes await handle(req, res, parsedUrl); } catch (err) { console.error('Error occurred handling', req.url, err); @@ -1066,19 +1202,36 @@ app.prepare().then(() => { } }); + // Create WebSocket handlers const scriptHandler = new ScriptExecutionHandler(httpServer); + // Handle WebSocket upgrades manually to avoid interfering with Next.js HMR + // We need to preserve Next.js's upgrade handlers and call them for non-matching paths + // Save any existing upgrade listeners (Next.js might have set them up) + const existingUpgradeListeners = httpServer.listeners('upgrade').slice(); + httpServer.removeAllListeners('upgrade'); + + // Add our upgrade handler that routes based on path httpServer.on('upgrade', (request, socket, head) => { const parsedUrl = parse(request.url || '', true); const { pathname } = parsedUrl; if (pathname === '/ws/script-execution') { + // Handle our custom WebSocket endpoint scriptHandler.handleUpgrade(request, socket, head); - return; + } else { + // For all other paths (including Next.js HMR), call existing listeners + // This allows Next.js to handle its own WebSocket upgrades + for (const listener of existingUpgradeListeners) { + try { + listener.call(httpServer, request, socket, head); + } catch (err) { + console.error('Error in upgrade listener:', err); + } + } } - - socket.destroy(); }); + // Note: TerminalHandler removed as it's not being used by the current application httpServer .once('error', (err) => { @@ -1089,10 +1242,38 @@ app.prepare().then(() => { console.log(`> Ready on http://${hostname}:${port}`); console.log(`> WebSocket server running on ws://${hostname}:${port}/ws/script-execution`); - await initializeRepositories(); - - initializeAutoSync(); - - setupGracefulShutdown(); + // Initialize auto sync module and run initialization + if (!autoSyncModule) { + try { + console.log('Dynamically importing autoSyncInit...'); + autoSyncModule = await import('./src/server/lib/autoSyncInit.js'); + console.log('autoSyncModule loaded, exports:', Object.keys(autoSyncModule)); + } catch (error) { + const err = error instanceof Error ? error : new Error(String(error)); + console.error('Failed to import autoSyncInit:', err.message); + console.error('Stack:', err.stack); + throw error; + } + } + + // Initialize default repositories + if (typeof autoSyncModule.initializeRepositories === 'function') { + console.log('Calling initializeRepositories...'); + await autoSyncModule.initializeRepositories(); + } else { + console.warn('initializeRepositories is not a function, type:', typeof autoSyncModule.initializeRepositories); + } + + // Initialize auto-sync service + if (typeof autoSyncModule.initializeAutoSync === 'function') { + console.log('Calling initializeAutoSync...'); + autoSyncModule.initializeAutoSync(); + } + + // Setup graceful shutdown handlers + if (typeof autoSyncModule.setupGracefulShutdown === 'function') { + console.log('Setting up graceful shutdown...'); + autoSyncModule.setupGracefulShutdown(); + } }); }); diff --git a/src/app/_components/AuthProvider.tsx b/src/app/_components/AuthProvider.tsx index 05e51cb..0888081 100644 --- a/src/app/_components/AuthProvider.tsx +++ b/src/app/_components/AuthProvider.tsx @@ -1,6 +1,13 @@ -'use client'; +"use client"; -import { createContext, useContext, useEffect, useState, useCallback, type ReactNode } from 'react'; +import { + createContext, + useContext, + useEffect, + useState, + useCallback, + type ReactNode, +} from "react"; interface AuthContextType { isAuthenticated: boolean; @@ -27,10 +34,13 @@ export function AuthProvider({ children }: AuthProviderProps) { const checkAuthInternal = async (retryCount = 0) => { try { // First check if setup is completed - const setupResponse = await fetch('/api/settings/auth-credentials'); + const setupResponse = await fetch("/api/settings/auth-credentials"); if (setupResponse.ok) { - const setupData = await setupResponse.json() as { setupCompleted: boolean; enabled: boolean }; - + const setupData = (await setupResponse.json()) as { + setupCompleted: boolean; + enabled: boolean; + }; + // If setup is not completed or auth is disabled, don't verify if (!setupData.setupCompleted || !setupData.enabled) { setIsAuthenticated(false); @@ -42,12 +52,12 @@ export function AuthProvider({ children }: AuthProviderProps) { } // Only verify authentication if setup is completed and auth is enabled - const response = await fetch('/api/auth/verify', { - credentials: 'include', // Ensure cookies are sent + const response = await fetch("/api/auth/verify", { + credentials: "include", // Ensure cookies are sent }); if (response.ok) { - const data = await response.json() as { - username: string; + const data = (await response.json()) as { + username: string; expirationTime?: number | null; timeUntilExpiration?: number | null; }; @@ -58,7 +68,7 @@ export function AuthProvider({ children }: AuthProviderProps) { setIsAuthenticated(false); setUsername(null); setExpirationTime(null); - + // Retry logic for failed auth checks (max 2 retries) if (retryCount < 2) { setTimeout(() => { @@ -68,11 +78,11 @@ export function AuthProvider({ children }: AuthProviderProps) { } } } catch (error) { - console.error('Error checking auth:', error); + console.error("Error checking auth:", error); setIsAuthenticated(false); setUsername(null); setExpirationTime(null); - + // Retry logic for network errors (max 2 retries) if (retryCount < 2) { setTimeout(() => { @@ -89,44 +99,49 @@ export function AuthProvider({ children }: AuthProviderProps) { return checkAuthInternal(0); }, []); - const login = async (username: string, password: string): Promise => { + const login = async ( + username: string, + password: string, + ): Promise => { try { - const response = await fetch('/api/auth/login', { - method: 'POST', + const response = await fetch("/api/auth/login", { + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify({ username, password }), - credentials: 'include', // Ensure cookies are received + credentials: "include", // Ensure cookies are received }); if (response.ok) { - const data = await response.json() as { username: string }; + const data = (await response.json()) as { + username: string; + expirationTime?: number; + }; setIsAuthenticated(true); setUsername(data.username); - - // Check auth again to get expiration time - // Add a small delay to ensure the httpOnly cookie is available - await new Promise((resolve) => { - setTimeout(() => { - void checkAuth().then(() => resolve()); - }, 150); - }); + // Set expiration time from login response if available + if (data.expirationTime) { + setExpirationTime(data.expirationTime); + } + // Don't call checkAuth after login - we already know we're authenticated + // The cookie is set by the server response return true; } else { const errorData = await response.json(); - console.error('Login failed:', errorData.error); + console.error("Login failed:", errorData.error); return false; } } catch (error) { - console.error('Login error:', error); + console.error("Login error:", error); return false; } }; const logout = () => { // Clear the auth cookie by setting it to expire - document.cookie = 'auth-token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; + document.cookie = + "auth-token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; setIsAuthenticated(false); setUsername(null); setExpirationTime(null); @@ -156,7 +171,7 @@ export function AuthProvider({ children }: AuthProviderProps) { export function useAuth() { const context = useContext(AuthContext); if (context === undefined) { - throw new Error('useAuth must be used within an AuthProvider'); + throw new Error("useAuth must be used within an AuthProvider"); } return context; } diff --git a/src/app/_components/BackupWarningModal.tsx b/src/app/_components/BackupWarningModal.tsx index ca5d851..d5e3f5f 100644 --- a/src/app/_components/BackupWarningModal.tsx +++ b/src/app/_components/BackupWarningModal.tsx @@ -1,8 +1,8 @@ -'use client'; +"use client"; -import { Button } from './ui/button'; -import { AlertTriangle } from 'lucide-react'; -import { useRegisterModal } from './modal/ModalStackProvider'; +import { Button } from "./ui/button"; +import { AlertTriangle } from "lucide-react"; +import { useRegisterModal } from "./modal/ModalStackProvider"; interface BackupWarningModalProps { isOpen: boolean; @@ -13,33 +13,43 @@ interface BackupWarningModalProps { export function BackupWarningModal({ isOpen, onClose, - onProceed + onProceed, }: BackupWarningModalProps) { - useRegisterModal(isOpen, { id: 'backup-warning-modal', allowEscape: true, onClose }); + useRegisterModal(isOpen, { + id: "backup-warning-modal", + allowEscape: true, + onClose, + }); if (!isOpen) return null; return ( -
-
+
+
{/* Header */} -
+
- -

Backup Failed

+ +

+ Backup Failed +

{/* Content */}
-

- The backup failed, but you can still proceed with the update if you wish. -

- Warning: Proceeding without a backup means you won't be able to restore the container if something goes wrong during the update. +

+ The backup failed, but you can still proceed with the update if you + wish. +
+
+ Warning: Proceeding + without a backup means you won't be able to restore the + container if something goes wrong during the update.

{/* Action Buttons */} -
+
@@ -62,6 +72,3 @@ export function BackupWarningModal({
); } - - - diff --git a/src/app/_components/BackupsTab.tsx b/src/app/_components/BackupsTab.tsx index d114eee..5048e8a 100644 --- a/src/app/_components/BackupsTab.tsx +++ b/src/app/_components/BackupsTab.tsx @@ -1,18 +1,27 @@ -'use client'; +"use client"; -import { useState, useEffect, useCallback, startTransition } from 'react'; -import { api } from '~/trpc/react'; -import { Button } from './ui/button'; -import { Badge } from './ui/badge'; -import { RefreshCw, ChevronDown, ChevronRight, HardDrive, Database, Server, CheckCircle, AlertCircle } from 'lucide-react'; +import { useState, useEffect } from "react"; +import { api } from "~/trpc/react"; +import { Button } from "./ui/button"; +import { Badge } from "./ui/badge"; +import { + RefreshCw, + ChevronDown, + ChevronRight, + HardDrive, + Database, + Server, + CheckCircle, + AlertCircle, +} from "lucide-react"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, -} from './ui/dropdown-menu'; -import { ConfirmationModal } from './ConfirmationModal'; -import { LoadingModal } from './LoadingModal'; +} from "./ui/dropdown-menu"; +import { ConfirmationModal } from "./ConfirmationModal"; +import { LoadingModal } from "./LoadingModal"; interface Backup { id: number; @@ -28,19 +37,32 @@ interface Backup { server_color: string | null; } - +interface ContainerBackups { + container_id: string; + hostname: string; + backups: Backup[]; +} export function BackupsTab() { - const [expandedContainers, setExpandedContainers] = useState>(new Set()); + const [expandedContainers, setExpandedContainers] = useState>( + new Set(), + ); const [hasAutoDiscovered, setHasAutoDiscovered] = useState(false); const [restoreConfirmOpen, setRestoreConfirmOpen] = useState(false); - const [selectedBackup, setSelectedBackup] = useState<{ backup: Backup; containerId: string } | null>(null); + const [selectedBackup, setSelectedBackup] = useState<{ + backup: Backup; + containerId: string; + } | null>(null); const [restoreProgress, setRestoreProgress] = useState([]); const [restoreSuccess, setRestoreSuccess] = useState(false); const [restoreError, setRestoreError] = useState(null); const [shouldPollRestore, setShouldPollRestore] = useState(false); - const { data: backupsData, refetch: refetchBackups, isLoading } = api.backups.getAllBackupsGrouped.useQuery(); + const { + data: backupsData, + refetch: refetchBackups, + isLoading, + } = api.backups.getAllBackupsGrouped.useQuery(); const discoverMutation = api.backups.discoverBackups.useMutation({ onSuccess: () => { void refetchBackups(); @@ -48,32 +70,34 @@ export function BackupsTab() { }); // Poll for restore progress - const { data: restoreLogsData } = api.backups.getRestoreProgress.useQuery(undefined, { - enabled: shouldPollRestore, - refetchInterval: 1000, // Poll every second - refetchIntervalInBackground: true, - }); + const { data: restoreLogsData } = api.backups.getRestoreProgress.useQuery( + undefined, + { + enabled: shouldPollRestore, + refetchInterval: 1000, // Poll every second + refetchIntervalInBackground: true, + }, + ); // Update restore progress when log data changes useEffect(() => { if (restoreLogsData?.success && restoreLogsData.logs) { - startTransition(() => { - setRestoreProgress(restoreLogsData.logs); - - // Stop polling when restore is complete - if (restoreLogsData.isComplete) { - setShouldPollRestore(false); - // Check if restore was successful or failed - const lastLog = restoreLogsData.logs[restoreLogsData.logs.length - 1] ?? ''; - if (lastLog.includes('Restore completed successfully')) { - setRestoreSuccess(true); - setRestoreError(null); - } else if (lastLog.includes('Error:') || lastLog.includes('failed')) { - setRestoreError(lastLog); - setRestoreSuccess(false); - } + setRestoreProgress(restoreLogsData.logs); + + // Stop polling when restore is complete + if (restoreLogsData.isComplete) { + setShouldPollRestore(false); + // Check if restore was successful or failed + const lastLog = + restoreLogsData.logs[restoreLogsData.logs.length - 1] ?? ""; + if (lastLog.includes("Restore completed successfully")) { + setRestoreSuccess(true); + setRestoreError(null); + } else if (lastLog.includes("Error:") || lastLog.includes("failed")) { + setRestoreError(lastLog); + setRestoreSuccess(false); } - }); + } } }, [restoreLogsData]); @@ -81,17 +105,22 @@ export function BackupsTab() { onMutate: () => { // Start polling for progress setShouldPollRestore(true); - setRestoreProgress(['Starting restore...']); + setRestoreProgress(["Starting restore..."]); setRestoreError(null); setRestoreSuccess(false); }, onSuccess: (result) => { // Stop polling - progress will be updated from logs setShouldPollRestore(false); - + if (result.success) { // Update progress with all messages from backend (fallback if polling didn't work) - const progressMessages = restoreProgress.length > 0 ? restoreProgress : (result.progress?.map(p => p.message) ?? ['Restore completed successfully']); + const progressMessages = + restoreProgress.length > 0 + ? restoreProgress + : (result.progress?.map((p) => p.message) ?? [ + "Restore completed successfully", + ]); setRestoreProgress(progressMessages); setRestoreSuccess(true); setRestoreError(null); @@ -99,8 +128,10 @@ export function BackupsTab() { setSelectedBackup(null); // Keep success message visible - user can dismiss manually } else { - setRestoreError(result.error ?? 'Restore failed'); - setRestoreProgress(result.progress?.map(p => p.message) ?? restoreProgress); + setRestoreError(result.error ?? "Restore failed"); + setRestoreProgress( + result.progress?.map((p) => p.message) ?? restoreProgress, + ); setRestoreSuccess(false); setRestoreConfirmOpen(false); setSelectedBackup(null); @@ -110,34 +141,33 @@ export function BackupsTab() { onError: (error) => { // Stop polling on error setShouldPollRestore(false); - setRestoreError(error.message ?? 'Restore failed'); + setRestoreError(error.message ?? "Restore failed"); setRestoreConfirmOpen(false); setSelectedBackup(null); setRestoreProgress([]); }, }); - - // Update progress text in modal based on current progress - const currentProgressText = restoreProgress.length > 0 - ? restoreProgress[restoreProgress.length - 1] - : 'Restoring backup...'; - const handleDiscoverBackups = useCallback(() => { - discoverMutation.mutate(); - }, [discoverMutation]); + // Update progress text in modal based on current progress + const currentProgressText = + restoreProgress.length > 0 + ? restoreProgress[restoreProgress.length - 1] + : "Restoring backup..."; // Auto-discover backups when tab is first opened useEffect(() => { if (!hasAutoDiscovered && !isLoading && backupsData) { // Only auto-discover if there are no backups yet - if (!backupsData.backups || backupsData.backups.length === 0) { - handleDiscoverBackups(); + if (!backupsData.backups?.length) { + void handleDiscoverBackups(); } - startTransition(() => { - setHasAutoDiscovered(true); - }); + setHasAutoDiscovered(true); } - }, [hasAutoDiscovered, isLoading, backupsData, handleDiscoverBackups]); + }, [hasAutoDiscovered, isLoading, backupsData]); + + const handleDiscoverBackups = () => { + discoverMutation.mutate(); + }; const handleRestoreClick = (backup: Backup, containerId: string) => { setSelectedBackup({ backup, containerId }); @@ -149,21 +179,15 @@ export function BackupsTab() { const handleRestoreConfirm = () => { if (!selectedBackup) return; - - // Ensure server_id is available - if (!selectedBackup.backup.server_id) { - setRestoreError('Server ID is required for restore operation'); - return; - } - + setRestoreConfirmOpen(false); setRestoreError(null); setRestoreSuccess(false); - + restoreMutation.mutate({ backupId: selectedBackup.backup.id, containerId: selectedBackup.containerId, - serverId: selectedBackup.backup.server_id, + serverId: selectedBackup.backup.server_id ?? 0, }); }; @@ -178,39 +202,41 @@ export function BackupsTab() { }; const formatFileSize = (bytes: bigint | null): string => { - if (!bytes) return 'Unknown size'; + if (!bytes) return "Unknown size"; const b = Number(bytes); - if (b === 0) return '0 B'; + if (b === 0) return "0 B"; const k = 1024; - const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + const sizes = ["B", "KB", "MB", "GB", "TB"]; const i = Math.floor(Math.log(b) / Math.log(k)); return `${(b / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`; }; const formatDate = (date: Date | null): string => { - if (!date) return 'Unknown date'; + if (!date) return "Unknown date"; return new Date(date).toLocaleString(); }; const getStorageTypeIcon = (type: string) => { switch (type) { - case 'pbs': + case "pbs": return ; - case 'local': + case "local": return ; default: return ; } }; - const getStorageTypeBadgeVariant = (type: string): 'default' | 'secondary' | 'outline' => { + const getStorageTypeBadgeVariant = ( + type: string, + ): "default" | "secondary" | "outline" => { switch (type) { - case 'pbs': - return 'default'; - case 'local': - return 'secondary'; + case "pbs": + return "default"; + case "local": + return "secondary"; default: - return 'outline'; + return "outline"; } }; @@ -222,8 +248,8 @@ export function BackupsTab() { {/* Header with refresh button */}
-

Backups

-

+

Backups

+

Discovered backups grouped by container ID

@@ -232,31 +258,38 @@ export function BackupsTab() { disabled={isDiscovering} className="flex items-center gap-2" > - - {isDiscovering ? 'Discovering...' : 'Discover Backups'} + + {isDiscovering ? "Discovering..." : "Discover Backups"}
{/* Loading state */} {(isLoading || isDiscovering) && backups.length === 0 && ( -
- +
+

- {isDiscovering ? 'Discovering backups...' : 'Loading backups...'} + {isDiscovering ? "Discovering backups..." : "Loading backups..."}

)} {/* Empty state */} {!isLoading && !isDiscovering && backups.length === 0 && ( -
- -

No backups found

+
+ +

+ No backups found +

- Click "Discover Backups" to scan for backups on your servers. + Click "Discover Backups" to scan for backups on your + servers.

@@ -265,40 +298,42 @@ export function BackupsTab() { {/* Backups list */} {!isLoading && backups.length > 0 && (
- {backups.map((container) => { + {backups.map((container: ContainerBackups) => { const isExpanded = expandedContainers.has(container.container_id); const backupCount = container.backups.length; return (
{/* Container header - collapsible */} - + handleRestoreClick(backup, container.container_id)} + onClick={() => + handleRestoreClick( + backup, + container.container_id, + ) + } disabled={restoreMutation.isPending} className="text-muted-foreground hover:text-foreground hover:bg-muted/20 focus:bg-muted/20" > @@ -392,9 +434,9 @@ export function BackupsTab() { {/* Error state */} {backupsData && !backupsData.success && ( -
+

- Error loading backups: {backupsData.error ?? 'Unknown error'} + Error loading backups: {backupsData.error ?? "Unknown error"}

)} @@ -418,10 +460,11 @@ export function BackupsTab() { )} {/* Restore Progress Modal */} - {(restoreMutation.isPending || (restoreSuccess && restoreProgress.length > 0)) && ( + {(restoreMutation.isPending || + (restoreSuccess && restoreProgress.length > 0)) && ( -
+
+
- - Restore Completed Successfully + + + Restore Completed Successfully +
-

+

The container has been restored from backup.

@@ -460,11 +505,11 @@ export function BackupsTab() { {/* Restore Error */} {restoreError && ( -
-
+
+
- - Restore Failed + + Restore Failed
-

- {restoreError} -

+

{restoreError}

{restoreProgress.length > 0 && ( -
+
{restoreProgress.map((message, index) => ( -

+

{message}

))} @@ -506,4 +549,3 @@ export function BackupsTab() {
); } - diff --git a/src/app/_components/DownloadedScriptsTab.tsx b/src/app/_components/DownloadedScriptsTab.tsx index 5e0c50d..bb158c3 100644 --- a/src/app/_components/DownloadedScriptsTab.tsx +++ b/src/app/_components/DownloadedScriptsTab.tsx @@ -1,41 +1,53 @@ -'use client'; +"use client"; -import React, { useState, useRef, useEffect } from 'react'; -import { api } from '~/trpc/react'; -import { ScriptCard } from './ScriptCard'; -import { ScriptCardList } from './ScriptCardList'; -import { ScriptDetailModal } from './ScriptDetailModal'; -import { CategorySidebar } from './CategorySidebar'; -import { FilterBar, type FilterState } from './FilterBar'; -import { ViewToggle } from './ViewToggle'; -import { Button } from './ui/button'; -import type { ScriptCard as ScriptCardType } from '~/types/script'; -import { getDefaultFilters, mergeFiltersWithDefaults } from './filterUtils'; +import React, { useState, useRef, useEffect } from "react"; +import { api } from "~/trpc/react"; +import { ScriptCard } from "./ScriptCard"; +import { ScriptCardList } from "./ScriptCardList"; +import { ScriptDetailModal } from "./ScriptDetailModal"; +import { CategorySidebar } from "./CategorySidebar"; +import { FilterBar, type FilterState } from "./FilterBar"; +import { ViewToggle } from "./ViewToggle"; +import { Button } from "./ui/button"; +import type { ScriptCard as ScriptCardType } from "~/types/script"; +import type { Server } from "~/types/server"; +import { getDefaultFilters, mergeFiltersWithDefaults } from "./filterUtils"; interface DownloadedScriptsTabProps { onInstallScript?: ( scriptPath: string, scriptName: string, mode?: "local" | "ssh", - server?: any, + server?: Server, ) => void; } -export function DownloadedScriptsTab({ onInstallScript }: DownloadedScriptsTabProps) { +export function DownloadedScriptsTab({ + onInstallScript, +}: DownloadedScriptsTabProps) { const [selectedSlug, setSelectedSlug] = useState(null); const [isModalOpen, setIsModalOpen] = useState(false); const [selectedCategory, setSelectedCategory] = useState(null); - const [viewMode, setViewMode] = useState<'card' | 'list'>('card'); + const [viewMode, setViewMode] = useState<"card" | "list">("card"); const [filters, setFilters] = useState(getDefaultFilters()); const [saveFiltersEnabled, setSaveFiltersEnabled] = useState(false); const [isLoadingFilters, setIsLoadingFilters] = useState(true); const gridRef = useRef(null); - const { data: scriptCardsData, isLoading: githubLoading, error: githubError, refetch } = api.scripts.getScriptCardsWithCategories.useQuery(); - const { data: localScriptsData, isLoading: localLoading, error: localError } = api.scripts.getAllDownloadedScripts.useQuery(); + const { + data: scriptCardsData, + isLoading: githubLoading, + error: githubError, + refetch, + } = api.scripts.getScriptCardsWithCategories.useQuery(); + const { + data: localScriptsData, + isLoading: localLoading, + error: localError, + } = api.scripts.getAllDownloadedScripts.useQuery(); const { data: scriptData } = api.scripts.getScriptBySlug.useQuery( - { slug: selectedSlug ?? '' }, - { enabled: !!selectedSlug } + { slug: selectedSlug ?? "" }, + { enabled: !!selectedSlug }, ); // Load SAVE_FILTER setting, saved filters, and view mode on component mount @@ -43,7 +55,7 @@ export function DownloadedScriptsTab({ onInstallScript }: DownloadedScriptsTabPr const loadSettings = async () => { try { // Load SAVE_FILTER setting - const saveFilterResponse = await fetch('/api/settings/save-filter'); + const saveFilterResponse = await fetch("/api/settings/save-filter"); let saveFilterEnabled = false; if (saveFilterResponse.ok) { const saveFilterData = await saveFilterResponse.json(); @@ -53,9 +65,11 @@ export function DownloadedScriptsTab({ onInstallScript }: DownloadedScriptsTabPr // Load saved filters if SAVE_FILTER is enabled if (saveFilterEnabled) { - const filtersResponse = await fetch('/api/settings/filters'); + const filtersResponse = await fetch("/api/settings/filters"); if (filtersResponse.ok) { - const filtersData = await filtersResponse.json() as { filters?: Partial }; + const filtersData = (await filtersResponse.json()) as { + filters?: Partial; + }; if (filtersData.filters) { setFilters(mergeFiltersWithDefaults(filtersData.filters)); } @@ -63,16 +77,20 @@ export function DownloadedScriptsTab({ onInstallScript }: DownloadedScriptsTabPr } // Load view mode - const viewModeResponse = await fetch('/api/settings/view-mode'); + const viewModeResponse = await fetch("/api/settings/view-mode"); if (viewModeResponse.ok) { const viewModeData = await viewModeResponse.json(); const viewMode = viewModeData.viewMode; - if (viewMode && typeof viewMode === 'string' && (viewMode === 'card' || viewMode === 'list')) { + if ( + viewMode && + typeof viewMode === "string" && + (viewMode === "card" || viewMode === "list") + ) { setViewMode(viewMode); } } } catch (error) { - console.error('Error loading settings:', error); + console.error("Error loading settings:", error); } finally { setIsLoadingFilters(false); } @@ -87,15 +105,15 @@ export function DownloadedScriptsTab({ onInstallScript }: DownloadedScriptsTabPr const saveFilters = async () => { try { - await fetch('/api/settings/filters', { - method: 'POST', + await fetch("/api/settings/filters", { + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify({ filters }), }); } catch (error) { - console.error('Error saving filters:', error); + console.error("Error saving filters:", error); } }; @@ -110,15 +128,15 @@ export function DownloadedScriptsTab({ onInstallScript }: DownloadedScriptsTabPr const saveViewMode = async () => { try { - await fetch('/api/settings/view-mode', { - method: 'POST', + await fetch("/api/settings/view-mode", { + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify({ viewMode }), }); } catch (error) { - console.error('Error saving view mode:', error); + console.error("Error saving view mode:", error); } }; @@ -129,31 +147,32 @@ export function DownloadedScriptsTab({ onInstallScript }: DownloadedScriptsTabPr // Extract categories from metadata const categories = React.useMemo((): string[] => { - if (!scriptCardsData?.success || !scriptCardsData.metadata?.categories) return []; - + if (!scriptCardsData?.success || !scriptCardsData.metadata?.categories) + return []; + return (scriptCardsData.metadata.categories as any[]) .filter((cat) => cat.id !== 0) // Exclude Miscellaneous for main list .sort((a, b) => a.sort_order - b.sort_order) .map((cat) => cat.name as string) - .filter((name): name is string => typeof name === 'string'); + .filter((name): name is string => typeof name === "string"); }, [scriptCardsData]); // Get GitHub scripts with download status (deduplicated) const combinedScripts = React.useMemo((): ScriptCardType[] => { if (!scriptCardsData?.success) return []; - + // Use Map to deduplicate by slug/name const scriptMap = new Map(); - - scriptCardsData.cards?.forEach(script => { + + scriptCardsData.cards?.forEach((script: ScriptCardType) => { if (script?.name && script?.slug) { // Use slug as unique identifier, only keep first occurrence if (!scriptMap.has(script.slug)) { scriptMap.set(script.slug, { ...script, - source: 'github' as const, + source: "github" as const, isDownloaded: false, // Will be updated by status check - isUpToDate: false, // Will be updated by status check + isUpToDate: false, // Will be updated by status check }); } } @@ -165,68 +184,77 @@ export function DownloadedScriptsTab({ onInstallScript }: DownloadedScriptsTabPr // Update scripts with download status and filter to only downloaded scripts const downloadedScripts = React.useMemo((): ScriptCardType[] => { // Helper to normalize identifiers so underscores vs hyphens don't break matches - const normalizeId = (s?: string): string => (s ?? '') - .toLowerCase() - .replace(/\.(sh|bash|py|js|ts)$/g, '') - .replace(/[^a-z0-9]+/g, '-') - .replace(/^-+|-+$/g, ''); + const normalizeId = (s?: string): string => + (s ?? "") + .toLowerCase() + .replace(/\.(sh|bash|py|js|ts)$/g, "") + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, ""); return combinedScripts - .map(script => { + .map((script) => { if (!script?.name) { return script; // Return as-is if invalid } - + // Check if there's a corresponding local script - const hasLocalVersion = localScriptsData?.scripts?.some(local => { - if (!local?.name) return false; - - // Primary: Exact slug-to-slug matching (most reliable, prevents false positives) - if (local.slug && script.slug) { - if (local.slug.toLowerCase() === script.slug.toLowerCase()) { - return true; + const hasLocalVersion = + localScriptsData?.scripts?.some((local) => { + if (!local?.name) return false; + + // Primary: Exact slug-to-slug matching (most reliable, prevents false positives) + if (local.slug && script.slug) { + if (local.slug.toLowerCase() === script.slug.toLowerCase()) { + return true; + } } - } - - // Secondary: Check install basenames (for edge cases where install script names differ from slugs) - // Only use normalized matching for install basenames, not for slug/name matching - const normalizedLocal = normalizeId(local.name); - const matchesInstallBasename = (script as any)?.install_basenames?.some((base: string) => normalizeId(base) === normalizedLocal) ?? false; - return matchesInstallBasename; - }) ?? false; - + + // Secondary: Check install basenames (for edge cases where install script names differ from slugs) + // Only use normalized matching for install basenames, not for slug/name matching + const normalizedLocal = normalizeId(local.name); + const matchesInstallBasename = + (script as any)?.install_basenames?.some( + (base: string) => normalizeId(base) === normalizedLocal, + ) ?? false; + return matchesInstallBasename; + }) ?? false; + return { ...script, isDownloaded: hasLocalVersion, }; }) - .filter(script => script.isDownloaded); // Only show downloaded scripts + .filter((script) => script.isDownloaded); // Only show downloaded scripts }, [combinedScripts, localScriptsData]); // Count scripts per category (using downloaded scripts only) const categoryCounts = React.useMemo((): Record => { if (!scriptCardsData?.success) return {}; - + const counts: Record = {}; - + // Initialize all categories with 0 categories.forEach((categoryName: string) => { counts[categoryName] = 0; }); - + // Count each unique downloaded script only once per category - downloadedScripts.forEach(script => { + downloadedScripts.forEach((script) => { if (script.categoryNames && script.slug) { const countedCategories = new Set(); script.categoryNames.forEach((categoryName: unknown) => { - if (typeof categoryName === 'string' && counts[categoryName] !== undefined && !countedCategories.has(categoryName)) { + if ( + typeof categoryName === "string" && + counts[categoryName] !== undefined && + !countedCategories.has(categoryName) + ) { countedCategories.add(categoryName); counts[categoryName]++; } }); } }); - + return counts; }, [categories, downloadedScripts, scriptCardsData?.success]); @@ -237,15 +265,15 @@ export function DownloadedScriptsTab({ onInstallScript }: DownloadedScriptsTabPr // Filter by search query if (filters.searchQuery?.trim()) { const query = filters.searchQuery.toLowerCase().trim(); - + if (query.length >= 1) { - scripts = scripts.filter(script => { - if (!script || typeof script !== 'object') { + scripts = scripts.filter((script) => { + if (!script || typeof script !== "object") { return false; } - const name = (script.name ?? '').toLowerCase(); - const slug = (script.slug ?? '').toLowerCase(); + const name = (script.name ?? "").toLowerCase(); + const slug = (script.slug ?? "").toLowerCase(); return name.includes(query) ?? slug.includes(query); }); @@ -254,9 +282,9 @@ export function DownloadedScriptsTab({ onInstallScript }: DownloadedScriptsTabPr // Filter by category using real category data from downloaded scripts if (selectedCategory) { - scripts = scripts.filter(script => { + scripts = scripts.filter((script) => { if (!script) return false; - + // Check if the downloaded script has categoryNames that include the selected category return script.categoryNames?.includes(selectedCategory) ?? false; }); @@ -264,7 +292,7 @@ export function DownloadedScriptsTab({ onInstallScript }: DownloadedScriptsTabPr // Filter by updateable status if (filters.showUpdatable !== null) { - scripts = scripts.filter(script => { + scripts = scripts.filter((script) => { if (!script) return false; const isUpdatable = script.updateable ?? false; return filters.showUpdatable ? isUpdatable : !isUpdatable; @@ -273,28 +301,30 @@ export function DownloadedScriptsTab({ onInstallScript }: DownloadedScriptsTabPr // Filter by script types if (filters.selectedTypes.length > 0) { - scripts = scripts.filter(script => { + scripts = scripts.filter((script) => { if (!script) return false; - const scriptType = (script.type ?? '').toLowerCase(); - + const scriptType = (script.type ?? "").toLowerCase(); + // Map non-standard types to standard categories - const mappedType = scriptType === 'turnkey' ? 'ct' : scriptType; - - return filters.selectedTypes.some(type => type.toLowerCase() === mappedType); + const mappedType = scriptType === "turnkey" ? "ct" : scriptType; + + return filters.selectedTypes.some( + (type) => type.toLowerCase() === mappedType, + ); }); } // Filter by repositories if (filters.selectedRepositories.length > 0) { - scripts = scripts.filter(script => { + scripts = scripts.filter((script) => { if (!script) return false; const repoUrl = script.repository_url; - + // If script has no repository_url, exclude it when filtering by repositories if (!repoUrl) { return false; } - + // Only include scripts from selected repositories return filters.selectedRepositories.includes(repoUrl); }); @@ -303,18 +333,18 @@ export function DownloadedScriptsTab({ onInstallScript }: DownloadedScriptsTabPr // Apply sorting scripts.sort((a, b) => { if (!a || !b) return 0; - + let compareValue = 0; - + switch (filters.sortBy) { - case 'name': - compareValue = (a.name ?? '').localeCompare(b.name ?? ''); + case "name": + compareValue = (a.name ?? "").localeCompare(b.name ?? ""); break; - case 'created': + case "created": // Get creation date from script metadata in JSON format (date_created: "YYYY-MM-DD") - const aCreated = a?.date_created ?? ''; - const bCreated = b?.date_created ?? ''; - + const aCreated = a?.date_created ?? ""; + const bCreated = b?.date_created ?? ""; + // If both have dates, compare them directly if (aCreated && bCreated) { // For dates: asc = oldest first (2020 before 2024), desc = newest first (2024 before 2020) @@ -327,15 +357,15 @@ export function DownloadedScriptsTab({ onInstallScript }: DownloadedScriptsTabPr compareValue = 1; } else { // Both have no dates, fallback to name comparison - compareValue = (a.name ?? '').localeCompare(b.name ?? ''); + compareValue = (a.name ?? "").localeCompare(b.name ?? ""); } break; default: - compareValue = (a.name ?? '').localeCompare(b.name ?? ''); + compareValue = (a.name ?? "").localeCompare(b.name ?? ""); } - + // Apply sort order - return filters.sortOrder === 'asc' ? compareValue : -compareValue; + return filters.sortOrder === "asc" ? compareValue : -compareValue; }); return scripts; @@ -343,8 +373,10 @@ export function DownloadedScriptsTab({ onInstallScript }: DownloadedScriptsTabPr // Calculate filter counts for FilterBar const filterCounts = React.useMemo(() => { - const updatableCount = downloadedScripts.filter(script => script?.updateable).length; - + const updatableCount = downloadedScripts.filter( + (script) => script?.updateable, + ).length; + return { installedCount: downloadedScripts.length, updatableCount }; }, [downloadedScripts]); @@ -362,13 +394,13 @@ export function DownloadedScriptsTab({ onInstallScript }: DownloadedScriptsTabPr useEffect(() => { if (selectedCategory && gridRef.current) { const timeoutId = setTimeout(() => { - gridRef.current?.scrollIntoView({ - behavior: 'smooth', - block: 'start', - inline: 'nearest' + gridRef.current?.scrollIntoView({ + behavior: "smooth", + block: "start", + inline: "nearest", }); }, 100); - + return () => clearTimeout(timeoutId); } }, [selectedCategory]); @@ -387,22 +419,38 @@ export function DownloadedScriptsTab({ onInstallScript }: DownloadedScriptsTabPr if (githubLoading || localLoading) { return (
-
- Loading downloaded scripts... +
+ + Loading downloaded scripts... +
); } if (githubError || localError) { return ( -
+
- - + + -

Failed to load downloaded scripts

-

- {githubError?.message ?? localError?.message ?? 'Unknown error occurred'} +

+ Failed to load downloaded scripts +

+

+ {githubError?.message ?? + localError?.message ?? + "Unknown error occurred"}

- ) : ( - viewMode === 'card' ? ( -
- {filteredScripts.map((script, index) => { + ) : viewMode === "card" ? ( +
+ {filteredScripts.map((script, index) => { // Add validation to ensure script has required properties - if (!script || typeof script !== 'object') { + if (!script || typeof script !== "object") { return null; } - + // Create a unique key by combining slug, name, and index to handle duplicates - const uniqueKey = `${script.slug ?? 'unknown'}-${script.name ?? 'unnamed'}-${index}`; - + const uniqueKey = `${script.slug ?? "unknown"}-${script.name ?? "unnamed"}-${index}`; + return ( ); })} -
- ) : ( -
- {filteredScripts.map((script, index) => { +
+ ) : ( +
+ {filteredScripts.map((script, index) => { // Add validation to ensure script has required properties - if (!script || typeof script !== 'object') { + if (!script || typeof script !== "object") { return null; } - + // Create a unique key by combining slug, name, and index to handle duplicates - const uniqueKey = `${script.slug ?? 'unknown'}-${script.name ?? 'unnamed'}-${index}`; - + const uniqueKey = `${script.slug ?? "unknown"}-${script.name ?? "unnamed"}-${index}`; + return ( ); })} -
- ) +
)} { try { @@ -98,29 +108,33 @@ export function FilterBar({ }; return ( -
+
{/* Loading State */} {isLoadingFilters && (
-
-
+
+
Loading saved filters...
)} - {/* Filter Header */} {!isLoadingFilters && (
-

Filter Scripts

+

+ Filter Scripts +

- + - - {/* Type Dropdown */} -
- - - {isTypeDropdownOpen && ( -
-
- {SCRIPT_TYPES.map((type) => { - const IconComponent = type.Icon; - return ( - - ); - })} -
-
- -
-
- )} -
- - {/* Repository Filter Buttons - Only show if more than one enabled repo */} - {enabledRepos.length > 1 && enabledRepos.map((repo) => { - const isSelected = filters.selectedRepositories.includes(repo.url); - return ( +
+ {/* Updateable Filter */} - ); - })} - {/* Sort By Dropdown */} -
- + {/* Type Dropdown */} +
+ - {isSortDropdownOpen && ( -
-
- - -
+ {isTypeDropdownOpen && ( +
+
+ {SCRIPT_TYPES.map((type) => { + const IconComponent = type.Icon; + return ( + + ); + })} +
+
+ +
+
+ )}
- )} -
- {/* Sort Order Button */} - -
+ {/* Repository Filter Buttons - Only show if more than one enabled repo */} + {enabledRepos.length > 1 && + enabledRepos.map((repo: { id: number; url: string }) => { + const repoUrl = String(repo.url); + const isSelected = + filters.selectedRepositories.includes(repoUrl); + return ( + + ); + })} - {/* Filter Summary and Clear All */} -
-
-
- {filteredCount === totalScripts ? ( - Showing all {totalScripts} scripts - ) : ( - - {filteredCount} of {totalScripts} scripts{" "} - {hasActiveFilters && ( - - (filtered) + {/* Sort By Dropdown */} +
+ + + {isSortDropdownOpen && ( +
+
+ + +
+
+ )} +
+ + {/* Sort Order Button */} + +
+ + {/* Filter Summary and Clear All */} +
+
+
+ {filteredCount === totalScripts ? ( + Showing all {totalScripts} scripts + ) : ( + + {filteredCount} of {totalScripts} scripts{" "} + {hasActiveFilters && ( + (filtered) + )} )} - +
+ + {/* Filter Persistence Status */} + {!isLoadingFilters && saveFiltersEnabled && ( +
+ + + + Filters are being saved automatically +
+ )} +
+ + {hasActiveFilters && ( + )}
- - {/* Filter Persistence Status */} - {!isLoadingFilters && saveFiltersEnabled && ( -
- - - - Filters are being saved automatically -
- )} -
- - {hasActiveFilters && ( - - )} -
)} diff --git a/src/app/_components/GeneralSettingsModal.tsx b/src/app/_components/GeneralSettingsModal.tsx index 50476f9..023325b 100644 --- a/src/app/_components/GeneralSettingsModal.tsx +++ b/src/app/_components/GeneralSettingsModal.tsx @@ -1,40 +1,66 @@ -'use client'; +"use client"; -import { useState, useEffect } from 'react'; -import { Button } from './ui/button'; -import { Input } from './ui/input'; -import { Toggle } from './ui/toggle'; -import { ContextualHelpIcon } from './ContextualHelpIcon'; -import { useTheme } from './ThemeProvider'; -import { useRegisterModal } from './modal/ModalStackProvider'; -import { api } from '~/trpc/react'; -import { useAuth } from './AuthProvider'; -import { Trash2, ExternalLink } from 'lucide-react'; -import type { FilterState } from './FilterBar'; +import { useState, useEffect } from "react"; +import { Button } from "./ui/button"; +import { Input } from "./ui/input"; +import { Toggle } from "./ui/toggle"; +import { ContextualHelpIcon } from "./ContextualHelpIcon"; +import { useTheme } from "./ThemeProvider"; +import { useRegisterModal } from "./modal/ModalStackProvider"; +import { api } from "~/trpc/react"; +import { useAuth } from "./AuthProvider"; +import { Trash2, ExternalLink } from "lucide-react"; + +interface AutoSyncSettings { + autoSyncEnabled: boolean; + syncIntervalType: "predefined" | "custom"; + syncIntervalPredefined: string; + syncIntervalCron: string; + autoDownloadNew: boolean; + autoUpdateExisting: boolean; + notificationEnabled: boolean; + appriseUrls: string[]; + lastAutoSync: string; + lastAutoSyncError: string | null; + lastAutoSyncErrorTime: string | null; +} interface GeneralSettingsModalProps { isOpen: boolean; onClose: () => void; } -export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalProps) { - useRegisterModal(isOpen, { id: 'general-settings-modal', allowEscape: true, onClose }); +export function GeneralSettingsModal({ + isOpen, + onClose, +}: GeneralSettingsModalProps) { + useRegisterModal(isOpen, { + id: "general-settings-modal", + allowEscape: true, + onClose, + }); const { theme, setTheme } = useTheme(); const { isAuthenticated, expirationTime, checkAuth } = useAuth(); - const [activeTab, setActiveTab] = useState<'general' | 'github' | 'auth' | 'auto-sync' | 'repositories'>('general'); - const [sessionExpirationDisplay, setSessionExpirationDisplay] = useState(''); - const [githubToken, setGithubToken] = useState(''); + const [activeTab, setActiveTab] = useState< + "general" | "github" | "auth" | "auto-sync" | "repositories" + >("general"); + const [sessionExpirationDisplay, setSessionExpirationDisplay] = + useState(""); + const [githubToken, setGithubToken] = useState(""); const [saveFilter, setSaveFilter] = useState(false); - const [savedFilters, setSavedFilters] = useState | null>(null); + const [savedFilters, setSavedFilters] = useState(null); const [colorCodingEnabled, setColorCodingEnabled] = useState(false); const [isLoading, setIsLoading] = useState(false); const [isSaving, setIsSaving] = useState(false); - const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); - + const [message, setMessage] = useState<{ + type: "success" | "error"; + text: string; + } | null>(null); + // Auth state - const [authUsername, setAuthUsername] = useState(''); - const [authPassword, setAuthPassword] = useState(''); - const [authConfirmPassword, setAuthConfirmPassword] = useState(''); + const [authUsername, setAuthUsername] = useState(""); + const [authPassword, setAuthPassword] = useState(""); + const [authConfirmPassword, setAuthConfirmPassword] = useState(""); const [authEnabled, setAuthEnabled] = useState(false); const [authHasCredentials, setAuthHasCredentials] = useState(false); const [authSetupCompleted, setAuthSetupCompleted] = useState(false); @@ -43,29 +69,36 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr // Auto-sync state const [autoSyncEnabled, setAutoSyncEnabled] = useState(false); - const [syncIntervalType, setSyncIntervalType] = useState<'predefined' | 'custom'>('predefined'); - const [syncIntervalPredefined, setSyncIntervalPredefined] = useState('1hour'); - const [syncIntervalCron, setSyncIntervalCron] = useState(''); + const [syncIntervalType, setSyncIntervalType] = useState< + "predefined" | "custom" + >("predefined"); + const [syncIntervalPredefined, setSyncIntervalPredefined] = useState("1hour"); + const [syncIntervalCron, setSyncIntervalCron] = useState(""); const [autoDownloadNew, setAutoDownloadNew] = useState(false); const [autoUpdateExisting, setAutoUpdateExisting] = useState(false); const [notificationEnabled, setNotificationEnabled] = useState(false); const [appriseUrls, setAppriseUrls] = useState([]); - const [appriseUrlsText, setAppriseUrlsText] = useState(''); - const [lastAutoSync, setLastAutoSync] = useState(''); - const [lastAutoSyncError, setLastAutoSyncError] = useState(null); - const [lastAutoSyncErrorTime, setLastAutoSyncErrorTime] = useState(null); - const [cronValidationError, setCronValidationError] = useState(''); + const [appriseUrlsText, setAppriseUrlsText] = useState(""); + const [lastAutoSync, setLastAutoSync] = useState(""); + const [lastAutoSyncError, setLastAutoSyncError] = useState( + null, + ); + const [lastAutoSyncErrorTime, setLastAutoSyncErrorTime] = useState< + string | null + >(null); + const [cronValidationError, setCronValidationError] = useState(""); // Repository management state - const [newRepoUrl, setNewRepoUrl] = useState(''); + const [newRepoUrl, setNewRepoUrl] = useState(""); const [newRepoEnabled, setNewRepoEnabled] = useState(true); const [isAddingRepo, setIsAddingRepo] = useState(false); const [deletingRepoId, setDeletingRepoId] = useState(null); // Repository queries and mutations - const { data: repositoriesData, refetch: refetchRepositories } = api.repositories.getAll.useQuery(undefined, { - enabled: isOpen && activeTab === 'repositories' - }); + const { data: repositoriesData, refetch: refetchRepositories } = + api.repositories.getAll.useQuery(undefined, { + enabled: isOpen && activeTab === "repositories", + }); const createRepoMutation = api.repositories.create.useMutation(); const updateRepoMutation = api.repositories.update.useMutation(); const deleteRepoMutation = api.repositories.delete.useMutation(); @@ -85,13 +118,13 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr const loadGithubToken = async () => { setIsLoading(true); try { - const response = await fetch('/api/settings/github-token'); + const response = await fetch("/api/settings/github-token"); if (response.ok) { const data = await response.json(); - setGithubToken((data.token as string) ?? ''); + setGithubToken((data.token as string) ?? ""); } } catch (error) { - console.error('Error loading GitHub token:', error); + console.error("Error loading GitHub token:", error); } finally { setIsLoading(false); } @@ -99,94 +132,106 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr const loadSaveFilter = async () => { try { - const response = await fetch('/api/settings/save-filter'); + const response = await fetch("/api/settings/save-filter"); if (response.ok) { const data = await response.json(); setSaveFilter((data.enabled as boolean) ?? false); } } catch (error) { - console.error('Error loading save filter setting:', error); + console.error("Error loading save filter setting:", error); } }; const saveSaveFilter = async (enabled: boolean) => { try { - const response = await fetch('/api/settings/save-filter', { - method: 'POST', + const response = await fetch("/api/settings/save-filter", { + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify({ enabled }), }); if (response.ok) { setSaveFilter(enabled); - setMessage({ type: 'success', text: 'Save filter setting updated!' }); - + setMessage({ type: "success", text: "Save filter setting updated!" }); + // If disabling save filters, clear saved filters if (!enabled) { await clearSavedFilters(); } } else { const errorData = await response.json(); - setMessage({ type: 'error', text: errorData.error ?? 'Failed to save setting' }); + setMessage({ + type: "error", + text: errorData.error ?? "Failed to save setting", + }); } } catch { - setMessage({ type: 'error', text: 'Failed to save setting' }); + setMessage({ type: "error", text: "Failed to save setting" }); } }; const loadSavedFilters = async () => { try { - const response = await fetch('/api/settings/filters'); + const response = await fetch("/api/settings/filters"); if (response.ok) { - const data = await response.json() as { filters?: Partial }; - setSavedFilters(data.filters ?? null); + const data = await response.json(); + setSavedFilters(data.filters); } } catch (error) { - console.error('Error loading saved filters:', error); + console.error("Error loading saved filters:", error); } }; const clearSavedFilters = async () => { try { - const response = await fetch('/api/settings/filters', { - method: 'DELETE', + const response = await fetch("/api/settings/filters", { + method: "DELETE", }); if (response.ok) { setSavedFilters(null); - setMessage({ type: 'success', text: 'Saved filters cleared!' }); + setMessage({ type: "success", text: "Saved filters cleared!" }); } else { const errorData = await response.json(); - setMessage({ type: 'error', text: errorData.error ?? 'Failed to clear filters' }); + setMessage({ + type: "error", + text: errorData.error ?? "Failed to clear filters", + }); } } catch { - setMessage({ type: 'error', text: 'Failed to clear filters' }); + setMessage({ type: "error", text: "Failed to clear filters" }); } }; const saveGithubToken = async () => { setIsSaving(true); setMessage(null); - + try { - const response = await fetch('/api/settings/github-token', { - method: 'POST', + const response = await fetch("/api/settings/github-token", { + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify({ token: githubToken }), }); if (response.ok) { - setMessage({ type: 'success', text: 'GitHub token saved successfully!' }); + setMessage({ + type: "success", + text: "GitHub token saved successfully!", + }); } else { const errorData = await response.json(); - setMessage({ type: 'error', text: (errorData.error as string | undefined) ?? 'Failed to save token' }); + setMessage({ + type: "error", + text: errorData.error ?? "Failed to save token", + }); } } catch { - setMessage({ type: 'error', text: 'Failed to save token' }); + setMessage({ type: "error", text: "Failed to save token" }); } finally { setIsSaving(false); } @@ -194,37 +239,46 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr const loadColorCodingSetting = async () => { try { - const response = await fetch('/api/settings/color-coding'); + const response = await fetch("/api/settings/color-coding"); if (response.ok) { const data = await response.json(); setColorCodingEnabled(Boolean(data.enabled)); } } catch (error) { - console.error('Error loading color coding setting:', error); + console.error("Error loading color coding setting:", error); } }; const saveColorCodingSetting = async (enabled: boolean) => { try { - const response = await fetch('/api/settings/color-coding', { - method: 'POST', + const response = await fetch("/api/settings/color-coding", { + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify({ enabled }), }); if (response.ok) { setColorCodingEnabled(enabled); - setMessage({ type: 'success', text: 'Color coding setting saved successfully' }); + setMessage({ + type: "success", + text: "Color coding setting saved successfully", + }); setTimeout(() => setMessage(null), 3000); } else { - setMessage({ type: 'error', text: 'Failed to save color coding setting' }); + setMessage({ + type: "error", + text: "Failed to save color coding setting", + }); setTimeout(() => setMessage(null), 3000); } } catch (error) { - console.error('Error saving color coding setting:', error); - setMessage({ type: 'error', text: 'Failed to save color coding setting' }); + console.error("Error saving color coding setting:", error); + setMessage({ + type: "error", + text: "Failed to save color coding setting", + }); setTimeout(() => setMessage(null), 3000); } }; @@ -232,17 +286,23 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr const loadAuthCredentials = async () => { setAuthLoading(true); try { - const response = await fetch('/api/settings/auth-credentials'); + const response = await fetch("/api/settings/auth-credentials"); if (response.ok) { - const data = await response.json() as { username: string; enabled: boolean; hasCredentials: boolean; setupCompleted: boolean; sessionDurationDays?: number }; - setAuthUsername(data.username ?? ''); + const data = (await response.json()) as { + username: string; + enabled: boolean; + hasCredentials: boolean; + setupCompleted: boolean; + sessionDurationDays?: number; + }; + setAuthUsername(data.username ?? ""); setAuthEnabled(data.enabled ?? false); setAuthHasCredentials(data.hasCredentials ?? false); setAuthSetupCompleted(data.setupCompleted ?? false); setSessionDurationDays(data.sessionDurationDays ?? 7); } } catch (error) { - console.error('Error loading auth credentials:', error); + console.error("Error loading auth credentials:", error); } finally { setAuthLoading(false); } @@ -250,35 +310,39 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr // Format expiration time display const formatExpirationTime = (expTime: number | null): string => { - if (!expTime) return 'No active session'; - + if (!expTime) return "No active session"; + const now = Date.now(); const timeUntilExpiration = expTime - now; - + if (timeUntilExpiration <= 0) { - return 'Session expired'; + return "Session expired"; } - + const days = Math.floor(timeUntilExpiration / (1000 * 60 * 60 * 24)); - const hours = Math.floor((timeUntilExpiration % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); - const minutes = Math.floor((timeUntilExpiration % (1000 * 60 * 60)) / (1000 * 60)); - + const hours = Math.floor( + (timeUntilExpiration % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60), + ); + const minutes = Math.floor( + (timeUntilExpiration % (1000 * 60 * 60)) / (1000 * 60), + ); + const parts: string[] = []; if (days > 0) { - parts.push(`${days} ${days === 1 ? 'day' : 'days'}`); + parts.push(`${days} ${days === 1 ? "day" : "days"}`); } if (hours > 0) { - parts.push(`${hours} ${hours === 1 ? 'hour' : 'hours'}`); + parts.push(`${hours} ${hours === 1 ? "hour" : "hours"}`); } if (minutes > 0 && days === 0) { - parts.push(`${minutes} ${minutes === 1 ? 'minute' : 'minutes'}`); + parts.push(`${minutes} ${minutes === 1 ? "minute" : "minutes"}`); } - + if (parts.length === 0) { - return 'Less than a minute'; + return "Less than a minute"; } - - return parts.join(', '); + + return parts.join(", "); }; // Update expiration display periodically @@ -287,58 +351,64 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr if (expirationTime) { setSessionExpirationDisplay(formatExpirationTime(expirationTime)); } else { - setSessionExpirationDisplay(''); + setSessionExpirationDisplay(""); } }; updateExpirationDisplay(); - + // Update every minute const interval = setInterval(updateExpirationDisplay, 60000); - + return () => clearInterval(interval); }, [expirationTime]); // Refresh auth when tab changes to auth tab useEffect(() => { - if (activeTab === 'auth' && isOpen) { + if (activeTab === "auth" && isOpen) { void checkAuth(); } }, [activeTab, isOpen, checkAuth]); const saveAuthCredentials = async () => { if (authPassword !== authConfirmPassword) { - setMessage({ type: 'error', text: 'Passwords do not match' }); + setMessage({ type: "error", text: "Passwords do not match" }); return; } setAuthLoading(true); setMessage(null); - + try { - const response = await fetch('/api/settings/auth-credentials', { - method: 'POST', + const response = await fetch("/api/settings/auth-credentials", { + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, - body: JSON.stringify({ - username: authUsername, + body: JSON.stringify({ + username: authUsername, password: authPassword, - enabled: authEnabled + enabled: authEnabled, }), }); if (response.ok) { - setMessage({ type: 'success', text: 'Authentication credentials updated successfully!' }); - setAuthPassword(''); - setAuthConfirmPassword(''); + setMessage({ + type: "success", + text: "Authentication credentials updated successfully!", + }); + setAuthPassword(""); + setAuthConfirmPassword(""); void loadAuthCredentials(); } else { const errorData = await response.json(); - setMessage({ type: 'error', text: errorData.error ?? 'Failed to save credentials' }); + setMessage({ + type: "error", + text: errorData.error ?? "Failed to save credentials", + }); } } catch { - setMessage({ type: 'error', text: 'Failed to save credentials' }); + setMessage({ type: "error", text: "Failed to save credentials" }); } finally { setAuthLoading(false); } @@ -346,33 +416,42 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr const saveSessionDuration = async (days: number) => { if (days < 1 || days > 365) { - setMessage({ type: 'error', text: 'Session duration must be between 1 and 365 days' }); + setMessage({ + type: "error", + text: "Session duration must be between 1 and 365 days", + }); return; } setAuthLoading(true); setMessage(null); - + try { - const response = await fetch('/api/settings/auth-credentials', { - method: 'PATCH', + const response = await fetch("/api/settings/auth-credentials", { + method: "PATCH", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify({ sessionDurationDays: days }), }); if (response.ok) { - setMessage({ type: 'success', text: `Session duration updated to ${days} days` }); + setMessage({ + type: "success", + text: `Session duration updated to ${days} days`, + }); setSessionDurationDays(days); setTimeout(() => setMessage(null), 3000); } else { const errorData = await response.json(); - setMessage({ type: 'error', text: errorData.error ?? 'Failed to update session duration' }); + setMessage({ + type: "error", + text: errorData.error ?? "Failed to update session duration", + }); setTimeout(() => setMessage(null), 3000); } } catch { - setMessage({ type: 'error', text: 'Failed to update session duration' }); + setMessage({ type: "error", text: "Failed to update session duration" }); setTimeout(() => setMessage(null), 3000); } finally { setAuthLoading(false); @@ -382,28 +461,31 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr const toggleAuthEnabled = async (enabled: boolean) => { setAuthLoading(true); setMessage(null); - + try { - const response = await fetch('/api/settings/auth-credentials', { - method: 'PATCH', + const response = await fetch("/api/settings/auth-credentials", { + method: "PATCH", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify({ enabled }), }); if (response.ok) { setAuthEnabled(enabled); - setMessage({ - type: 'success', - text: `Authentication ${enabled ? 'enabled' : 'disabled'} successfully!` + setMessage({ + type: "success", + text: `Authentication ${enabled ? "enabled" : "disabled"} successfully!`, }); } else { const errorData = await response.json(); - setMessage({ type: 'error', text: errorData.error ?? 'Failed to update auth status' }); + setMessage({ + type: "error", + text: errorData.error ?? "Failed to update auth status", + }); } } catch { - setMessage({ type: 'error', text: 'Failed to update auth status' }); + setMessage({ type: "error", text: "Failed to update auth status" }); } finally { setAuthLoading(false); } @@ -412,52 +494,40 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr // Auto-sync functions const loadAutoSyncSettings = async () => { try { - const response = await fetch('/api/settings/auto-sync'); + const response = await fetch("/api/settings/auto-sync"); if (response.ok) { - const data = await response.json() as { - settings?: { - autoSyncEnabled?: boolean; - syncIntervalType?: 'custom' | 'predefined'; - syncIntervalPredefined?: string; - syncIntervalCron?: string; - autoDownloadNew?: boolean; - autoUpdateExisting?: boolean; - notificationEnabled?: boolean; - appriseUrls?: string[]; - lastAutoSync?: string; - lastAutoSyncError?: string | null; - lastAutoSyncErrorTime?: string | null; - } + const data = (await response.json()) as { + settings: AutoSyncSettings | null; }; const settings = data.settings; if (settings) { setAutoSyncEnabled(settings.autoSyncEnabled ?? false); - setSyncIntervalType(settings.syncIntervalType ?? 'predefined'); - setSyncIntervalPredefined(settings.syncIntervalPredefined ?? '1hour'); - setSyncIntervalCron(settings.syncIntervalCron ?? ''); + setSyncIntervalType(settings.syncIntervalType ?? "predefined"); + setSyncIntervalPredefined(settings.syncIntervalPredefined ?? "1hour"); + setSyncIntervalCron(settings.syncIntervalCron ?? ""); setAutoDownloadNew(settings.autoDownloadNew ?? false); setAutoUpdateExisting(settings.autoUpdateExisting ?? false); setNotificationEnabled(settings.notificationEnabled ?? false); setAppriseUrls(settings.appriseUrls ?? []); - setAppriseUrlsText((settings.appriseUrls ?? []).join('\n')); - setLastAutoSync(settings.lastAutoSync ?? ''); + setAppriseUrlsText((settings.appriseUrls ?? []).join("\n")); + setLastAutoSync(settings.lastAutoSync ?? ""); setLastAutoSyncError(settings.lastAutoSyncError ?? null); setLastAutoSyncErrorTime(settings.lastAutoSyncErrorTime ?? null); } } } catch (error) { - console.error('Error loading auto-sync settings:', error); + console.error("Error loading auto-sync settings:", error); } }; const saveAutoSyncSettings = async () => { setIsSaving(true); setMessage(null); - + try { - const response = await fetch('/api/settings/auto-sync', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, + const response = await fetch("/api/settings/auto-sync", { + method: "POST", + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ autoSyncEnabled, syncIntervalType, @@ -466,20 +536,26 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr autoDownloadNew, autoUpdateExisting, notificationEnabled, - appriseUrls: appriseUrls - }) + appriseUrls: appriseUrls, + }), }); - + if (response.ok) { - setMessage({ type: 'success', text: 'Auto-sync settings saved successfully!' }); + setMessage({ + type: "success", + text: "Auto-sync settings saved successfully!", + }); setTimeout(() => setMessage(null), 3000); } else { const errorData = await response.json(); - setMessage({ type: 'error', text: errorData.error ?? 'Failed to save auto-sync settings' }); + setMessage({ + type: "error", + text: errorData.error ?? "Failed to save auto-sync settings", + }); } } catch (error) { - console.error('Error saving auto-sync settings:', error); - setMessage({ type: 'error', text: 'Failed to save auto-sync settings' }); + console.error("Error saving auto-sync settings:", error); + setMessage({ type: "error", text: "Failed to save auto-sync settings" }); } finally { setIsSaving(false); } @@ -487,26 +563,27 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr const handleAppriseUrlsChange = (text: string) => { setAppriseUrlsText(text); - const urls = text.split('\n').filter(url => url.trim() !== ''); + const urls = text.split("\n").filter((url) => url.trim() !== ""); setAppriseUrls(urls); }; const validateCronExpression = (cron: string) => { if (!cron.trim()) { - setCronValidationError(''); + setCronValidationError(""); return true; } - + // Basic cron validation - you might want to use a library like cron-validator - const cronRegex = /^(\*|([0-5]?\d)) (\*|([01]?\d|2[0-3])) (\*|([012]?\d|3[01])) (\*|([0]?\d|1[0-2])) (\*|([0-6]))$/; + const cronRegex = + /^(\*|([0-5]?\d)) (\*|([01]?\d|2[0-3])) (\*|([012]?\d|3[01])) (\*|([0]?\d|1[0-2])) (\*|([0-6]))$/; const isValid = cronRegex.test(cron); - + if (!isValid) { - setCronValidationError('Invalid cron expression format'); + setCronValidationError("Invalid cron expression format"); return false; } - - setCronValidationError(''); + + setCronValidationError(""); return true; }; @@ -517,56 +594,73 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr const testNotification = async () => { try { - const response = await fetch('/api/settings/auto-sync', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ testNotification: true }) + const response = await fetch("/api/settings/auto-sync", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ testNotification: true }), }); - + if (response.ok) { - setMessage({ type: 'success', text: 'Test notification sent successfully!' }); + setMessage({ + type: "success", + text: "Test notification sent successfully!", + }); } else { const errorData = await response.json(); - setMessage({ type: 'error', text: errorData.error ?? 'Failed to send test notification' }); + setMessage({ + type: "error", + text: errorData.error ?? "Failed to send test notification", + }); } } catch (error) { - console.error('Error sending test notification:', error); - setMessage({ type: 'error', text: 'Failed to send test notification' }); + console.error("Error sending test notification:", error); + setMessage({ type: "error", text: "Failed to send test notification" }); } }; const triggerManualSync = async () => { try { - const response = await fetch('/api/settings/auto-sync', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ triggerManualSync: true }) + const response = await fetch("/api/settings/auto-sync", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ triggerManualSync: true }), }); - + if (response.ok) { - setMessage({ type: 'success', text: 'Manual sync triggered successfully!' }); + setMessage({ + type: "success", + text: "Manual sync triggered successfully!", + }); // Reload settings to get updated last sync time await loadAutoSyncSettings(); } else { const errorData = await response.json(); - setMessage({ type: 'error', text: errorData.error ?? 'Failed to trigger manual sync' }); + setMessage({ + type: "error", + text: errorData.error ?? "Failed to trigger manual sync", + }); } } catch (error) { - console.error('Error triggering manual sync:', error); - setMessage({ type: 'error', text: 'Failed to trigger manual sync' }); + console.error("Error triggering manual sync:", error); + setMessage({ type: "error", text: "Failed to trigger manual sync" }); } }; if (!isOpen) return null; return ( -
-
+
+
{/* Header */} -
+
-

Settings

- +

+ Settings +

+
{/* Tabs */} -
-