Add Push-to-Main-App button to seed import UI

- New card with Dev/Prod target selector + dry-run toggle
- Auto-refreshes target status (exists/not found)
- Results grid: assets created/updated/skipped, customers, locations
- Sample actions and error display
- Calls existing /api/sync endpoint
This commit is contained in:
2026-05-25 00:11:58 -04:00
parent bd3bfea37c
commit a0b0c7abac
+159 -1
View File
@@ -585,6 +585,54 @@
</div>
</div>
<!-- Sync/Push to Main App ────────────────────────────────────────── -->
<div class="card" id="cardSync" style="margin-top:20px;border-color:var(--accent-bg);">
<h2 style="color:var(--accent2);">📤 Push to Main App</h2>
<div style="font-size:13px;color:var(--text3);margin-bottom:12px;">
Sync seed data (<strong id="syncSourcePath">seed-data/assets.db</strong>) into the canteen asset tracker main app DB
with schema mapping (customers/locations/assets).
</div>
<!-- Target selector -->
<div style="display:flex;gap:12px;margin-bottom:16px;">
<div style="flex:1;">
<div style="font-size:12px;color:var(--text2);margin-bottom:4px;">Target</div>
<div id="syncTargets" style="display:flex;gap:8px;">
<div class="mode-option selected" id="syncTargetDev" onclick="selectSyncTarget('dev')" style="flex:1;padding:12px;font-size:13px;">
<div class="title" style="font-size:14px;">🛠️ Dev</div>
<div class="desc" style="font-size:11px;" id="syncTargetDevDesc">Loading...</div>
</div>
<div class="mode-option" id="syncTargetProd" onclick="selectSyncTarget('prod')" style="flex:1;padding:12px;font-size:13px;border-color:var(--amber-bg);">
<div class="title" style="font-size:14px;">🏭 Prod</div>
<div class="desc" style="font-size:11px;" id="syncTargetProdDesc">Loading...</div>
</div>
</div>
</div>
</div>
<!-- Options -->
<div class="toggle-row">
<label class="toggle-switch">
<input type="checkbox" id="toggleDryRun" checked>
<span class="toggle-slider"></span>
</label>
<span class="toggle-label">Dry run — preview changes without writing</span>
</div>
<!-- Action buttons -->
<div class="btn-row" style="margin-top:16px;">
<button class="btn btn-primary" id="btnSync" onclick="runSync()" style="background:var(--accent2);">
📤 Push to Main App
</button>
<button class="btn btn-sm btn-secondary" onclick="refreshSyncTargets()">🔄 Refresh targets</button>
<span id="syncStatus" style="font-size:13px;color:var(--text2);"></span>
</div>
<!-- Results -->
<div id="syncResult" style="display:none;margin-top:16px;"></div>
</div>
<!-- Database Management ────────────────────────────────────────────── -->
<div class="card" style="margin-top:20px;border-color:rgba(255,80,80,0.2);">
<h2 style="color:var(--red);">⚠️ Database Management</h2>
@@ -1076,9 +1124,119 @@
statusEl.innerHTML = `<span style="color:var(--red)">🗑️ Deleted ${d.records_deleted} records. Database is now empty.</span>`;
refreshDashboard();
} catch (e) {
statusEl.innerHTML = `<span style="color:var(--red)">❌ ${esc(e.message)}</span>`;
statusEl.innerHTML = `<span style="color:var(--red);">❌ ${esc(e.message)}</span>`;
}
}
// ─── Sync / Push to Main App ──────────────────────────────────────
let syncTarget = 'dev';
async function refreshSyncTargets() {
const d = await fetch('/api/sync/targets').then(r => r.json());
const src = d.source || {};
document.getElementById('syncSourcePath').textContent = src.path || 'seed-data/assets.db';
for (const [name, info] of Object.entries(d.targets || {})) {
const descEl = document.getElementById('syncTarget' + name.charAt(0).toUpperCase() + name.slice(1) + 'Desc');
if (!descEl) continue;
if (info.exists) {
descEl.innerHTML = `<span style="color:var(--green);">✓ ${info.path.replace(/^.*\\//, '')}</span>`;
} else {
descEl.innerHTML = `<span style="color:var(--red);">✗ Not found</span>`;
}
}
}
function selectSyncTarget(target) {
syncTarget = target;
document.getElementById('syncTargetDev').classList.toggle('selected', target === 'dev');
document.getElementById('syncTargetProd').classList.toggle('selected', target === 'prod');
}
async function runSync() {
const btn = document.getElementById('btnSync');
const status = document.getElementById('syncStatus');
const result = document.getElementById('syncResult');
btn.disabled = true;
btn.innerHTML = '<span class="spinner"></span> Pushing...';
status.textContent = '';
result.style.display = 'none';
document.getElementById('badgeStatus').textContent = 'Syncing...';
const dryRun = document.getElementById('toggleDryRun').checked;
const params = new URLSearchParams({ target: syncTarget, dry_run: dryRun });
try {
const res = await fetch('/api/sync?' + params, { method: 'POST' });
if (!res.ok) {
const err = await res.json();
throw new Error(err.detail || 'Sync failed');
}
const d = await res.json();
showSyncResult(d);
document.getElementById('badgeStatus').textContent = dryRun ? 'Dry run ✓' : 'Synced ✓';
refreshDashboard();
} catch (e) {
status.innerHTML = `<span style="color:var(--red);">❌ ${esc(e.message)}</span>`;
document.getElementById('badgeStatus').textContent = 'Error';
} finally {
btn.disabled = false;
btn.innerHTML = '📤 Push to Main App';
}
}
function showSyncResult(d) {
const el = document.getElementById('syncResult');
const isDry = d.dry_run;
let html = `<div class="info-banner" style="background:${isDry ? 'var(--amber-bg)' : 'var(--green-bg)'};border-color:${isDry ? 'var(--amber)' : 'var(--green)'};color:${isDry ? 'var(--amber)' : 'var(--green)'};">`;
html += isDry ? '🔍 Dry run — no changes written' : '✅ Sync complete!';
html += '</div>';
html += '<div class="stats-grid">';
html += `<div class="stat-card"><div class="value">${fmt(d.assets_created)}</div><div class="label">Assets Created</div></div>`;
html += `<div class="stat-card"><div class="value">${fmt(d.assets_updated)}</div><div class="label">Assets Updated</div></div>`;
html += `<div class="stat-card"><div class="value">${fmt(d.assets_skipped || 0)}</div><div class="label">Skipped</div></div>`;
html += `<div class="stat-card"><div class="value">${fmt(d.customers_created)}</div><div class="label">Customers Created</div></div>`;
html += `<div class="stat-card"><div class="value">${fmt(d.locations_created)}</div><div class="label">Locations Created</div></div>`;
html += `<div class="stat-card"><div class="value green">${fmt(d.total_actions)}</div><div class="label">Total Actions</div></div>`;
html += '</div>';
// Source/target info
html += `<div style="font-size:12px;color:var(--text3);margin:8px 0;">`;
html += `Source: <code>${esc(d.source)}</code><br>`;
html += `Target: <code>${esc(d.target)}</code>`;
if (d.target_before) {
const b = d.target_before;
html += `<br>Target had: ${b.customers} customers, ${b.locations} locations, ${b.assets} assets`;
}
html += '</div>';
// Sample actions
if (d.first_10?.length) {
html += '<div style="margin-top:12px;"><div style="font-size:12px;font-weight:600;color:var(--text2);margin-bottom:6px;">Sample actions:</div>';
html += '<div style="font-family:monospace;font-size:11px;line-height:1.6;background:var(--card2);padding:12px;border-radius:var(--radius-sm);border:1px solid var(--border);">';
html += d.first_10.map(a => esc(a)).join('<br>');
html += '</div></div>';
}
// Errors
if (d.errors?.length) {
html += `<div style="margin-top:12px;padding:12px;background:var(--red-bg);border-radius:var(--radius-sm);font-size:12px;color:var(--red);">`;
html += `<strong>Errors (${d.errors.length}):</strong><br>`;
html += d.errors.map(e => esc(e)).join('<br>');
html += '</div>';
}
el.innerHTML = html;
el.style.display = '';
el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
// Init sync targets on load
setTimeout(refreshSyncTargets, 100);
</script>
</body>
</html>