Compare commits
30 Commits
fixes_upda
...
feat/447
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
944a527972 | ||
|
|
34eade3971 | ||
|
|
9b77fc7ddb | ||
|
|
db12ac4219 | ||
|
|
f66d1db861 | ||
|
|
886c3e37ff | ||
|
|
38deb09aa9 | ||
|
|
6d326dce1f | ||
|
|
6c8e177d3e | ||
|
|
879a548345 | ||
|
|
64cd81d5ba | ||
|
|
61e75949c8 | ||
|
|
a5d24bfad7 | ||
|
|
04595c0093 | ||
|
|
06fdb4889d | ||
|
|
38d4f9f918 | ||
|
|
63dc7c6983 | ||
|
|
d57c6059fc | ||
|
|
eb152f9fae | ||
|
|
1a8e98fec0 | ||
|
|
83a1c7ea31 | ||
|
|
79c63a7d3d | ||
|
|
753721eee0 | ||
|
|
09607296af | ||
|
|
c88040084a | ||
|
|
2573eb7314 | ||
|
|
414c356446 | ||
|
|
c38ded7a39 | ||
|
|
0cfed84cd0 | ||
|
|
9611bc9bcf |
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
|
|
||||||
## 🔗 Related PR / Issue
|
## 🔗 Related PR / Issue
|
||||||
Link: #
|
Fixes: #
|
||||||
|
|
||||||
|
|
||||||
## ✅ Prerequisites (**X** in brackets)
|
## ✅ Prerequisites (**X** in brackets)
|
||||||
|
|||||||
847
package-lock.json
generated
847
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
48
package.json
48
package.json
@@ -25,33 +25,33 @@
|
|||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/adapter-better-sqlite3": "^7.1.0",
|
"@prisma/adapter-better-sqlite3": "^7.2.0",
|
||||||
"@prisma/client": "^7.1.0",
|
"@prisma/client": "^7.2.0",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@t3-oss/env-nextjs": "^0.13.10",
|
"@t3-oss/env-nextjs": "^0.13.10",
|
||||||
"@tailwindcss/typography": "^0.5.19",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@tanstack/react-query": "^5.90.12",
|
"@tanstack/react-query": "^5.90.18",
|
||||||
"@trpc/client": "^11.8.0",
|
"@trpc/client": "^11.8.1",
|
||||||
"@trpc/react-query": "^11.8.1",
|
"@trpc/react-query": "^11.8.1",
|
||||||
"@trpc/server": "^11.8.0",
|
"@trpc/server": "^11.8.1",
|
||||||
"@types/react-syntax-highlighter": "^15.5.13",
|
"@types/react-syntax-highlighter": "^15.5.13",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"@xterm/addon-fit": "^0.10.0",
|
"@xterm/addon-fit": "^0.11.0",
|
||||||
"@xterm/addon-web-links": "^0.12.0",
|
"@xterm/addon-web-links": "^0.12.0",
|
||||||
"@xterm/xterm": "^6.0.0",
|
"@xterm/xterm": "^6.0.0",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
"better-sqlite3": "^12.5.0",
|
"better-sqlite3": "^12.6.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cron-validator": "^1.4.0",
|
"cron-validator": "^1.4.0",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
"next": "^16.0.10",
|
"next": "^16.1.3",
|
||||||
"node-cron": "^4.2.1",
|
"node-cron": "^4.2.1",
|
||||||
"node-pty": "^1.0.0",
|
"node-pty": "^1.1.0",
|
||||||
"react": "^19.2.3",
|
"react": "^19.2.3",
|
||||||
"react-dom": "^19.2.3",
|
"react-dom": "^19.2.3",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
@@ -62,8 +62,8 @@
|
|||||||
"strip-ansi": "^7.1.2",
|
"strip-ansi": "^7.1.2",
|
||||||
"superjson": "^2.2.6",
|
"superjson": "^2.2.6",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"ws": "^8.18.3",
|
"ws": "^8.19.0",
|
||||||
"zod": "^4.1.13"
|
"zod": "^4.3.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4.1.18",
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
@@ -73,26 +73,26 @@
|
|||||||
"@types/bcryptjs": "^3.0.0",
|
"@types/bcryptjs": "^3.0.0",
|
||||||
"@types/better-sqlite3": "^7.6.13",
|
"@types/better-sqlite3": "^7.6.13",
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/node": "^24.10.4",
|
"@types/node": "^24.10.9",
|
||||||
"@types/node-cron": "^3.0.11",
|
"@types/node-cron": "^3.0.11",
|
||||||
"@types/react": "^19.2.7",
|
"@types/react": "^19.2.8",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "^5.1.2",
|
"@vitejs/plugin-react": "^5.1.2",
|
||||||
"@vitest/coverage-v8": "^4.0.16",
|
"@vitest/coverage-v8": "^4.0.17",
|
||||||
"@vitest/ui": "^4.0.14",
|
"@vitest/ui": "^4.0.17",
|
||||||
"baseline-browser-mapping": "^2.9.3",
|
"baseline-browser-mapping": "^2.9.15",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.2",
|
||||||
"eslint-config-next": "^16.1.0",
|
"eslint-config-next": "^16.1.3",
|
||||||
"jsdom": "^27.3.0",
|
"jsdom": "^27.4.0",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"prettier": "^3.7.4",
|
"prettier": "^3.8.0",
|
||||||
"prettier-plugin-tailwindcss": "^0.7.2",
|
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||||
"prisma": "^7.1.0",
|
"prisma": "^7.2.0",
|
||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.1.18",
|
||||||
"tsx": "^4.21.0",
|
"tsx": "^4.21.0",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"typescript-eslint": "^8.48.1",
|
"typescript-eslint": "^8.53.0",
|
||||||
"vitest": "^4.0.14"
|
"vitest": "^4.0.17"
|
||||||
},
|
},
|
||||||
"ct3aMetadata": {
|
"ct3aMetadata": {
|
||||||
"initVersion": "7.39.3"
|
"initVersion": "7.39.3"
|
||||||
@@ -104,4 +104,4 @@
|
|||||||
"overrides": {
|
"overrides": {
|
||||||
"prismjs": "^1.30.0"
|
"prismjs": "^1.30.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
SCRIPT_DIR="$(dirname "$0")"
|
SCRIPT_DIR="$(dirname "$0")"
|
||||||
source "$SCRIPT_DIR/../core/build.func"
|
source "$SCRIPT_DIR/../core/build.func"
|
||||||
# Copyright (c) 2021-2025 tteck
|
# Copyright (c) 2021-2026 tteck
|
||||||
# Author: tteck (tteckster)
|
# Author: tteck (tteckster)
|
||||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||||
# Source: https://www.debian.org/
|
# Source: https://www.debian.org/
|
||||||
@@ -40,5 +40,5 @@ start
|
|||||||
build_container
|
build_container
|
||||||
description
|
description
|
||||||
|
|
||||||
msg_ok "Completed Successfully!\n"
|
msg_ok "Completed successfully!\n"
|
||||||
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
|
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
# Copyright (c) 2021-2025 tteck
|
# Copyright (c) 2021-2026 tteck
|
||||||
# Author: tteck (tteckster)
|
# Author: tteck (tteckster)
|
||||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||||
# Source: https://www.debian.org/
|
# Source: https://www.debian.org/
|
||||||
|
|||||||
@@ -1610,6 +1610,7 @@ class ScriptExecutionHandler {
|
|||||||
// TerminalHandler removed - not used by current application
|
// TerminalHandler removed - not used by current application
|
||||||
|
|
||||||
app.prepare().then(() => {
|
app.prepare().then(() => {
|
||||||
|
console.log('> Next.js app prepared successfully');
|
||||||
const httpServer = createServer(async (req, res) => {
|
const httpServer = createServer(async (req, res) => {
|
||||||
try {
|
try {
|
||||||
// Be sure to pass `true` as the second argument to `url.parse`.
|
// Be sure to pass `true` as the second argument to `url.parse`.
|
||||||
@@ -1715,4 +1716,9 @@ app.prepare().then(() => {
|
|||||||
autoSyncModule.setupGracefulShutdown();
|
autoSyncModule.setupGracefulShutdown();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}).catch((err) => {
|
||||||
|
console.error('> Failed to start server:', err.message);
|
||||||
|
console.error('> If you see "Could not find a production build", run: npm run build');
|
||||||
|
console.error('> Full error:', err);
|
||||||
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -58,6 +58,11 @@ export function ConfigurationModal({
|
|||||||
// Advanced mode state
|
// Advanced mode state
|
||||||
const [advancedVars, setAdvancedVars] = useState<EnvVars>({});
|
const [advancedVars, setAdvancedVars] = useState<EnvVars>({});
|
||||||
|
|
||||||
|
// Discovered SSH keys on the Proxmox host (advanced mode only)
|
||||||
|
const [discoveredSshKeys, setDiscoveredSshKeys] = useState<string[]>([]);
|
||||||
|
const [discoveredSshKeysLoading, setDiscoveredSshKeysLoading] = useState(false);
|
||||||
|
const [discoveredSshKeysError, setDiscoveredSshKeysError] = useState<string | null>(null);
|
||||||
|
|
||||||
// Validation errors
|
// Validation errors
|
||||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
@@ -119,6 +124,38 @@ export function ConfigurationModal({
|
|||||||
}
|
}
|
||||||
}, [actualScript, server, mode, resources, slug]);
|
}, [actualScript, server, mode, resources, slug]);
|
||||||
|
|
||||||
|
// Discover SSH keys on the Proxmox host when advanced mode is open
|
||||||
|
useEffect(() => {
|
||||||
|
if (!server?.id || !isOpen || mode !== 'advanced') {
|
||||||
|
setDiscoveredSshKeys([]);
|
||||||
|
setDiscoveredSshKeysError(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let cancelled = false;
|
||||||
|
setDiscoveredSshKeysLoading(true);
|
||||||
|
setDiscoveredSshKeysError(null);
|
||||||
|
fetch(`/api/servers/${server.id}/discover-ssh-keys`)
|
||||||
|
.then((res) => {
|
||||||
|
if (!res.ok) throw new Error(res.status === 404 ? 'Server not found' : res.statusText);
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then((data: { keys?: string[] }) => {
|
||||||
|
if (!cancelled && Array.isArray(data.keys)) setDiscoveredSshKeys(data.keys);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (!cancelled) {
|
||||||
|
setDiscoveredSshKeys([]);
|
||||||
|
setDiscoveredSshKeysError(err instanceof Error ? err.message : 'Could not detect keys');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (!cancelled) setDiscoveredSshKeysLoading(false);
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [server?.id, isOpen, mode]);
|
||||||
|
|
||||||
// Validation functions
|
// Validation functions
|
||||||
const validateIPv4 = (ip: string): boolean => {
|
const validateIPv4 = (ip: string): boolean => {
|
||||||
if (!ip) return true; // Empty is allowed (auto)
|
if (!ip) return true; // Empty is allowed (auto)
|
||||||
@@ -275,6 +312,16 @@ export function ConfigurationModal({
|
|||||||
if ((hasPassword || hasSSHKey) && envVars.var_ssh !== 'no') {
|
if ((hasPassword || hasSSHKey) && envVars.var_ssh !== 'no') {
|
||||||
envVars.var_ssh = 'yes';
|
envVars.var_ssh = 'yes';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Normalize var_tags: accept both comma and semicolon, output comma-separated
|
||||||
|
const rawTags = envVars.var_tags;
|
||||||
|
if (typeof rawTags === 'string' && rawTags.trim() !== '') {
|
||||||
|
envVars.var_tags = rawTags
|
||||||
|
.split(/[,;]/)
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(',');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove empty string values (but keep 0, false, etc.)
|
// Remove empty string values (but keep 0, false, etc.)
|
||||||
@@ -644,13 +691,13 @@ export function ConfigurationModal({
|
|||||||
</div>
|
</div>
|
||||||
<div className="col-span-2">
|
<div className="col-span-2">
|
||||||
<label className="block text-sm font-medium text-foreground mb-2">
|
<label className="block text-sm font-medium text-foreground mb-2">
|
||||||
Tags (comma-separated)
|
Tags (comma or semicolon separated)
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
value={typeof advancedVars.var_tags === 'boolean' ? '' : String(advancedVars.var_tags ?? '')}
|
value={typeof advancedVars.var_tags === 'boolean' ? '' : String(advancedVars.var_tags ?? '')}
|
||||||
onChange={(e) => updateAdvancedVar('var_tags', e.target.value)}
|
onChange={(e) => updateAdvancedVar('var_tags', e.target.value)}
|
||||||
placeholder="community-script"
|
placeholder="e.g. tag1; tag2"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -677,11 +724,40 @@ export function ConfigurationModal({
|
|||||||
<label className="block text-sm font-medium text-foreground mb-2">
|
<label className="block text-sm font-medium text-foreground mb-2">
|
||||||
SSH Authorized Key
|
SSH Authorized Key
|
||||||
</label>
|
</label>
|
||||||
|
{discoveredSshKeysLoading && (
|
||||||
|
<p className="text-sm text-muted-foreground mb-2">Detecting SSH keys...</p>
|
||||||
|
)}
|
||||||
|
{discoveredSshKeysError && !discoveredSshKeysLoading && (
|
||||||
|
<p className="text-sm text-muted-foreground mb-2">Could not detect keys on host</p>
|
||||||
|
)}
|
||||||
|
{discoveredSshKeys.length > 0 && !discoveredSshKeysLoading && (
|
||||||
|
<div className="mb-2">
|
||||||
|
<label htmlFor="discover-ssh-key" className="sr-only">Use detected key</label>
|
||||||
|
<select
|
||||||
|
id="discover-ssh-key"
|
||||||
|
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus:ring-2 focus:ring-ring focus:outline-none mb-2"
|
||||||
|
value=""
|
||||||
|
onChange={(e) => {
|
||||||
|
const idx = e.target.value;
|
||||||
|
if (idx === '') return;
|
||||||
|
const key = discoveredSshKeys[Number(idx)];
|
||||||
|
if (key) updateAdvancedVar('var_ssh_authorized_key', key);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="">— Select or paste below —</option>
|
||||||
|
{discoveredSshKeys.map((key, i) => (
|
||||||
|
<option key={i} value={i}>
|
||||||
|
{key.length > 44 ? `${key.slice(0, 44)}...` : key}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
value={typeof advancedVars.var_ssh_authorized_key === 'boolean' ? '' : String(advancedVars.var_ssh_authorized_key ?? '')}
|
value={typeof advancedVars.var_ssh_authorized_key === 'boolean' ? '' : String(advancedVars.var_ssh_authorized_key ?? '')}
|
||||||
onChange={(e) => updateAdvancedVar('var_ssh_authorized_key', e.target.value)}
|
onChange={(e) => updateAdvancedVar('var_ssh_authorized_key', e.target.value)}
|
||||||
placeholder="ssh-rsa AAAA..."
|
placeholder="Or paste a public key: ssh-rsa AAAA..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import { ScriptDetailModal } from "./ScriptDetailModal";
|
|||||||
import { CategorySidebar } from "./CategorySidebar";
|
import { CategorySidebar } from "./CategorySidebar";
|
||||||
import { FilterBar, type FilterState } from "./FilterBar";
|
import { FilterBar, type FilterState } from "./FilterBar";
|
||||||
import { ViewToggle } from "./ViewToggle";
|
import { ViewToggle } from "./ViewToggle";
|
||||||
|
import { ConfirmationModal } from "./ConfirmationModal";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
|
import { RefreshCw } from "lucide-react";
|
||||||
import type { ScriptCard as ScriptCardType } from "~/types/script";
|
import type { ScriptCard as ScriptCardType } from "~/types/script";
|
||||||
import type { Server } from "~/types/server";
|
import type { Server } from "~/types/server";
|
||||||
import { getDefaultFilters, mergeFiltersWithDefaults } from "./filterUtils";
|
import { getDefaultFilters, mergeFiltersWithDefaults } from "./filterUtils";
|
||||||
@@ -32,8 +34,15 @@ export function DownloadedScriptsTab({
|
|||||||
const [filters, setFilters] = useState<FilterState>(getDefaultFilters());
|
const [filters, setFilters] = useState<FilterState>(getDefaultFilters());
|
||||||
const [saveFiltersEnabled, setSaveFiltersEnabled] = useState(false);
|
const [saveFiltersEnabled, setSaveFiltersEnabled] = useState(false);
|
||||||
const [isLoadingFilters, setIsLoadingFilters] = useState(true);
|
const [isLoadingFilters, setIsLoadingFilters] = useState(true);
|
||||||
|
const [updateAllConfirmOpen, setUpdateAllConfirmOpen] = useState(false);
|
||||||
|
const [updateResult, setUpdateResult] = useState<{
|
||||||
|
successCount: number;
|
||||||
|
failCount: number;
|
||||||
|
failed: { slug: string; error: string }[];
|
||||||
|
} | null>(null);
|
||||||
const gridRef = useRef<HTMLDivElement>(null);
|
const gridRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const utils = api.useUtils();
|
||||||
const {
|
const {
|
||||||
data: scriptCardsData,
|
data: scriptCardsData,
|
||||||
isLoading: githubLoading,
|
isLoading: githubLoading,
|
||||||
@@ -50,6 +59,30 @@ export function DownloadedScriptsTab({
|
|||||||
{ enabled: !!selectedSlug },
|
{ enabled: !!selectedSlug },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const loadMultipleScriptsMutation = api.scripts.loadMultipleScripts.useMutation({
|
||||||
|
onSuccess: (data) => {
|
||||||
|
void utils.scripts.getAllDownloadedScripts.invalidate();
|
||||||
|
void utils.scripts.getScriptCardsWithCategories.invalidate();
|
||||||
|
setUpdateResult({
|
||||||
|
successCount: data.successful?.length ?? 0,
|
||||||
|
failCount: data.failed?.length ?? 0,
|
||||||
|
failed: (data.failed ?? []).map((f) => ({
|
||||||
|
slug: f.slug,
|
||||||
|
error: f.error ?? "Unknown error",
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
setTimeout(() => setUpdateResult(null), 8000);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
setUpdateResult({
|
||||||
|
successCount: 0,
|
||||||
|
failCount: 1,
|
||||||
|
failed: [{ slug: "Request failed", error: error.message }],
|
||||||
|
});
|
||||||
|
setTimeout(() => setUpdateResult(null), 8000);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// Load SAVE_FILTER setting, saved filters, and view mode on component mount
|
// Load SAVE_FILTER setting, saved filters, and view mode on component mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadSettings = async () => {
|
const loadSettings = async () => {
|
||||||
@@ -416,6 +449,21 @@ export function DownloadedScriptsTab({
|
|||||||
setSelectedSlug(null);
|
setSelectedSlug(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleUpdateAllClick = () => {
|
||||||
|
setUpdateResult(null);
|
||||||
|
setUpdateAllConfirmOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateAllConfirm = () => {
|
||||||
|
setUpdateAllConfirmOpen(false);
|
||||||
|
const slugs = downloadedScripts
|
||||||
|
.map((s) => s.slug)
|
||||||
|
.filter((slug): slug is string => Boolean(slug));
|
||||||
|
if (slugs.length > 0) {
|
||||||
|
loadMultipleScriptsMutation.mutate({ slugs });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (githubLoading || localLoading) {
|
if (githubLoading || localLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center py-12">
|
<div className="flex items-center justify-center py-12">
|
||||||
@@ -508,6 +556,43 @@ export function DownloadedScriptsTab({
|
|||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<div className="order-1 min-w-0 flex-1 lg:order-2" ref={gridRef}>
|
<div className="order-1 min-w-0 flex-1 lg:order-2" ref={gridRef}>
|
||||||
|
{/* Update all downloaded scripts */}
|
||||||
|
<div className="mb-4 flex flex-wrap items-center gap-3">
|
||||||
|
<Button
|
||||||
|
onClick={handleUpdateAllClick}
|
||||||
|
disabled={loadMultipleScriptsMutation.isPending}
|
||||||
|
variant="secondary"
|
||||||
|
size="default"
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
{loadMultipleScriptsMutation.isPending ? (
|
||||||
|
<>
|
||||||
|
<RefreshCw className="h-4 w-4 animate-spin" />
|
||||||
|
<span>Updating...</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<RefreshCw className="h-4 w-4" />
|
||||||
|
<span>Update all downloaded scripts</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
{updateResult && (
|
||||||
|
<span className="text-muted-foreground text-sm">
|
||||||
|
Updated {updateResult.successCount} successfully
|
||||||
|
{updateResult.failCount > 0
|
||||||
|
? `, ${updateResult.failCount} failed`
|
||||||
|
: ""}
|
||||||
|
.
|
||||||
|
{updateResult.failCount > 0 && updateResult.failed.length > 0 && (
|
||||||
|
<span className="ml-1" title={updateResult.failed.map((f) => `${f.slug}: ${f.error}`).join("\n")}>
|
||||||
|
(hover for details)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Enhanced Filter Bar */}
|
{/* Enhanced Filter Bar */}
|
||||||
<FilterBar
|
<FilterBar
|
||||||
filters={filters}
|
filters={filters}
|
||||||
@@ -621,6 +706,17 @@ export function DownloadedScriptsTab({
|
|||||||
onClose={handleCloseModal}
|
onClose={handleCloseModal}
|
||||||
onInstallScript={onInstallScript}
|
onInstallScript={onInstallScript}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ConfirmationModal
|
||||||
|
isOpen={updateAllConfirmOpen}
|
||||||
|
onClose={() => setUpdateAllConfirmOpen(false)}
|
||||||
|
onConfirm={handleUpdateAllConfirm}
|
||||||
|
title="Update all downloaded scripts"
|
||||||
|
message={`Update all ${downloadedScripts.length} downloaded scripts? This may take several minutes.`}
|
||||||
|
variant="simple"
|
||||||
|
confirmButtonText="Update all"
|
||||||
|
cancelButtonText="Cancel"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
96
src/app/api/servers/[id]/discover-ssh-keys/route.ts
Normal file
96
src/app/api/servers/[id]/discover-ssh-keys/route.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import type { NextRequest } from 'next/server';
|
||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { getDatabase } from '../../../../../server/database-prisma';
|
||||||
|
import { getSSHExecutionService } from '../../../../../server/ssh-execution-service';
|
||||||
|
import type { Server } from '~/types/server';
|
||||||
|
|
||||||
|
const DISCOVER_TIMEOUT_MS = 10_000;
|
||||||
|
|
||||||
|
/** Match lines that look like SSH public keys (same as build.func) */
|
||||||
|
const SSH_PUBKEY_RE = /^(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-(ssh-ed25519|ecdsa-sha2-nistp256))\s+/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a command on the Proxmox host and return buffered stdout.
|
||||||
|
* Resolves when the process exits or rejects on timeout/spawn error.
|
||||||
|
*/
|
||||||
|
function runRemoteCommand(
|
||||||
|
server: Server,
|
||||||
|
command: string,
|
||||||
|
timeoutMs: number
|
||||||
|
): Promise<{ stdout: string; exitCode: number }> {
|
||||||
|
const ssh = getSSHExecutionService();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const chunks: string[] = [];
|
||||||
|
let settled = false;
|
||||||
|
|
||||||
|
const finish = (stdout: string, exitCode: number) => {
|
||||||
|
if (settled) return;
|
||||||
|
settled = true;
|
||||||
|
clearTimeout(timer);
|
||||||
|
resolve({ stdout, exitCode });
|
||||||
|
};
|
||||||
|
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
if (settled) return;
|
||||||
|
settled = true;
|
||||||
|
reject(new Error('SSH discover keys timeout'));
|
||||||
|
}, timeoutMs);
|
||||||
|
|
||||||
|
ssh
|
||||||
|
.executeCommand(
|
||||||
|
server,
|
||||||
|
command,
|
||||||
|
(data: string) => chunks.push(data),
|
||||||
|
() => {},
|
||||||
|
(code: number) => finish(chunks.join(''), code)
|
||||||
|
)
|
||||||
|
.catch((err) => {
|
||||||
|
if (!settled) {
|
||||||
|
settled = true;
|
||||||
|
clearTimeout(timer);
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
_request: NextRequest,
|
||||||
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const { id: idParam } = await params;
|
||||||
|
const id = parseInt(idParam);
|
||||||
|
if (isNaN(id)) {
|
||||||
|
return NextResponse.json({ error: 'Invalid server ID' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = getDatabase();
|
||||||
|
const server = await db.getServerById(id) as Server | null;
|
||||||
|
|
||||||
|
if (!server) {
|
||||||
|
return NextResponse.json({ error: 'Server not found' }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same paths as native build.func ssh_discover_default_files()
|
||||||
|
const remoteScript = `bash -c 'for f in /root/.ssh/authorized_keys /root/.ssh/authorized_keys2 /root/.ssh/*.pub /etc/ssh/authorized_keys /etc/ssh/authorized_keys.d/* 2>/dev/null; do [ -f "$f" ] && [ -r "$f" ] && grep -E "^(ssh-(rsa|ed25519)|ecdsa-sha2-nistp256|sk-)" "$f" 2>/dev/null; done | sort -u'`;
|
||||||
|
|
||||||
|
const { stdout } = await runRemoteCommand(server, remoteScript, DISCOVER_TIMEOUT_MS);
|
||||||
|
|
||||||
|
const keys = stdout
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter((line) => line.length > 0 && SSH_PUBKEY_RE.test(line));
|
||||||
|
|
||||||
|
return NextResponse.json({ keys });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error discovering SSH keys:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
},
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -238,6 +238,27 @@ export const versionRouter = createTRPCRouter({
|
|||||||
// Clear/create the log file
|
// Clear/create the log file
|
||||||
await writeFile(logPath, '', 'utf-8');
|
await writeFile(logPath, '', 'utf-8');
|
||||||
|
|
||||||
|
// Always fetch the latest update.sh from GitHub before running
|
||||||
|
// This ensures we always use the newest update script, avoiding
|
||||||
|
// the "chicken-and-egg" problem where old scripts can't update properly
|
||||||
|
const updateScriptUrl = 'https://raw.githubusercontent.com/community-scripts/ProxmoxVE-Local/main/update.sh';
|
||||||
|
try {
|
||||||
|
const response = await fetch(updateScriptUrl);
|
||||||
|
if (response.ok) {
|
||||||
|
const latestScript = await response.text();
|
||||||
|
await writeFile(updateScriptPath, latestScript, { mode: 0o755 });
|
||||||
|
// Log that we fetched the latest script
|
||||||
|
await writeFile(logPath, '[INFO] Fetched latest update.sh from GitHub\n', { flag: 'a' });
|
||||||
|
} else {
|
||||||
|
// If fetch fails, log warning but continue with local script
|
||||||
|
await writeFile(logPath, `[WARNING] Could not fetch latest update.sh (HTTP ${response.status}), using local version\n`, { flag: 'a' });
|
||||||
|
}
|
||||||
|
} catch (fetchError) {
|
||||||
|
// If fetch fails, log warning but continue with local script
|
||||||
|
const errorMsg = fetchError instanceof Error ? fetchError.message : 'Unknown error';
|
||||||
|
await writeFile(logPath, `[WARNING] Could not fetch latest update.sh: ${errorMsg}, using local version\n`, { flag: 'a' });
|
||||||
|
}
|
||||||
|
|
||||||
// Spawn the update script as a detached process using nohup
|
// Spawn the update script as a detached process using nohup
|
||||||
// This allows it to run independently and kill the parent Node.js process
|
// This allows it to run independently and kill the parent Node.js process
|
||||||
// Redirect output to log file
|
// Redirect output to log file
|
||||||
|
|||||||
54
update.sh
54
update.sh
@@ -710,11 +710,14 @@ install_and_build() {
|
|||||||
log "Building application..."
|
log "Building application..."
|
||||||
# Set NODE_ENV to production for build
|
# Set NODE_ENV to production for build
|
||||||
export NODE_ENV=production
|
export NODE_ENV=production
|
||||||
|
# Unset TURBOPACK to prevent "Multiple bundler flags" error with --webpack
|
||||||
|
unset TURBOPACK 2>/dev/null || true
|
||||||
|
export TURBOPACK=''
|
||||||
|
|
||||||
# Create temporary file for npm build output
|
# Create temporary file for npm build output
|
||||||
local build_log="/tmp/npm_build_$$.log"
|
local build_log="/tmp/npm_build_$$.log"
|
||||||
|
|
||||||
if ! npm run build >"$build_log" 2>&1; then
|
if ! TURBOPACK='' npm run build >"$build_log" 2>&1; then
|
||||||
log_error "Failed to build application"
|
log_error "Failed to build application"
|
||||||
log_error "npm run build output:"
|
log_error "npm run build output:"
|
||||||
cat "$build_log" | while read -r line; do
|
cat "$build_log" | while read -r line; do
|
||||||
@@ -781,6 +784,23 @@ start_with_npm() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Re-enable the systemd service on failure to prevent users from being locked out
|
||||||
|
re_enable_service_on_failure() {
|
||||||
|
if check_service; then
|
||||||
|
log "Re-enabling systemd service after failure..."
|
||||||
|
if systemctl enable pvescriptslocal.service 2>/dev/null; then
|
||||||
|
log_success "Service re-enabled"
|
||||||
|
if systemctl start pvescriptslocal.service 2>/dev/null; then
|
||||||
|
log_success "Service started"
|
||||||
|
else
|
||||||
|
log_warning "Failed to start service - manual intervention may be required"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log_warning "Failed to re-enable service - manual intervention may be required"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# Rollback function
|
# Rollback function
|
||||||
rollback() {
|
rollback() {
|
||||||
log_warning "Rolling back to previous version..."
|
log_warning "Rolling back to previous version..."
|
||||||
@@ -852,6 +872,9 @@ rollback() {
|
|||||||
log_error "No backup directory found for rollback"
|
log_error "No backup directory found for rollback"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Re-enable the service so users aren't locked out
|
||||||
|
re_enable_service_on_failure
|
||||||
|
|
||||||
log_error "Update failed. Please check the logs and try again."
|
log_error "Update failed. Please check the logs and try again."
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -870,14 +893,14 @@ check_node_version() {
|
|||||||
|
|
||||||
log "Detected Node.js version: $current"
|
log "Detected Node.js version: $current"
|
||||||
|
|
||||||
if ((major_version < 24)); then
|
if ((major_version == 24)); then
|
||||||
|
log_success "Node.js 24 already installed"
|
||||||
|
elif ((major_version < 24)); then
|
||||||
log_warning "Node.js < 24 detected → upgrading to Node.js 24 LTS..."
|
log_warning "Node.js < 24 detected → upgrading to Node.js 24 LTS..."
|
||||||
upgrade_node_to_24
|
upgrade_node_to_24
|
||||||
elif ((major_version > 24)); then
|
else
|
||||||
log_warning "Node.js > 24 detected → script tested only up to Node 24"
|
log_warning "Node.js > 24 detected → script tested only up to Node 24"
|
||||||
log "Continuing anyway…"
|
log "Continuing anyway…"
|
||||||
else
|
|
||||||
log_success "Node.js 24 already installed"
|
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -885,22 +908,39 @@ check_node_version() {
|
|||||||
upgrade_node_to_24() {
|
upgrade_node_to_24() {
|
||||||
log "Preparing Node.js 24 upgrade…"
|
log "Preparing Node.js 24 upgrade…"
|
||||||
|
|
||||||
# Remove old nodesource repo if it exists
|
# Remove old nodesource repo files if they exist
|
||||||
if [ -f /etc/apt/sources.list.d/nodesource.list ]; then
|
if [ -f /etc/apt/sources.list.d/nodesource.list ]; then
|
||||||
|
log "Removing old nodesource.list file..."
|
||||||
rm -f /etc/apt/sources.list.d/nodesource.list
|
rm -f /etc/apt/sources.list.d/nodesource.list
|
||||||
fi
|
fi
|
||||||
|
if [ -f /etc/apt/sources.list.d/nodesource.sources ]; then
|
||||||
|
log "Removing old nodesource.sources file..."
|
||||||
|
rm -f /etc/apt/sources.list.d/nodesource.sources
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Update apt cache first
|
||||||
|
log "Updating apt cache..."
|
||||||
|
apt-get update >>"$LOG_FILE" 2>&1 || true
|
||||||
|
|
||||||
# Install NodeSource repo for Node.js 24
|
# Install NodeSource repo for Node.js 24
|
||||||
curl -fsSL https://deb.nodesource.com/setup_24.x -o /tmp/node24_setup.sh
|
log "Downloading Node.js 24 setup script..."
|
||||||
|
if ! curl -fsSL https://deb.nodesource.com/setup_24.x -o /tmp/node24_setup.sh; then
|
||||||
|
log_error "Failed to download Node.js 24 setup script"
|
||||||
|
re_enable_service_on_failure
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
if ! bash /tmp/node24_setup.sh >/tmp/node24_setup.log 2>&1; then
|
if ! bash /tmp/node24_setup.sh >/tmp/node24_setup.log 2>&1; then
|
||||||
log_error "Failed to configure Node.js 24 repository"
|
log_error "Failed to configure Node.js 24 repository"
|
||||||
tail -20 /tmp/node24_setup.log | while read -r line; do log_error "$line"; done
|
tail -20 /tmp/node24_setup.log | while read -r line; do log_error "$line"; done
|
||||||
|
re_enable_service_on_failure
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log "Installing Node.js 24…"
|
log "Installing Node.js 24…"
|
||||||
if ! apt-get install -y nodejs >>"$LOG_FILE" 2>&1; then
|
if ! apt-get install -y nodejs >>"$LOG_FILE" 2>&1; then
|
||||||
log_error "Failed to install Node.js 24"
|
log_error "Failed to install Node.js 24"
|
||||||
|
re_enable_service_on_failure
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user