From 40805f39f7b7ccc32c650e8bd8a20a88841231b3 Mon Sep 17 00:00:00 2001 From: CanbiZ <47820557+MickLesk@users.noreply.github.com> Date: Fri, 28 Nov 2025 11:44:26 +0100 Subject: [PATCH 01/52] Update dependencies and adjust TypeScript JSX setting Upgraded multiple dependencies and devDependencies in package.json to their latest versions for improved stability and features. Changed the TypeScript 'jsx' compiler option from 'react-jsx' to 'preserve' in tsconfig.json to better align with project requirements. --- package.json | 60 +++++++++++++++++++++++++-------------------------- tsconfig.json | 2 +- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/package.json b/package.json index 5626c1f..8f6ae88 100644 --- a/package.json +++ b/package.json @@ -22,71 +22,71 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@prisma/client": "^6.19.0", + "@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", "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", + "vitest": "^4.0.14" }, "ct3aMetadata": { "initVersion": "7.39.3" diff --git a/tsconfig.json b/tsconfig.json index ebfdd44..7b62e00 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,7 +22,7 @@ "noEmit": true, "module": "ESNext", "moduleResolution": "Bundler", - "jsx": "react-jsx", + "jsx": "preserve", "plugins": [ { "name": "next" From f467b9ad7b7338a7673763f5aba2b9b88458115d Mon Sep 17 00:00:00 2001 From: root Date: Fri, 28 Nov 2025 11:49:39 +0100 Subject: [PATCH 02/52] fix vulnerabilities --- package-lock.json | 325 ++++++++++++++++++++++++---------------------- package.json | 4 +- 2 files changed, 172 insertions(+), 157 deletions(-) diff --git a/package-lock.json b/package-lock.json index d2cd548..ca8ccfa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,71 +8,71 @@ "name": "pve-scripts-local", "version": "0.1.0", "dependencies": { - "@prisma/client": "^6.19.0", + "@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", "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", "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", + "vitest": "^4.0.14" }, "engines": { "node": ">=24.0.0" @@ -1838,15 +1838,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 +1854,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 +1870,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 +1886,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 +1902,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 +1918,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 +1934,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 +1950,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 +1966,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" ], @@ -2037,17 +2037,19 @@ "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, + "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,6 +2060,12 @@ } } }, + "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", @@ -3427,9 +3435,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 +3445,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 +4405,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 +4427,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 +4437,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 +4455,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 +4482,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 +4495,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 +4509,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 +4524,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 +4534,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 +4553,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": { @@ -6085,13 +6093,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", @@ -8484,9 +8492,9 @@ } }, "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" @@ -9566,12 +9574,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 +9592,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": { @@ -9831,6 +9839,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", @@ -10092,9 +10111,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, @@ -11414,9 +11433,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" @@ -12199,24 +12218,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 +12258,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 +12273,6 @@ "@opentelemetry/api": { "optional": true }, - "@types/debug": { - "optional": true - }, "@types/node": { "optional": true }, diff --git a/package.json b/package.json index 8f6ae88..a1d3d68 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,7 @@ "postcss": "^8.5.6", "prettier": "^3.7.1", "prettier-plugin-tailwindcss": "^0.7.1", - "prisma": "^7.0.1", + "prisma": "^6.19.0", "tailwindcss": "^4.1.17", "typescript": "^5.9.3", "typescript-eslint": "^8.48.0", @@ -98,4 +98,4 @@ "overrides": { "prismjs": "^1.30.0" } -} \ No newline at end of file +} From 9c759ba99bfac3e2971b041db8a2c7b586dc3eb7 Mon Sep 17 00:00:00 2001 From: CanbiZ <47820557+MickLesk@users.noreply.github.com> Date: Fri, 28 Nov 2025 11:53:04 +0100 Subject: [PATCH 03/52] fix: ESLint/TypeScript fixes - nullish coalescing, regexp-exec, optional-chain, unescaped-entities, unused-vars, type-safety --- src/app/_components/BackupWarningModal.tsx | 47 +- src/app/_components/BackupsTab.tsx | 244 +- src/app/_components/DownloadedScriptsTab.tsx | 377 +-- src/app/_components/FilterBar.tsx | 649 ++--- src/app/_components/GeneralSettingsModal.tsx | 1390 +++++++---- src/app/_components/HelpModal.tsx | 2220 ++++++++++++----- src/app/_components/InstalledScriptsTab.tsx | 2046 +++++++++------ src/app/_components/LoadingModal.tsx | 71 +- src/app/_components/PBSCredentialsModal.tsx | 217 +- src/app/_components/ScriptCard.tsx | 108 +- src/app/_components/ScriptCardList.tsx | 209 +- src/app/_components/ScriptDetailModal.tsx | 565 +++-- src/app/_components/ScriptVersionModal.tsx | 163 +- src/app/_components/ServerForm.tsx | 737 +++--- src/app/_components/ServerStoragesModal.tsx | 190 +- src/app/_components/TextViewer.tsx | 294 ++- .../_components/UpdateConfirmationModal.tsx | 174 +- src/app/page.tsx | 278 ++- 18 files changed, 6229 insertions(+), 3750 deletions(-) diff --git a/src/app/_components/BackupWarningModal.tsx b/src/app/_components/BackupWarningModal.tsx index d93f5c9..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 2e24b7b..4f579b1 100644 --- a/src/app/_components/BackupsTab.tsx +++ b/src/app/_components/BackupsTab.tsx @@ -1,18 +1,27 @@ -'use client'; +"use client"; -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 { 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; @@ -35,16 +44,25 @@ interface ContainerBackups { } 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(); @@ -52,26 +70,30 @@ 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) { 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')) { + 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')) { + } else if (lastLog.includes("Error:") || lastLog.includes("failed")) { setRestoreError(lastLog); setRestoreSuccess(false); } @@ -83,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); @@ -101,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); @@ -112,17 +141,18 @@ 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 currentProgressText = + restoreProgress.length > 0 + ? restoreProgress[restoreProgress.length - 1] + : "Restoring backup..."; // Auto-discover backups when tab is first opened useEffect(() => { @@ -149,11 +179,11 @@ export function BackupsTab() { const handleRestoreConfirm = () => { if (!selectedBackup) return; - + setRestoreConfirmOpen(false); setRestoreError(null); setRestoreSuccess(false); - + restoreMutation.mutate({ backupId: selectedBackup.backup.id, containerId: selectedBackup.containerId, @@ -172,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"; } }; @@ -216,8 +248,8 @@ export function BackupsTab() { {/* Header with refresh button */}
-

Backups

-

+

Backups

+

Discovered backups grouped by container ID

@@ -226,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.

@@ -266,33 +305,35 @@ export function BackupsTab() { 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" > @@ -386,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"}

)} @@ -412,7 +460,8 @@ 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.

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

