Files

1118 lines
75 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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 &rarr; 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 &amp; 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 &rarr;
</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 &harr; 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 &rarr;
</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 &rarr;
</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 &rarr; 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">&times;</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">&times;</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>