1118 lines
75 KiB
HTML
1118 lines
75 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en" class="dark">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Pangolin Sentinel — Unraid to VPS Backup & Update Hub</title>
|
||
<!-- Tailwind CSS CDN -->
|
||
<script src="https://cdn.tailwindcss.com"></script>
|
||
<script>
|
||
tailwind.config = {
|
||
darkMode: 'class',
|
||
theme: {
|
||
extend: {
|
||
colors: {
|
||
brand: {
|
||
500: '#6366f1',
|
||
600: '#4f46e5',
|
||
},
|
||
unraid: {
|
||
500: '#ff7700',
|
||
600: '#e06600',
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
<!-- Vue 3 CDN -->
|
||
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
||
<!-- Custom CSS -->
|
||
<link rel="icon" type="image/png" href="/assets/icon.png">
|
||
<link rel="stylesheet" href="/assets/style.css">
|
||
</head>
|
||
<body class="bg-slate-950 text-slate-100 min-h-screen flex flex-col selection:bg-indigo-500 selection:text-white">
|
||
<div id="app" v-cloak class="min-h-screen flex flex-col">
|
||
|
||
<!-- Auth View -->
|
||
<div v-if="!authState.isLoggedIn && authState.enabled" class="flex-1 flex items-center justify-center p-6">
|
||
<!-- Setup Screen -->
|
||
<div v-if="authState.setupRequired" class="glass-panel p-8 rounded-2xl max-w-md w-full shadow-2xl border border-indigo-500/30">
|
||
<div class="flex items-center gap-3 mb-6">
|
||
<img src="/assets/icon.png" class="w-12 h-12 rounded-xl object-cover shadow-lg border border-indigo-500/30" alt="Logo">
|
||
<div>
|
||
<h1 class="text-2xl font-extrabold text-white">Initial Setup</h1>
|
||
<p class="text-xs text-slate-400">Pangolin Sentinel (Unraid Hub)</p>
|
||
</div>
|
||
</div>
|
||
<p class="text-sm text-slate-300 mb-6 leading-relaxed">
|
||
Configure an administrator password to secure your Unraid-to-VPS update and backup dashboard.
|
||
</p>
|
||
<div v-if="authState.error" class="mb-4 p-3 rounded-lg bg-red-500/20 border border-red-500/40 text-red-300 text-xs">
|
||
{{ authState.error }}
|
||
</div>
|
||
<form @submit.prevent="handleSetup" class="space-y-4">
|
||
<div>
|
||
<label class="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1">Admin Password</label>
|
||
<input type="password" v-model="authState.setupPassword" required placeholder="Minimum 6 characters" class="w-full bg-slate-900 border border-slate-700 rounded-lg px-3.5 py-2.5 text-sm focus:outline-none focus:border-indigo-500 text-white">
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1">Confirm Password</label>
|
||
<input type="password" v-model="authState.setupPasswordConfirm" required placeholder="Re-enter password" class="w-full bg-slate-900 border border-slate-700 rounded-lg px-3.5 py-2.5 text-sm focus:outline-none focus:border-indigo-500 text-white">
|
||
</div>
|
||
<button type="submit" class="w-full mt-2 py-2.5 px-4 bg-indigo-600 hover:bg-indigo-500 text-white font-semibold rounded-lg shadow-lg shadow-indigo-600/30 transition">
|
||
Save Password & Open Dashboard
|
||
</button>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- Login Screen -->
|
||
<div v-else class="glass-panel p-8 rounded-2xl max-w-md w-full shadow-2xl border border-slate-800">
|
||
<div class="flex items-center gap-3 mb-6">
|
||
<img src="/assets/icon.png" class="w-12 h-12 rounded-xl object-cover shadow-lg border border-indigo-500/30" alt="Logo">
|
||
<div>
|
||
<h1 class="text-2xl font-extrabold text-white">Pangolin Sentinel</h1>
|
||
<p class="text-xs text-slate-400">Unraid → Remote VPS Hub</p>
|
||
</div>
|
||
</div>
|
||
<div v-if="authState.error" class="mb-4 p-3 rounded-lg bg-red-500/20 border border-red-500/40 text-red-300 text-xs">
|
||
{{ authState.error }}
|
||
</div>
|
||
<form @submit.prevent="handleLogin" class="space-y-4">
|
||
<div>
|
||
<label class="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1">Admin Password</label>
|
||
<input type="password" v-model="authState.password" required autofocus placeholder="Enter password" class="w-full bg-slate-900 border border-slate-700 rounded-lg px-3.5 py-2.5 text-sm focus:outline-none focus:border-indigo-500 text-white">
|
||
</div>
|
||
<button type="submit" class="w-full py-2.5 px-4 bg-indigo-600 hover:bg-indigo-500 text-white font-semibold rounded-lg shadow-lg shadow-indigo-600/30 transition">
|
||
Unlock Dashboard
|
||
</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Main Application -->
|
||
<div v-else class="flex-1 flex flex-col">
|
||
<!-- Navbar -->
|
||
<header class="border-b border-slate-800/80 bg-slate-900/70 backdrop-blur-md sticky top-0 z-30 px-6 py-3.5">
|
||
<div class="max-w-7xl mx-auto flex items-center justify-between">
|
||
<!-- Brand & Server Info -->
|
||
<div class="flex items-center gap-4 flex-shrink-0">
|
||
<div class="flex items-center gap-3 cursor-pointer group" @click="currentTab = 'dashboard'">
|
||
<div class="relative flex-shrink-0 p-0.5 rounded-xl bg-gradient-to-tr from-amber-500/40 via-indigo-500/50 to-indigo-600/40 shadow-lg shadow-indigo-950/60 border border-indigo-500/40 group-hover:border-amber-500/50 transition">
|
||
<img src="/assets/icon.png" class="w-10 h-10 rounded-[10px] object-cover" alt="Pangolin Sentinel Logo">
|
||
</div>
|
||
<div class="flex flex-col">
|
||
<div class="font-extrabold text-base tracking-tight text-white flex items-center gap-2 whitespace-nowrap">
|
||
Pangolin Sentinel
|
||
<span class="text-[10px] font-bold px-2 py-0.5 rounded-full bg-amber-500/20 text-amber-300 border border-amber-500/30 whitespace-nowrap">Unraid Hub</span>
|
||
</div>
|
||
<span class="text-[11px] text-slate-400 font-medium whitespace-nowrap">VPS Sentinel & Backup Hub</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Remote VPS Connection Indicator -->
|
||
<div class="hidden sm:flex items-center gap-2 px-3 py-1 rounded-full bg-slate-800/80 border border-slate-700/60 text-xs text-slate-300">
|
||
<span class="w-2 h-2 rounded-full" :class="settings.vps_host && systemStats?.vps_connected ? 'bg-emerald-400 pulse-emerald' : settings.vps_host ? 'bg-rose-400' : 'bg-amber-400'"></span>
|
||
<span class="font-mono text-slate-200">
|
||
VPS: {{ settings.vps_host || 'Not Connected' }}
|
||
</span>
|
||
<span class="text-slate-500">•</span>
|
||
<span class="text-slate-400 text-[11px]">{{ systemStats?.vps_connected ? 'SSH Active' : 'Setup Required' }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Active Task Pill -->
|
||
<div v-if="activeJob && activeJob.status === 'running'" @click="currentTab = 'console'" class="cursor-pointer flex items-center gap-2 px-3 py-1.5 rounded-full bg-amber-500/20 border border-amber-500/40 text-amber-300 text-xs font-medium animate-pulse">
|
||
<svg class="w-3.5 h-3.5 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
|
||
<span>Running: {{ activeJob.name }}</span>
|
||
</div>
|
||
|
||
<!-- Navigation Links -->
|
||
<nav class="hidden md:flex items-center gap-1">
|
||
<button @click="currentTab = 'dashboard'" :class="currentTab === 'dashboard' ? 'bg-indigo-600/20 text-indigo-300 border-indigo-500/40' : 'text-slate-400 hover:text-slate-200 border-transparent'" class="px-3 py-1.5 text-xs font-semibold rounded-lg border transition">
|
||
Overview
|
||
</button>
|
||
<button @click="currentTab = 'pangolin'" :class="currentTab === 'pangolin' ? 'bg-indigo-600/20 text-indigo-300 border-indigo-500/40' : 'text-slate-400 hover:text-slate-200 border-transparent'" class="px-3 py-1.5 text-xs font-semibold rounded-lg border transition">
|
||
Pangolin Stack
|
||
</button>
|
||
<button @click="currentTab = 'updates'" :class="currentTab === 'updates' ? 'bg-indigo-600/20 text-indigo-300 border-indigo-500/40' : 'text-slate-400 hover:text-slate-200 border-transparent'" class="px-3 py-1.5 text-xs font-semibold rounded-lg border transition relative">
|
||
VPS Updates
|
||
<span v-if="updatesData.total > 0" class="ml-1 px-1.5 py-0.2 rounded-full text-[10px] font-bold bg-indigo-500 text-white">
|
||
{{ updatesData.total }}
|
||
</span>
|
||
</button>
|
||
<button @click="currentTab = 'backups'" :class="currentTab === 'backups' ? 'bg-indigo-600/20 text-indigo-300 border-indigo-500/40' : 'text-slate-400 hover:text-slate-200 border-transparent'" class="px-3 py-1.5 text-xs font-semibold rounded-lg border transition">
|
||
Unraid Backups
|
||
<span class="ml-1 text-slate-400 text-[10px]">({{ backups.length }})</span>
|
||
</button>
|
||
<button @click="currentTab = 'automation'" :class="currentTab === 'automation' ? 'bg-indigo-600/20 text-indigo-300 border-indigo-500/40' : 'text-slate-400 hover:text-slate-200 border-transparent'" class="px-3 py-1.5 text-xs font-semibold rounded-lg border transition">
|
||
Automation
|
||
</button>
|
||
<button @click="currentTab = 'console'" :class="currentTab === 'console' ? 'bg-indigo-600/20 text-indigo-300 border-indigo-500/40' : 'text-slate-400 hover:text-slate-200 border-transparent'" class="px-3 py-1.5 text-xs font-semibold rounded-lg border transition">
|
||
Live Console
|
||
</button>
|
||
<button @click="currentTab = 'settings'" :class="currentTab === 'settings' ? 'bg-indigo-600/20 text-indigo-300 border-indigo-500/40' : 'text-slate-400 hover:text-slate-200 border-transparent'" class="px-3 py-1.5 text-xs font-semibold rounded-lg border transition flex items-center gap-1">
|
||
<span>Settings</span>
|
||
<span v-if="!settings.vps_host" class="w-2 h-2 rounded-full bg-amber-400"></span>
|
||
</button>
|
||
</nav>
|
||
|
||
<div class="flex items-center gap-3">
|
||
<button @click="handleLogout" title="Logout" class="p-2 text-slate-400 hover:text-rose-400 hover:bg-slate-800 rounded-lg transition">
|
||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/></svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- Main Container -->
|
||
<main class="flex-1 max-w-7xl w-full mx-auto p-4 sm:p-6 space-y-6">
|
||
|
||
<!-- Banner: VPS Not Configured Yet -->
|
||
<div v-if="!settings.vps_host" class="p-6 rounded-2xl bg-gradient-to-r from-amber-500/20 via-indigo-600/15 to-slate-900 border border-amber-500/40 flex flex-col md:flex-row items-start md:items-center justify-between gap-5 shadow-2xl">
|
||
<div class="flex items-start gap-4">
|
||
<div class="w-12 h-12 rounded-xl bg-amber-500/20 text-amber-400 flex items-center justify-center shrink-0">
|
||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||
</div>
|
||
<div>
|
||
<h3 class="text-lg font-bold text-white">Connect Your Remote Pangolin VPS</h3>
|
||
<p class="text-xs text-slate-300 mt-1 max-w-2xl leading-relaxed">
|
||
Pangolin Sentinel is running on your local Unraid server. Enter your VPS IP/hostname and authenticate with SSH to start pulling hot backups to Unraid and managing remote updates.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<button @click="currentTab = 'settings'" class="px-5 py-2.5 bg-amber-500 hover:bg-amber-400 text-slate-950 font-bold rounded-xl text-xs transition shadow-lg shrink-0">
|
||
Configure VPS Connection →
|
||
</button>
|
||
</div>
|
||
|
||
<!-- ==================== TAB 1: OVERVIEW ==================== -->
|
||
<div v-if="currentTab === 'dashboard'" class="space-y-6">
|
||
|
||
<!-- Primary Actions Card -->
|
||
<div class="glass-panel p-6 rounded-2xl relative overflow-hidden">
|
||
<div class="flex flex-col lg:flex-row items-start lg:items-center justify-between gap-6">
|
||
<div class="flex items-start sm:items-center gap-4">
|
||
<div class="hidden sm:flex flex-shrink-0 p-1 rounded-2xl bg-gradient-to-tr from-amber-500/30 via-indigo-500/40 to-purple-600/30 border border-indigo-500/40 shadow-xl shadow-indigo-950/60">
|
||
<img src="/assets/icon.png" class="w-16 h-16 rounded-[14px] object-cover" alt="Pangolin Sentinel Icon">
|
||
</div>
|
||
<div>
|
||
<span class="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-medium bg-amber-500/10 text-amber-400 border border-amber-500/20 mb-2">
|
||
<span class="w-1.5 h-1.5 rounded-full bg-amber-400"></span>
|
||
Unraid ↔ Pangolin VPS Bridge
|
||
</span>
|
||
<h2 class="text-2xl sm:text-3xl font-extrabold text-white tracking-tight">VPS Backups & Remote Updates</h2>
|
||
<p class="text-sm text-slate-400 mt-1 max-w-2xl">
|
||
Safely pull zero-lock SQLite hot backups and SSL certificates from your VPS to Unraid storage, and trigger remote Docker stack and OS updates.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex flex-wrap items-center gap-3">
|
||
<button @click="backupModal.show = true" :disabled="!settings.vps_host" class="px-4 py-2.5 bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-500 text-white font-semibold rounded-xl shadow-lg shadow-emerald-600/20 transition flex items-center gap-2 text-xs sm:text-sm disabled:opacity-40">
|
||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4"/></svg>
|
||
<span>Pull Backup to Unraid</span>
|
||
</button>
|
||
|
||
<button @click="updatePangolinStack" :disabled="!settings.vps_host" class="px-4 py-2.5 bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-500 text-white font-semibold rounded-xl shadow-lg shadow-indigo-600/20 transition flex items-center gap-2 text-xs sm:text-sm disabled:opacity-40">
|
||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
|
||
<span>Update Pangolin on VPS</span>
|
||
</button>
|
||
|
||
<button @click="currentTab = 'updates'" :disabled="!settings.vps_host" class="px-4 py-2.5 bg-slate-800 hover:bg-slate-700 text-slate-200 border border-slate-700 font-semibold rounded-xl transition flex items-center gap-2 text-xs sm:text-sm disabled:opacity-40">
|
||
<svg class="w-4 h-4 text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/></svg>
|
||
<span>VPS OS Updates</span>
|
||
<span v-if="updatesData.total > 0" class="px-1.5 py-0.5 rounded-full text-[10px] bg-amber-500/20 text-amber-300 font-bold">{{ updatesData.total }}</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Status & Hardware Gauges (Remote VPS + Local Unraid Storage) -->
|
||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5">
|
||
<!-- Remote VPS CPU -->
|
||
<div class="glass-panel p-5 rounded-2xl border border-slate-800 flex flex-col justify-between">
|
||
<div class="flex items-center justify-between text-slate-400 mb-3">
|
||
<span class="text-xs font-semibold uppercase tracking-wider">Remote VPS CPU</span>
|
||
<span class="w-2 h-2 rounded-full" :class="systemStats?.vps_connected ? 'bg-emerald-400' : 'bg-slate-600'"></span>
|
||
</div>
|
||
<div class="flex items-baseline justify-between mb-2">
|
||
<span class="text-3xl font-extrabold text-white">{{ systemStats?.cpu?.percent || 0 }}%</span>
|
||
<span class="text-xs text-slate-400 font-mono">{{ systemStats?.cpu?.cores || 1 }} Cores</span>
|
||
</div>
|
||
<div class="w-full bg-slate-800 rounded-full h-2 overflow-hidden mb-2">
|
||
<div class="h-2 rounded-full bg-indigo-500 transition-all duration-500" :style="{ width: (systemStats?.cpu?.percent || 0) + '%' }"></div>
|
||
</div>
|
||
<div class="text-[11px] text-slate-400 flex justify-between font-mono">
|
||
<span>Load: {{ systemStats?.cpu?.load_avg?.join(', ') || '0, 0, 0' }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Remote VPS RAM -->
|
||
<div class="glass-panel p-5 rounded-2xl border border-slate-800 flex flex-col justify-between">
|
||
<div class="flex items-center justify-between text-slate-400 mb-3">
|
||
<span class="text-xs font-semibold uppercase tracking-wider">Remote VPS RAM</span>
|
||
<span class="w-2 h-2 rounded-full" :class="systemStats?.vps_connected ? 'bg-emerald-400' : 'bg-slate-600'"></span>
|
||
</div>
|
||
<div class="flex items-baseline justify-between mb-2">
|
||
<span class="text-3xl font-extrabold text-white">{{ systemStats?.memory?.percent || 0 }}%</span>
|
||
<span class="text-xs text-slate-400 font-mono">{{ systemStats?.memory?.formatted_used || '0 B' }}</span>
|
||
</div>
|
||
<div class="w-full bg-slate-800 rounded-full h-2 overflow-hidden mb-2">
|
||
<div class="h-2 rounded-full bg-emerald-500 transition-all duration-500" :style="{ width: (systemStats?.memory?.percent || 0) + '%' }"></div>
|
||
</div>
|
||
<div class="text-[11px] text-slate-400 flex justify-between font-mono">
|
||
<span>Total: {{ systemStats?.memory?.formatted_total || '0 B' }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Remote VPS Root Disk -->
|
||
<div class="glass-panel p-5 rounded-2xl border border-slate-800 flex flex-col justify-between">
|
||
<div class="flex items-center justify-between text-slate-400 mb-3">
|
||
<span class="text-xs font-semibold uppercase tracking-wider">VPS Disk (/)</span>
|
||
<span class="w-2 h-2 rounded-full" :class="systemStats?.vps_connected ? 'bg-emerald-400' : 'bg-slate-600'"></span>
|
||
</div>
|
||
<div class="flex items-baseline justify-between mb-2">
|
||
<span class="text-3xl font-extrabold text-white">{{ systemStats?.disk?.percent || 0 }}%</span>
|
||
<span class="text-xs text-slate-400 font-mono">{{ systemStats?.disk?.formatted_used || '0 B' }}</span>
|
||
</div>
|
||
<div class="w-full bg-slate-800 rounded-full h-2 overflow-hidden mb-2">
|
||
<div class="h-2 rounded-full bg-cyan-500 transition-all duration-500" :style="{ width: (systemStats?.disk?.percent || 0) + '%' }"></div>
|
||
</div>
|
||
<div class="text-[11px] text-slate-400 flex justify-between font-mono">
|
||
<span>Free: {{ systemStats?.disk?.formatted_free || '0 B' }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Local Unraid Backup Storage Meter -->
|
||
<div class="glass-panel p-5 rounded-2xl border border-amber-500/30 flex flex-col justify-between">
|
||
<div class="flex items-center justify-between text-amber-400 mb-3">
|
||
<span class="text-xs font-semibold uppercase tracking-wider">Unraid Backup Storage</span>
|
||
<span class="text-[10px] font-bold px-1.5 py-0.5 rounded bg-amber-500/20">Local</span>
|
||
</div>
|
||
<div class="flex items-baseline justify-between mb-2">
|
||
<span class="text-3xl font-extrabold text-white">{{ systemStats?.unraid_disk?.percent || 0 }}%</span>
|
||
<span class="text-xs text-slate-400 font-mono">{{ backups.length }} Archives</span>
|
||
</div>
|
||
<div class="w-full bg-slate-800 rounded-full h-2 overflow-hidden mb-2">
|
||
<div class="h-2 rounded-full bg-amber-500 transition-all duration-500" :style="{ width: (systemStats?.unraid_disk?.percent || 0) + '%' }"></div>
|
||
</div>
|
||
<div class="text-[11px] text-slate-400 flex justify-between font-mono">
|
||
<span class="truncate max-w-[150px]">{{ systemStats?.unraid_disk?.path || '/var/backups' }}</span>
|
||
<span>Free: {{ systemStats?.unraid_disk?.formatted_free || 'N/A' }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Two Columns: Remote Pangolin Stack + Local Backups -->
|
||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||
|
||
<!-- Remote Stack Card -->
|
||
<div class="glass-panel p-6 rounded-2xl border border-slate-800">
|
||
<div class="flex items-center justify-between mb-4">
|
||
<div class="flex items-center gap-2.5">
|
||
<div class="w-8 h-8 rounded-lg bg-indigo-500/20 text-indigo-400 flex items-center justify-center">
|
||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/></svg>
|
||
</div>
|
||
<div>
|
||
<h3 class="text-base font-bold text-white">Remote Pangolin Stack</h3>
|
||
<p class="text-xs text-slate-400 font-mono">{{ settings.vps_host || 'VPS not configured' }} ({{ settings.vps_pangolin_dir }})</p>
|
||
</div>
|
||
</div>
|
||
<button @click="currentTab = 'pangolin'" class="text-xs text-indigo-400 hover:text-indigo-300 font-semibold">
|
||
Manage →
|
||
</button>
|
||
</div>
|
||
|
||
<div v-if="pangolinStatus.containers.length > 0" class="space-y-2.5">
|
||
<div v-for="c in pangolinStatus.containers" :key="c.id" class="p-3 rounded-xl bg-slate-900/60 border border-slate-800 flex items-center justify-between">
|
||
<div class="flex items-center gap-3">
|
||
<span class="w-2.5 h-2.5 rounded-full" :class="c.state === 'running' ? 'bg-emerald-400' : 'bg-rose-500'"></span>
|
||
<div>
|
||
<div class="text-xs font-bold text-white font-mono">{{ c.name }}</div>
|
||
<div class="text-[11px] text-slate-400 font-mono truncate max-w-xs">{{ c.image }}</div>
|
||
</div>
|
||
</div>
|
||
<span class="px-2 py-0.5 rounded text-[10px] font-bold" :class="c.state === 'running' ? 'bg-emerald-500/20 text-emerald-300' : 'bg-rose-500/20 text-rose-300'">
|
||
{{ c.state ? c.state.toUpperCase() : 'STOPPED' }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-else class="p-6 text-center rounded-xl bg-slate-900/40 border border-dashed border-slate-800">
|
||
<p class="text-xs text-slate-400">No active Pangolin containers detected on remote VPS. Check your connection in Settings.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Stored Backups on Unraid -->
|
||
<div class="glass-panel p-6 rounded-2xl border border-slate-800">
|
||
<div class="flex items-center justify-between mb-4">
|
||
<div class="flex items-center gap-2.5">
|
||
<div class="w-8 h-8 rounded-lg bg-emerald-500/20 text-emerald-400 flex items-center justify-center">
|
||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4"/></svg>
|
||
</div>
|
||
<div>
|
||
<h3 class="text-base font-bold text-white">Backups on Unraid Server</h3>
|
||
<p class="text-xs text-slate-400">Total stored: {{ backups.length }} snapshot archives</p>
|
||
</div>
|
||
</div>
|
||
<button @click="currentTab = 'backups'" class="text-xs text-emerald-400 hover:text-emerald-300 font-semibold">
|
||
View All →
|
||
</button>
|
||
</div>
|
||
|
||
<div v-if="backups.length > 0" class="space-y-2.5">
|
||
<div v-for="b in backups.slice(0, 4)" :key="b.id" class="p-3 rounded-xl bg-slate-900/60 border border-slate-800 flex items-center justify-between">
|
||
<div class="flex items-center gap-3">
|
||
<span class="px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-wider" :class="b.scope === 'pangolin' ? 'bg-emerald-500/20 text-emerald-300' : 'bg-purple-500/20 text-purple-300'">
|
||
{{ b.scope }}
|
||
</span>
|
||
<div>
|
||
<div class="text-xs font-semibold text-white truncate max-w-xs">{{ b.filename }}</div>
|
||
<div class="text-[11px] text-slate-400">{{ b.created_at.replace('T', ' ').replace('Z', '') }}</div>
|
||
</div>
|
||
</div>
|
||
<div class="text-right">
|
||
<div class="text-xs font-bold text-slate-200 font-mono">{{ b.formatted_size }}</div>
|
||
<a :href="downloadBackupUrl(b)" class="text-[11px] text-indigo-400 hover:underline">Download</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-else class="p-6 text-center rounded-xl bg-slate-900/40 border border-dashed border-slate-800">
|
||
<p class="text-xs text-slate-400">No backups stored on Unraid yet. Click "Pull Backup to Unraid" above.</p>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- ==================== TAB 2: PANGOLIN STACK ==================== -->
|
||
<div v-if="currentTab === 'pangolin'" class="space-y-6">
|
||
|
||
<!-- Stack Header -->
|
||
<div class="glass-panel p-6 rounded-2xl flex flex-col md:flex-row items-start md:items-center justify-between gap-4">
|
||
<div>
|
||
<h2 class="text-xl font-bold text-white flex items-center gap-2">
|
||
Remote Pangolin Stack
|
||
<span class="px-2 py-0.5 rounded text-[11px] font-bold bg-indigo-500/20 text-indigo-300 border border-indigo-500/30">
|
||
{{ pangolinStatus.running_count }} Containers Active
|
||
</span>
|
||
</h2>
|
||
<p class="text-xs text-slate-400 mt-1 font-mono">
|
||
{{ settings.vps_host }}:{{ settings.vps_pangolin_dir }}
|
||
</p>
|
||
</div>
|
||
|
||
<button @click="updatePangolinStack" :disabled="!settings.vps_host" class="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg text-xs font-semibold shadow-lg transition flex items-center gap-2 disabled:opacity-40">
|
||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
|
||
Update Remote Stack & Pull Images
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Containers List -->
|
||
<div v-if="pangolinStatus.containers.length > 0" class="grid grid-cols-1 md:grid-cols-3 gap-5">
|
||
<div v-for="c in pangolinStatus.containers" :key="c.id" class="glass-panel p-5 rounded-2xl border border-slate-800 flex flex-col justify-between">
|
||
<div>
|
||
<div class="flex items-center justify-between mb-3">
|
||
<div class="font-bold text-sm text-white font-mono flex items-center gap-2">
|
||
<span class="w-2.5 h-2.5 rounded-full" :class="c.state === 'running' ? 'bg-emerald-400' : 'bg-slate-600'"></span>
|
||
{{ c.name }}
|
||
</div>
|
||
<span class="px-2 py-0.5 rounded text-[10px] font-bold" :class="c.state === 'running' ? 'bg-emerald-500/20 text-emerald-300' : 'bg-slate-800 text-slate-400'">
|
||
{{ c.state.toUpperCase() }}
|
||
</span>
|
||
</div>
|
||
<div class="text-xs text-slate-400 font-mono mb-2 truncate">Image: {{ c.image }}</div>
|
||
<div class="text-xs text-slate-500 font-mono mb-4 text-[11px]">{{ c.status }}</div>
|
||
</div>
|
||
|
||
<div class="pt-3 border-t border-slate-800 flex items-center justify-between">
|
||
<button @click="viewContainerLogs(c.name)" class="text-xs text-indigo-400 hover:text-indigo-300 font-semibold">Logs</button>
|
||
<div class="flex items-center gap-1.5">
|
||
<button @click="handleContainerAction(c.name, 'restart')" class="px-2.5 py-1 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded text-[11px] font-medium">Restart</button>
|
||
<button v-if="c.state === 'running'" @click="handleContainerAction(c.name, 'stop')" class="px-2.5 py-1 bg-rose-500/20 text-rose-300 rounded text-[11px] font-medium">Stop</button>
|
||
<button v-else @click="handleContainerAction(c.name, 'start')" class="px-2.5 py-1 bg-emerald-500/20 text-emerald-300 rounded text-[11px] font-medium">Start</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Remote Config Editor -->
|
||
<div class="glass-panel p-6 rounded-2xl border border-slate-800">
|
||
<div class="flex items-center justify-between mb-4">
|
||
<div>
|
||
<h3 class="text-base font-bold text-white">Remote Stack Configuration Editor</h3>
|
||
<p class="text-xs text-slate-400">Directly view and edit files on your remote VPS. Files are auto-backed up before writing.</p>
|
||
</div>
|
||
|
||
<button @click="saveStackFile" :disabled="savingFile || !settings.vps_host" class="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg text-xs font-semibold shadow-md transition disabled:opacity-40">
|
||
{{ savingFile ? 'Saving to VPS...' : 'Save File to VPS' }}
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Tabs -->
|
||
<div class="flex border-b border-slate-800 gap-2 mb-3">
|
||
<button v-for="(content, key) in stackFiles" :key="key" @click="selectStackFile(key)" :class="activeFileKey === key ? 'border-indigo-500 text-indigo-400 bg-slate-900' : 'border-transparent text-slate-400'" class="px-3.5 py-2 border-b-2 text-xs font-mono font-semibold transition">
|
||
{{ key }}
|
||
</button>
|
||
</div>
|
||
|
||
<textarea v-model="activeFileContent" rows="18" class="w-full bg-[#080d1a] border border-slate-800 rounded-xl p-4 font-mono text-xs text-slate-200 focus:outline-none focus:border-indigo-500/60 leading-relaxed"></textarea>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- ==================== TAB 3: VPS UPDATES ==================== -->
|
||
<div v-if="currentTab === 'updates'" class="space-y-6">
|
||
|
||
<!-- Updates Overview Header -->
|
||
<div class="glass-panel p-6 rounded-2xl flex flex-col md:flex-row items-start md:items-center justify-between gap-5">
|
||
<div>
|
||
<span class="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-medium bg-indigo-500/10 text-indigo-400 border border-indigo-500/20 mb-2">
|
||
<span class="w-1.5 h-1.5 rounded-full bg-indigo-400"></span>
|
||
Remote VPS Package Manager (apt-get)
|
||
</span>
|
||
<h2 class="text-2xl font-extrabold text-white">Remote VPS Operating System Updates</h2>
|
||
<p class="text-xs text-slate-400 mt-1">
|
||
Target: <span class="font-mono text-slate-300">{{ settings.vps_host }}</span> • OS: <span class="text-slate-300">{{ systemStats?.os }}</span>
|
||
</p>
|
||
</div>
|
||
|
||
<div class="flex items-center gap-3">
|
||
<button @click="checkUpdates" :disabled="checkingUpdates || !settings.vps_host" class="px-3.5 py-2 bg-slate-800 hover:bg-slate-700 text-slate-200 border border-slate-700 rounded-xl text-xs font-semibold transition flex items-center gap-2 disabled:opacity-40">
|
||
<svg :class="checkingUpdates ? 'animate-spin' : ''" class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
|
||
<span>{{ checkingUpdates ? 'Checking VPS...' : 'Scan for Updates' }}</span>
|
||
</button>
|
||
|
||
<button @click="triggerSystemUpgrade(false)" :disabled="updatesData.total === 0 || !settings.vps_host" class="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 disabled:opacity-40 text-white rounded-xl text-xs font-semibold shadow-lg transition flex items-center gap-2">
|
||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
|
||
<span>Upgrade All on VPS ({{ updatesData.total }})</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Reboot Warning Banner -->
|
||
<div v-if="systemStats?.reboot_required" class="p-5 rounded-2xl bg-rose-500/15 border border-rose-500/40 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
||
<div class="flex items-center gap-3">
|
||
<div class="w-10 h-10 rounded-xl bg-rose-500/20 text-rose-400 flex items-center justify-center shrink-0">
|
||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>
|
||
</div>
|
||
<div>
|
||
<h4 class="text-sm font-bold text-rose-200">VPS Reboot Required</h4>
|
||
<p class="text-xs text-rose-300/80">A kernel or system update requires rebooting your remote VPS.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<button @click="triggerReboot" class="px-4 py-2 bg-rose-600 hover:bg-rose-500 text-white rounded-xl text-xs font-semibold shadow-lg transition shrink-0">
|
||
Reboot Remote VPS
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Upgradable Packages Table -->
|
||
<div class="glass-panel p-6 rounded-2xl border border-slate-800">
|
||
<div class="flex items-center justify-between mb-4">
|
||
<h3 class="text-base font-bold text-white">Upgradable Packages on Remote VPS</h3>
|
||
<div class="text-xs text-slate-400">
|
||
<span class="font-bold text-slate-200">{{ updatesData.total }}</span> total •
|
||
<span :class="updatesData.security_count > 0 ? 'text-rose-400 font-bold' : 'text-slate-400'">{{ updatesData.security_count }} security</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="updatesData.packages.length > 0" class="overflow-x-auto">
|
||
<table class="w-full text-left text-xs">
|
||
<thead class="text-slate-400 border-b border-slate-800 uppercase tracking-wider font-semibold text-[10px]">
|
||
<tr>
|
||
<th class="py-3 px-3">Package</th>
|
||
<th class="py-3 px-3">Current Version</th>
|
||
<th class="py-3 px-3">New Version</th>
|
||
<th class="py-3 px-3">Repository</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="divide-y divide-slate-800 font-mono">
|
||
<tr v-for="pkg in updatesData.packages" :key="pkg.name" class="hover:bg-slate-900/40">
|
||
<td class="py-3 px-3 font-semibold text-white flex items-center gap-2">
|
||
<span v-if="pkg.is_security" class="px-1.5 py-0.5 rounded text-[9px] bg-rose-500/20 text-rose-300 font-bold">SEC</span>
|
||
{{ pkg.name }}
|
||
</td>
|
||
<td class="py-3 px-3 text-slate-400">{{ pkg.current_version }}</td>
|
||
<td class="py-3 px-3 text-emerald-400 font-bold">{{ pkg.new_version }}</td>
|
||
<td class="py-3 px-3 text-slate-500">{{ pkg.repository }} ({{ pkg.arch }})</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div v-else class="text-center py-12">
|
||
<div class="w-12 h-12 rounded-full bg-emerald-500/10 text-emerald-400 flex items-center justify-center mx-auto mb-3">
|
||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
|
||
</div>
|
||
<div class="text-sm font-bold text-white">Remote VPS is Up to Date</div>
|
||
<p class="text-xs text-slate-400 mt-1">All packages are at their latest versions.</p>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- ==================== TAB 4: BACKUPS (STORED ON UNRAID) ==================== -->
|
||
<div v-if="currentTab === 'backups'" class="space-y-6">
|
||
|
||
<!-- Action Bar -->
|
||
<div class="glass-panel p-6 rounded-2xl flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
||
<div>
|
||
<h2 class="text-xl font-bold text-white flex items-center gap-2">
|
||
Pangolin Backups Stored on Unraid
|
||
<span class="px-2 py-0.5 rounded text-[11px] font-bold bg-amber-500/20 text-amber-300 border border-amber-500/30">
|
||
Local Storage
|
||
</span>
|
||
</h2>
|
||
<p class="text-xs text-slate-400 mt-1">
|
||
Directory: <span class="font-mono text-slate-300">{{ settings.backup_dir }}</span> • Retention: Keep newest <span class="font-bold text-indigo-400">{{ settings.retention_count }}</span>
|
||
</p>
|
||
</div>
|
||
|
||
<div class="flex items-center gap-3">
|
||
<label class="px-3.5 py-2 bg-slate-800 hover:bg-slate-700 text-slate-200 border border-slate-700 rounded-xl text-xs font-semibold transition cursor-pointer flex items-center gap-2">
|
||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"/></svg>
|
||
<span>Upload to Unraid</span>
|
||
<input type="file" @change="handleUploadBackup" accept=".tar.gz,.tgz" class="hidden">
|
||
</label>
|
||
|
||
<button @click="backupModal.show = true" :disabled="!settings.vps_host" class="px-4 py-2 bg-emerald-600 hover:bg-emerald-500 text-white rounded-xl text-xs font-semibold shadow-lg transition flex items-center gap-2 disabled:opacity-40">
|
||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg>
|
||
<span>Pull New Backup from VPS</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Backups Table -->
|
||
<div class="glass-panel p-6 rounded-2xl border border-slate-800">
|
||
<div v-if="backupsLoading" class="text-center py-12 text-slate-400 text-xs">
|
||
Loading backup archives...
|
||
</div>
|
||
|
||
<div v-else-if="backups.length > 0" class="overflow-x-auto">
|
||
<table class="w-full text-left text-xs">
|
||
<thead class="text-slate-400 border-b border-slate-800 uppercase tracking-wider font-semibold text-[10px]">
|
||
<tr>
|
||
<th class="py-3 px-3">Archive File</th>
|
||
<th class="py-3 px-3">Scope</th>
|
||
<th class="py-3 px-3">Size</th>
|
||
<th class="py-3 px-3">Created</th>
|
||
<th class="py-3 px-3">SHA-256 Checksum</th>
|
||
<th class="py-3 px-3 text-right">Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="divide-y divide-slate-800 font-mono">
|
||
<tr v-for="b in backups" :key="b.id" class="hover:bg-slate-900/40">
|
||
<td class="py-3.5 px-3">
|
||
<div class="font-bold text-white text-xs">{{ b.filename }}</div>
|
||
<div v-if="b.description" class="text-[11px] text-slate-400 font-sans mt-0.5">{{ b.description }}</div>
|
||
</td>
|
||
<td class="py-3.5 px-3">
|
||
<span class="px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-wider" :class="b.scope === 'pangolin' ? 'bg-emerald-500/20 text-emerald-300 border border-emerald-500/30' : 'bg-purple-500/20 text-purple-300 border border-purple-500/30'">
|
||
{{ b.scope }}
|
||
</span>
|
||
</td>
|
||
<td class="py-3.5 px-3 text-slate-200 font-bold">{{ b.formatted_size }}</td>
|
||
<td class="py-3.5 px-3 text-slate-400 text-[11px]">{{ b.created_at.replace('T', ' ').replace('Z', '') }}</td>
|
||
<td class="py-3.5 px-3 text-slate-500 text-[10px]">
|
||
<span :title="b.sha256" class="cursor-pointer hover:text-indigo-400" @click="copyToClipboard(b.sha256)">
|
||
{{ b.sha256 ? b.sha256.substring(0, 12) + '...' : 'N/A' }} 📋
|
||
</span>
|
||
</td>
|
||
<td class="py-3.5 px-3 text-right space-x-2 font-sans">
|
||
<button @click="inspectBackupArchive(b)" class="text-slate-300 hover:text-white text-xs font-medium">Inspect</button>
|
||
<button @click="verifyBackupArchive(b)" class="text-cyan-400 hover:text-cyan-300 text-xs font-medium">Verify</button>
|
||
<a :href="downloadBackupUrl(b)" class="text-indigo-400 hover:text-indigo-300 text-xs font-medium">Download</a>
|
||
<button @click="promptRestore(b)" class="text-amber-400 hover:text-amber-300 text-xs font-semibold">Restore to VPS</button>
|
||
<button @click="deleteBackupArchive(b)" class="text-rose-400 hover:text-rose-300 text-xs font-medium">Delete</button>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div v-else class="text-center py-16">
|
||
<svg class="w-12 h-12 text-slate-600 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4"/></svg>
|
||
<div class="text-base font-bold text-white">No Backups on Unraid Server</div>
|
||
<p class="text-xs text-slate-400 mt-1 mb-4">Click below to pull a complete hot backup from your Pangolin VPS.</p>
|
||
<button @click="backupModal.show = true" :disabled="!settings.vps_host" class="px-4 py-2 bg-emerald-600 hover:bg-emerald-500 text-white rounded-lg text-xs font-semibold disabled:opacity-40">
|
||
Pull First Backup
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- ==================== TAB 5: AUTOMATION ==================== -->
|
||
<div v-if="currentTab === 'automation'" class="space-y-6">
|
||
|
||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||
|
||
<!-- Automated Backups -->
|
||
<div class="glass-panel p-6 rounded-2xl border border-slate-800 flex flex-col justify-between">
|
||
<div>
|
||
<div class="flex items-center justify-between mb-4">
|
||
<div class="flex items-center gap-2.5">
|
||
<div class="w-8 h-8 rounded-lg bg-indigo-500/20 text-indigo-400 flex items-center justify-center">
|
||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||
</div>
|
||
<h3 class="text-base font-bold text-white">Scheduled VPS → Unraid Backups</h3>
|
||
</div>
|
||
<label class="relative inline-flex items-center cursor-pointer">
|
||
<input type="checkbox" v-model="settings.scheduled_backup_enabled" class="sr-only peer">
|
||
<div class="w-11 h-6 bg-slate-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-slate-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-indigo-600"></div>
|
||
</label>
|
||
</div>
|
||
|
||
<div class="space-y-4">
|
||
<div>
|
||
<label class="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1">Backup Scope</label>
|
||
<select v-model="settings.scheduled_backup_scope" class="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-xs text-slate-200">
|
||
<option value="pangolin">Pangolin Stack (Zero-Lock SQLite + Configs)</option>
|
||
<option value="full">Full VPS Disaster Recovery (Pangolin + Host /etc)</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1">Cron Expression</label>
|
||
<input type="text" v-model="settings.scheduled_backup_cron" placeholder="0 3 * * *" class="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-xs font-mono text-slate-200">
|
||
<p class="text-[11px] text-slate-500 mt-1">Default: "0 3 * * *" runs nightly at 3:00 AM UTC.</p>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1">Unraid Storage Retention Count</label>
|
||
<input type="number" min="2" max="100" v-model.number="settings.retention_count" class="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-xs text-slate-200">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="pt-6 mt-6 border-t border-slate-800">
|
||
<button @click="saveSettings" :disabled="settingsLoading" class="w-full py-2.5 bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg text-xs font-semibold transition">
|
||
Save Schedule Settings
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Webhooks -->
|
||
<div class="glass-panel p-6 rounded-2xl border border-slate-800 flex flex-col justify-between">
|
||
<div>
|
||
<div class="flex items-center gap-2.5 mb-4">
|
||
<div class="w-8 h-8 rounded-lg bg-emerald-500/20 text-emerald-400 flex items-center justify-center">
|
||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/></svg>
|
||
</div>
|
||
<h3 class="text-base font-bold text-white">Alerts & Notifications</h3>
|
||
</div>
|
||
|
||
<div class="space-y-4">
|
||
<div>
|
||
<label class="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1">Webhook Provider</label>
|
||
<select v-model="settings.webhook_type" class="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-xs text-slate-200">
|
||
<option value="discord">Discord Webhook</option>
|
||
<option value="telegram">Telegram Bot</option>
|
||
<option value="generic">Generic JSON Webhook (n8n / Home Assistant)</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div v-if="settings.webhook_type !== 'telegram'">
|
||
<label class="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1">Webhook URL</label>
|
||
<input type="text" v-model="settings.webhook_url" placeholder="https://discord.com/api/webhooks/..." class="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-xs font-mono text-slate-200">
|
||
</div>
|
||
|
||
<div v-if="settings.webhook_type === 'telegram'" class="space-y-3">
|
||
<div>
|
||
<label class="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1">Bot Token</label>
|
||
<input type="text" v-model="settings.telegram_bot_token" placeholder="123456:ABC-DEF..." class="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-xs font-mono text-slate-200">
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1">Chat ID</label>
|
||
<input type="text" v-model="settings.telegram_chat_id" placeholder="-100123456789" class="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-xs font-mono text-slate-200">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="space-y-2 pt-2">
|
||
<label class="flex items-center gap-2 text-xs text-slate-300">
|
||
<input type="checkbox" v-model="settings.notify_on_backup" class="rounded bg-slate-900 border-slate-700 text-indigo-600 focus:ring-0">
|
||
<span>Notify on backup pull completion or errors</span>
|
||
</label>
|
||
<label class="flex items-center gap-2 text-xs text-slate-300">
|
||
<input type="checkbox" v-model="settings.notify_on_update" class="rounded bg-slate-900 border-slate-700 text-indigo-600 focus:ring-0">
|
||
<span>Notify when new VPS updates are detected</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="pt-6 mt-6 border-t border-slate-800 flex items-center gap-3">
|
||
<button @click="testWebhook" :disabled="testingWebhook" class="px-4 py-2.5 bg-slate-800 hover:bg-slate-700 text-slate-200 border border-slate-700 rounded-lg text-xs font-semibold transition">
|
||
{{ testingWebhook ? 'Sending...' : 'Test Webhook' }}
|
||
</button>
|
||
<button @click="saveSettings" :disabled="settingsLoading" class="flex-1 py-2.5 bg-emerald-600 hover:bg-emerald-500 text-white rounded-lg text-xs font-semibold transition shadow-md">
|
||
Save Webhook Settings
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- ==================== TAB 6: LIVE CONSOLE ==================== -->
|
||
<div v-if="currentTab === 'console'" class="space-y-6">
|
||
|
||
<div class="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
||
|
||
<!-- History -->
|
||
<div class="glass-panel p-5 rounded-2xl border border-slate-800 lg:col-span-1 flex flex-col h-[650px]">
|
||
<div class="flex items-center justify-between mb-4">
|
||
<h3 class="text-sm font-bold text-white">Execution History</h3>
|
||
<span class="text-[11px] text-slate-400 font-mono">{{ jobs.length }} runs</span>
|
||
</div>
|
||
|
||
<div class="flex-1 overflow-y-auto space-y-2 pr-1">
|
||
<div v-for="j in jobs" :key="j.id" @click="selectedJobId = j.id" :class="selectedJobId === j.id ? 'bg-indigo-600/20 border-indigo-500/50' : 'bg-slate-900/40 border-slate-800'" class="p-3 rounded-xl border cursor-pointer transition">
|
||
<div class="flex items-center justify-between mb-1">
|
||
<span class="text-xs font-bold text-white truncate max-w-[130px]">{{ j.name }}</span>
|
||
<span class="px-1.5 py-0.5 rounded text-[9px] font-bold" :class="j.status === 'completed' ? 'bg-emerald-500/20 text-emerald-400' : j.status === 'running' ? 'bg-amber-500/20 text-amber-400 animate-pulse' : 'bg-rose-500/20 text-rose-400'">
|
||
{{ j.status.toUpperCase() }}
|
||
</span>
|
||
</div>
|
||
<div class="text-[10px] text-slate-400 font-mono flex items-center justify-between">
|
||
<span>{{ j.type }}</span>
|
||
<span>{{ j.duration ? j.duration + 's' : 'active' }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Terminal -->
|
||
<div class="glass-panel p-5 rounded-2xl border border-slate-800 lg:col-span-3 flex flex-col h-[650px]">
|
||
<div class="flex items-center justify-between pb-3 border-b border-slate-800 mb-3">
|
||
<div class="flex items-center gap-2">
|
||
<div class="flex gap-1.5">
|
||
<span class="w-3 h-3 rounded-full bg-rose-500/80"></span>
|
||
<span class="w-3 h-3 rounded-full bg-amber-500/80"></span>
|
||
<span class="w-3 h-3 rounded-full bg-emerald-500/80"></span>
|
||
</div>
|
||
<span class="text-xs font-mono font-bold text-slate-300 ml-2">
|
||
{{ displayedJob ? displayedJob.name : 'Console Standby' }}
|
||
</span>
|
||
<span v-if="displayedJob?.status === 'running'" class="px-2 py-0.5 rounded text-[10px] bg-amber-500/20 text-amber-300 border border-amber-500/30 animate-pulse">
|
||
Live SSH Stream
|
||
</span>
|
||
</div>
|
||
|
||
<div class="flex items-center gap-3">
|
||
<label class="flex items-center gap-1.5 text-xs text-slate-400 cursor-pointer">
|
||
<input type="checkbox" v-model="autoScroll" class="rounded bg-slate-900 border-slate-700 text-indigo-600 focus:ring-0">
|
||
<span>Auto-scroll</span>
|
||
</label>
|
||
<button v-if="displayedJob" @click="copyToClipboard(displayedJob.logs.join('\n'))" class="text-xs text-slate-400 hover:text-white">
|
||
Copy Logs
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div ref="terminalRef" class="flex-1 terminal-window rounded-xl p-4 overflow-y-auto font-mono text-xs text-emerald-400/90 leading-relaxed">
|
||
<div v-if="displayedJob && displayedJob.logs && displayedJob.logs.length > 0">
|
||
<div v-for="(line, idx) in displayedJob.logs" :key="idx" class="terminal-line" :class="line.includes('ERROR') || line.includes('error') ? 'text-rose-400 font-bold' : line.includes('PASSED') || line.includes('successfully') ? 'text-emerald-300 font-bold' : line.includes('===') ? 'text-cyan-300 font-bold' : 'text-slate-300'">
|
||
{{ line }}
|
||
</div>
|
||
</div>
|
||
<div v-else class="text-slate-600 text-center py-24 font-mono">
|
||
[Terminal Ready] — Connect to your VPS and trigger an update or backup to view live streaming logs.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- ==================== TAB 7: SETTINGS & VPS CONNECTION ==================== -->
|
||
<div v-if="currentTab === 'settings'" class="space-y-6">
|
||
|
||
<!-- VPS Connection Card -->
|
||
<div class="glass-panel p-6 rounded-2xl border border-indigo-500/40 shadow-xl">
|
||
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 mb-6">
|
||
<div>
|
||
<h3 class="text-lg font-bold text-white flex items-center gap-2">
|
||
<span class="w-2.5 h-2.5 rounded-full" :class="settings.vps_host && systemStats?.vps_connected ? 'bg-emerald-400' : 'bg-amber-400'"></span>
|
||
Remote Pangolin VPS Connection (SSH)
|
||
</h3>
|
||
<p class="text-xs text-slate-400 mt-1">Configure the remote server address and SSH credentials used by Unraid to connect.</p>
|
||
</div>
|
||
|
||
<div class="flex items-center gap-2.5">
|
||
<button @click="testVpsConnection" :disabled="connectionTest.testing || !settings.vps_host" class="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg text-xs font-semibold shadow-md transition flex items-center gap-2 disabled:opacity-40">
|
||
<svg v-if="connectionTest.testing" class="w-3.5 h-3.5 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
|
||
<span>{{ connectionTest.testing ? 'Testing SSH...' : 'Test Connection' }}</span>
|
||
</button>
|
||
<button @click="saveSettings" :disabled="settingsLoading" class="px-4 py-2 bg-emerald-600 hover:bg-emerald-500 text-white rounded-lg text-xs font-semibold shadow-md transition">
|
||
Save Settings
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Connection Diagnostics result banner if tested -->
|
||
<div v-if="connectionTest.result" class="mb-6 p-4 rounded-xl border text-xs" :class="connectionTest.result.connected ? 'bg-emerald-500/10 border-emerald-500/30 text-emerald-300' : 'bg-rose-500/10 border-rose-500/30 text-rose-300'">
|
||
<div class="font-bold mb-1 flex items-center gap-2">
|
||
<span>{{ connectionTest.result.connected ? '✅ SSH Connection Successful!' : '❌ Connection Failed' }}</span>
|
||
</div>
|
||
<div v-if="connectionTest.result.connected" class="grid grid-cols-2 sm:grid-cols-4 gap-2 font-mono text-[11px] mt-2 pt-2 border-t border-emerald-500/20">
|
||
<div>OS: <span class="text-white">{{ connectionTest.result.os }}</span></div>
|
||
<div>Docker: <span class="text-white">{{ connectionTest.result.docker_running ? 'Running' : 'Not Running' }}</span></div>
|
||
<div>Pangolin Dir: <span class="text-white">{{ connectionTest.result.pangolin_dir_exists ? 'Found' : 'Missing' }}</span></div>
|
||
<div>SQLite DB: <span class="text-white">{{ connectionTest.result.database_file ? 'Found' : 'None' }}</span></div>
|
||
</div>
|
||
<div v-else class="mt-1 font-mono text-[11px]">
|
||
{{ connectionTest.result.error }}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
|
||
<div>
|
||
<label class="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1">VPS Host / IP Address</label>
|
||
<input type="text" v-model="settings.vps_host" placeholder="e.g. 198.51.100.25 or vps.domain.com" class="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-xs font-mono text-white">
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1">SSH Port</label>
|
||
<input type="number" v-model.number="settings.vps_port" placeholder="22" class="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-xs font-mono text-white">
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1">SSH Username</label>
|
||
<input type="text" v-model="settings.vps_user" placeholder="root" class="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-xs font-mono text-white">
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Auth Method Selector -->
|
||
<div class="mb-6">
|
||
<label class="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2">Authentication Method</label>
|
||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||
<label class="p-3 rounded-xl border cursor-pointer flex items-center gap-3" :class="settings.vps_auth_method === 'key' ? 'bg-indigo-600/20 border-indigo-500/60' : 'bg-slate-900/60 border-slate-800'">
|
||
<input type="radio" v-model="settings.vps_auth_method" value="key" class="text-indigo-600">
|
||
<div>
|
||
<div class="text-xs font-bold text-white">SSH Key Pair (Recommended)</div>
|
||
<div class="text-[11px] text-slate-400">Uses ed25519 key generated on this Unraid server.</div>
|
||
</div>
|
||
</label>
|
||
|
||
<label class="p-3 rounded-xl border cursor-pointer flex items-center gap-3" :class="settings.vps_auth_method === 'password' ? 'bg-indigo-600/20 border-indigo-500/60' : 'bg-slate-900/60 border-slate-800'">
|
||
<input type="radio" v-model="settings.vps_auth_method" value="password" class="text-indigo-600">
|
||
<div>
|
||
<div class="text-xs font-bold text-white">SSH Password</div>
|
||
<div class="text-[11px] text-slate-400">Authenticate using root / user SSH password.</div>
|
||
</div>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- SSH Key Public Setup Instructions -->
|
||
<div v-if="settings.vps_auth_method === 'key'" class="p-4 rounded-xl bg-slate-900/80 border border-slate-800 mb-6">
|
||
<div class="flex items-center justify-between mb-2">
|
||
<span class="text-xs font-bold text-indigo-300">Unraid Server Public Key:</span>
|
||
<button @click="copyToClipboard(publicKeyData.public_key)" class="text-xs text-indigo-400 hover:text-indigo-300 font-semibold flex items-center gap-1">
|
||
<span>Copy Public Key</span>
|
||
</button>
|
||
</div>
|
||
<div class="p-2.5 rounded bg-black/60 font-mono text-[11px] text-slate-300 break-all select-all border border-slate-800">
|
||
{{ publicKeyData.public_key }}
|
||
</div>
|
||
<p class="text-[11px] text-slate-400 mt-2">
|
||
💡 <span class="font-bold text-slate-300">How to authorize:</span> Log in to your VPS terminal once and run:
|
||
<br>
|
||
<code class="text-amber-300 select-all cursor-pointer" @click="copyToClipboard(publicKeyData.command_hint)">{{ publicKeyData.command_hint }}</code>
|
||
</p>
|
||
</div>
|
||
|
||
<!-- SSH Password Input -->
|
||
<div v-if="settings.vps_auth_method === 'password'" class="mb-6">
|
||
<label class="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1">VPS SSH Password</label>
|
||
<input type="password" v-model="settings.vps_ssh_password" placeholder="Enter remote VPS SSH password" class="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-xs font-mono text-white">
|
||
</div>
|
||
|
||
<!-- Paths -->
|
||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||
<div>
|
||
<label class="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1">Remote Pangolin Path on VPS</label>
|
||
<input type="text" v-model="settings.vps_pangolin_dir" placeholder="/opt/pangolin" class="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-xs font-mono text-white">
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1">Local Unraid Backup Storage Path</label>
|
||
<input type="text" v-model="settings.backup_dir" placeholder="/mnt/user/backups/pangolin" class="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-xs font-mono text-white">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Password Change -->
|
||
<div class="glass-panel p-6 rounded-2xl border border-slate-800">
|
||
<h3 class="text-base font-bold text-white mb-4">Change Admin Password</h3>
|
||
<form @submit.prevent="handleChangePassword" class="space-y-4 max-w-md">
|
||
<div>
|
||
<label class="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1">Current Password</label>
|
||
<input type="password" v-model="changePasswordForm.oldPassword" required class="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-xs text-white">
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1">New Password</label>
|
||
<input type="password" v-model="changePasswordForm.newPassword" required placeholder="Min 6 characters" class="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-xs text-white">
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1">Confirm New Password</label>
|
||
<input type="password" v-model="changePasswordForm.confirmPassword" required class="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-xs text-white">
|
||
</div>
|
||
<button type="submit" class="py-2 px-4 bg-emerald-600 hover:bg-emerald-500 text-white rounded-lg text-xs font-semibold">
|
||
Update Admin Password
|
||
</button>
|
||
</form>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</main>
|
||
|
||
<!-- Footer -->
|
||
<footer class="border-t border-slate-800/80 py-4 px-6 text-center text-xs text-slate-500">
|
||
Pangolin Sentinel • Unraid Server to Remote VPS Backup & Update Hub
|
||
</footer>
|
||
</div>
|
||
|
||
<!-- ==================== MODALS ==================== -->
|
||
|
||
<!-- Create Backup Modal -->
|
||
<div v-if="backupModal.show" class="fixed inset-0 bg-black/70 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
||
<div class="glass-panel p-6 rounded-2xl max-w-lg w-full border border-slate-700 shadow-2xl">
|
||
<h3 class="text-lg font-bold text-white mb-2">Pull Hot-Backup from VPS to Unraid</h3>
|
||
<p class="text-xs text-slate-400 mb-5 leading-relaxed">
|
||
Executes a zero-lock SQLite hot-backup on your remote VPS ({{ settings.vps_host }}), bundles configs and certificates, and downloads the archive directly into Unraid storage.
|
||
</p>
|
||
|
||
<div class="space-y-4 mb-6">
|
||
<div>
|
||
<label class="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2">Backup Scope</label>
|
||
<div class="grid grid-cols-1 gap-2.5">
|
||
<label class="p-3 rounded-xl border cursor-pointer flex items-start gap-3 transition" :class="backupModal.scope === 'pangolin' ? 'bg-indigo-600/20 border-indigo-500/60' : 'bg-slate-900/60 border-slate-800'">
|
||
<input type="radio" v-model="backupModal.scope" value="pangolin" class="mt-1 text-indigo-600">
|
||
<div>
|
||
<div class="text-xs font-bold text-white">Pangolin Stack (Recommended)</div>
|
||
<div class="text-[11px] text-slate-400">Zero-lock SQLite hot snapshot, docker-compose.yml, config.yml, .env, and Traefik SSL certificates.</div>
|
||
</div>
|
||
</label>
|
||
|
||
<label class="p-3 rounded-xl border cursor-pointer flex items-start gap-3 transition" :class="backupModal.scope === 'full' ? 'bg-indigo-600/20 border-indigo-500/60' : 'bg-slate-900/60 border-slate-800'">
|
||
<input type="radio" v-model="backupModal.scope" value="full" class="mt-1 text-indigo-600">
|
||
<div>
|
||
<div class="text-xs font-bold text-white">Full Disaster Recovery</div>
|
||
<div class="text-[11px] text-slate-400">Combines Pangolin stack with VPS host configurations (/etc/ufw, network, crontabs).</div>
|
||
</div>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1">Description / Note</label>
|
||
<input type="text" v-model="backupModal.description" placeholder="e.g. Weekly automated snapshot or pre-update backup" class="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-xs text-slate-200">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex items-center justify-end gap-3">
|
||
<button @click="backupModal.show = false" class="px-4 py-2 bg-slate-800 text-slate-300 rounded-lg text-xs font-semibold">Cancel</button>
|
||
<button @click="triggerBackup" class="px-4 py-2 bg-emerald-600 hover:bg-emerald-500 text-white rounded-lg text-xs font-semibold shadow-lg">Start Backup Pull</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Inspect Archive Modal -->
|
||
<div v-if="inspectModal.show" class="fixed inset-0 bg-black/70 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
||
<div class="glass-panel p-6 rounded-2xl max-w-2xl w-full border border-slate-700 shadow-2xl flex flex-col max-h-[85vh]">
|
||
<div class="flex items-center justify-between pb-3 border-b border-slate-800 mb-3">
|
||
<div>
|
||
<h3 class="text-base font-bold text-white">Unraid Archive Explorer</h3>
|
||
<p class="text-xs text-slate-400 font-mono">{{ inspectModal.backup?.filename }}</p>
|
||
</div>
|
||
<button @click="inspectModal.show = false" class="text-slate-400 hover:text-white">×</button>
|
||
</div>
|
||
|
||
<div v-if="inspectModal.loading" class="text-center py-12 text-slate-400 text-xs">
|
||
Reading archive manifest...
|
||
</div>
|
||
|
||
<div v-else class="flex-1 overflow-y-auto space-y-1 font-mono text-xs pr-1">
|
||
<div v-for="m in inspectModal.members" :key="m.name" class="p-2 rounded hover:bg-slate-900/60 flex items-center justify-between">
|
||
<span class="text-slate-200 truncate mr-2">{{ m.name }}</span>
|
||
<span class="text-slate-500 text-[11px] shrink-0">{{ m.formatted_size }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="pt-4 border-t border-slate-800 mt-3 text-right">
|
||
<button @click="inspectModal.show = false" class="px-4 py-2 bg-slate-800 text-slate-300 rounded-lg text-xs font-semibold">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Restore to VPS Modal -->
|
||
<div v-if="restoreModal.show" class="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
||
<div class="glass-panel p-6 rounded-2xl max-w-md w-full border border-amber-500/40 shadow-2xl">
|
||
<div class="flex items-center gap-3 mb-4 text-amber-400">
|
||
<svg class="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>
|
||
<h3 class="text-lg font-bold text-white">Restore Backup to Remote VPS</h3>
|
||
</div>
|
||
|
||
<p class="text-xs text-slate-300 mb-4 leading-relaxed">
|
||
This will upload <span class="font-mono text-amber-300 font-bold">{{ restoreModal.backup?.filename }}</span> from your Unraid server to <span class="font-mono text-white">{{ settings.vps_host }}</span>, stop the Pangolin containers, replace the database and configs, and restart the stack.
|
||
</p>
|
||
|
||
<div class="mb-5">
|
||
<label class="block text-xs font-semibold text-slate-400 uppercase tracking-wider mb-1">Type "RESTORE" to proceed</label>
|
||
<input type="text" v-model="restoreModal.confirmText" placeholder="RESTORE" class="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-xs font-mono text-white">
|
||
</div>
|
||
|
||
<div class="flex items-center justify-end gap-3">
|
||
<button @click="restoreModal.show = false" class="px-4 py-2 bg-slate-800 text-slate-300 rounded-lg text-xs font-semibold">Cancel</button>
|
||
<button @click="executeRestore" :disabled="restoreModal.confirmText !== 'RESTORE'" class="px-4 py-2 bg-amber-600 hover:bg-amber-500 disabled:opacity-40 text-white rounded-lg text-xs font-semibold shadow-lg">
|
||
Push Restore to VPS
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Container Logs Modal -->
|
||
<div v-if="containerLogs.visible" class="fixed inset-0 bg-black/70 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
||
<div class="glass-panel p-6 rounded-2xl max-w-3xl w-full border border-slate-700 shadow-2xl flex flex-col max-h-[85vh]">
|
||
<div class="flex items-center justify-between pb-3 border-b border-slate-800 mb-3">
|
||
<div class="flex items-center gap-2">
|
||
<span class="w-2.5 h-2.5 rounded-full bg-emerald-400"></span>
|
||
<h3 class="text-base font-bold text-white font-mono">{{ containerLogs.name }} Logs (Remote VPS)</h3>
|
||
</div>
|
||
<button @click="containerLogs.visible = false" class="text-slate-400 hover:text-white">×</button>
|
||
</div>
|
||
|
||
<pre class="flex-1 bg-[#090d16] border border-slate-800 rounded-xl p-4 overflow-y-auto font-mono text-xs text-slate-300 leading-relaxed">{{ containerLogs.logs }}</pre>
|
||
|
||
<div class="pt-4 border-t border-slate-800 mt-3 text-right">
|
||
<button @click="containerLogs.visible = false" class="px-4 py-2 bg-slate-800 text-slate-300 rounded-lg text-xs font-semibold">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Toast Notifications -->
|
||
<div class="fixed bottom-5 right-5 z-50 flex flex-col gap-2 max-w-sm pointer-events-none">
|
||
<div v-for="t in toasts" :key="t.id" class="p-3.5 rounded-xl shadow-2xl text-xs font-semibold pointer-events-auto border flex items-center gap-2.5 transition" :class="t.type === 'success' ? 'bg-emerald-950/90 text-emerald-200 border-emerald-500/40' : t.type === 'error' ? 'bg-rose-950/90 text-rose-200 border-rose-500/40' : t.type === 'warning' ? 'bg-amber-950/90 text-amber-200 border-amber-500/40' : 'bg-slate-900/90 text-slate-200 border-slate-700'">
|
||
<span v-if="t.type === 'success'">✅</span>
|
||
<span v-else-if="t.type === 'error'">❌</span>
|
||
<span v-else-if="t.type === 'warning'">⚠️</span>
|
||
<span v-else>ℹ️</span>
|
||
<span class="flex-1 leading-snug">{{ t.message }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<script src="/assets/app.js"></script>
|
||
</body>
|
||
</html>
|