Initial commit: Pangolin Sentinel Docker & Unraid Hub
This commit is contained in:
1 parent
178b40ba37
commit
b4095b82ea
24 files changed
+4355
No files matched your search
@@ -0,0 +1,757 @@
|
||||
const { createApp, ref, reactive, computed, onMounted, onUnmounted, nextTick } = Vue;
|
||||
|
||||
createApp({
|
||||
setup() {
|
||||
// Auth State
|
||||
const authState = reactive({
|
||||
enabled: true,
|
||||
setupRequired: false,
|
||||
isLoggedIn: false,
|
||||
token: localStorage.getItem("pangolin_token") || "",
|
||||
password: "",
|
||||
setupPassword: "",
|
||||
setupPasswordConfirm: "",
|
||||
error: ""
|
||||
});
|
||||
|
||||
// Navigation Tab
|
||||
const currentTab = ref("dashboard");
|
||||
|
||||
// Alerts / Toasts
|
||||
const toasts = ref([]);
|
||||
const showToast = (message, type = "info") => {
|
||||
const id = Date.now();
|
||||
toasts.value.push({ id, message, type });
|
||||
setTimeout(() => {
|
||||
toasts.value = toasts.value.filter(t => t.id !== id);
|
||||
}, 4500);
|
||||
};
|
||||
|
||||
// System & VPS Stats State
|
||||
const systemStats = ref(null);
|
||||
const updatesData = ref({ total: 0, security_count: 0, packages: [] });
|
||||
const checkingUpdates = ref(false);
|
||||
|
||||
// SSH & VPS Connection State
|
||||
const publicKeyData = ref({ public_key: "", command_hint: "" });
|
||||
const connectionTest = reactive({
|
||||
testing: false,
|
||||
result: null
|
||||
});
|
||||
|
||||
// Settings State
|
||||
const settings = ref({
|
||||
vps_host: "",
|
||||
vps_port: 22,
|
||||
vps_user: "root",
|
||||
vps_auth_method: "key",
|
||||
vps_ssh_key_path: "/opt/pangolin-manager/data/id_ed25519",
|
||||
vps_ssh_password: "",
|
||||
vps_pangolin_dir: "/opt/pangolin",
|
||||
backup_dir: "/mnt/user/backups/pangolin",
|
||||
retention_count: 10,
|
||||
scheduled_backup_enabled: true,
|
||||
scheduled_backup_cron: "0 3 * * *",
|
||||
scheduled_backup_scope: "pangolin",
|
||||
scheduled_update_check_enabled: true,
|
||||
webhook_url: "",
|
||||
webhook_type: "discord",
|
||||
telegram_bot_token: "",
|
||||
telegram_chat_id: "",
|
||||
notify_on_backup: true,
|
||||
notify_on_update: true,
|
||||
auth_enabled: true
|
||||
});
|
||||
const settingsLoading = ref(false);
|
||||
const testingWebhook = ref(false);
|
||||
|
||||
// Pangolin Stack State
|
||||
const pangolinStatus = ref({
|
||||
vps_configured: false,
|
||||
connected: false,
|
||||
installed: false,
|
||||
path: "/opt/pangolin",
|
||||
containers: [],
|
||||
running_count: 0,
|
||||
total_count: 0,
|
||||
api_online: false,
|
||||
database: { found: false, path: null, size_bytes: 0 }
|
||||
});
|
||||
const stackFiles = ref({});
|
||||
const activeFileKey = ref("docker-compose.yml");
|
||||
const activeFileContent = ref("");
|
||||
const savingFile = ref(false);
|
||||
const containerLogs = ref({ name: "", logs: "", visible: false });
|
||||
|
||||
// Backups State (Stored on Unraid)
|
||||
const backups = ref([]);
|
||||
const backupsLoading = ref(false);
|
||||
const backupModal = reactive({
|
||||
show: false,
|
||||
scope: "pangolin",
|
||||
description: ""
|
||||
});
|
||||
const inspectModal = reactive({
|
||||
show: false,
|
||||
backup: null,
|
||||
members: [],
|
||||
loading: false
|
||||
});
|
||||
const restoreModal = reactive({
|
||||
show: false,
|
||||
backup: null,
|
||||
confirmText: ""
|
||||
});
|
||||
const verifyState = reactive({});
|
||||
|
||||
// Password change
|
||||
const changePasswordForm = reactive({
|
||||
oldPassword: "",
|
||||
newPassword: "",
|
||||
confirmPassword: ""
|
||||
});
|
||||
|
||||
// Jobs & Live Console State
|
||||
const jobs = ref([]);
|
||||
const activeJob = ref(null);
|
||||
const selectedJobId = ref(null);
|
||||
const autoScroll = ref(true);
|
||||
const terminalRef = ref(null);
|
||||
let ws = null;
|
||||
let statsInterval = null;
|
||||
|
||||
// API Helper with Auth
|
||||
const api = async (endpoint, options = {}) => {
|
||||
const headers = options.headers || {};
|
||||
if (authState.token) {
|
||||
headers["Authorization"] = `Bearer ${authState.token}`;
|
||||
}
|
||||
options.headers = headers;
|
||||
|
||||
try {
|
||||
const res = await fetch(endpoint, options);
|
||||
if (res.status === 401) {
|
||||
authState.isLoggedIn = false;
|
||||
throw new Error("Session expired. Please log in.");
|
||||
}
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
throw new Error(data.detail || data.error || `HTTP error ${res.status}`);
|
||||
}
|
||||
return data;
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
// Check Auth Status
|
||||
const checkAuth = async () => {
|
||||
try {
|
||||
const res = await fetch("/api/auth/status");
|
||||
const data = await res.json();
|
||||
authState.enabled = data.auth_enabled;
|
||||
authState.setupRequired = data.setup_required;
|
||||
if (!authState.enabled) {
|
||||
authState.isLoggedIn = true;
|
||||
initDashboard();
|
||||
} else if (authState.token && !authState.setupRequired) {
|
||||
try {
|
||||
await api("/api/system/stats");
|
||||
authState.isLoggedIn = true;
|
||||
initDashboard();
|
||||
} catch (e) {
|
||||
authState.isLoggedIn = false;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Auth check failed:", e);
|
||||
}
|
||||
};
|
||||
|
||||
// Login
|
||||
const handleLogin = async () => {
|
||||
authState.error = "";
|
||||
try {
|
||||
const res = await fetch("/api/auth/login", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ password: authState.password })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.detail || "Login failed");
|
||||
|
||||
authState.token = data.token;
|
||||
localStorage.setItem("pangolin_token", data.token);
|
||||
authState.isLoggedIn = true;
|
||||
authState.password = "";
|
||||
showToast("Logged in successfully", "success");
|
||||
initDashboard();
|
||||
} catch (err) {
|
||||
authState.error = err.message;
|
||||
}
|
||||
};
|
||||
|
||||
// Setup
|
||||
const handleSetup = async () => {
|
||||
authState.error = "";
|
||||
if (authState.setupPassword !== authState.setupPasswordConfirm) {
|
||||
authState.error = "Passwords do not match.";
|
||||
return;
|
||||
}
|
||||
if (authState.setupPassword.length < 6) {
|
||||
authState.error = "Password must be at least 6 characters.";
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/auth/setup", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ password: authState.setupPassword })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.detail || "Setup failed");
|
||||
|
||||
authState.token = data.token;
|
||||
localStorage.setItem("pangolin_token", data.token);
|
||||
authState.setupRequired = false;
|
||||
authState.isLoggedIn = true;
|
||||
showToast("Admin account configured!", "success");
|
||||
initDashboard();
|
||||
} catch (err) {
|
||||
authState.error = err.message;
|
||||
}
|
||||
};
|
||||
|
||||
// Logout
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await fetch("/api/auth/logout", { method: "POST" });
|
||||
} catch (e) {}
|
||||
localStorage.removeItem("pangolin_token");
|
||||
authState.token = "";
|
||||
authState.isLoggedIn = false;
|
||||
showToast("Logged out", "info");
|
||||
};
|
||||
|
||||
// Load Public Key
|
||||
const fetchPublicKey = async () => {
|
||||
try {
|
||||
publicKeyData.value = await api("/api/remote/public-key");
|
||||
} catch (e) {
|
||||
console.error("Failed to load public key:", e);
|
||||
}
|
||||
};
|
||||
|
||||
// Test VPS Connection
|
||||
const testVpsConnection = async () => {
|
||||
connectionTest.testing = true;
|
||||
connectionTest.result = null;
|
||||
try {
|
||||
const res = await api("/api/remote/test-connection", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ settings: settings.value })
|
||||
});
|
||||
connectionTest.result = res;
|
||||
if (res.connected) {
|
||||
showToast(`Connected to ${res.hostname} (${res.os})!`, "success");
|
||||
await fetchSystemStats();
|
||||
await fetchPangolinStatus();
|
||||
} else {
|
||||
showToast(`Connection failed: ${res.error}`, "error");
|
||||
}
|
||||
} catch (e) {
|
||||
connectionTest.result = { connected: false, error: e.message };
|
||||
showToast(e.message, "error");
|
||||
} finally {
|
||||
connectionTest.testing = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Load Stats
|
||||
const fetchSystemStats = async () => {
|
||||
try {
|
||||
systemStats.value = await api("/api/system/stats");
|
||||
} catch (e) {
|
||||
console.error("Failed to load system stats:", e);
|
||||
}
|
||||
};
|
||||
|
||||
// Check Updates
|
||||
const checkUpdates = async () => {
|
||||
checkingUpdates.value = true;
|
||||
try {
|
||||
updatesData.value = await api("/api/system/updates");
|
||||
showToast(`Found ${updatesData.value.total} package update(s) on VPS`, "info");
|
||||
} catch (e) {
|
||||
showToast(`Update check error: ${e.message}`, "error");
|
||||
} finally {
|
||||
checkingUpdates.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Upgrade VPS packages
|
||||
const triggerSystemUpgrade = async (dist = false) => {
|
||||
if (!confirm(`Are you sure you want to upgrade packages on remote VPS (${settings.value.vps_host})?`)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await api("/api/system/upgrade", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ dist_upgrade: dist })
|
||||
});
|
||||
showToast("Remote VPS package upgrade started!", "info");
|
||||
currentTab.value = "console";
|
||||
selectedJobId.value = res.job_id;
|
||||
} catch (e) {
|
||||
showToast(e.message, "error");
|
||||
}
|
||||
};
|
||||
|
||||
// Reboot VPS
|
||||
const triggerReboot = async () => {
|
||||
if (!confirm(`⚠️ WARNING: Are you sure you want to REBOOT the remote VPS (${settings.value.vps_host})?`)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await api("/api/system/reboot", { method: "POST" });
|
||||
showToast("Remote VPS Reboot scheduled in 1 minute.", "warning");
|
||||
} catch (e) {
|
||||
showToast(e.message, "error");
|
||||
}
|
||||
};
|
||||
|
||||
// Pangolin Stack Calls
|
||||
const fetchPangolinStatus = async () => {
|
||||
try {
|
||||
pangolinStatus.value = await api("/api/pangolin/status");
|
||||
} catch (e) {
|
||||
console.error("Failed to load Pangolin status:", e);
|
||||
}
|
||||
};
|
||||
|
||||
const updatePangolinStack = async () => {
|
||||
if (!confirm(`Update Pangolin stack on remote VPS (${settings.value.vps_host})? This will pull latest Docker images and recreate containers.`)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await api("/api/pangolin/update", { method: "POST" });
|
||||
showToast("Remote Pangolin stack update started!", "info");
|
||||
currentTab.value = "console";
|
||||
selectedJobId.value = res.job_id;
|
||||
} catch (e) {
|
||||
showToast(e.message, "error");
|
||||
}
|
||||
};
|
||||
|
||||
const handleContainerAction = async (name, action) => {
|
||||
try {
|
||||
await api(`/api/pangolin/containers/${name}/action`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ action })
|
||||
});
|
||||
showToast(`Container ${name} ${action} succeeded on VPS`, "success");
|
||||
await fetchPangolinStatus();
|
||||
} catch (e) {
|
||||
showToast(e.message, "error");
|
||||
}
|
||||
};
|
||||
|
||||
const viewContainerLogs = async (name) => {
|
||||
try {
|
||||
const data = await api(`/api/pangolin/containers/${name}/logs?lines=200`);
|
||||
containerLogs.value = {
|
||||
name,
|
||||
logs: data.logs || "No logs available.",
|
||||
visible: true
|
||||
};
|
||||
} catch (e) {
|
||||
showToast(e.message, "error");
|
||||
}
|
||||
};
|
||||
|
||||
const loadStackFiles = async () => {
|
||||
try {
|
||||
const files = await api("/api/pangolin/files");
|
||||
stackFiles.value = files;
|
||||
const keys = Object.keys(files);
|
||||
if (keys.length > 0 && (!activeFileKey.value || !files[activeFileKey.value])) {
|
||||
activeFileKey.value = keys[0];
|
||||
}
|
||||
if (activeFileKey.value && files[activeFileKey.value]) {
|
||||
activeFileContent.value = files[activeFileKey.value];
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to load stack files:", e);
|
||||
}
|
||||
};
|
||||
|
||||
const selectStackFile = (key) => {
|
||||
activeFileKey.value = key;
|
||||
activeFileContent.value = stackFiles.value[key] || "";
|
||||
};
|
||||
|
||||
const saveStackFile = async () => {
|
||||
savingFile.value = true;
|
||||
try {
|
||||
await api("/api/pangolin/files", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
filename: activeFileKey.value,
|
||||
content: activeFileContent.value
|
||||
})
|
||||
});
|
||||
stackFiles.value[activeFileKey.value] = activeFileContent.value;
|
||||
showToast(`Saved ${activeFileKey.value} on remote VPS (snapshot created)`, "success");
|
||||
} catch (e) {
|
||||
showToast(e.message, "error");
|
||||
} finally {
|
||||
savingFile.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Backups
|
||||
const fetchBackups = async () => {
|
||||
backupsLoading.value = true;
|
||||
try {
|
||||
backups.value = await api("/api/backups");
|
||||
} catch (e) {
|
||||
console.error("Failed to load backups:", e);
|
||||
} finally {
|
||||
backupsLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const triggerBackup = async () => {
|
||||
try {
|
||||
const res = await api("/api/backups", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
scope: backupModal.scope,
|
||||
description: backupModal.description
|
||||
})
|
||||
});
|
||||
backupModal.show = false;
|
||||
backupModal.description = "";
|
||||
showToast(`Pulling backup from VPS to Unraid (${backupModal.scope.toUpperCase()})...`, "info");
|
||||
currentTab.value = "console";
|
||||
selectedJobId.value = res.job_id;
|
||||
} catch (e) {
|
||||
showToast(e.message, "error");
|
||||
}
|
||||
};
|
||||
|
||||
const inspectBackupArchive = async (b) => {
|
||||
inspectModal.backup = b;
|
||||
inspectModal.show = true;
|
||||
inspectModal.loading = true;
|
||||
inspectModal.members = [];
|
||||
try {
|
||||
const data = await api(`/api/backups/${b.id}/inspect`);
|
||||
inspectModal.members = data.members;
|
||||
} catch (e) {
|
||||
showToast(`Inspect error: ${e.message}`, "error");
|
||||
} finally {
|
||||
inspectModal.loading = false;
|
||||
}
|
||||
};
|
||||
|
||||
const verifyBackupArchive = async (b) => {
|
||||
verifyState[b.id] = { loading: true };
|
||||
try {
|
||||
const res = await api(`/api/backups/${b.id}/verify`, { method: "POST" });
|
||||
verifyState[b.id] = {
|
||||
loading: false,
|
||||
verified: res.verified,
|
||||
details: res.verified ? "Integrity OK on Unraid storage" : "Verification FAILED"
|
||||
};
|
||||
showToast(res.verified ? `Backup ${b.id} integrity verified on Unraid!` : "Integrity check failed!", res.verified ? "success" : "error");
|
||||
} catch (e) {
|
||||
verifyState[b.id] = { loading: false, verified: false, details: e.message };
|
||||
showToast(e.message, "error");
|
||||
}
|
||||
};
|
||||
|
||||
const promptRestore = (b) => {
|
||||
restoreModal.backup = b;
|
||||
restoreModal.confirmText = "";
|
||||
restoreModal.show = true;
|
||||
};
|
||||
|
||||
const executeRestore = async () => {
|
||||
if (restoreModal.confirmText !== "RESTORE") {
|
||||
showToast("Please type RESTORE to confirm.", "warning");
|
||||
return;
|
||||
}
|
||||
const b = restoreModal.backup;
|
||||
restoreModal.show = false;
|
||||
try {
|
||||
const res = await api(`/api/backups/${b.id}/restore`, { method: "POST" });
|
||||
showToast(`Pushed restore job to remote VPS!`, "warning");
|
||||
currentTab.value = "console";
|
||||
selectedJobId.value = res.job_id;
|
||||
} catch (e) {
|
||||
showToast(e.message, "error");
|
||||
}
|
||||
};
|
||||
|
||||
const deleteBackupArchive = async (b) => {
|
||||
if (!confirm(`Permanently delete backup "${b.filename}" from Unraid?`)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await api(`/api/backups/${b.id}`, { method: "DELETE" });
|
||||
showToast("Backup deleted from Unraid", "info");
|
||||
await fetchBackups();
|
||||
} catch (e) {
|
||||
showToast(e.message, "error");
|
||||
}
|
||||
};
|
||||
|
||||
const handleUploadBackup = async (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
try {
|
||||
showToast(`Uploading ${file.name} to Unraid...`, "info");
|
||||
const headers = {};
|
||||
if (authState.token) headers["Authorization"] = `Bearer ${authState.token}`;
|
||||
const res = await fetch("/api/backups/upload", {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: formData
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.detail || "Upload failed");
|
||||
showToast("Backup uploaded to Unraid storage!", "success");
|
||||
await fetchBackups();
|
||||
} catch (e) {
|
||||
showToast(e.message, "error");
|
||||
} finally {
|
||||
event.target.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
const downloadBackupUrl = (b) => {
|
||||
return `/api/backups/${b.id}/download?token=${encodeURIComponent(authState.token)}`;
|
||||
};
|
||||
|
||||
// Settings
|
||||
const fetchSettings = async () => {
|
||||
try {
|
||||
settings.value = await api("/api/settings");
|
||||
} catch (e) {
|
||||
console.error("Failed to load settings:", e);
|
||||
}
|
||||
};
|
||||
|
||||
const saveSettings = async () => {
|
||||
settingsLoading.value = true;
|
||||
try {
|
||||
await api("/api/settings", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ settings: settings.value })
|
||||
});
|
||||
showToast("Configuration saved successfully!", "success");
|
||||
await fetchSystemStats();
|
||||
await fetchPangolinStatus();
|
||||
} catch (e) {
|
||||
showToast(e.message, "error");
|
||||
} finally {
|
||||
settingsLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const testWebhook = async () => {
|
||||
testingWebhook.value = true;
|
||||
try {
|
||||
const res = await api("/api/notifications/test", { method: "POST" });
|
||||
if (res.success) {
|
||||
showToast("Webhook test sent successfully!", "success");
|
||||
} else {
|
||||
showToast(`Webhook error: ${res.error || res.text}`, "error");
|
||||
}
|
||||
} catch (e) {
|
||||
showToast(`Webhook test error: ${e.message}`, "error");
|
||||
} finally {
|
||||
testingWebhook.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Jobs & WebSocket Console
|
||||
const fetchJobs = async () => {
|
||||
try {
|
||||
jobs.value = await api("/api/jobs");
|
||||
if (!selectedJobId.value && jobs.value.length > 0) {
|
||||
selectedJobId.value = jobs.value[0].id;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to load jobs:", e);
|
||||
}
|
||||
};
|
||||
|
||||
const displayedJob = computed(() => {
|
||||
if (activeJob.value && (!selectedJobId.value || selectedJobId.value === activeJob.value.id)) {
|
||||
return activeJob.value;
|
||||
}
|
||||
return jobs.value.find(j => j.id === selectedJobId.value) || activeJob.value || null;
|
||||
});
|
||||
|
||||
const connectWebSocket = () => {
|
||||
const proto = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
const url = `${proto}//${window.location.host}/ws/jobs`;
|
||||
|
||||
try {
|
||||
ws = new WebSocket(url);
|
||||
ws.onopen = () => {
|
||||
setInterval(() => {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) ws.send("ping");
|
||||
}, 20000);
|
||||
};
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const msg = JSON.parse(event.data);
|
||||
if (msg.type === "job_started") {
|
||||
activeJob.value = msg.job;
|
||||
selectedJobId.value = msg.job.id;
|
||||
jobs.value.unshift(msg.job);
|
||||
} else if (msg.type === "log") {
|
||||
if (activeJob.value && activeJob.value.id === msg.job_id) {
|
||||
activeJob.value.logs.push(msg.data);
|
||||
scrollTerminal();
|
||||
}
|
||||
const target = jobs.value.find(j => j.id === msg.job_id);
|
||||
if (target && target !== activeJob.value) {
|
||||
target.logs.push(msg.data);
|
||||
}
|
||||
} else if (msg.type === "job_finished") {
|
||||
if (activeJob.value && activeJob.value.id === msg.job.id) {
|
||||
activeJob.value = msg.job;
|
||||
}
|
||||
const idx = jobs.value.findIndex(j => j.id === msg.job.id);
|
||||
if (idx !== -1) jobs.value[idx] = msg.job;
|
||||
|
||||
if (msg.job.type === "backup") fetchBackups();
|
||||
if (msg.job.type === "update_pangolin" || msg.job.type === "restore") fetchPangolinStatus();
|
||||
if (msg.job.type === "update_system") fetchSystemStats();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("WS error:", e);
|
||||
}
|
||||
};
|
||||
ws.onclose = () => setTimeout(connectWebSocket, 4000);
|
||||
} catch (e) {
|
||||
console.error("WS connection error:", e);
|
||||
}
|
||||
};
|
||||
|
||||
const scrollTerminal = () => {
|
||||
if (!autoScroll.value) return;
|
||||
nextTick(() => {
|
||||
if (terminalRef.value) {
|
||||
terminalRef.value.scrollTop = terminalRef.value.scrollHeight;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const copyToClipboard = (text) => {
|
||||
navigator.clipboard.writeText(text);
|
||||
showToast("Copied to clipboard!", "info");
|
||||
};
|
||||
|
||||
// Dashboard init
|
||||
const initDashboard = async () => {
|
||||
await fetchSettings();
|
||||
await fetchPublicKey();
|
||||
await fetchSystemStats();
|
||||
await fetchPangolinStatus();
|
||||
await fetchBackups();
|
||||
await fetchJobs();
|
||||
loadStackFiles();
|
||||
if (settings.value.vps_host) {
|
||||
checkUpdates();
|
||||
}
|
||||
connectWebSocket();
|
||||
|
||||
if (statsInterval) clearInterval(statsInterval);
|
||||
statsInterval = setInterval(() => {
|
||||
if (authState.isLoggedIn && settings.value.vps_host) {
|
||||
fetchSystemStats();
|
||||
}
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
onMounted(() => checkAuth());
|
||||
onUnmounted(() => {
|
||||
if (statsInterval) clearInterval(statsInterval);
|
||||
if (ws) ws.close();
|
||||
});
|
||||
|
||||
return {
|
||||
authState,
|
||||
currentTab,
|
||||
toasts,
|
||||
showToast,
|
||||
systemStats,
|
||||
updatesData,
|
||||
checkingUpdates,
|
||||
publicKeyData,
|
||||
connectionTest,
|
||||
settings,
|
||||
settingsLoading,
|
||||
testingWebhook,
|
||||
pangolinStatus,
|
||||
stackFiles,
|
||||
activeFileKey,
|
||||
activeFileContent,
|
||||
savingFile,
|
||||
containerLogs,
|
||||
backups,
|
||||
backupsLoading,
|
||||
backupModal,
|
||||
inspectModal,
|
||||
restoreModal,
|
||||
verifyState,
|
||||
changePasswordForm,
|
||||
jobs,
|
||||
activeJob,
|
||||
selectedJobId,
|
||||
displayedJob,
|
||||
autoScroll,
|
||||
terminalRef,
|
||||
handleLogin,
|
||||
handleSetup,
|
||||
handleLogout,
|
||||
testVpsConnection,
|
||||
fetchSystemStats,
|
||||
checkUpdates,
|
||||
triggerSystemUpgrade,
|
||||
triggerReboot,
|
||||
fetchPangolinStatus,
|
||||
updatePangolinStack,
|
||||
handleContainerAction,
|
||||
viewContainerLogs,
|
||||
selectStackFile,
|
||||
saveStackFile,
|
||||
fetchBackups,
|
||||
triggerBackup,
|
||||
inspectBackupArchive,
|
||||
verifyBackupArchive,
|
||||
promptRestore,
|
||||
executeRestore,
|
||||
deleteBackupArchive,
|
||||
handleUploadBackup,
|
||||
downloadBackupUrl,
|
||||
saveSettings,
|
||||
testWebhook,
|
||||
copyToClipboard
|
||||
};
|
||||
}
|
||||
}).mount("#app");
|
||||
Reference in new issue
Block a user