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:
+159
-1
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user