diff --git a/web/static/index.html b/web/static/index.html
index 3c925ed..107169d 100644
--- a/web/static/index.html
+++ b/web/static/index.html
@@ -585,6 +585,54 @@
+
+
⚠️ Database Management
@@ -1076,9 +1124,119 @@
statusEl.innerHTML = `
🗑️ Deleted ${d.records_deleted} records. Database is now empty.`;
refreshDashboard();
} catch (e) {
- statusEl.innerHTML = `
❌ ${esc(e.message)}`;
+ statusEl.innerHTML = `
❌ ${esc(e.message)}`;
}
}
+
+ // ─── 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 = `
✓ ${info.path.replace(/^.*\\//, '')}`;
+ } else {
+ descEl.innerHTML = `
✗ Not found`;
+ }
+ }
+ }
+
+ 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 = '
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 = `
❌ ${esc(e.message)}`;
+ 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 = `
`;
+ html += isDry ? '🔍 Dry run — no changes written' : '✅ Sync complete!';
+ html += '
';
+
+ html += '
';
+ html += `
${fmt(d.assets_created)}
Assets Created
`;
+ html += `
${fmt(d.assets_updated)}
Assets Updated
`;
+ html += `
${fmt(d.assets_skipped || 0)}
Skipped
`;
+ html += `
${fmt(d.customers_created)}
Customers Created
`;
+ html += `
${fmt(d.locations_created)}
Locations Created
`;
+ html += `
${fmt(d.total_actions)}
Total Actions
`;
+ html += '
';
+
+ // Source/target info
+ html += `
`;
+ html += `Source: ${esc(d.source)}
`;
+ html += `Target: ${esc(d.target)}`;
+ if (d.target_before) {
+ const b = d.target_before;
+ html += `
Target had: ${b.customers} customers, ${b.locations} locations, ${b.assets} assets`;
+ }
+ html += '
';
+
+ // Sample actions
+ if (d.first_10?.length) {
+ html += '
Sample actions:
';
+ html += '
';
+ html += d.first_10.map(a => esc(a)).join('
');
+ html += '
';
+ }
+
+ // Errors
+ if (d.errors?.length) {
+ html += `
`;
+ html += `Errors (${d.errors.length}):
`;
+ html += d.errors.map(e => esc(e)).join('
');
+ html += '
';
+ }
+
+ el.innerHTML = html;
+ el.style.display = '';
+ el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
+ }
+
+ // Init sync targets on load
+ setTimeout(refreshSyncTargets, 100);