Files
dashcaddy/status/js/snapshot.js
Krystie 77688daec7 Add service filter, batch operations, and snapshot features
- Service Filter Bar: search by name, filter by status (online/offline)
- Batch Operations: multi-select containers for start/stop/restart
- Container Snapshots: create and manage Docker checkpoints
- Added filter bar and batch action bar to index.html
- Added snapshot button to Admin tools section
- New JS modules: service-filter.js, batch-operations.js, snapshot.js
- Updated build.js to include new modules in bundle
2026-05-15 02:17:24 -07:00

181 lines
7.8 KiB
JavaScript

// ========== CONTAINER SNAPSHOT / CHECKPOINT ==========
(function() {
// Inject modal HTML
injectModal('snapshot-modal', `<div id="snapshot-modal" class="weather-modal">
<div class="weather-modal-content" style="min-width: 700px; max-width: 850px;">
<h3>💾 Container Snapshots</h3>
<p class="modal-subtitle">
Create and manage Docker container checkpoints for instant state recovery.
</p>
<div id="snapshot-container-select-wrapper" style="margin-bottom: 16px;">
<label style="font-size: 0.85rem; color: var(--muted);">Select Container:</label>
<select id="snapshot-container-select" style="width: 100%; padding: 8px 12px; margin-top: 4px; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; color: var(--fg); font-size: 0.9rem;">
<option value="">-- Select a container --</option>
</select>
</div>
<div id="snapshot-details" style="display: none; margin-bottom: 16px; padding: 12px; border: 1px solid var(--border); border-radius: 8px; background: var(--card-hover);">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; font-size: 0.85rem;">
<div><span style="color: var(--muted);">Image:</span> <span id="snapshot-image"></span></div>
<div><span style="color: var(--muted);">Status:</span> <span id="snapshot-status"></span></div>
<div><span style="color: var(--muted);">Created:</span> <span id="snapshot-created"></span></div>
<div><span style="color: var(--muted);">Container ID:</span> <span id="snapshot-id" style="font-family: monospace;"></span></div>
</div>
</div>
<div class="panel-tabs" style="margin-bottom: 12px;">
<button class="panel-tab active" data-panel="snapshot-create">Create Snapshot</button>
<button class="panel-tab" data-panel="snapshot-list">Manage Snapshots</button>
</div>
<div id="snapshot-create" class="panel-section active">
<div style="margin-bottom: 12px;">
<label style="font-size: 0.85rem; color: var(--muted);">Snapshot Name:</label>
<input type="text" id="snapshot-name" placeholder="e.g., before-update-2024"
style="width: 100%; padding: 8px 12px; margin-top: 4px; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; color: var(--fg); font-size: 0.9rem; box-sizing: border-box;" />
</div>
<div style="margin-bottom: 12px;">
<label class="checkbox-label" style="font-size: 0.85rem;">
<input type="checkbox" id="snapshot-leave-running" checked />
Leave container running after checkpoint (resume without restart)
</label>
</div>
<button id="snapshot-create-btn" class="btn-accent-solid" style="width: 100%;">💾 Create Snapshot</button>
<div id="snapshot-create-status" style="margin-top: 12px; text-align: center; font-size: 0.85rem;"></div>
</div>
<div id="snapshot-list" class="panel-section" style="display: none;">
<div id="snapshot-list-container" class="scroll-container" style="max-height: 300px;">
<div class="panel-empty"><span class="empty-icon">💾</span>Select a container to view its snapshots</div>
</div>
</div>
<div class="weather-modal-buttons modal-footer-bar">
<button id="snapshot-close">Close</button>
</div>
</div>
</div>`);
const modal = document.getElementById('snapshot-modal');
const openBtn = document.getElementById('snapshot-btn');
const closeBtn = document.getElementById('snapshot-close');
const containerSelect = document.getElementById('snapshot-container-select');
const detailsDiv = document.getElementById('snapshot-details');
const createBtn = document.getElementById('snapshot-create-btn');
const createStatus = document.getElementById('snapshot-create-status');
let currentContainerId = null;
async function loadContainers() {
try {
const res = await fetch('/api/v1/containers');
const data = await res.json();
if (!data.success || !data.containers) return;
containerSelect.innerHTML = '<option value="">-- Select a container --</option>';
for (const c of data.containers) {
const opt = document.createElement('option');
opt.value = c.id;
opt.textContent = `${c.name || c.id} (${c.image || 'unknown'})`;
opt.dataset.name = c.name;
opt.dataset.image = c.image;
opt.dataset.status = c.status;
opt.dataset.created = c.created;
containerSelect.appendChild(opt);
}
} catch (e) {
console.error('Failed to load containers:', e);
}
}
function showContainerDetails(opt) {
if (!opt || !opt.value) {
detailsDiv.style.display = 'none';
currentContainerId = null;
return;
}
currentContainerId = opt.value;
document.getElementById('snapshot-image').textContent = opt.dataset.image || '-';
document.getElementById('snapshot-status').textContent = opt.dataset.status || '-';
document.getElementById('snapshot-created').textContent = opt.dataset.created ? new Date(opt.dataset.created * 1000).toLocaleString() : '-';
document.getElementById('snapshot-id').textContent = opt.value.substring(0, 12);
detailsDiv.style.display = '';
}
async function createSnapshot() {
if (!currentContainerId) {
createStatus.textContent = 'Please select a container first';
createStatus.style.color = 'var(--bad-fg)';
return;
}
const name = document.getElementById('snapshot-name').value.trim();
if (!name) {
createStatus.textContent = 'Please enter a snapshot name';
createStatus.style.color = 'var(--bad-fg)';
return;
}
const leaveRunning = document.getElementById('snapshot-leave-running').checked;
createBtn.disabled = true;
createBtn.textContent = 'Creating...';
createStatus.textContent = '';
try {
const res = await fetch(`/api/v1/containers/${encodeURIComponent(currentContainerId)}/checkpoint`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, leaveRunning })
});
const data = await res.json();
if (data.success) {
createStatus.textContent = `✓ Snapshot "${name}" created successfully`;
createStatus.style.color = 'var(--ok-fg)';
document.getElementById('snapshot-name').value = '';
} else {
createStatus.textContent = `✗ Failed: ${data.error || 'Unknown error'}`;
createStatus.style.color = 'var(--bad-fg)';
}
} catch (e) {
createStatus.textContent = `✗ Error: ${e.message}`;
createStatus.style.color = 'var(--bad-fg)';
} finally {
createBtn.disabled = false;
createBtn.textContent = '💾 Create Snapshot';
}
}
function openModal() {
modal.classList.add('show');
loadContainers();
}
function closeModal() {
modal.classList.remove('show');
detailsDiv.style.display = 'none';
currentContainerId = null;
containerSelect.selectedIndex = 0;
}
openBtn?.addEventListener('click', openModal);
closeBtn?.addEventListener('click', closeModal);
wireModal(modal, closeBtn);
containerSelect?.addEventListener('change', (e) => {
const opt = containerSelect.options[containerSelect.selectedIndex];
showContainerDetails(opt);
});
createBtn?.addEventListener('click', createSnapshot);
// Tab switching
modal?.querySelectorAll('.panel-tab').forEach(tab => {
tab.addEventListener('click', () => {
modal.querySelectorAll('.panel-tab').forEach(t => t.classList.remove('active'));
modal.querySelectorAll('.panel-section').forEach(s => s.classList.remove('active'));
tab.classList.add('active');
modal.querySelector(`#${tab.dataset.panel}`).classList.add('active');
});
});
})();