Compare commits
1 Commits
fix/185
...
update-ver
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83228155bb |
34
.github/workflows/publish_release.yml
vendored
34
.github/workflows/publish_release.yml
vendored
@@ -45,39 +45,7 @@ jobs:
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git commit -m "chore: add VERSION $version" --allow-empty
|
||||
|
||||
|
||||
- name: Sync upstream JSONs
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
tmp_dir=$(mktemp -d)
|
||||
api_url="https://api.github.com/repos/community-scripts/ProxmoxVE/contents/frontend/public/json"
|
||||
# Fetch file list (no subfolders)
|
||||
curl -sSL -H "Authorization: token $GH_TOKEN" "$api_url" \
|
||||
| jq -r '.[] | select(.type=="file") | .name' > "$tmp_dir/files.txt"
|
||||
|
||||
# Download each file
|
||||
while IFS= read -r name; do
|
||||
curl -sSL -H "Authorization: token $GH_TOKEN" \
|
||||
"https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/frontend/public/json/$name" \
|
||||
-o "$tmp_dir/$name"
|
||||
done < "$tmp_dir/files.txt"
|
||||
|
||||
mkdir -p json
|
||||
rsync -a --delete "$tmp_dir/" json/
|
||||
|
||||
# Stage and amend commit to include JSON updates (and VERSION)
|
||||
git add json VERSION
|
||||
if ! git diff --cached --quiet; then
|
||||
git commit --amend --no-edit
|
||||
fi
|
||||
|
||||
|
||||
- name: Push changes
|
||||
run: |
|
||||
git push --force-with-lease --set-upstream origin "update-version-${{ steps.draft.outputs.tag_name }}"
|
||||
git push --set-upstream origin "$branch"
|
||||
|
||||
|
||||
- name: Create PR with GitHub CLI
|
||||
|
||||
153
package-lock.json
generated
153
package-lock.json
generated
@@ -27,7 +27,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lucide-react": "^0.546.0",
|
||||
"next": "^15.5.6",
|
||||
"next": "^15.5.5",
|
||||
"node-pty": "^1.0.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
@@ -51,18 +51,18 @@
|
||||
"@types/bcryptjs": "^3.0.0",
|
||||
"@types/better-sqlite3": "^7.6.8",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/node": "^24.8.1",
|
||||
"@types/node": "^24.8.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"@vitejs/plugin-react": "^5.0.2",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"@vitest/ui": "^3.2.4",
|
||||
"eslint": "^9.38.0",
|
||||
"eslint-config-next": "^15.5.6",
|
||||
"eslint": "^9.23.0",
|
||||
"eslint-config-next": "^15.5.5",
|
||||
"jsdom": "^27.0.0",
|
||||
"postcss": "^8.5.3",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-tailwindcss": "^0.7.1",
|
||||
"prettier-plugin-tailwindcss": "^0.7.0",
|
||||
"prisma": "^6.17.1",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"typescript": "^5.8.2",
|
||||
@@ -1116,13 +1116,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-array": {
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz",
|
||||
"integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==",
|
||||
"version": "0.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz",
|
||||
"integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@eslint/object-schema": "^2.1.7",
|
||||
"@eslint/object-schema": "^2.1.6",
|
||||
"debug": "^4.3.1",
|
||||
"minimatch": "^3.1.2"
|
||||
},
|
||||
@@ -1131,9 +1131,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-helpers": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.1.tgz",
|
||||
"integrity": "sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw==",
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz",
|
||||
"integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -1181,9 +1181,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "9.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz",
|
||||
"integrity": "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==",
|
||||
"version": "9.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz",
|
||||
"integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1194,9 +1194,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/object-schema": {
|
||||
"version": "2.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
|
||||
"integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
|
||||
"version": "2.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz",
|
||||
"integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
@@ -1840,15 +1840,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/env": {
|
||||
"version": "15.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.6.tgz",
|
||||
"integrity": "sha512-3qBGRW+sCGzgbpc5TS1a0p7eNxnOarGVQhZxfvTdnV0gFI61lX7QNtQ4V1TSREctXzYn5NetbUsLvyqwLFJM6Q==",
|
||||
"version": "15.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.5.tgz",
|
||||
"integrity": "sha512-2Zhvss36s/yL+YSxD5ZL5dz5pI6ki1OLxYlh6O77VJ68sBnlUrl5YqhBgCy7FkdMsp9RBeGFwpuDCdpJOqdKeQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@next/eslint-plugin-next": {
|
||||
"version": "15.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.6.tgz",
|
||||
"integrity": "sha512-YxDvsT2fwy1j5gMqk3ppXlsgDopHnkM4BoxSVASbvvgh5zgsK8lvWerDzPip8k3WVzsTZ1O7A7si1KNfN4OZfQ==",
|
||||
"version": "15.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.5.tgz",
|
||||
"integrity": "sha512-FMzm412l9oFB8zdRD+K6HQ1VzlS+sNNsdg0MfvTg0i8lfCyTgP/RFxiu/pGJqZ/IQnzn9xSiLkjOVI7Iv4nbdQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1856,9 +1856,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-arm64": {
|
||||
"version": "15.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.6.tgz",
|
||||
"integrity": "sha512-ES3nRz7N+L5Umz4KoGfZ4XX6gwHplwPhioVRc25+QNsDa7RtUF/z8wJcbuQ2Tffm5RZwuN2A063eapoJ1u4nPg==",
|
||||
"version": "15.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.5.tgz",
|
||||
"integrity": "sha512-lYExGHuFIHeOxf40mRLWoA84iY2sLELB23BV5FIDHhdJkN1LpRTPc1MDOawgTo5ifbM5dvAwnGuHyNm60G1+jw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1872,9 +1872,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-x64": {
|
||||
"version": "15.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.6.tgz",
|
||||
"integrity": "sha512-JIGcytAyk9LQp2/nuVZPAtj8uaJ/zZhsKOASTjxDug0SPU9LAM3wy6nPU735M1OqacR4U20LHVF5v5Wnl9ptTA==",
|
||||
"version": "15.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.5.tgz",
|
||||
"integrity": "sha512-cacs/WQqa96IhqUm+7CY+z/0j9sW6X80KE07v3IAJuv+z0UNvJtKSlT/T1w1SpaQRa9l0wCYYZlRZUhUOvEVmg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1888,9 +1888,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||
"version": "15.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.6.tgz",
|
||||
"integrity": "sha512-qvz4SVKQ0P3/Im9zcS2RmfFL/UCQnsJKJwQSkissbngnB/12c6bZTCB0gHTexz1s6d/mD0+egPKXAIRFVS7hQg==",
|
||||
"version": "15.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.5.tgz",
|
||||
"integrity": "sha512-tLd90SvkRFik6LSfuYjcJEmwqcNEnVYVOyKTacSazya/SLlSwy/VYKsDE4GIzOBd+h3gW+FXqShc2XBavccHCg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1904,9 +1904,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-musl": {
|
||||
"version": "15.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.6.tgz",
|
||||
"integrity": "sha512-FsbGVw3SJz1hZlvnWD+T6GFgV9/NYDeLTNQB2MXoPN5u9VA9OEDy6fJEfePfsUKAhJufFbZLgp0cPxMuV6SV0w==",
|
||||
"version": "15.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.5.tgz",
|
||||
"integrity": "sha512-ekV76G2R/l3nkvylkfy9jBSYHeB4QcJ7LdDseT6INnn1p51bmDS1eGoSoq+RxfQ7B1wt+Qa0pIl5aqcx0GLpbw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1920,9 +1920,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-gnu": {
|
||||
"version": "15.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.6.tgz",
|
||||
"integrity": "sha512-3QnHGFWlnvAgyxFxt2Ny8PTpXtQD7kVEeaFat5oPAHHI192WKYB+VIKZijtHLGdBBvc16tiAkPTDmQNOQ0dyrA==",
|
||||
"version": "15.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.5.tgz",
|
||||
"integrity": "sha512-tI+sBu+3FmWtqlqD4xKJcj3KJtqbniLombKTE7/UWyyoHmOyAo3aZ7QcEHIOgInXOG1nt0rwh0KGmNbvSB0Djg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1936,9 +1936,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-musl": {
|
||||
"version": "15.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.6.tgz",
|
||||
"integrity": "sha512-OsGX148sL+TqMK9YFaPFPoIaJKbFJJxFzkXZljIgA9hjMjdruKht6xDCEv1HLtlLNfkx3c5w2GLKhj7veBQizQ==",
|
||||
"version": "15.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.5.tgz",
|
||||
"integrity": "sha512-kDRh+epN/ulroNJLr+toDjN+/JClY5L+OAWjOrrKCI0qcKvTw9GBx7CU/rdA2bgi4WpZN3l0rf/3+b8rduEwrQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1952,9 +1952,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||
"version": "15.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.6.tgz",
|
||||
"integrity": "sha512-ONOMrqWxdzXDJNh2n60H6gGyKed42Ieu6UTVPZteXpuKbLZTH4G4eBMsr5qWgOBA+s7F+uB4OJbZnrkEDnZ5Fg==",
|
||||
"version": "15.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.5.tgz",
|
||||
"integrity": "sha512-GDgdNPFFqiKjTrmfw01sMMRWhVN5wOCmFzPloxa7ksDfX6TZt62tAK986f0ZYqWpvDFqeBCLAzmgTURvtQBdgw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1968,9 +1968,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-x64-msvc": {
|
||||
"version": "15.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.6.tgz",
|
||||
"integrity": "sha512-pxK4VIjFRx1MY92UycLOOw7dTdvccWsNETQ0kDHkBlcFH1GrTLUjSiHU1ohrznnux6TqRHgv5oflhfIWZwVROQ==",
|
||||
"version": "15.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.5.tgz",
|
||||
"integrity": "sha512-5kE3oRJxc7M8RmcTANP8RGoJkaYlwIiDD92gSwCjJY0+j8w8Sl1lvxgQ3bxfHY2KkHFai9tpy/Qx1saWV8eaJQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -3749,9 +3749,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.8.1.tgz",
|
||||
"integrity": "sha512-alv65KGRadQVfVcG69MuB4IzdYVpRwMG/mq8KWOaoOdyY617P5ivaDiMCGOFDWD2sAn5Q0mR3mRtUOgm99hL9Q==",
|
||||
"version": "24.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.8.0.tgz",
|
||||
"integrity": "sha512-5x08bUtU8hfboMTrJ7mEO4CpepS9yBwAqcL52y86SWNmbPX8LVbNs3EP4cNrIZgdjk2NAlP2ahNihozpoZIxSg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.14.0"
|
||||
@@ -6080,24 +6080,25 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "9.38.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz",
|
||||
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
|
||||
"version": "9.37.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz",
|
||||
"integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
"@eslint/config-array": "^0.21.1",
|
||||
"@eslint/config-helpers": "^0.4.1",
|
||||
"@eslint/config-array": "^0.21.0",
|
||||
"@eslint/config-helpers": "^0.4.0",
|
||||
"@eslint/core": "^0.16.0",
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@eslint/js": "9.38.0",
|
||||
"@eslint/js": "9.37.0",
|
||||
"@eslint/plugin-kit": "^0.4.0",
|
||||
"@humanfs/node": "^0.16.6",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
"@humanwhocodes/retry": "^0.4.2",
|
||||
"@types/estree": "^1.0.6",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"ajv": "^6.12.4",
|
||||
"chalk": "^4.0.0",
|
||||
"cross-spawn": "^7.0.6",
|
||||
@@ -6140,13 +6141,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-config-next": {
|
||||
"version": "15.5.6",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.5.6.tgz",
|
||||
"integrity": "sha512-cGr3VQlPsZBEv8rtYp4BpG1KNXDqGvPo9VC1iaCgIA11OfziC/vczng+TnAS3WpRIR3Q5ye/6yl+CRUuZ1fPGg==",
|
||||
"version": "15.5.5",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.5.5.tgz",
|
||||
"integrity": "sha512-f8lRSSelp6cqrYjxEMjJ5En3WV913gTu/w9goYShnIujwDSQlKt4x9MwSDiduE9R5mmFETK44+qlQDxeSA0rUA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@next/eslint-plugin-next": "15.5.6",
|
||||
"@next/eslint-plugin-next": "15.5.5",
|
||||
"@rushstack/eslint-patch": "^1.10.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0",
|
||||
"@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0",
|
||||
@@ -9633,12 +9634,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/next": {
|
||||
"version": "15.5.6",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-15.5.6.tgz",
|
||||
"integrity": "sha512-zTxsnI3LQo3c9HSdSf91O1jMNsEzIXDShXd4wVdg9y5shwLqBXi4ZtUUJyB86KGVSJLZx0PFONvO54aheGX8QQ==",
|
||||
"version": "15.5.5",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-15.5.5.tgz",
|
||||
"integrity": "sha512-OQVdBPtpBfq7HxFN0kOVb7rXXOSIkt5lTzDJDGRBcOyVvNRIWFauMqi1gIHd1pszq1542vMOGY0HP4CaiALfkA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@next/env": "15.5.6",
|
||||
"@next/env": "15.5.5",
|
||||
"@swc/helpers": "0.5.15",
|
||||
"caniuse-lite": "^1.0.30001579",
|
||||
"postcss": "8.4.31",
|
||||
@@ -9651,14 +9652,14 @@
|
||||
"node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@next/swc-darwin-arm64": "15.5.6",
|
||||
"@next/swc-darwin-x64": "15.5.6",
|
||||
"@next/swc-linux-arm64-gnu": "15.5.6",
|
||||
"@next/swc-linux-arm64-musl": "15.5.6",
|
||||
"@next/swc-linux-x64-gnu": "15.5.6",
|
||||
"@next/swc-linux-x64-musl": "15.5.6",
|
||||
"@next/swc-win32-arm64-msvc": "15.5.6",
|
||||
"@next/swc-win32-x64-msvc": "15.5.6",
|
||||
"@next/swc-darwin-arm64": "15.5.5",
|
||||
"@next/swc-darwin-x64": "15.5.5",
|
||||
"@next/swc-linux-arm64-gnu": "15.5.5",
|
||||
"@next/swc-linux-arm64-musl": "15.5.5",
|
||||
"@next/swc-linux-x64-gnu": "15.5.5",
|
||||
"@next/swc-linux-x64-musl": "15.5.5",
|
||||
"@next/swc-win32-arm64-msvc": "15.5.5",
|
||||
"@next/swc-win32-x64-msvc": "15.5.5",
|
||||
"sharp": "^0.34.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -10204,9 +10205,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier-plugin-tailwindcss": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.7.1.tgz",
|
||||
"integrity": "sha512-Bzv1LZcuiR1Sk02iJTS1QzlFNp/o5l2p3xkopwOrbPmtMeh3fK9rVW5M3neBQzHq+kGKj/4LGQMTNcTH4NGPtQ==",
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.7.0.tgz",
|
||||
"integrity": "sha512-zpRZhkfwq1cNmbKhmKzXKuKFdkgXZXlf6p+KttD75v6pGz1FxmcKMc4RKdw97GYBKBbout4113HSLaBJAomFDw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
||||
10
package.json
10
package.json
@@ -41,7 +41,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lucide-react": "^0.546.0",
|
||||
"next": "^15.5.6",
|
||||
"next": "^15.5.5",
|
||||
"node-pty": "^1.0.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
@@ -65,18 +65,18 @@
|
||||
"@types/bcryptjs": "^3.0.0",
|
||||
"@types/better-sqlite3": "^7.6.8",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/node": "^24.8.1",
|
||||
"@types/node": "^24.8.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"@vitejs/plugin-react": "^5.0.2",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"@vitest/ui": "^3.2.4",
|
||||
"eslint": "^9.38.0",
|
||||
"eslint-config-next": "^15.5.6",
|
||||
"eslint": "^9.23.0",
|
||||
"eslint-config-next": "^15.5.5",
|
||||
"jsdom": "^27.0.0",
|
||||
"postcss": "^8.5.3",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-tailwindcss": "^0.7.1",
|
||||
"prettier-plugin-tailwindcss": "^0.7.0",
|
||||
"prisma": "^6.17.1",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"typescript": "^5.8.2",
|
||||
|
||||
14
server.js
14
server.js
@@ -8,26 +8,12 @@ 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';
|
||||
// Fallback minimal global error handlers for Node runtime (avoid TS import)
|
||||
function registerGlobalErrorHandlers() {
|
||||
if (registerGlobalErrorHandlers._registered) return;
|
||||
registerGlobalErrorHandlers._registered = true;
|
||||
process.on('uncaughtException', (err) => {
|
||||
console.error('uncaught_exception', err);
|
||||
});
|
||||
process.on('unhandledRejection', (reason) => {
|
||||
console.error('unhandled_rejection', reason);
|
||||
});
|
||||
}
|
||||
registerGlobalErrorHandlers._registered = false;
|
||||
|
||||
const dev = process.env.NODE_ENV !== 'production';
|
||||
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
|
||||
|
||||
@@ -5,14 +5,12 @@ import { Button } from './ui/button';
|
||||
import { Input } from './ui/input';
|
||||
import { useAuth } from './AuthProvider';
|
||||
import { Lock, User, AlertCircle } from 'lucide-react';
|
||||
import { useRegisterModal } from './modal/ModalStackProvider';
|
||||
|
||||
interface AuthModalProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export function AuthModal({ isOpen }: AuthModalProps) {
|
||||
useRegisterModal(isOpen, { id: 'auth-modal', allowEscape: false, onClose: () => null });
|
||||
const { login } = useAuth();
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { Button } from './ui/button';
|
||||
import { AlertTriangle, Info } from 'lucide-react';
|
||||
import { useRegisterModal } from './modal/ModalStackProvider';
|
||||
|
||||
interface ConfirmationModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -29,12 +28,10 @@ export function ConfirmationModal({
|
||||
cancelButtonText = 'Cancel'
|
||||
}: ConfirmationModalProps) {
|
||||
const [typedText, setTypedText] = useState('');
|
||||
const isDanger = variant === 'danger';
|
||||
const allowEscape = useMemo(() => !isDanger, [isDanger]);
|
||||
|
||||
useRegisterModal(isOpen, { id: 'confirmation-modal', allowEscape, onClose });
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const isDanger = variant === 'danger';
|
||||
const isConfirmEnabled = isDanger ? typedText === confirmText : true;
|
||||
|
||||
const handleConfirm = () => {
|
||||
|
||||
@@ -169,13 +169,6 @@ 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, '');
|
||||
|
||||
return combinedScripts
|
||||
.map(script => {
|
||||
if (!script?.name) {
|
||||
@@ -185,13 +178,9 @@ export function DownloadedScriptsTab({ onInstallScript }: DownloadedScriptsTabPr
|
||||
// Check if there's a corresponding local script
|
||||
const hasLocalVersion = localScriptsData?.scripts?.some(local => {
|
||||
if (!local?.name) return false;
|
||||
const normalizedLocal = normalizeId(local.name);
|
||||
const matchesNameOrSlug = (
|
||||
normalizedLocal === normalizeId(script.name) ||
|
||||
normalizedLocal === normalizeId(script.slug)
|
||||
);
|
||||
const matchesInstallBasename = (script as any)?.install_basenames?.some((base: string) => normalizeId(base) === normalizedLocal) ?? false;
|
||||
return matchesNameOrSlug || matchesInstallBasename;
|
||||
const localName = local.name.replace(/\.sh$/, '');
|
||||
return localName.toLowerCase() === script.name.toLowerCase() ||
|
||||
localName.toLowerCase() === (script.slug ?? '').toLowerCase();
|
||||
}) ?? false;
|
||||
|
||||
return {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Button } from './ui/button';
|
||||
import { AlertCircle, CheckCircle } from 'lucide-react';
|
||||
import { useRegisterModal } from './modal/ModalStackProvider';
|
||||
|
||||
interface ErrorModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -22,7 +21,6 @@ export function ErrorModal({
|
||||
details,
|
||||
type = 'error'
|
||||
}: ErrorModalProps) {
|
||||
useRegisterModal(isOpen, { id: 'error-modal', allowEscape: true, onClose });
|
||||
// Auto-close after 10 seconds
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
|
||||
@@ -5,7 +5,6 @@ import type { Server } from '../../types/server';
|
||||
import { Button } from './ui/button';
|
||||
import { ColorCodedDropdown } from './ColorCodedDropdown';
|
||||
import { SettingsModal } from './SettingsModal';
|
||||
import { useRegisterModal } from './modal/ModalStackProvider';
|
||||
|
||||
|
||||
interface ExecutionModeModalProps {
|
||||
@@ -16,7 +15,6 @@ interface ExecutionModeModalProps {
|
||||
}
|
||||
|
||||
export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: ExecutionModeModalProps) {
|
||||
useRegisterModal(isOpen, { id: 'execution-mode-modal', allowEscape: true, onClose });
|
||||
const [servers, setServers] = useState<Server[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
@@ -6,7 +6,6 @@ import { Input } from './ui/input';
|
||||
import { Toggle } from './ui/toggle';
|
||||
import { ContextualHelpIcon } from './ContextualHelpIcon';
|
||||
import { useTheme } from './ThemeProvider';
|
||||
import { useRegisterModal } from './modal/ModalStackProvider';
|
||||
|
||||
interface GeneralSettingsModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -14,7 +13,6 @@ interface GeneralSettingsModalProps {
|
||||
}
|
||||
|
||||
export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalProps) {
|
||||
useRegisterModal(isOpen, { id: 'general-settings-modal', allowEscape: true, onClose });
|
||||
const { theme, setTheme } = useTheme();
|
||||
const [activeTab, setActiveTab] = useState<'general' | 'github' | 'auth'>('general');
|
||||
const [githubToken, setGithubToken] = useState('');
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { useState } from 'react';
|
||||
import { Button } from './ui/button';
|
||||
import { HelpCircle, Server, Settings, RefreshCw, Package, HardDrive, FolderOpen, Search, Download } from 'lucide-react';
|
||||
import { useRegisterModal } from './modal/ModalStackProvider';
|
||||
|
||||
interface HelpModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -14,7 +13,6 @@ interface HelpModalProps {
|
||||
type HelpSection = 'server-settings' | 'general-settings' | 'sync-button' | 'available-scripts' | 'downloaded-scripts' | 'installed-scripts' | 'lxc-settings' | 'update-system';
|
||||
|
||||
export function HelpModal({ isOpen, onClose, initialSection = 'server-settings' }: HelpModalProps) {
|
||||
useRegisterModal(isOpen, { id: 'help-modal', allowEscape: true, onClose });
|
||||
const [activeSection, setActiveSection] = useState<HelpSection>(initialSection as HelpSection);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
@@ -9,7 +9,6 @@ import { ContextualHelpIcon } from './ContextualHelpIcon';
|
||||
import { LoadingModal } from './LoadingModal';
|
||||
import { ConfirmationModal } from './ConfirmationModal';
|
||||
import { RefreshCw, AlertTriangle, CheckCircle } from 'lucide-react';
|
||||
import { useRegisterModal } from './modal/ModalStackProvider';
|
||||
|
||||
interface InstalledScript {
|
||||
id: number;
|
||||
@@ -42,7 +41,6 @@ interface LXCSettingsModalProps {
|
||||
}
|
||||
|
||||
export function LXCSettingsModal({ isOpen, script, onClose, onSave: _onSave }: LXCSettingsModalProps) {
|
||||
useRegisterModal(isOpen, { id: 'lxc-settings-modal', allowEscape: true, onClose });
|
||||
const [activeTab, setActiveTab] = useState<string>('common');
|
||||
const [showConfirmation, setShowConfirmation] = useState(false);
|
||||
const [showResultModal, setShowResultModal] = useState(false);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { useRegisterModal } from './modal/ModalStackProvider';
|
||||
|
||||
interface LoadingModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -9,7 +8,6 @@ interface LoadingModalProps {
|
||||
}
|
||||
|
||||
export function LoadingModal({ isOpen, action }: LoadingModalProps) {
|
||||
useRegisterModal(isOpen, { id: 'loading-modal', allowEscape: false, onClose: () => null });
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { useState } from 'react';
|
||||
import { X, Copy, Check, Server, Globe } from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import { useRegisterModal } from './modal/ModalStackProvider';
|
||||
|
||||
interface PublicKeyModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -14,7 +13,6 @@ interface PublicKeyModalProps {
|
||||
}
|
||||
|
||||
export function PublicKeyModal({ isOpen, onClose, publicKey, serverName, serverIp }: PublicKeyModalProps) {
|
||||
useRegisterModal(isOpen, { id: 'public-key-modal', allowEscape: true, onClose });
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [commandCopied, setCommandCopied] = useState(false);
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import { api } from '~/trpc/react';
|
||||
import { Button } from './ui/button';
|
||||
import { Badge } from './ui/badge';
|
||||
import { X, ExternalLink, Calendar, Tag, Loader2 } from 'lucide-react';
|
||||
import { useRegisterModal } from './modal/ModalStackProvider';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
|
||||
@@ -35,7 +34,6 @@ const markVersionAsSeen = (version: string): void => {
|
||||
};
|
||||
|
||||
export function ReleaseNotesModal({ isOpen, onClose, highlightVersion }: ReleaseNotesModalProps) {
|
||||
useRegisterModal(isOpen, { id: 'release-notes-modal', allowEscape: true, onClose });
|
||||
const [currentVersion, setCurrentVersion] = useState<string | null>(null);
|
||||
const { data: releasesData, isLoading, error } = api.version.getAllReleases.useQuery(undefined, {
|
||||
enabled: isOpen
|
||||
|
||||
@@ -9,7 +9,6 @@ import { TextViewer } from "./TextViewer";
|
||||
import { ExecutionModeModal } from "./ExecutionModeModal";
|
||||
import { TypeBadge, UpdateableBadge, PrivilegedBadge, NoteBadge } from "./Badge";
|
||||
import { Button } from "./ui/button";
|
||||
import { useRegisterModal } from './modal/ModalStackProvider';
|
||||
|
||||
interface ScriptDetailModalProps {
|
||||
script: Script | null;
|
||||
@@ -29,7 +28,6 @@ export function ScriptDetailModal({
|
||||
onClose,
|
||||
onInstallScript,
|
||||
}: ScriptDetailModalProps) {
|
||||
useRegisterModal(isOpen, { id: 'script-detail-modal', allowEscape: true, onClose });
|
||||
const [imageError, setImageError] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [loadMessage, setLoadMessage] = useState<string | null>(null);
|
||||
|
||||
@@ -200,13 +200,6 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
|
||||
|
||||
// Update scripts with download status
|
||||
const scriptsWithStatus = React.useMemo((): ScriptCardType[] => {
|
||||
// Helper to normalize identifiers for robust matching
|
||||
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 => {
|
||||
if (!script?.name) {
|
||||
return script; // Return as-is if invalid
|
||||
@@ -215,13 +208,9 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
|
||||
// Check if there's a corresponding local script
|
||||
const hasLocalVersion = localScriptsData?.scripts?.some(local => {
|
||||
if (!local?.name) return false;
|
||||
const normalizedLocal = normalizeId(local.name);
|
||||
const matchesNameOrSlug = (
|
||||
normalizedLocal === normalizeId(script.name) ||
|
||||
normalizedLocal === normalizeId(script.slug)
|
||||
);
|
||||
const matchesInstallBasename = (script as any)?.install_basenames?.some((base: string) => normalizeId(base) === normalizedLocal) ?? false;
|
||||
return matchesNameOrSlug || matchesInstallBasename;
|
||||
const localName = local.name.replace(/\.sh$/, '');
|
||||
return localName.toLowerCase() === script.name.toLowerCase() ||
|
||||
localName.toLowerCase() === (script.slug ?? '').toLowerCase();
|
||||
}) ?? false;
|
||||
|
||||
return {
|
||||
@@ -662,8 +651,8 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
|
||||
onViewModeChange={setViewMode}
|
||||
/>
|
||||
|
||||
{/* Newest Scripts Carousel - Always show when there are newest scripts */}
|
||||
{newestScripts.length > 0 && (
|
||||
{/* Newest Scripts Carousel - Only show when no filters are active */}
|
||||
{!hasActiveFilters && newestScripts.length > 0 && (
|
||||
<div className="mb-8">
|
||||
<div className="bg-card border-l-4 border-l-primary border border-border rounded-lg p-6 shadow-lg">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
|
||||
@@ -122,21 +122,7 @@ export function ServerForm({ onSubmit, initialData, isEditing = false, onCancel
|
||||
const handleChange = (field: keyof CreateServerData) => (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
|
||||
) => {
|
||||
// Special handling for numeric ssh_port: keep it strictly numeric
|
||||
if (field === 'ssh_port') {
|
||||
const raw = (e.target as HTMLInputElement).value ?? '';
|
||||
const digitsOnly = raw.replace(/\D+/g, '');
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
ssh_port: digitsOnly ? parseInt(digitsOnly, 10) : undefined,
|
||||
}));
|
||||
if (errors.ssh_port) {
|
||||
setErrors(prev => ({ ...prev, ssh_port: undefined }));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setFormData(prev => ({ ...prev, [field]: (e.target as HTMLInputElement).value }));
|
||||
setFormData(prev => ({ ...prev, [field]: e.target.value }));
|
||||
// Clear error when user starts typing
|
||||
if (errors[field]) {
|
||||
setErrors(prev => ({ ...prev, [field]: undefined }));
|
||||
@@ -260,17 +246,14 @@ export function ServerForm({ onSubmit, initialData, isEditing = false, onCancel
|
||||
<input
|
||||
type="number"
|
||||
id="ssh_port"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
autoComplete="off"
|
||||
value={formData.ssh_port ?? 22}
|
||||
onChange={handleChange('ssh_port')}
|
||||
className={`w-full px-3 py-2 border rounded-md shadow-sm bg-card text-foreground placeholder-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring ${
|
||||
errors.ssh_port ? 'border-destructive' : 'border-border'
|
||||
}`}
|
||||
placeholder="22"
|
||||
min={1}
|
||||
max={65535}
|
||||
min="1"
|
||||
max="65535"
|
||||
/>
|
||||
{errors.ssh_port && <p className="mt-1 text-sm text-destructive">{errors.ssh_port}</p>}
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,6 @@ import { ServerForm } from './ServerForm';
|
||||
import { ServerList } from './ServerList';
|
||||
import { Button } from './ui/button';
|
||||
import { ContextualHelpIcon } from './ContextualHelpIcon';
|
||||
import { useRegisterModal } from './modal/ModalStackProvider';
|
||||
|
||||
interface SettingsModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -14,7 +13,6 @@ interface SettingsModalProps {
|
||||
}
|
||||
|
||||
export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
|
||||
useRegisterModal(isOpen, { id: 'settings-modal', allowEscape: true, onClose });
|
||||
const [servers, setServers] = useState<Server[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Button } from './ui/button';
|
||||
import { Input } from './ui/input';
|
||||
import { Toggle } from './ui/toggle';
|
||||
import { Lock, User, Shield, AlertCircle } from 'lucide-react';
|
||||
import { useRegisterModal } from './modal/ModalStackProvider';
|
||||
|
||||
interface SetupModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -13,7 +12,6 @@ interface SetupModalProps {
|
||||
}
|
||||
|
||||
export function SetupModal({ isOpen, onComplete }: SetupModalProps) {
|
||||
useRegisterModal(isOpen, { id: 'setup-modal', allowEscape: true, onClose: () => null });
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef } from 'react';
|
||||
|
||||
type RegisteredModal = { id: string; allowEscape: boolean; onClose: () => void };
|
||||
|
||||
interface ModalStackContextValue {
|
||||
register: (modal: RegisteredModal) => () => void;
|
||||
}
|
||||
|
||||
const ModalStackContext = createContext<ModalStackContextValue | null>(null);
|
||||
|
||||
export function ModalStackProvider({ children }: { children: React.ReactNode }) {
|
||||
const stackRef = useRef<RegisteredModal[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key !== 'Escape') return;
|
||||
|
||||
for (let i = stackRef.current.length - 1; i >= 0; i -= 1) {
|
||||
const modal = stackRef.current[i];
|
||||
if (modal?.allowEscape) {
|
||||
modal.onClose();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', onKeyDown);
|
||||
return () => window.removeEventListener('keydown', onKeyDown);
|
||||
}, []);
|
||||
|
||||
const register = useCallback((modal: RegisteredModal) => {
|
||||
stackRef.current.push(modal);
|
||||
return () => {
|
||||
stackRef.current = stackRef.current.filter((m) => m !== modal);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const value = useMemo(() => ({ register }), [register]);
|
||||
|
||||
return (
|
||||
<ModalStackContext.Provider value={value}>
|
||||
{children}
|
||||
</ModalStackContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useRegisterModal(enabled: boolean, modal: RegisteredModal) {
|
||||
const ctx = useContext(ModalStackContext);
|
||||
useEffect(() => {
|
||||
if (!ctx || !enabled) return;
|
||||
return ctx.register(modal);
|
||||
}, [ctx, enabled, modal]);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,8 @@ import type { NextRequest } from 'next/server';
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getDatabase } from '../../../../server/database-prisma';
|
||||
import type { CreateServerData } from '../../../../types/server';
|
||||
import { withApiLogging } from '../../../../server/logging/withApiLogging';
|
||||
|
||||
export const GET = withApiLogging(async function GET(
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
@@ -29,16 +28,16 @@ export const GET = withApiLogging(async function GET(
|
||||
}
|
||||
|
||||
return NextResponse.json(server);
|
||||
} catch {
|
||||
// Error handled by withApiLogging
|
||||
} catch (error) {
|
||||
console.error('Error fetching server:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch server' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}, { redactBody: true });
|
||||
}
|
||||
|
||||
export const PUT = withApiLogging(async function PUT(
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
@@ -63,9 +62,8 @@ export const PUT = withApiLogging(async function PUT(
|
||||
);
|
||||
}
|
||||
|
||||
// Coerce and validate SSH port
|
||||
const port = ssh_port !== undefined ? parseInt(String(ssh_port), 10) : 22;
|
||||
if (Number.isNaN(port) || port < 1 || port > 65535) {
|
||||
// Validate SSH port
|
||||
if (ssh_port !== undefined && (ssh_port < 1 || ssh_port > 65535)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'SSH port must be between 1 and 65535' },
|
||||
{ status: 400 }
|
||||
@@ -113,7 +111,7 @@ export const PUT = withApiLogging(async function PUT(
|
||||
auth_type: authType,
|
||||
ssh_key,
|
||||
ssh_key_passphrase,
|
||||
ssh_port: port,
|
||||
ssh_port: ssh_port ?? 22,
|
||||
color,
|
||||
key_generated: key_generated ?? false,
|
||||
ssh_key_path
|
||||
@@ -126,7 +124,7 @@ export const PUT = withApiLogging(async function PUT(
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
// Error handled by withApiLogging
|
||||
console.error('Error updating server:', error);
|
||||
|
||||
// Handle unique constraint violation
|
||||
if (error instanceof Error && error.message.includes('UNIQUE constraint failed')) {
|
||||
@@ -141,9 +139,9 @@ export const PUT = withApiLogging(async function PUT(
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}, { redactBody: true });
|
||||
}
|
||||
|
||||
export const DELETE = withApiLogging(async function DELETE(
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
@@ -179,12 +177,12 @@ export const DELETE = withApiLogging(async function DELETE(
|
||||
changes: 1
|
||||
}
|
||||
);
|
||||
} catch {
|
||||
// Error handled by withApiLogging
|
||||
} catch (error) {
|
||||
console.error('Error deleting server:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to delete server' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}, { redactBody: true });
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,8 @@ import type { NextRequest } from 'next/server';
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getSSHService } from '../../../../server/ssh-service';
|
||||
import { getDatabase } from '../../../../server/database-prisma';
|
||||
import { withApiLogging } from '../../../../server/logging/withApiLogging';
|
||||
|
||||
export const POST = withApiLogging(async function POST(_request: NextRequest) {
|
||||
export async function POST(_request: NextRequest) {
|
||||
try {
|
||||
const sshService = getSSHService();
|
||||
const db = getDatabase();
|
||||
@@ -21,7 +20,7 @@ export const POST = withApiLogging(async function POST(_request: NextRequest) {
|
||||
serverId: serverId
|
||||
});
|
||||
} catch (error) {
|
||||
// Error handled by withApiLogging
|
||||
console.error('Error generating SSH key pair:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
@@ -30,4 +29,4 @@ export const POST = withApiLogging(async function POST(_request: NextRequest) {
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}, { redactBody: true });
|
||||
}
|
||||
|
||||
@@ -2,23 +2,22 @@ import type { NextRequest } from 'next/server';
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getDatabase } from '../../../server/database-prisma';
|
||||
import type { CreateServerData } from '../../../types/server';
|
||||
import { withApiLogging } from '../../../server/logging/withApiLogging';
|
||||
|
||||
export const GET = withApiLogging(async function GET() {
|
||||
export async function GET() {
|
||||
try {
|
||||
const db = getDatabase();
|
||||
const servers = await db.getAllServers();
|
||||
return NextResponse.json(servers);
|
||||
} catch {
|
||||
// Error handled by withApiLogging
|
||||
} catch (error) {
|
||||
console.error('Error fetching servers:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch servers' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}, { redactBody: true });
|
||||
}
|
||||
|
||||
export const POST = withApiLogging(async function POST(request: NextRequest) {
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { name, ip, user, password, auth_type, ssh_key, ssh_key_passphrase, ssh_port, color, key_generated, ssh_key_path }: CreateServerData = body;
|
||||
@@ -31,9 +30,8 @@ export const POST = withApiLogging(async function POST(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
// Coerce and validate SSH port
|
||||
const port = ssh_port !== undefined ? parseInt(String(ssh_port), 10) : 22;
|
||||
if (Number.isNaN(port) || port < 1 || port > 65535) {
|
||||
// Validate SSH port
|
||||
if (ssh_port !== undefined && (ssh_port < 1 || ssh_port > 65535)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'SSH port must be between 1 and 65535' },
|
||||
{ status: 400 }
|
||||
@@ -71,7 +69,7 @@ export const POST = withApiLogging(async function POST(request: NextRequest) {
|
||||
auth_type: authType,
|
||||
ssh_key,
|
||||
ssh_key_passphrase,
|
||||
ssh_port: port,
|
||||
ssh_port: ssh_port ?? 22,
|
||||
color,
|
||||
key_generated: key_generated ?? false,
|
||||
ssh_key_path
|
||||
@@ -84,10 +82,11 @@ export const POST = withApiLogging(async function POST(request: NextRequest) {
|
||||
},
|
||||
{ status: 201 }
|
||||
);
|
||||
} catch {
|
||||
// Error handled by withApiLogging
|
||||
} catch (error) {
|
||||
console.error('Error creating server:', error);
|
||||
|
||||
// Handle unique constraint violation
|
||||
if (Error instanceof Error && Error.message.includes('UNIQUE constraint failed')) {
|
||||
if (error instanceof Error && error.message.includes('UNIQUE constraint failed')) {
|
||||
return NextResponse.json(
|
||||
{ error: 'A server with this name already exists' },
|
||||
{ status: 409 }
|
||||
@@ -99,5 +98,5 @@ export const POST = withApiLogging(async function POST(request: NextRequest) {
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}, { redactBody: true });
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,8 @@ import { NextResponse } from 'next/server';
|
||||
import { getAuthConfig, updateAuthCredentials, updateAuthEnabled } from '~/lib/auth';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { withApiLogging } from '../../../../server/logging/withApiLogging';
|
||||
|
||||
export const GET = withApiLogging(async function GET() {
|
||||
export async function GET() {
|
||||
try {
|
||||
const authConfig = getAuthConfig();
|
||||
|
||||
@@ -15,16 +14,16 @@ export const GET = withApiLogging(async function GET() {
|
||||
hasCredentials: authConfig.hasCredentials,
|
||||
setupCompleted: authConfig.setupCompleted,
|
||||
});
|
||||
} catch {
|
||||
// Error handled by withApiLogging
|
||||
} catch (error) {
|
||||
console.error('Error reading auth credentials:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to read auth configuration' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}, { redactBody: true });
|
||||
}
|
||||
|
||||
export const POST = withApiLogging(async function POST(request: NextRequest) {
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { username, password, enabled } = await request.json() as { username: string; password: string; enabled?: boolean };
|
||||
|
||||
@@ -55,16 +54,16 @@ export const POST = withApiLogging(async function POST(request: NextRequest) {
|
||||
success: true,
|
||||
message: 'Authentication credentials updated successfully'
|
||||
});
|
||||
} catch {
|
||||
// Error handled by withApiLogging
|
||||
} catch (error) {
|
||||
console.error('Error updating auth credentials:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to update auth credentials' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}, { redactBody: true });
|
||||
}
|
||||
|
||||
export const PATCH = withApiLogging(async function PATCH(request: NextRequest) {
|
||||
export async function PATCH(request: NextRequest) {
|
||||
try {
|
||||
const { enabled } = await request.json() as { enabled: boolean };
|
||||
|
||||
@@ -108,11 +107,11 @@ export const PATCH = withApiLogging(async function PATCH(request: NextRequest) {
|
||||
success: true,
|
||||
message: `Authentication ${enabled ? 'enabled' : 'disabled'} successfully`
|
||||
});
|
||||
} catch {
|
||||
// Error handled by withApiLogging
|
||||
} catch (error) {
|
||||
console.error('Error updating auth enabled status:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to update auth status' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}, { redactBody: true });
|
||||
}
|
||||
|
||||
@@ -2,9 +2,8 @@ import type { NextRequest } from 'next/server';
|
||||
import { NextResponse } from 'next/server';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { withApiLogging } from '../../../../server/logging/withApiLogging';
|
||||
|
||||
export const POST = withApiLogging(async function POST(request: NextRequest) {
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { token } = await request.json();
|
||||
|
||||
@@ -40,16 +39,16 @@ export const POST = withApiLogging(async function POST(request: NextRequest) {
|
||||
fs.writeFileSync(envPath, envContent);
|
||||
|
||||
return NextResponse.json({ success: true, message: 'GitHub token saved successfully' });
|
||||
} catch {
|
||||
// Error handled by withApiLogging
|
||||
} catch (error) {
|
||||
console.error('Error saving GitHub token:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to save GitHub token' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}, { redactBody: true });
|
||||
}
|
||||
|
||||
export const GET = withApiLogging(async function GET() {
|
||||
export async function GET() {
|
||||
try {
|
||||
// Path to the .env file
|
||||
const envPath = path.join(process.cwd(), '.env');
|
||||
@@ -66,11 +65,11 @@ export const GET = withApiLogging(async function GET() {
|
||||
const token = githubTokenMatch ? githubTokenMatch[1] : null;
|
||||
|
||||
return NextResponse.json({ token });
|
||||
} catch {
|
||||
// Error handled by withApiLogging
|
||||
} catch (error) {
|
||||
console.error('Error reading GitHub token:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to read GitHub token' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}, { redactBody: true });
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { type NextRequest } from "next/server";
|
||||
import { env } from "~/env.js";
|
||||
import { appRouter } from "~/server/api/root";
|
||||
import { createTRPCContext } from "~/server/api/trpc";
|
||||
import logger from "../../../../server/logging/logger";
|
||||
|
||||
const handler = (req: NextRequest) =>
|
||||
fetchRequestHandler({
|
||||
@@ -15,7 +14,9 @@ const handler = (req: NextRequest) =>
|
||||
onError:
|
||||
env.NODE_ENV === "development"
|
||||
? ({ path, error }) => {
|
||||
logger.error("trpc_error", { path: path ?? "<no-path>" }, error);
|
||||
console.error(
|
||||
`[ERROR] tRPC failed on ${path ?? "<no-path>"}: ${error.message}`,
|
||||
);
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
|
||||
@@ -7,7 +7,6 @@ import { TRPCReactProvider } from "~/trpc/react";
|
||||
import { AuthProvider } from "./_components/AuthProvider";
|
||||
import { AuthGuard } from "./_components/AuthGuard";
|
||||
import { ThemeProvider } from "./_components/ThemeProvider";
|
||||
import { ModalStackProvider } from "./_components/modal/ModalStackProvider";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "PVE Scripts local",
|
||||
@@ -42,11 +41,9 @@ export default function RootLayout({
|
||||
<ThemeProvider>
|
||||
<TRPCReactProvider>
|
||||
<AuthProvider>
|
||||
<ModalStackProvider>
|
||||
<AuthGuard>
|
||||
{children}
|
||||
</AuthGuard>
|
||||
</ModalStackProvider>
|
||||
<AuthGuard>
|
||||
{children}
|
||||
</AuthGuard>
|
||||
</AuthProvider>
|
||||
</TRPCReactProvider>
|
||||
</ThemeProvider>
|
||||
|
||||
@@ -908,44 +908,21 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
// Check if the container config file still exists
|
||||
const checkCommand = `test -f "/etc/pve/lxc/${scriptData.container_id}.conf" && echo "exists" || echo "not_found"`;
|
||||
|
||||
// Await full command completion to avoid early false negatives
|
||||
const containerExists = await new Promise<boolean>((resolve) => {
|
||||
let combinedOutput = '';
|
||||
let resolved = false;
|
||||
|
||||
const finish = () => {
|
||||
if (resolved) return;
|
||||
resolved = true;
|
||||
const out = combinedOutput.trim();
|
||||
if (out.includes('exists')) {
|
||||
resolve(true);
|
||||
} else if (out.includes('not_found')) {
|
||||
resolve(false);
|
||||
} else {
|
||||
// Unknown output; treat as not found but log for diagnostics
|
||||
console.warn(`cleanupOrphanedScripts: unexpected output for ${String(scriptData.script_name)} (${String(scriptData.container_id)}): ${out}`);
|
||||
resolve(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Add a guard timeout so we don't hang indefinitely
|
||||
const timer = setTimeout(() => {
|
||||
console.warn(`cleanupOrphanedScripts: timeout while checking ${String(scriptData.script_name)} on server ${String((server as any).name)}`);
|
||||
finish();
|
||||
}, 15000);
|
||||
|
||||
|
||||
void sshExecutionService.executeCommand(
|
||||
|
||||
server as Server,
|
||||
checkCommand,
|
||||
(data: string) => {
|
||||
combinedOutput += data;
|
||||
resolve(data.trim() === 'exists');
|
||||
},
|
||||
(error: string) => {
|
||||
combinedOutput += error;
|
||||
console.error(`Error checking container ${scriptData.script_name}:`, error);
|
||||
resolve(false);
|
||||
},
|
||||
(_exitCode: number) => {
|
||||
clearTimeout(timer);
|
||||
finish();
|
||||
resolve(false);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -177,15 +177,6 @@ export const scriptsRouter = createTRPCRouter({
|
||||
const firstInstallMethod = script?.install_methods?.[0];
|
||||
const os = firstInstallMethod?.resources?.os;
|
||||
const version = firstInstallMethod?.resources?.version;
|
||||
// Extract install basenames for robust local matching (e.g., execute.sh -> execute)
|
||||
const install_basenames = (script?.install_methods ?? [])
|
||||
.map(m => m?.script)
|
||||
.filter((p): p is string => typeof p === 'string')
|
||||
.map(p => {
|
||||
const parts = p.split('/');
|
||||
const file = parts[parts.length - 1] ?? '';
|
||||
return file.replace(/\.(sh|bash|py|js|ts)$/i, '');
|
||||
});
|
||||
|
||||
return {
|
||||
...card,
|
||||
@@ -198,7 +189,6 @@ export const scriptsRouter = createTRPCRouter({
|
||||
version: version,
|
||||
// Add interface port
|
||||
interface_port: script?.interface_port,
|
||||
install_basenames,
|
||||
} as ScriptCard;
|
||||
});
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ class DatabaseServicePrisma {
|
||||
// Server CRUD operations
|
||||
async createServer(serverData: CreateServerData) {
|
||||
const { name, ip, user, password, auth_type, ssh_key, ssh_key_passphrase, ssh_port, color, key_generated } = serverData;
|
||||
const normalizedPort = ssh_port !== undefined ? parseInt(String(ssh_port), 10) : 22;
|
||||
|
||||
let ssh_key_path = null;
|
||||
|
||||
@@ -39,7 +38,7 @@ class DatabaseServicePrisma {
|
||||
auth_type: auth_type ?? 'password',
|
||||
ssh_key,
|
||||
ssh_key_passphrase,
|
||||
ssh_port: Number.isNaN(normalizedPort) ? 22 : normalizedPort,
|
||||
ssh_port: ssh_port ?? 22,
|
||||
ssh_key_path,
|
||||
key_generated: Boolean(key_generated),
|
||||
color,
|
||||
@@ -61,7 +60,6 @@ class DatabaseServicePrisma {
|
||||
|
||||
async updateServer(id: number, serverData: CreateServerData) {
|
||||
const { name, ip, user, password, auth_type, ssh_key, ssh_key_passphrase, ssh_port, color, key_generated } = serverData;
|
||||
const normalizedPort = ssh_port !== undefined ? parseInt(String(ssh_port), 10) : undefined;
|
||||
|
||||
// Get existing server to check for key changes
|
||||
const existingServer = await this.getServerById(id);
|
||||
@@ -111,7 +109,7 @@ class DatabaseServicePrisma {
|
||||
auth_type: auth_type ?? 'password',
|
||||
ssh_key,
|
||||
ssh_key_passphrase,
|
||||
ssh_port: normalizedPort ?? 22,
|
||||
ssh_port: ssh_port ?? 22,
|
||||
ssh_key_path,
|
||||
key_generated: key_generated !== undefined ? Boolean(key_generated) : (existingServer?.key_generated ?? false),
|
||||
color,
|
||||
|
||||
@@ -4,8 +4,6 @@ const globalForPrisma = globalThis as unknown as {
|
||||
prisma: PrismaClient | undefined;
|
||||
};
|
||||
|
||||
export const prisma = globalForPrisma.prisma ?? new PrismaClient({
|
||||
log: ['warn', 'error']
|
||||
});
|
||||
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import logger from './logger';
|
||||
import { toSafeError } from './prismaSafeError';
|
||||
|
||||
let registered = false;
|
||||
|
||||
export function registerGlobalErrorHandlers() {
|
||||
if (registered) return;
|
||||
registered = true;
|
||||
|
||||
process.on('uncaughtException', (err) => {
|
||||
const safe = toSafeError(err);
|
||||
logger.error('uncaught_exception', { name: safe.name, code: safe.code }, err);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (reason) => {
|
||||
const safe = toSafeError(reason as any);
|
||||
logger.error('unhandled_rejection', { name: safe.name, code: safe.code }, reason);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
import { redactObject } from './redact';
|
||||
|
||||
type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
||||
|
||||
const LOG_LEVELS: Record<LogLevel, number> = {
|
||||
debug: 10,
|
||||
info: 20,
|
||||
warn: 30,
|
||||
error: 40
|
||||
};
|
||||
|
||||
const envLevel = (process.env.LOG_LEVEL as LogLevel) || 'info';
|
||||
const currentLevel = LOG_LEVELS[envLevel] ?? LOG_LEVELS.info;
|
||||
|
||||
function safeMeta(meta?: unknown): unknown {
|
||||
if (meta === undefined) return undefined;
|
||||
try {
|
||||
return redactObject(meta);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function safeError(err: unknown): { name?: string; code?: string; stack?: string } | undefined {
|
||||
if (!err) return undefined;
|
||||
if (err instanceof Error) {
|
||||
return {
|
||||
name: err.name,
|
||||
code: (err as any).code,
|
||||
stack: formatStack(err.stack)
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function formatStack(stack?: string): string | undefined {
|
||||
if (!stack) return undefined;
|
||||
const lines = stack.split('\n').slice(0, 10);
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function log(level: LogLevel, message: string, meta?: unknown, err?: unknown) {
|
||||
if (LOG_LEVELS[level] < currentLevel) return;
|
||||
const payload: Record<string, unknown> = {
|
||||
level,
|
||||
msg: message,
|
||||
time: new Date().toISOString(),
|
||||
};
|
||||
const redactedMeta = safeMeta(meta);
|
||||
if (redactedMeta !== undefined) payload.meta = redactedMeta;
|
||||
const safeErr = safeError(err);
|
||||
if (safeErr) payload.err = safeErr;
|
||||
|
||||
const line = JSON.stringify(payload);
|
||||
if (level === 'error') {
|
||||
console.error(line);
|
||||
} else if (level === 'warn') {
|
||||
console.warn(line);
|
||||
} else {
|
||||
console.log(line);
|
||||
}
|
||||
}
|
||||
|
||||
export const logger = {
|
||||
debug(message: string, meta?: unknown) {
|
||||
log('debug', message, meta);
|
||||
},
|
||||
info(message: string, meta?: unknown) {
|
||||
log('info', message, meta);
|
||||
},
|
||||
warn(message: string, meta?: unknown) {
|
||||
log('warn', message, meta);
|
||||
},
|
||||
error(message: string, meta?: unknown, err?: unknown) {
|
||||
log('error', message, meta, err);
|
||||
}
|
||||
};
|
||||
|
||||
export default logger;
|
||||
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
type SafeError = { code?: string; name: string; safeMessage: string };
|
||||
|
||||
export function toSafeError(err: unknown): SafeError {
|
||||
if (err && typeof err === 'object') {
|
||||
const name = (err as any).name as string | undefined;
|
||||
const code = (err as any).code as string | undefined;
|
||||
|
||||
// Prisma error names to map
|
||||
if (name === 'PrismaClientValidationError') {
|
||||
return { name, code, safeMessage: 'Invalid input' };
|
||||
}
|
||||
if (name === 'PrismaClientKnownRequestError') {
|
||||
// Avoid echoing message which may include parameters
|
||||
return { name, code, safeMessage: 'Database constraint or known request error' };
|
||||
}
|
||||
if (name === 'PrismaClientUnknownRequestError') {
|
||||
return { name, code, safeMessage: 'Database request failed' };
|
||||
}
|
||||
if (name === 'PrismaClientRustPanicError') {
|
||||
return { name, code, safeMessage: 'Database engine error' };
|
||||
}
|
||||
if (name === 'PrismaClientInitializationError') {
|
||||
return { name, code, safeMessage: 'Database initialization failed' };
|
||||
}
|
||||
if (name === 'PrismaClientFetchEngineError') {
|
||||
return { name, code, safeMessage: 'Database engine fetch error' };
|
||||
}
|
||||
|
||||
if (name) {
|
||||
return { name, code, safeMessage: 'Unhandled server error' };
|
||||
}
|
||||
}
|
||||
return { name: 'Error', safeMessage: 'Unhandled server error' };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
const DEFAULT_MASK = '***REDACTED***';
|
||||
|
||||
const DEFAULT_SENSITIVE_KEYS = [
|
||||
'password',
|
||||
'ssh_key',
|
||||
'sshKey',
|
||||
'ssh_key_passphrase',
|
||||
'sshKeyPassphrase',
|
||||
'token',
|
||||
'access_token',
|
||||
'refresh_token',
|
||||
'authorization',
|
||||
'cookie',
|
||||
'set-cookie',
|
||||
'secret',
|
||||
'apiKey',
|
||||
'apikey'
|
||||
];
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return Object.prototype.toString.call(value) === '[object Object]';
|
||||
}
|
||||
|
||||
export function redactObject(
|
||||
value: unknown,
|
||||
opts?: { keys?: string[]; mask?: string }
|
||||
): unknown {
|
||||
const keys = (opts?.keys ?? DEFAULT_SENSITIVE_KEYS).map(k => k.toLowerCase());
|
||||
const mask = opts?.mask ?? DEFAULT_MASK;
|
||||
|
||||
const visit = (val: unknown): unknown => {
|
||||
if (val === null || val === undefined) return val;
|
||||
if (typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean') return val;
|
||||
if (Array.isArray(val)) return val.map(visit);
|
||||
if (val instanceof Map) {
|
||||
const mapped = new Map();
|
||||
for (const [k, v] of val.entries()) {
|
||||
const shouldRedact = typeof k === 'string' && keys.includes(k.toLowerCase());
|
||||
mapped.set(k, shouldRedact ? mask : visit(v));
|
||||
}
|
||||
return mapped;
|
||||
}
|
||||
if (isPlainObject(val)) {
|
||||
const out: Record<string, unknown> = {};
|
||||
for (const [k, v] of Object.entries(val)) {
|
||||
const shouldRedact = keys.includes(k.toLowerCase());
|
||||
out[k] = shouldRedact ? mask : visit(v);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
return maskIfLikelySecret(val, mask);
|
||||
};
|
||||
|
||||
return visit(value);
|
||||
}
|
||||
|
||||
function maskIfLikelySecret(val: unknown, mask: string): unknown {
|
||||
// For safety, non-serializable or unexpected values get masked when logged
|
||||
try {
|
||||
JSON.stringify(val);
|
||||
return val;
|
||||
} catch {
|
||||
return mask;
|
||||
}
|
||||
}
|
||||
|
||||
export function summarizeBody(body: unknown): { keys: string[]; size?: number } {
|
||||
try {
|
||||
if (isPlainObject(body)) {
|
||||
const keys = Object.keys(body);
|
||||
const size = Buffer.from(JSON.stringify(body)).length;
|
||||
return { keys, size };
|
||||
}
|
||||
if (Array.isArray(body)) {
|
||||
const size = Buffer.from(JSON.stringify(body)).length;
|
||||
return { keys: ['<array>'], size };
|
||||
}
|
||||
return { keys: [typeof body], size: undefined };
|
||||
} catch {
|
||||
return { keys: ['<unserializable>'] };
|
||||
}
|
||||
}
|
||||
|
||||
export const SENSITIVE_KEYS = DEFAULT_SENSITIVE_KEYS;
|
||||
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import type { NextRequest } from 'next/server';
|
||||
import { NextResponse } from 'next/server';
|
||||
import logger from './logger';
|
||||
import { redactObject, summarizeBody } from './redact';
|
||||
import { toSafeError } from './prismaSafeError';
|
||||
|
||||
type Handler = (request: NextRequest, context?: any) => Promise<Response> | Response;
|
||||
|
||||
export function withApiLogging(
|
||||
handler: Handler,
|
||||
opts?: { redactBody?: boolean }
|
||||
) {
|
||||
const { redactBody = false } = opts ?? {};
|
||||
|
||||
return async function wrapped(request: NextRequest, context?: any) {
|
||||
const url = new URL(request.url);
|
||||
const method = request.method;
|
||||
const path = url.pathname;
|
||||
const queryKeys = Array.from(url.searchParams.keys());
|
||||
|
||||
try {
|
||||
let meta: Record<string, unknown> = { method, path, queryKeys };
|
||||
if (method !== 'GET' && method !== 'HEAD') {
|
||||
try {
|
||||
const body = await request.clone().json();
|
||||
meta = {
|
||||
...meta,
|
||||
body: redactBody ? undefined : redactObject(body),
|
||||
bodySummary: redactBody ? summarizeBody(body) : undefined
|
||||
};
|
||||
} catch {
|
||||
// Ignore non-JSON bodies
|
||||
}
|
||||
}
|
||||
logger.info('api_request', meta);
|
||||
|
||||
const response = await handler(request, context);
|
||||
logger.info('api_response', { method, path, status: response.status });
|
||||
return response;
|
||||
} catch (err) {
|
||||
const safe = toSafeError(err);
|
||||
logger.error('api_error', { method, path, code: safe.code, name: safe.name }, err);
|
||||
return NextResponse.json({ error: safe.safeMessage }, { status: 500 });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -60,8 +60,6 @@ export interface ScriptCard {
|
||||
os?: string;
|
||||
version?: string;
|
||||
interface_port?: number | null;
|
||||
// Optional: basenames of install scripts (without extension)
|
||||
install_basenames?: string[];
|
||||
}
|
||||
|
||||
export interface GitHubFile {
|
||||
|
||||
Reference in New Issue
Block a user