Buckets:

TwanAPI/Twan / app.js
download
raw
26.2 kB
function hugpanel() {
return {
// ── Auth State ──
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: '' },
// ── State ──
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
files: [],
currentPath: '',
filesLoading: false,
showNewFile: false,
showNewFolder: false,
newFileName: '',
newFolderName: '',
// Editor
editorFile: null,
editorContent: '',
editorOriginal: '',
editorDirty: false,
// Terminal
term: null,
termWs: null,
termFit: null,
termZone: null,
// Ports
ports: [],
newPort: null,
newPortLabel: '',
// Backup
backupStatus: { configured: false, admin_url: null, running: false, last: null, error: null, progress: '' },
backupList: [],
backupLoading: false,
backupFilterZone: '',
// Create Zone
showCreateZone: false,
createZoneName: '',
createZoneDesc: '',
showEditZone: false,
editZoneName: '',
editZoneDesc: '',
// Rename
showRename: false,
renameOldPath: '',
renameNewName: '',
// Toast
toast: { show: false, message: '', type: 'info' },
// ── Computed ──
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);
},
// ── Init ──
async init() {
// Load backup status to get adminApiUrl
await this.loadBackupStatus();
if (this.backupStatus.admin_url) {
this.adminApiUrl = this.backupStatus.admin_url;
localStorage.setItem('hugpanel_admin_url', this.adminApiUrl);
}
// Load config (MOTD, registration state, zone limit) early
await this._loadZoneLimit();
// Try to restore session from stored token
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 {
// Token invalid/expired — clear it
this.token = null;
localStorage.removeItem('hugpanel_token');
}
} catch {
// Worker unreachable — keep token, let user retry
}
} else if (!this.adminApiUrl) {
// No admin URL available — can't verify token but don't clear it
} else {
// No token stored
this.token = null;
}
this.authLoading = false;
if (this.user) {
await this._loadPanel();
}
this.$nextTick(() => lucide.createIcons());
// Watch for icon updates
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();
});
});
// Track desktop breakpoint
const mql = window.matchMedia('(min-width: 1024px)');
mql.addEventListener('change', (e) => { this.isDesktop = e.matches; });
// Persist session state
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));
// Keyboard shortcut
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === 's' && this.activeTab === 'editor') {
e.preventDefault();
this.saveFile();
}
});
},
// ── Toast ──
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();
},
// ── API Helper ──
async api(url, options = {}) {
try {
const headers = options.headers || {};
// Add JWT token to all API calls
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;
}
},
// ── Auth ──
async _loadPanel() {
await this.loadZones();
await this.loadBackupStatus();
// Restore saved zone if it still exists
if (this.currentZone && this.zones.some(z => z.name === this.currentZone)) {
await this.selectZone(this.currentZone);
} else {
this.currentZone = null;
}
// Fetch zone limit
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();
},
// ── Zones ──
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 {}
},
// ── Files ──
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');
}
},
// ── Terminal ──
initTerminal() {
if (!this.currentZone) return;
// Already connected to same zone
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;
// WebSocket
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 }));
}
});
// Send initial size
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');
};
// Resize handler
this._resizeHandler = () => this.termFit?.fit();
window.addEventListener('resize', this._resizeHandler);
// ResizeObserver for container
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;
},
// ── Ports ──
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 {}
},
// ── Backup ──
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);
},
};
}

Xet Storage Details

Size:
26.2 kB
·
Xet hash:
ebab438fff9e06d67892a53f986fbf4fa7e4797568eda7716bce0a2439939929

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.