| function hugpanel() { |
| return { |
| |
| user: null, |
| token: localStorage.getItem('hugpanel_token'), |
| adminApiUrl: localStorage.getItem('hugpanel_admin_url') || '', |
| authLoading: true, |
| authMode: 'login', |
| authError: '', |
| authSubmitting: false, |
| loginForm: { username: '', password: '' }, |
| registerForm: { username: '', email: '', password: '' }, |
|
|
| |
| sidebarOpen: false, |
| zones: [], |
| currentZone: localStorage.getItem('hugpanel_zone') || null, |
| activeTab: localStorage.getItem('hugpanel_tab') || 'files', |
| maxZones: 0, |
| motd: '', |
| registrationDisabled: false, |
| isDesktop: window.innerWidth >= 1024, |
| tabs: [ |
| { id: 'files', label: 'Files', icon: 'folder' }, |
| { id: 'editor', label: 'Editor', icon: 'file-code' }, |
| { id: 'terminal', label: 'Terminal', icon: 'terminal' }, |
| { id: 'ports', label: 'Ports', icon: 'radio' }, |
| { id: 'backup', label: 'Backup', icon: 'cloud' }, |
| ], |
|
|
| |
| files: [], |
| currentPath: '', |
| filesLoading: false, |
| showNewFile: false, |
| showNewFolder: false, |
| newFileName: '', |
| newFolderName: '', |
|
|
| |
| editorFile: null, |
| editorContent: '', |
| editorOriginal: '', |
| editorDirty: false, |
|
|
| |
| term: null, |
| termWs: null, |
| termFit: null, |
| termZone: null, |
|
|
| |
| ports: [], |
| newPort: null, |
| newPortLabel: '', |
|
|
| |
| backupStatus: { configured: false, admin_url: null, running: false, last: null, error: null, progress: '' }, |
| backupList: [], |
| backupLoading: false, |
| backupFilterZone: '', |
|
|
| |
| showCreateZone: false, |
| createZoneName: '', |
| createZoneDesc: '', |
| showEditZone: false, |
| editZoneName: '', |
| editZoneDesc: '', |
|
|
| |
| showRename: false, |
| renameOldPath: '', |
| renameNewName: '', |
|
|
| |
| toast: { show: false, message: '', type: 'info' }, |
|
|
| |
| get currentPathParts() { |
| return this.currentPath ? this.currentPath.split('/').filter(Boolean) : []; |
| }, |
|
|
| get filteredBackupList() { |
| if (!this.backupFilterZone) return this.backupList; |
| return this.backupList.filter((item) => item.zone_name === this.backupFilterZone); |
| }, |
|
|
| |
| async init() { |
| |
| await this.loadBackupStatus(); |
| if (this.backupStatus.admin_url) { |
| this.adminApiUrl = this.backupStatus.admin_url; |
| localStorage.setItem('hugpanel_admin_url', this.adminApiUrl); |
| } |
|
|
| |
| await this._loadZoneLimit(); |
|
|
| |
| this.syncAuthCookie(); |
|
|
| if (this.token && this.adminApiUrl) { |
| try { |
| const resp = await fetch(`${this.adminApiUrl}/auth/me`, { |
| headers: { 'Authorization': `Bearer ${this.token}` }, |
| }); |
| if (resp.ok) { |
| const data = await resp.json(); |
| this.user = data.user; |
| } else { |
| |
| this.token = null; |
| localStorage.removeItem('hugpanel_token'); |
| } |
| } catch { |
| |
| } |
| } else if (!this.adminApiUrl) { |
| |
| } else { |
| |
| this.token = null; |
| } |
|
|
| this.authLoading = false; |
|
|
| if (this.user) { |
| await this._loadPanel(); |
| } |
|
|
| this.$nextTick(() => lucide.createIcons()); |
|
|
| |
| this.$watch('zones', () => this.$nextTick(() => lucide.createIcons())); |
| this.$watch('files', () => this.$nextTick(() => lucide.createIcons())); |
| this.$watch('activeTab', () => this.$nextTick(() => lucide.createIcons())); |
| this.$watch('currentZone', () => this.$nextTick(() => lucide.createIcons())); |
| this.$watch('ports', () => this.$nextTick(() => lucide.createIcons())); |
| this.$watch('backupList', () => this.$nextTick(() => lucide.createIcons())); |
| this.$watch('backupStatus', () => this.$nextTick(() => lucide.createIcons())); |
| this.$watch('showCreateZone', () => { |
| this.$nextTick(() => { |
| lucide.createIcons(); |
| if (this.showCreateZone) this.$refs.zoneNameInput?.focus(); |
| }); |
| }); |
| this.$watch('showNewFile', () => { |
| this.$nextTick(() => { if (this.showNewFile) this.$refs.newFileInput?.focus(); }); |
| }); |
| this.$watch('showNewFolder', () => { |
| this.$nextTick(() => { if (this.showNewFolder) this.$refs.newFolderInput?.focus(); }); |
| }); |
| this.$watch('showEditZone', () => { |
| this.$nextTick(() => { |
| lucide.createIcons(); |
| if (this.showEditZone) this.$refs.editZoneNameInput?.focus(); |
| }); |
| }); |
| this.$watch('showRename', () => { |
| this.$nextTick(() => { |
| lucide.createIcons(); |
| if (this.showRename) this.$refs.renameInput?.focus(); |
| }); |
| }); |
|
|
| |
| const mql = window.matchMedia('(min-width: 1024px)'); |
| mql.addEventListener('change', (e) => { this.isDesktop = e.matches; }); |
|
|
| |
| this.$watch('currentZone', (val) => { |
| if (val) localStorage.setItem('hugpanel_zone', val); |
| else localStorage.removeItem('hugpanel_zone'); |
| }); |
| this.$watch('activeTab', (val) => localStorage.setItem('hugpanel_tab', val)); |
|
|
| |
| document.addEventListener('keydown', (e) => { |
| if (e.ctrlKey && e.key === 's' && this.activeTab === 'editor') { |
| e.preventDefault(); |
| this.saveFile(); |
| } |
| }); |
| }, |
|
|
| |
| notify(message, type = 'info') { |
| this.toast = { show: true, message, type }; |
| setTimeout(() => { this.toast.show = false; }, 3000); |
| }, |
|
|
| setAuthCookie(token) { |
| const secure = location.protocol === 'https:' ? '; Secure' : ''; |
| document.cookie = `token=${encodeURIComponent(token)}; Path=/; SameSite=Lax${secure}`; |
| }, |
|
|
| clearAuthCookie() { |
| document.cookie = 'token=; Path=/; Max-Age=0; SameSite=Lax'; |
| }, |
|
|
| syncAuthCookie() { |
| if (this.token) this.setAuthCookie(this.token); |
| else this.clearAuthCookie(); |
| }, |
|
|
| |
| async api(url, options = {}) { |
| try { |
| const headers = options.headers || {}; |
| |
| if (this.token) { |
| headers['Authorization'] = `Bearer ${this.token}`; |
| } |
| const resp = await fetch(url, { ...options, headers: { ...headers, ...options.headers } }); |
| if (!resp.ok) { |
| const data = await resp.json().catch(() => ({ detail: resp.statusText })); |
| throw new Error(data.detail || resp.statusText); |
| } |
| return await resp.json(); |
| } catch (err) { |
| this.notify(err.message, 'error'); |
| throw err; |
| } |
| }, |
|
|
| |
| async _loadPanel() { |
| await this.loadZones(); |
| await this.loadBackupStatus(); |
| |
| if (this.currentZone && this.zones.some(z => z.name === this.currentZone)) { |
| await this.selectZone(this.currentZone); |
| } else { |
| this.currentZone = null; |
| } |
| |
| await this._loadZoneLimit(); |
| }, |
|
|
| async _loadZoneLimit() { |
| if (!this.adminApiUrl) return; |
| try { |
| const resp = await fetch(`${this.adminApiUrl}/config`); |
| if (resp.ok) { |
| const data = await resp.json(); |
| this.maxZones = data.max_zones || 0; |
| this.motd = data.motd || ''; |
| this.registrationDisabled = !!data.disable_registration; |
| } |
| } catch {} |
| }, |
|
|
| async login() { |
| if (!this.adminApiUrl) { |
| this.authError = 'ADMIN_API_URL chưa cấu hình trên server'; |
| return; |
| } |
| this.authError = ''; |
| this.authSubmitting = true; |
| try { |
| const resp = await fetch(`${this.adminApiUrl}/auth/login`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify(this.loginForm), |
| }); |
| const data = await resp.json(); |
| if (!resp.ok) { |
| this.authError = data.error || 'Đăng nhập thất bại'; |
| this.authSubmitting = false; |
| return; |
| } |
| this.token = data.token; |
| this.user = data.user; |
| localStorage.setItem('hugpanel_token', data.token); |
| this.syncAuthCookie(); |
| await this._loadPanel(); |
| this.$nextTick(() => lucide.createIcons()); |
| } catch (err) { |
| this.authError = 'Không thể kết nối Admin Server'; |
| } |
| this.authSubmitting = false; |
| }, |
|
|
| async register() { |
| if (!this.adminApiUrl) { |
| this.authError = 'ADMIN_API_URL chưa cấu hình trên server'; |
| return; |
| } |
| this.authError = ''; |
| this.authSubmitting = true; |
| try { |
| const resp = await fetch(`${this.adminApiUrl}/auth/register`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify(this.registerForm), |
| }); |
| const data = await resp.json(); |
| if (!resp.ok) { |
| this.authError = data.error || 'Đăng ký thất bại'; |
| this.authSubmitting = false; |
| return; |
| } |
| this.token = data.token; |
| this.user = data.user; |
| localStorage.setItem('hugpanel_token', data.token); |
| this.syncAuthCookie(); |
| await this._loadPanel(); |
| this.$nextTick(() => lucide.createIcons()); |
| } catch (err) { |
| this.authError = 'Không thể kết nối Admin Server'; |
| } |
| this.authSubmitting = false; |
| }, |
|
|
| logout() { |
| this.token = null; |
| this.user = null; |
| this.clearAuthCookie(); |
| localStorage.removeItem('hugpanel_token'); |
| localStorage.removeItem('hugpanel_admin_url'); |
| localStorage.removeItem('hugpanel_zone'); |
| localStorage.removeItem('hugpanel_tab'); |
| this.currentZone = null; |
| this.disconnectTerminal(); |
| }, |
|
|
| |
| async loadZones() { |
| try { |
| this.zones = await this.api('/api/zones'); |
| } catch { this.zones = []; } |
| }, |
|
|
| async selectZone(name) { |
| this.currentZone = name; |
| this.currentPath = ''; |
| this.editorFile = null; |
| this.editorDirty = false; |
| this.activeTab = 'files'; |
| this.disconnectTerminal(); |
| await this.loadFiles(); |
| await this.loadPorts(); |
| if (this.backupStatus.configured) { |
| await this.loadBackupList(); |
| } |
| }, |
|
|
| async createZone() { |
| if (!this.createZoneName.trim()) return; |
| if (this.maxZones > 0 && this.zones.length >= this.maxZones) { |
| this.notify(`Đã đạt giới hạn ${this.maxZones} zones`, 'error'); |
| return; |
| } |
| const form = new FormData(); |
| form.append('name', this.createZoneName.trim()); |
| form.append('description', this.createZoneDesc.trim()); |
| try { |
| await this.api('/api/zones', { method: 'POST', body: form }); |
| this.showCreateZone = false; |
| this.createZoneName = ''; |
| this.createZoneDesc = ''; |
| await this.loadZones(); |
| this.notify('Zone đã được tạo'); |
| } catch {} |
| }, |
|
|
| startEditZone(zone = null) { |
| const current = zone || this.zones.find((item) => item.name === this.currentZone); |
| if (!current) return; |
| this.editZoneName = current.name || ''; |
| this.editZoneDesc = current.description || ''; |
| this.showEditZone = true; |
| }, |
|
|
| async saveZoneSettings() { |
| if (!this.currentZone) return; |
| const form = new FormData(); |
| form.append('new_name', this.editZoneName.trim()); |
| form.append('description', this.editZoneDesc.trim()); |
| try { |
| const data = await this.api(`/api/zones/${this.currentZone}`, { method: 'PATCH', body: form }); |
| const previous = this.currentZone; |
| this.currentZone = data.name; |
| this.showEditZone = false; |
| await this.loadZones(); |
| if (previous !== data.name) { |
| await this.selectZone(data.name); |
| } else { |
| await this.loadFiles(); |
| } |
| this.notify('Đã cập nhật zone'); |
| } catch {} |
| }, |
|
|
| async confirmDeleteZone() { |
| if (!this.currentZone) return; |
| if (!confirm(`Xoá zone "${this.currentZone}"? Toàn bộ dữ liệu sẽ bị mất.`)) return; |
| try { |
| await this.api(`/api/zones/${this.currentZone}`, { method: 'DELETE' }); |
| this.disconnectTerminal(); |
| this.currentZone = null; |
| await this.loadZones(); |
| this.notify('Zone đã bị xoá'); |
| } catch {} |
| }, |
|
|
| |
| async loadFiles() { |
| if (!this.currentZone) return; |
| this.filesLoading = true; |
| try { |
| this.files = await this.api(`/api/zones/${this.currentZone}/files?path=${encodeURIComponent(this.currentPath)}`); |
| } catch { this.files = []; } |
| this.filesLoading = false; |
| }, |
|
|
| navigateTo(path) { |
| this.currentPath = path; |
| this.loadFiles(); |
| }, |
|
|
| navigateUp() { |
| const parts = this.currentPath.split('/').filter(Boolean); |
| parts.pop(); |
| this.currentPath = parts.join('/'); |
| this.loadFiles(); |
| }, |
|
|
| joinPath(base, name) { |
| return base ? `${base}/${name}` : name; |
| }, |
|
|
| async openFile(path) { |
| if (this.editorDirty && !confirm('Bạn có thay đổi chưa lưu. Bỏ qua?')) return; |
| try { |
| const data = await this.api(`/api/zones/${this.currentZone}/files/read?path=${encodeURIComponent(path)}`); |
| this.editorFile = path; |
| this.editorContent = data.content; |
| this.editorOriginal = data.content; |
| this.editorDirty = false; |
| this.activeTab = 'editor'; |
| } catch {} |
| }, |
|
|
| async saveFile() { |
| if (!this.editorFile || !this.editorDirty) return; |
| const form = new FormData(); |
| form.append('path', this.editorFile); |
| form.append('content', this.editorContent); |
| try { |
| await this.api(`/api/zones/${this.currentZone}/files/write`, { method: 'POST', body: form }); |
| this.editorOriginal = this.editorContent; |
| this.editorDirty = false; |
| this.notify('Đã lưu'); |
| } catch {} |
| }, |
|
|
| async createFile() { |
| if (!this.newFileName.trim()) return; |
| const path = this.joinPath(this.currentPath, this.newFileName.trim()); |
| const form = new FormData(); |
| form.append('path', path); |
| form.append('content', ''); |
| try { |
| await this.api(`/api/zones/${this.currentZone}/files/write`, { method: 'POST', body: form }); |
| this.newFileName = ''; |
| this.showNewFile = false; |
| await this.loadFiles(); |
| } catch {} |
| }, |
|
|
| async createFolder() { |
| if (!this.newFolderName.trim()) return; |
| const path = this.joinPath(this.currentPath, this.newFolderName.trim()); |
| const form = new FormData(); |
| form.append('path', path); |
| try { |
| await this.api(`/api/zones/${this.currentZone}/files/mkdir`, { method: 'POST', body: form }); |
| this.newFolderName = ''; |
| this.showNewFolder = false; |
| await this.loadFiles(); |
| } catch {} |
| }, |
|
|
| async uploadFile(event) { |
| const fileList = event.target.files; |
| if (!fileList || fileList.length === 0) return; |
| for (const file of fileList) { |
| const form = new FormData(); |
| form.append('path', this.currentPath); |
| form.append('file', file); |
| try { |
| await this.api(`/api/zones/${this.currentZone}/files/upload`, { method: 'POST', body: form }); |
| } catch {} |
| } |
| event.target.value = ''; |
| await this.loadFiles(); |
| this.notify(`Đã upload ${fileList.length} file`); |
| }, |
|
|
| async deleteFile(path, isDir) { |
| const label = isDir ? 'thư mục' : 'file'; |
| if (!confirm(`Xoá ${label} "${path}"?`)) return; |
| try { |
| await this.api(`/api/zones/${this.currentZone}/files?path=${encodeURIComponent(path)}`, { method: 'DELETE' }); |
| if (this.editorFile === path) { |
| this.editorFile = null; |
| this.editorDirty = false; |
| } |
| await this.loadFiles(); |
| } catch {} |
| }, |
|
|
| async downloadFile(path, name) { |
| try { |
| const resp = await fetch( |
| `/api/zones/${this.currentZone}/files/download?path=${encodeURIComponent(path)}`, |
| { headers: this.token ? { 'Authorization': `Bearer ${this.token}` } : {} } |
| ); |
| if (!resp.ok) throw new Error('Download failed'); |
| const blob = await resp.blob(); |
| const url = URL.createObjectURL(blob); |
| const a = document.createElement('a'); |
| a.href = url; |
| a.download = name; |
| a.click(); |
| URL.revokeObjectURL(url); |
| } catch (err) { |
| this.notify(err.message, 'error'); |
| } |
| }, |
|
|
| startRename(file) { |
| this.renameOldPath = this.joinPath(this.currentPath, file.name); |
| this.renameNewName = file.name; |
| this.showRename = true; |
| }, |
|
|
| async doRename() { |
| if (!this.renameNewName.trim()) return; |
| const form = new FormData(); |
| form.append('old_path', this.renameOldPath); |
| form.append('new_name', this.renameNewName.trim()); |
| try { |
| await this.api(`/api/zones/${this.currentZone}/files/rename`, { method: 'POST', body: form }); |
| this.showRename = false; |
| await this.loadFiles(); |
| } catch {} |
| }, |
|
|
| getFileIcon(name) { |
| const ext = name.split('.').pop()?.toLowerCase(); |
| const map = { |
| js: 'file-code', ts: 'file-code', py: 'file-code', go: 'file-code', |
| html: 'file-code', css: 'file-code', json: 'file-json', |
| md: 'file-text', txt: 'file-text', log: 'file-text', |
| jpg: 'image', jpeg: 'image', png: 'image', gif: 'image', svg: 'image', |
| zip: 'file-archive', tar: 'file-archive', gz: 'file-archive', |
| }; |
| return map[ext] || 'file'; |
| }, |
|
|
| formatSize(bytes) { |
| if (bytes === 0) return '0 B'; |
| const k = 1024; |
| const sizes = ['B', 'KB', 'MB', 'GB']; |
| const i = Math.floor(Math.log(bytes) / Math.log(k)); |
| return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; |
| }, |
|
|
| latestBackupForZone(zoneName) { |
| return this.backupList.find((item) => item.zone_name === zoneName) || null; |
| }, |
|
|
| zoneBackupCount(zoneName) { |
| return this.backupList.filter((item) => item.zone_name === zoneName).length; |
| }, |
|
|
| buildPortUrl(port) { |
| const base = port?.url || (`/port/${this.currentZone}/${port?.port}/`); |
| return new URL(base, location.origin).toString(); |
| }, |
|
|
| async copyText(value, message = 'Đã copy') { |
| try { |
| await navigator.clipboard.writeText(value); |
| this.notify(message); |
| } catch { |
| this.notify('Không thể copy', 'error'); |
| } |
| }, |
|
|
| |
| initTerminal() { |
| if (!this.currentZone) return; |
|
|
| |
| if (this.termZone === this.currentZone && this.term) { |
| this.$nextTick(() => this.termFit?.fit()); |
| return; |
| } |
|
|
| this.disconnectTerminal(); |
|
|
| const container = document.getElementById('terminal-container'); |
| if (!container) return; |
| container.innerHTML = ''; |
|
|
| this.term = new Terminal({ |
| cursorBlink: true, |
| fontSize: 14, |
| fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace", |
| theme: { |
| background: '#000000', |
| foreground: '#e4e4e7', |
| cursor: '#8b5cf6', |
| selectionBackground: '#8b5cf644', |
| black: '#18181b', |
| red: '#ef4444', |
| green: '#22c55e', |
| yellow: '#eab308', |
| blue: '#3b82f6', |
| magenta: '#a855f7', |
| cyan: '#06b6d4', |
| white: '#e4e4e7', |
| }, |
| allowProposedApi: true, |
| }); |
|
|
| this.termFit = new FitAddon.FitAddon(); |
| const webLinks = new WebLinksAddon.WebLinksAddon(); |
| this.term.loadAddon(this.termFit); |
| this.term.loadAddon(webLinks); |
| this.term.open(container); |
| this.termFit.fit(); |
| this.termZone = this.currentZone; |
|
|
| |
| const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'; |
| const wsUrl = `${proto}//${location.host}/ws/terminal/${this.currentZone}?token=${encodeURIComponent(this.token || '')}`; |
| this.termWs = new WebSocket(wsUrl); |
| this.termWs.binaryType = 'arraybuffer'; |
|
|
| this.termWs.onopen = () => { |
| this.term.onData((data) => { |
| if (this.termWs?.readyState === WebSocket.OPEN) { |
| this.termWs.send(JSON.stringify({ type: 'input', data })); |
| } |
| }); |
| this.term.onResize(({ rows, cols }) => { |
| if (this.termWs?.readyState === WebSocket.OPEN) { |
| this.termWs.send(JSON.stringify({ type: 'resize', rows, cols })); |
| } |
| }); |
| |
| const dims = this.termFit.proposeDimensions(); |
| if (dims) { |
| this.termWs.send(JSON.stringify({ type: 'resize', rows: dims.rows, cols: dims.cols })); |
| } |
| }; |
|
|
| this.termWs.onmessage = (e) => { |
| if (e.data instanceof ArrayBuffer) { |
| this.term.write(new Uint8Array(e.data)); |
| } else { |
| this.term.write(e.data); |
| } |
| }; |
|
|
| this.termWs.onclose = () => { |
| this.term?.write('\r\n\x1b[90m[Disconnected]\x1b[0m\r\n'); |
| }; |
|
|
| |
| this._resizeHandler = () => this.termFit?.fit(); |
| window.addEventListener('resize', this._resizeHandler); |
|
|
| |
| this._resizeObserver = new ResizeObserver(() => this.termFit?.fit()); |
| this._resizeObserver.observe(container); |
| }, |
|
|
| disconnectTerminal() { |
| if (this.termWs) { |
| this.termWs.close(); |
| this.termWs = null; |
| } |
| if (this.term) { |
| this.term.dispose(); |
| this.term = null; |
| } |
| if (this._resizeHandler) { |
| window.removeEventListener('resize', this._resizeHandler); |
| this._resizeHandler = null; |
| } |
| if (this._resizeObserver) { |
| this._resizeObserver.disconnect(); |
| this._resizeObserver = null; |
| } |
| this.termFit = null; |
| this.termZone = null; |
| }, |
|
|
| |
| async loadPorts() { |
| if (!this.currentZone) return; |
| try { |
| this.ports = await this.api(`/api/zones/${this.currentZone}/ports`); |
| } catch { this.ports = []; } |
| }, |
|
|
| async addPort() { |
| if (!this.newPort) return; |
| const form = new FormData(); |
| form.append('port', this.newPort); |
| form.append('label', this.newPortLabel); |
| try { |
| await this.api(`/api/zones/${this.currentZone}/ports`, { method: 'POST', body: form }); |
| this.newPort = null; |
| this.newPortLabel = ''; |
| await this.loadPorts(); |
| this.notify('Port đã được thêm'); |
| } catch {} |
| }, |
|
|
| async removePort(port) { |
| if (!confirm(`Xoá port ${port}?`)) return; |
| try { |
| await this.api(`/api/zones/${this.currentZone}/ports/${port}`, { method: 'DELETE' }); |
| await this.loadPorts(); |
| } catch {} |
| }, |
|
|
| |
| async loadBackupStatus() { |
| try { |
| this.backupStatus = await this.api('/api/backup/status'); |
| } catch {} |
| }, |
|
|
| async loadBackupList() { |
| this.backupLoading = true; |
| try { |
| this.backupList = await this.api('/api/backup/list'); |
| } catch { this.backupList = []; } |
| this.backupLoading = false; |
| }, |
|
|
| async backupZone(zoneName) { |
| if (!confirm(`Backup zone "${zoneName}" lên HuggingFace?`)) return; |
| try { |
| const res = await this.api(`/api/backup/zone/${zoneName}`, { method: 'POST' }); |
| this.notify(res.message); |
| this._pollBackupStatus(); |
| } catch {} |
| }, |
|
|
| async backupAll() { |
| if (!confirm('Backup tất cả zones lên HuggingFace?')) return; |
| try { |
| const res = await this.api('/api/backup/all', { method: 'POST' }); |
| this.notify(res.message); |
| this._pollBackupStatus(); |
| } catch {} |
| }, |
|
|
| async restoreZone(zoneName, backupName = null) { |
| if (!confirm(`Restore zone "${zoneName}" từ backup? Dữ liệu hiện tại sẽ bị ghi đè.`)) return; |
| try { |
| const query = backupName ? `?backup_name=${encodeURIComponent(backupName)}` : ''; |
| const res = await this.api(`/api/backup/restore/${zoneName}${query}`, { method: 'POST' }); |
| this.notify(res.message); |
| this._pollBackupStatus(); |
| } catch {} |
| }, |
|
|
| async restoreAll() { |
| if (!confirm('Restore tất cả zones từ backup? Dữ liệu hiện tại sẽ bị ghi đè.')) return; |
| try { |
| const res = await this.api('/api/backup/restore-all', { method: 'POST' }); |
| this.notify(res.message); |
| this._pollBackupStatus(); |
| } catch {} |
| }, |
|
|
| async deleteBackup(backupName) { |
| if (!confirm(`Xoá backup "${backupName}" trên cloud?`)) return; |
| try { |
| await this.api(`/api/backup/file?backup_name=${encodeURIComponent(backupName)}`, { method: 'DELETE' }); |
| await this.loadBackupList(); |
| this.notify('Đã xoá backup'); |
| } catch {} |
| }, |
|
|
| _pollBackupStatus() { |
| if (this._pollTimer) return; |
| this._pollTimer = setInterval(async () => { |
| await this.loadBackupStatus(); |
| if (!this.backupStatus.running) { |
| clearInterval(this._pollTimer); |
| this._pollTimer = null; |
| await this.loadBackupList(); |
| await this.loadZones(); |
| if (this.backupStatus.error) { |
| this.notify(this.backupStatus.error, 'error'); |
| } else { |
| this.notify(this.backupStatus.progress); |
| } |
| } |
| }, 2000); |
| }, |
| }; |
| } |