This commit is contained in:
@@ -1299,26 +1299,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-top:16px;">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<div class="card-title">Backup & Restore</div>
|
||||
<div class="card-subtitle">Export all connections to a JSON file, or import from a previous backup</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body" style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;">
|
||||
<button class="btn btn-secondary" id="btnExportConnections">
|
||||
<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="7,10 12,15 17,10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||
Export Connections
|
||||
</button>
|
||||
<button class="btn btn-secondary" id="btnImportConnections">
|
||||
<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>
|
||||
Import Connections
|
||||
</button>
|
||||
<input type="file" id="importFileInput" accept=".json" style="display:none;">
|
||||
<span id="importStatus" style="font-size:12px;color:var(--text3);"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Service Tunnels page ──────────────────────── -->
|
||||
|
||||
@@ -1427,86 +1427,6 @@ function setupEvents() {
|
||||
);
|
||||
});
|
||||
|
||||
// ── Export connections ───────────────────────────────────────────────────
|
||||
$('btnExportConnections')?.addEventListener('click', async () => {
|
||||
const state = currentState || {};
|
||||
const exportData = {
|
||||
version: 1,
|
||||
exportedAt: new Date().toISOString(),
|
||||
virtualHosts: state.virtualHosts || [],
|
||||
serviceTunnels: (state.serviceTunnels || []).map(t => ({ label: t.label, hsUrl: t.hsUrl, localPort: t.localPort })),
|
||||
sshConnections: sshConnections.map(c => ({ label: c.label, hsUrl: c.hsUrl, username: c.username }))
|
||||
};
|
||||
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'holesail-connections-' + new Date().toISOString().slice(0, 10) + '.json';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
showToast('Connections exported', 'success');
|
||||
});
|
||||
|
||||
// ── Import connections ───────────────────────────────────────────────────
|
||||
$('btnImportConnections')?.addEventListener('click', () => {
|
||||
$('importFileInput')?.click();
|
||||
});
|
||||
|
||||
$('importFileInput')?.addEventListener('change', async (e) => {
|
||||
const file = e.target.files && e.target.files[0];
|
||||
if (!file) return;
|
||||
const statusEl = $('importStatus');
|
||||
try {
|
||||
const text = await file.text();
|
||||
const data = JSON.parse(text);
|
||||
if (!data || typeof data !== 'object') throw new Error('Invalid file format');
|
||||
|
||||
let imported = 0;
|
||||
let failed = 0;
|
||||
|
||||
// Import virtual hosts
|
||||
for (const v of (data.virtualHosts || [])) {
|
||||
if (!v.hostname || !v.hsUrl) continue;
|
||||
await new Promise(resolve => {
|
||||
chrome.runtime.sendMessage(
|
||||
{ target: 'holesail-native', action: 'send', payload: { type: 'setVirtualHost', payload: { hostname: v.hostname, hsUrl: v.hsUrl } } },
|
||||
(r) => { if (r?.ok) imported++; else failed++; resolve(); }
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Import service tunnels
|
||||
for (const t of (data.serviceTunnels || [])) {
|
||||
if (!t.label || !t.hsUrl || !t.localPort) continue;
|
||||
await new Promise(resolve => {
|
||||
chrome.runtime.sendMessage(
|
||||
{ target: 'holesail-native', action: 'send', payload: { type: 'startServiceTunnel', payload: { label: t.label, hsUrl: t.hsUrl, localPort: t.localPort } } },
|
||||
(r) => { if (r?.ok) imported++; else failed++; resolve(); }
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Import SSH connections
|
||||
for (const c of (data.sshConnections || [])) {
|
||||
if (!c.hsUrl || !c.username) continue;
|
||||
const exists = sshConnections.some(x => x.hsUrl === c.hsUrl && x.username === c.username);
|
||||
if (!exists) {
|
||||
sshConnections.push({ id: generateSshId(), label: c.label || '', hsUrl: c.hsUrl, username: c.username });
|
||||
imported++;
|
||||
}
|
||||
}
|
||||
if (data.sshConnections && data.sshConnections.length) saveSshConnections();
|
||||
|
||||
if (statusEl) statusEl.textContent = `Imported ${imported} item(s)${failed ? ', ' + failed + ' failed' : ''}.`;
|
||||
showToast(`Imported ${imported} connection(s)`, 'success');
|
||||
refresh();
|
||||
} catch (err) {
|
||||
if (statusEl) statusEl.textContent = 'Import failed: ' + (err.message || 'Unknown error');
|
||||
showToast('Import failed', 'error');
|
||||
}
|
||||
// Reset file input so the same file can be re-imported
|
||||
e.target.value = '';
|
||||
});
|
||||
|
||||
// ── Add Virtual Host ────────────────────────────────────────────────────
|
||||
$('addVhostBtn')?.addEventListener('click', () => openModal('modal-addVhost'));
|
||||
|
||||
Reference in New Issue
Block a user