mirror of
https://github.com/community-scripts/ProxmoxVE-Local.git
synced 2026-05-15 20:32:06 -04:00
Compare commits
2 Commits
v1.0.0-pre
...
v1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
103c4dfe42 | ||
|
|
ed30b7b366 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pve-scripts-local",
|
||||
"version": "1.0.0-pre8",
|
||||
"version": "1.0.0-pre9",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -345,13 +345,11 @@ export function DownloadedScriptsTab() {
|
||||
scripts = scripts.filter((script) => {
|
||||
if (!script) return false;
|
||||
const scriptType = (script.type ?? "").toLowerCase();
|
||||
|
||||
// Map non-standard types to standard categories
|
||||
const mappedType = scriptType === "turnkey" ? "ct" : scriptType;
|
||||
|
||||
return filters.selectedTypes.some(
|
||||
(type) => type.toLowerCase() === mappedType,
|
||||
);
|
||||
return filters.selectedTypes.some((type) => {
|
||||
const t = type.toLowerCase();
|
||||
if (t === "ct") return scriptType === "ct" || scriptType === "lxc";
|
||||
return scriptType === t;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
TrendingDown,
|
||||
FlaskConical,
|
||||
Cpu,
|
||||
Settings2,
|
||||
} from "lucide-react";
|
||||
import { api } from "~/trpc/react";
|
||||
import { getDefaultFilters } from "./filterUtils";
|
||||
@@ -58,6 +59,9 @@ interface FilterBarProps {
|
||||
const SCRIPT_TYPES = [
|
||||
{ value: "ct", label: "LXC Container", Icon: Package },
|
||||
{ value: "vm", label: "Virtual Machine", Icon: Monitor },
|
||||
{ value: "pve", label: "PVE Tools", Icon: Settings2 },
|
||||
{ value: "addon", label: "Addons", Icon: Sparkles },
|
||||
{ value: "turnkey", label: "TurnKey", Icon: FileText },
|
||||
];
|
||||
|
||||
export function FilterBar({
|
||||
|
||||
@@ -253,6 +253,9 @@ export function GeneratorTab() {
|
||||
if (!scriptCardsData?.success || !scriptCardsData.cards) return [];
|
||||
const map = new Map<string, ScriptCard>();
|
||||
for (const s of scriptCardsData.cards) {
|
||||
const t = (s?.type ?? "").toLowerCase();
|
||||
// Generator only supports LXC scripts in pre9.
|
||||
if (!(t === "ct" || t === "lxc")) continue;
|
||||
if (s?.slug && !map.has(s.slug)) map.set(s.slug, s);
|
||||
}
|
||||
return Array.from(map.values()).sort((a, b) =>
|
||||
@@ -265,16 +268,17 @@ export function GeneratorTab() {
|
||||
[allScripts, selectedSlug],
|
||||
);
|
||||
|
||||
const isAddonScript = selectedScript?.type === "addon";
|
||||
const selectedType = (selectedScript?.type ?? "").toLowerCase();
|
||||
const isLxcType = selectedType === "ct" || selectedType === "lxc";
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
isAddonScript &&
|
||||
!isLxcType &&
|
||||
(installMode === "mydefaults" || installMode === "appdefaults")
|
||||
) {
|
||||
setInstallMode("default");
|
||||
}
|
||||
}, [isAddonScript, installMode]);
|
||||
}, [isLxcType, installMode]);
|
||||
|
||||
// Fetch full script detail (with install_methods) when a script is selected
|
||||
const { data: scriptDetailData } = api.scripts.getScriptBySlug.useQuery(
|
||||
@@ -287,10 +291,17 @@ export function GeneratorTab() {
|
||||
return scriptDetailData.script.execute_in ?? [];
|
||||
}, [scriptDetailData]);
|
||||
|
||||
const needsContainerPicker = useMemo(
|
||||
() => executeIn.some((e) => ["lxc", "vm", "pbs", "pmg"].includes(e)),
|
||||
[executeIn],
|
||||
);
|
||||
const executionPolicy = useMemo(() => {
|
||||
const has = (v: string) => executeIn.includes(v);
|
||||
const allowVm = has("vm");
|
||||
const allowLxcByMode =
|
||||
has("pbs") || has("pmg") || (has("lxc") && !isLxcType);
|
||||
const requiresContainer = allowVm || allowLxcByMode;
|
||||
const pinMode = has("pbs") ? "pbs" : has("pmg") ? "pmg" : null;
|
||||
return { requiresContainer, allowVm, allowLxcByMode, pinMode };
|
||||
}, [executeIn, isLxcType]);
|
||||
|
||||
const needsContainerPicker = executionPolicy.requiresContainer;
|
||||
|
||||
const { data: containersData, isLoading: containersLoading } =
|
||||
api.installedScripts.listContainersOnServer.useQuery(
|
||||
@@ -307,14 +318,14 @@ export function GeneratorTab() {
|
||||
status: string;
|
||||
pinned: boolean;
|
||||
}> = [];
|
||||
const wantLxc = executeIn.some((e) => ["lxc", "pbs", "pmg"].includes(e));
|
||||
const wantVm = executeIn.includes("vm");
|
||||
const wantLxc = executionPolicy.allowLxcByMode;
|
||||
const wantVm = executionPolicy.allowVm;
|
||||
if (wantLxc) {
|
||||
for (const c of containersData.lxc) {
|
||||
const pinned =
|
||||
(executeIn.includes("pbs") &&
|
||||
(executionPolicy.pinMode === "pbs" &&
|
||||
c.name.toLowerCase().includes("proxmox-backup-server")) ||
|
||||
(executeIn.includes("pmg") &&
|
||||
(executionPolicy.pinMode === "pmg" &&
|
||||
c.name.toLowerCase().includes("proxmox-mail-gateway"));
|
||||
opts.push({ ...c, isVm: false, pinned });
|
||||
}
|
||||
@@ -329,7 +340,7 @@ export function GeneratorTab() {
|
||||
if (!a.pinned && b.pinned) return 1;
|
||||
return parseInt(a.id, 10) - parseInt(b.id, 10);
|
||||
});
|
||||
}, [executeIn, containersData]);
|
||||
}, [executionPolicy, containersData]);
|
||||
|
||||
const scriptDetail: Script | null = useMemo(
|
||||
() =>
|
||||
@@ -792,7 +803,8 @@ export function GeneratorTab() {
|
||||
// Determine whether to execute inside the container.
|
||||
// execute_in: ["lxc"] (or vm/pbs/pmg) means the script runs INSIDE the
|
||||
// selected container rather than on the PVE host.
|
||||
const execInContainer = needsContainerPicker && !!selectedContainerId;
|
||||
const execInContainer =
|
||||
executionPolicy.requiresContainer && !!selectedContainerId;
|
||||
|
||||
openShell({
|
||||
sessionKey: `generator-${selectedScript.slug}-${Date.now()}`,
|
||||
@@ -818,7 +830,7 @@ export function GeneratorTab() {
|
||||
selectedScript,
|
||||
selectedServer,
|
||||
selectedContainerId,
|
||||
needsContainerPicker,
|
||||
executionPolicy,
|
||||
installMode,
|
||||
cpu,
|
||||
ram,
|
||||
@@ -1067,7 +1079,7 @@ export function GeneratorTab() {
|
||||
<div className="bg-muted/40 mb-5 flex flex-wrap rounded-lg p-0.5">
|
||||
{[
|
||||
{ key: "default", label: "Default" },
|
||||
...(!isAddonScript
|
||||
...(isLxcType
|
||||
? [
|
||||
{ key: "mydefaults", label: "My Defaults" },
|
||||
{ key: "appdefaults", label: "App Defaults" },
|
||||
|
||||
@@ -165,10 +165,20 @@ export function InstallCommandBlock({
|
||||
const [selectedServer, setSelectedServer] = useState<ServerType | null>(null);
|
||||
const [serversLoading, setServersLoading] = useState(false);
|
||||
|
||||
// Container picker state (for addon scripts with execute_in)
|
||||
const needsContainerPicker = !!executeIn?.some((e) =>
|
||||
CONTAINER_TYPES.includes(e as (typeof CONTAINER_TYPES)[number]),
|
||||
);
|
||||
const scriptTypeNorm = (scriptType ?? "").toLowerCase();
|
||||
const isLxcType = scriptTypeNorm === "ct" || scriptTypeNorm === "lxc";
|
||||
const executionPolicy = (() => {
|
||||
const has = (v: string) => !!executeIn?.includes(v);
|
||||
const allowVm = has("vm");
|
||||
const allowLxcByMode =
|
||||
has("pbs") || has("pmg") || (has("lxc") && !isLxcType);
|
||||
const requiresContainer = allowVm || allowLxcByMode;
|
||||
const pinMode = has("pbs") ? "pbs" : has("pmg") ? "pmg" : null;
|
||||
return { requiresContainer, allowVm, allowLxcByMode, pinMode };
|
||||
})();
|
||||
|
||||
// Container picker state driven by execute_in + script type policy
|
||||
const needsContainerPicker = executionPolicy.requiresContainer;
|
||||
const [selectedContainerId, setSelectedContainerId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
@@ -188,18 +198,18 @@ export function InstallCommandBlock({
|
||||
status?: string;
|
||||
pinned?: boolean;
|
||||
}[] = [];
|
||||
const wantLxc =
|
||||
!executeIn || executeIn.some((e) => ["lxc", "pbs", "pmg"].includes(e));
|
||||
const wantVm = !executeIn || executeIn.includes("vm");
|
||||
const wantLxc = executionPolicy.allowLxcByMode;
|
||||
const wantVm = executionPolicy.allowVm;
|
||||
|
||||
if (wantLxc) {
|
||||
for (const c of containerQuery.data.lxc) {
|
||||
const lower = (c.name ?? "").toLowerCase();
|
||||
const pinned = executeIn?.includes("pbs")
|
||||
? lower.includes("proxmox-backup-server")
|
||||
: executeIn?.includes("pmg")
|
||||
? lower.includes("proxmox-mail-gateway")
|
||||
: false;
|
||||
const pinned =
|
||||
executionPolicy.pinMode === "pbs"
|
||||
? lower.includes("proxmox-backup-server")
|
||||
: executionPolicy.pinMode === "pmg"
|
||||
? lower.includes("proxmox-mail-gateway")
|
||||
: false;
|
||||
opts.push({
|
||||
id: String(c.id),
|
||||
name: c.name ?? String(c.id),
|
||||
@@ -259,15 +269,19 @@ export function InstallCommandBlock({
|
||||
snapToStep(defaults?.hdd ?? 2, HDD_PRESETS),
|
||||
);
|
||||
|
||||
const isAddonScript = (scriptType ?? "").toLowerCase() === "addon";
|
||||
const showAdvanced =
|
||||
scriptType === "ct" || scriptType === "lxc" || isAddonScript;
|
||||
const isAddonScript = scriptTypeNorm === "addon";
|
||||
const showAdvanced = isLxcType || isAddonScript;
|
||||
|
||||
const canUseDefaultsTabs = isLxcType;
|
||||
|
||||
useEffect(() => {
|
||||
if (isAddonScript && (env === "mydefaults" || env === "appdefaults")) {
|
||||
if (
|
||||
!canUseDefaultsTabs &&
|
||||
(env === "mydefaults" || env === "appdefaults")
|
||||
) {
|
||||
setEnv("default");
|
||||
}
|
||||
}, [isAddonScript, env]);
|
||||
}, [canUseDefaultsTabs, env]);
|
||||
|
||||
const defaultPath = getDefaultInstallPath(scriptType, slug);
|
||||
const alpinePath = getAlpineInstallPath(scriptType, slug);
|
||||
@@ -355,7 +369,7 @@ export function InstallCommandBlock({
|
||||
onClick={() => setEnv("default")}
|
||||
label="Default"
|
||||
/>
|
||||
{!isAddonScript && (
|
||||
{canUseDefaultsTabs && (
|
||||
<OptionToggle
|
||||
selected={env === "mydefaults"}
|
||||
onClick={() => setEnv("mydefaults")}
|
||||
@@ -363,7 +377,7 @@ export function InstallCommandBlock({
|
||||
title={MODE_DESCRIPTIONS.mydefaults}
|
||||
/>
|
||||
)}
|
||||
{!isAddonScript && (
|
||||
{canUseDefaultsTabs && (
|
||||
<OptionToggle
|
||||
selected={env === "appdefaults"}
|
||||
onClick={() => setEnv("appdefaults")}
|
||||
|
||||
@@ -346,12 +346,21 @@ export function InstalledScriptsTab() {
|
||||
// For each script, find its container status
|
||||
currentScripts.forEach((script) => {
|
||||
if (script.container_id && data.statusMap) {
|
||||
const scopedKey = script.server_id
|
||||
? `${script.server_id}:${script.container_id}`
|
||||
: script.container_id;
|
||||
const containerStatus = (
|
||||
data.statusMap as Record<
|
||||
string,
|
||||
"running" | "stopped" | "unknown"
|
||||
>
|
||||
)[script.container_id];
|
||||
)[scopedKey] ??
|
||||
(
|
||||
data.statusMap as Record<
|
||||
string,
|
||||
"running" | "stopped" | "unknown"
|
||||
>
|
||||
)[script.container_id];
|
||||
if (containerStatus) {
|
||||
statusMap.set(script.id, containerStatus);
|
||||
} else {
|
||||
@@ -1571,7 +1580,6 @@ export function InstalledScriptsTab() {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
|
||||
{/* Shell Terminal — now rendered as FloatingShell dialog (see ShellContext) */}
|
||||
|
||||
{/* Header with Stats */}
|
||||
|
||||
@@ -238,6 +238,8 @@ export function ScriptDetailModal({
|
||||
const hasDifferences =
|
||||
comparisonData?.success && comparisonData.hasDifferences;
|
||||
const isUpToDate = hasLocalFiles && !hasDifferences;
|
||||
const scriptTypeNorm = (script.type ?? "").toLowerCase();
|
||||
const isLxcType = scriptTypeNorm === "ct" || scriptTypeNorm === "lxc";
|
||||
|
||||
return (
|
||||
<ModalPortal>
|
||||
@@ -507,7 +509,7 @@ export function ScriptDetailModal({
|
||||
className={`h-2 w-2 rounded-full ${scriptFilesData.ctExists ? "bg-success" : "bg-muted-foreground/30"}`}
|
||||
/>
|
||||
<span className="text-muted-foreground">
|
||||
Script:{" "}
|
||||
Script File:{" "}
|
||||
<span className="text-foreground font-medium">
|
||||
{scriptFilesData.ctExists
|
||||
? "Available"
|
||||
@@ -515,19 +517,32 @@ export function ScriptDetailModal({
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<div
|
||||
className={`h-2 w-2 rounded-full ${scriptFilesData.installExists ? "bg-success" : "bg-muted-foreground/30"}`}
|
||||
/>
|
||||
<span className="text-muted-foreground">
|
||||
Install Script:{" "}
|
||||
<span className="text-foreground font-medium">
|
||||
{scriptFilesData.installExists
|
||||
? "Available"
|
||||
: "Not loaded"}
|
||||
{isLxcType ? (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<div
|
||||
className={`h-2 w-2 rounded-full ${scriptFilesData.installExists ? "bg-success" : "bg-muted-foreground/30"}`}
|
||||
/>
|
||||
<span className="text-muted-foreground">
|
||||
Install Script:{" "}
|
||||
<span className="text-foreground font-medium">
|
||||
{scriptFilesData.installExists
|
||||
? "Available"
|
||||
: "Not loaded"}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<div className="bg-muted-foreground/30 h-2 w-2 rounded-full" />
|
||||
<span className="text-muted-foreground">
|
||||
Install Script:{" "}
|
||||
<span className="text-foreground font-medium">
|
||||
N/A for{" "}
|
||||
{script.type?.toUpperCase() ?? "this type"}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{hasLocalFiles && (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<div
|
||||
@@ -557,7 +572,11 @@ export function ScriptDetailModal({
|
||||
)}
|
||||
{scriptFilesData.files.length > 0 && (
|
||||
<div className="text-muted-foreground mt-1 text-xs break-words">
|
||||
Files: {scriptFilesData.files.join(", ")}
|
||||
Files ({scriptFilesData.files.length}):{" "}
|
||||
{scriptFilesData.files
|
||||
.slice()
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.join(", ")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -192,7 +192,7 @@ export function ScriptsGrid() {
|
||||
.filter((name): name is string => typeof name === "string");
|
||||
}, [scriptCardsData]);
|
||||
|
||||
// Get GitHub scripts with download status (deduplicated), excluding unsupported types
|
||||
// Get GitHub scripts with download status (deduplicated)
|
||||
const combinedScripts = React.useMemo((): ScriptCardType[] => {
|
||||
if (!scriptCardsData?.success) return [];
|
||||
|
||||
@@ -201,8 +201,6 @@ export function ScriptsGrid() {
|
||||
|
||||
scriptCardsData.cards?.forEach((script: ScriptCardType) => {
|
||||
if (script?.name && script?.slug) {
|
||||
// Exclude addon/pve types from the Scripts grid
|
||||
if (script.type === "addon" || script.type === "pve") return;
|
||||
// Use slug as unique identifier, only keep first occurrence
|
||||
if (!scriptMap.has(script.slug)) {
|
||||
scriptMap.set(script.slug, {
|
||||
@@ -403,13 +401,11 @@ export function ScriptsGrid() {
|
||||
scripts = scripts.filter((script) => {
|
||||
if (!script) return false;
|
||||
const scriptType = (script.type ?? "").toLowerCase();
|
||||
|
||||
// Map non-standard types to standard categories
|
||||
const mappedType = scriptType === "turnkey" ? "ct" : scriptType;
|
||||
|
||||
return filters.selectedTypes.some(
|
||||
(type) => type.toLowerCase() === mappedType,
|
||||
);
|
||||
return filters.selectedTypes.some((type) => {
|
||||
const t = type.toLowerCase();
|
||||
if (t === "ct") return scriptType === "ct" || scriptType === "lxc";
|
||||
return scriptType === t;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -625,7 +625,7 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
try {
|
||||
const serverTypeMap = await batchDetectContainerTypes(server);
|
||||
for (const [containerId, isVM] of serverTypeMap.entries()) {
|
||||
containerTypeMap.set(containerId, isVM);
|
||||
containerTypeMap.set(`${serverId}:${containerId}`, isVM);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error batch detecting types for server ${serverId}:`, error);
|
||||
@@ -639,8 +639,9 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
// Use SSH detection result, fall back to DB heuristic
|
||||
let is_vm = false;
|
||||
if (script.container_id && script.server_id) {
|
||||
if (containerTypeMap.has(script.container_id)) {
|
||||
is_vm = containerTypeMap.get(script.container_id) ?? false;
|
||||
const key = `${script.server_id}:${script.container_id}`;
|
||||
if (containerTypeMap.has(key)) {
|
||||
is_vm = containerTypeMap.get(key) ?? false;
|
||||
}
|
||||
// If not in batch detection map, default to LXC (false) for safety
|
||||
// Only the batch detection (pct list / qm list) can reliably determine VM vs LXC
|
||||
@@ -1639,7 +1640,12 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
const vmStatuses = parseListStatuses(qmOutput);
|
||||
|
||||
// Merge both status maps (VMs will overwrite containers if same ID, but that's unlikely)
|
||||
Object.assign(statusMap, containerStatuses, vmStatuses);
|
||||
for (const [id, status] of Object.entries(containerStatuses)) {
|
||||
statusMap[`${(server as any).id}:${id}`] = status;
|
||||
}
|
||||
for (const [id, status] of Object.entries(vmStatuses)) {
|
||||
statusMap[`${(server as any).id}:${id}`] = status;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error processing server ${(server as any).name}:`, error);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import type { Server } from "~/types/server";
|
||||
import { cacheLogos, getLocalLogoPath } from "~/server/services/logoCacheService";
|
||||
|
||||
// Script types not yet supported in PVE-Local
|
||||
const UNSUPPORTED_TYPES = ['pve'] as const;
|
||||
const UNSUPPORTED_TYPES = [] as const;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mapper: PocketBase record → internal Script type (used by scriptDownloader)
|
||||
|
||||
Reference in New Issue
Block a user