- {restoreError} -

+

{restoreError}

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

+

{message}

))} @@ -500,4 +549,3 @@ export function BackupsTab() {
); } - diff --git a/src/app/_components/DownloadedScriptsTab.tsx b/src/app/_components/DownloadedScriptsTab.tsx index 279906c..4d4c1e3 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,7 +65,7 @@ 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(); if (filtersData.filters) { @@ -63,16 +75,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 +103,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 +126,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 +145,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) => { 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 +182,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 +263,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 +280,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 +290,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 +299,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 +331,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 +355,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 +371,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 +392,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 +417,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 { - const match = url.match(/github\.com\/([^\/]+)\/([^\/]+)/); + const match = /github\.com\/([^\/]+)\/([^\/]+)/.exec(url); if (match) { return `${match[1]}/${match[2]}`; } @@ -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) => { + const isSelected = filters.selectedRepositories.includes( + repo.url, + ); + 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 5e8110c..c592c8b 100644 --- a/src/app/_components/GeneralSettingsModal.tsx +++ b/src/app/_components/GeneralSettingsModal.tsx @@ -1,39 +1,52 @@ -'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 { 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 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); 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); @@ -42,29 +55,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(); @@ -84,13 +104,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); } @@ -98,94 +118,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(); 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 ?? '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); } @@ -193,37 +225,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); } }; @@ -231,17 +272,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); } @@ -249,35 +296,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 @@ -286,58 +337,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); } @@ -345,33 +402,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); @@ -381,28 +447,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); } @@ -411,38 +480,38 @@ 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: any }; + const data = (await response.json()) as { settings: any }; 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, @@ -451,20 +520,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); } @@ -472,26 +547,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; }; @@ -502,56 +578,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 */} -
-