Compare commits
10 Commits
fix/197
...
fix/toggle
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ea6021c8a | ||
|
|
fcb9aafc8d | ||
|
|
266ff5a79f | ||
|
|
adccee027c | ||
|
|
ec38ab9c7a | ||
|
|
cacd4b3f86 | ||
|
|
f3c68bf351 | ||
|
|
f8d3b5b166 | ||
|
|
e4de9cc8f6 | ||
|
|
cc8fd3ea3a |
34
.github/workflows/publish_release.yml
vendored
34
.github/workflows/publish_release.yml
vendored
@@ -45,7 +45,39 @@ 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
|
||||
git push --set-upstream origin "$branch"
|
||||
|
||||
|
||||
- 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 }}"
|
||||
|
||||
|
||||
- name: Create PR with GitHub CLI
|
||||
|
||||
@@ -5,12 +5,14 @@ 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,8 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Button } from './ui/button';
|
||||
import { AlertTriangle, Info } from 'lucide-react';
|
||||
import { useRegisterModal } from './modal/ModalStackProvider';
|
||||
|
||||
interface ConfirmationModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -28,10 +29,12 @@ 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,6 +169,13 @@ 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) {
|
||||
@@ -178,9 +185,13 @@ 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 localName = local.name.replace(/\.sh$/, '');
|
||||
return localName.toLowerCase() === script.name.toLowerCase() ||
|
||||
localName.toLowerCase() === (script.slug ?? '').toLowerCase();
|
||||
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;
|
||||
}) ?? false;
|
||||
|
||||
return {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Button } from './ui/button';
|
||||
import { AlertCircle, CheckCircle } from 'lucide-react';
|
||||
import { useRegisterModal } from './modal/ModalStackProvider';
|
||||
|
||||
interface ErrorModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -21,6 +22,7 @@ export function ErrorModal({
|
||||
details,
|
||||
type = 'error'
|
||||
}: ErrorModalProps) {
|
||||
useRegisterModal(isOpen, { id: 'error-modal', allowEscape: true, onClose });
|
||||
// Auto-close after 10 seconds
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
|
||||
@@ -5,6 +5,7 @@ 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 {
|
||||
@@ -15,6 +16,7 @@ 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,6 +6,7 @@ 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;
|
||||
@@ -13,6 +14,7 @@ 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,6 +3,7 @@
|
||||
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;
|
||||
@@ -13,6 +14,7 @@ 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,6 +9,7 @@ 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;
|
||||
@@ -41,6 +42,7 @@ 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,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { useRegisterModal } from './modal/ModalStackProvider';
|
||||
|
||||
interface LoadingModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -8,6 +9,7 @@ interface LoadingModalProps {
|
||||
}
|
||||
|
||||
export function LoadingModal({ isOpen, action }: LoadingModalProps) {
|
||||
useRegisterModal(isOpen, { id: 'loading-modal', allowEscape: false, onClose: () => null });
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
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;
|
||||
@@ -13,6 +14,7 @@ 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,6 +5,7 @@ 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';
|
||||
|
||||
@@ -34,6 +35,7 @@ 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,6 +9,7 @@ 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;
|
||||
@@ -28,6 +29,7 @@ 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,6 +200,13 @@ 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
|
||||
@@ -208,9 +215,13 @@ 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 localName = local.name.replace(/\.sh$/, '');
|
||||
return localName.toLowerCase() === script.name.toLowerCase() ||
|
||||
localName.toLowerCase() === (script.slug ?? '').toLowerCase();
|
||||
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;
|
||||
}) ?? false;
|
||||
|
||||
return {
|
||||
@@ -651,8 +662,8 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
|
||||
onViewModeChange={setViewMode}
|
||||
/>
|
||||
|
||||
{/* Newest Scripts Carousel - Only show when no filters are active */}
|
||||
{!hasActiveFilters && newestScripts.length > 0 && (
|
||||
{/* Newest Scripts Carousel - Always show when there are newest scripts */}
|
||||
{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">
|
||||
|
||||
@@ -6,6 +6,7 @@ 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;
|
||||
@@ -13,6 +14,7 @@ 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,6 +5,7 @@ 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;
|
||||
@@ -12,6 +13,7 @@ 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('');
|
||||
|
||||
57
src/app/_components/modal/ModalStackProvider.tsx
Normal file
57
src/app/_components/modal/ModalStackProvider.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
'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]);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,10 +22,10 @@ const Toggle = React.forwardRef<HTMLInputElement, ToggleProps>(
|
||||
{...props}
|
||||
/>
|
||||
<div className={cn(
|
||||
"w-11 h-6 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary/20 rounded-full peer after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 dark:after:border-gray-500 after:border after:rounded-full after:h-5 after:w-5 after:transition-transform after:duration-300 after:ease-in-out transition-colors duration-300 ease-in-out",
|
||||
"w-11 h-6 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary/20 rounded-full peer after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 dark:after:border-gray-500 after:border after:rounded-full after:h-5 after:w-5 after:transition-transform after:duration-300 after:ease-in-out after:shadow-md transition-colors duration-300 ease-in-out border-2 border-gray-300 dark:border-gray-600",
|
||||
checked
|
||||
? "bg-primary after:translate-x-full"
|
||||
: "bg-gray-200 dark:bg-gray-600",
|
||||
? "bg-blue-500 dark:bg-blue-600 after:translate-x-full"
|
||||
: "bg-gray-300 dark:bg-gray-700",
|
||||
className
|
||||
)} />
|
||||
</label>
|
||||
|
||||
@@ -7,6 +7,7 @@ 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",
|
||||
@@ -41,9 +42,11 @@ export default function RootLayout({
|
||||
<ThemeProvider>
|
||||
<TRPCReactProvider>
|
||||
<AuthProvider>
|
||||
<AuthGuard>
|
||||
{children}
|
||||
</AuthGuard>
|
||||
<ModalStackProvider>
|
||||
<AuthGuard>
|
||||
{children}
|
||||
</AuthGuard>
|
||||
</ModalStackProvider>
|
||||
</AuthProvider>
|
||||
</TRPCReactProvider>
|
||||
</ThemeProvider>
|
||||
|
||||
@@ -908,21 +908,44 @@ 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) => {
|
||||
resolve(data.trim() === 'exists');
|
||||
combinedOutput += data;
|
||||
},
|
||||
(error: string) => {
|
||||
console.error(`Error checking container ${scriptData.script_name}:`, error);
|
||||
resolve(false);
|
||||
combinedOutput += error;
|
||||
},
|
||||
(_exitCode: number) => {
|
||||
resolve(false);
|
||||
clearTimeout(timer);
|
||||
finish();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -177,6 +177,15 @@ 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,
|
||||
@@ -189,6 +198,7 @@ export const scriptsRouter = createTRPCRouter({
|
||||
version: version,
|
||||
// Add interface port
|
||||
interface_port: script?.interface_port,
|
||||
install_basenames,
|
||||
} as ScriptCard;
|
||||
});
|
||||
|
||||
|
||||
@@ -60,6 +60,8 @@ 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