CI / Build & Test (push) Successful in 2m54s
Add docs/CONTRIBUTING.md covering the build system, dev workflow, all npm scripts, how to add new native host message types, code style, and debugging guidance. Add CHANGELOG.md at the project root documenting all features and fixes across the 1.0.0 release. Add JSDoc (@param, @returns) to all previously undocumented exported functions across 35 JS files: - native-host/holesail-manager/ (index, virtual-hosts, service-tunnels, servers, port-allocator) - native-host top-level managers (startup, connect-proxy, https-proxy, certificate-authority, ssh-manager, rdp-manager) - extension/background/ (logs, native-messaging, proxy, message-router) - extension/dashboard/core/ (utils, navigation, init) - extension/dashboard/ui/ (modal, toast, state-tag) - extension/dashboard/pages/ (all 10 page files) - extension/dashboard/refresh.js, events.js - extension/dashboard/data/hostname-validator.js - scripts/ (build-host, run-install)
151 lines
5.8 KiB
JavaScript
151 lines
5.8 KiB
JavaScript
/**
|
|
* Backups page — lists backup archives with size/date, and wires up
|
|
* create/restore/delete operations via native host messages.
|
|
* Depends on: core/utils.js ($, escapeHtml, timeAgo),
|
|
* ui/toast.js (showToast), ui/modal.js (openModal, closeModal, showModalError)
|
|
*/
|
|
|
|
let pendingRestoreFilename = null;
|
|
let pendingDeleteFilename = null;
|
|
|
|
function formatBytes(bytes) {
|
|
if (bytes < 1024) return bytes + ' B';
|
|
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
|
return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
|
|
}
|
|
|
|
/**
|
|
* Re-render the backups table with the provided list of backup entries.
|
|
* @param {Array<{filename: string, size: number, createdAt: number}>} backups
|
|
*/
|
|
function updateBackupsTable(backups) {
|
|
const tbody = $('backupsTable');
|
|
if (!tbody) return;
|
|
const countEl = $('backupCount');
|
|
if (countEl) countEl.textContent = backups ? backups.length : 0;
|
|
if (!backups || backups.length === 0) {
|
|
tbody.innerHTML = `<tr><td colspan="4"><div class="empty-state">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" style="width:32px;height:32px;color:var(--text4)"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17,8 12,3 7,8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
|
|
<div class="empty-state-title">No backups yet</div>
|
|
<div class="empty-state-desc">Click "Take Backup" to create your first backup</div>
|
|
</div></td></tr>`;
|
|
return;
|
|
}
|
|
tbody.innerHTML = backups.map((b) => {
|
|
const name = escapeHtml(b.filename);
|
|
const created = b.createdAt ? timeAgo(b.createdAt) : '—';
|
|
const size = b.size ? formatBytes(b.size) : '—';
|
|
return `<tr>
|
|
<td><span class="mono" style="font-size:12px;">${name}</span></td>
|
|
<td>${created}</td>
|
|
<td>${size}</td>
|
|
<td>
|
|
<div style="display:flex;gap:6px;">
|
|
<button class="btn btn-secondary btn-sm" data-backup-restore="${escapeHtml(b.filename)}">Restore</button>
|
|
<button class="btn btn-danger btn-sm" data-backup-delete="${escapeHtml(b.filename)}">Delete</button>
|
|
</div>
|
|
</td>
|
|
</tr>`;
|
|
}).join('');
|
|
}
|
|
|
|
/**
|
|
* Fetch the current backup list from the native host and re-render the table.
|
|
*/
|
|
function refreshBackups() {
|
|
chrome.runtime.sendMessage(
|
|
{ target: 'holesail-native', action: 'send', payload: { type: 'listBackups' } },
|
|
(response) => {
|
|
if (chrome.runtime.lastError) return;
|
|
if (response && response.ok) {
|
|
updateBackupsTable(response.backups || []);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Attach all event listeners for the Backups page.
|
|
* Called once during dashboard initialisation.
|
|
*/
|
|
function setupBackupEvents() {
|
|
$('btnTakeBackup')?.addEventListener('click', () => {
|
|
const btn = $('btnTakeBackup');
|
|
if (btn) { btn.disabled = true; btn.textContent = 'Creating…'; }
|
|
chrome.runtime.sendMessage(
|
|
{ target: 'holesail-native', action: 'send', payload: { type: 'createBackup' } },
|
|
(response) => {
|
|
if (btn) {
|
|
btn.disabled = false;
|
|
btn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17,8 12,3 7,8"/><line x1="12" y1="3" x2="12" y2="15"/></svg> Take Backup`;
|
|
}
|
|
if (response && response.ok) {
|
|
showToast('Backup created: ' + response.filename, 'success');
|
|
refreshBackups();
|
|
} else {
|
|
showToast(response?.error || 'Backup failed', 'error');
|
|
}
|
|
}
|
|
);
|
|
});
|
|
|
|
$('backupsTable')?.addEventListener('click', (e) => {
|
|
const restoreBtn = e.target.closest('[data-backup-restore]');
|
|
if (restoreBtn) {
|
|
pendingRestoreFilename = restoreBtn.dataset.backupRestore;
|
|
const nameEl = $('restoreBackupName');
|
|
if (nameEl) nameEl.textContent = pendingRestoreFilename;
|
|
openModal('modal-restoreBackup');
|
|
return;
|
|
}
|
|
const deleteBtn = e.target.closest('[data-backup-delete]');
|
|
if (deleteBtn) {
|
|
pendingDeleteFilename = deleteBtn.dataset.backupDelete;
|
|
const nameEl = $('deleteBackupName');
|
|
if (nameEl) nameEl.textContent = pendingDeleteFilename;
|
|
openModal('modal-deleteBackup');
|
|
}
|
|
});
|
|
|
|
$('restoreBackupConfirm')?.addEventListener('click', () => {
|
|
if (!pendingRestoreFilename) return;
|
|
const btn = $('restoreBackupConfirm');
|
|
if (btn) btn.disabled = true;
|
|
chrome.runtime.sendMessage(
|
|
{ target: 'holesail-native', action: 'send', payload: { type: 'restoreBackup', payload: { filename: pendingRestoreFilename } } },
|
|
(response) => {
|
|
if (btn) btn.disabled = false;
|
|
if (response && response.ok) {
|
|
closeModal('modal-restoreBackup');
|
|
const certsNote = response.restoredCerts ? ' Certificates restored.' : '';
|
|
showToast('Backup restored.' + certsNote + ' Restart tunnels to apply changes.', 'success');
|
|
pendingRestoreFilename = null;
|
|
refresh();
|
|
} else {
|
|
showModalError('modal-restoreBackup', 'restoreBackupError', response?.error || 'Restore failed');
|
|
}
|
|
}
|
|
);
|
|
});
|
|
|
|
$('deleteBackupConfirm')?.addEventListener('click', () => {
|
|
if (!pendingDeleteFilename) return;
|
|
const btn = $('deleteBackupConfirm');
|
|
if (btn) btn.disabled = true;
|
|
chrome.runtime.sendMessage(
|
|
{ target: 'holesail-native', action: 'send', payload: { type: 'deleteBackup', payload: { filename: pendingDeleteFilename } } },
|
|
(response) => {
|
|
if (btn) btn.disabled = false;
|
|
if (response && response.ok) {
|
|
closeModal('modal-deleteBackup');
|
|
showToast('Backup deleted', 'success');
|
|
pendingDeleteFilename = null;
|
|
refreshBackups();
|
|
} else {
|
|
showModalError('modal-deleteBackup', 'deleteBackupError', response?.error || 'Delete failed');
|
|
}
|
|
}
|
|
);
|
|
});
|
|
}
|