mirror of
https://github.com/zvx-echo6/recon.git
synced 2026-06-10 08:54:34 +02:00
cleanup: remove /api/contacts + /api/address_book handlers + pull entire /nav-i/* subtree (extraction #3 shadow) (#12)
* cleanup: remove /api/address_book handlers (extraction #3 shadow) Removes address_book_bp (lib/address_book_api.py: /api/address_book/lookup + /api/address_book/list) + its registration in lib/api.py. Edge-shadowed since extraction #3 — navi-contacts (:8423) serves /api/address_book/* on navi.echo6.co; no recon-side consumer (no template/JS reference). lib/address_book.py is KEPT — geocode.py (nickname short-circuit + annotation) and netsyms_api.py import it. NOT removed this PR: contacts_bp. The recon dashboard at /deleted-contacts (recon-product, stays) calls /api/contacts/<id>/{restore,restore-as,purge} via XHR, and recon.echo6.co proxies straight to recon:8420 (verified the Caddy block — no navi-contacts routing there). Removing contacts_bp would break those dashboard actions. Flagged for a decision; lib/contacts.py also stays (dashboard ContactsDB reads). See PR body. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * cleanup: deprecate /nav-i + /deleted-contacts; remove contacts_bp + lib/contacts.py Probe found recon's /deleted-contacts dashboard reads /opt/recon/data/contacts.db — frozen since extraction #3 moved write ownership to navi-contacts (/var/lib/navi-backend/contacts.db). The page has been silently rendering ~25-day stale data, and its restore/restore-as/purge XHRs hit recon's contacts_bp (the recon.echo6.co Caddy block proxies straight to recon:8420 — no navi-contacts routing there). Per Matt's decision, deprecate the pages entirely; they'll be re-surfaced later as a proper admin page consuming navi-contacts via API. Removed: - contacts_bp (lib/contacts_api.py, all 10 /api/contacts* routes) + its registration in lib/api.py — edge-shadowed by navi-contacts :8423 since #3, and now free of recon-product consumers once the dashboard goes. - /nav-i (navi_landing_page) + /deleted-contacts (deleted_contacts_page) route handlers; templates/navi/landing.html + templates/navi/deleted_contacts.html. - lib/contacts.py (ContactsDB) — the dashboard was its only non-contacts_bp consumer; both gone. - The two dead NAVI_SUBNAV entries (Overview→/nav-i, Deleted Contacts→ /deleted-contacts). Kept / adapted: - /nav-i/api-keys page (recon-product key management) stays. NAVI_SUBNAV reduced to just its API Keys entry; the base.html top-nav "Nav-I" link repointed /nav-i -> /nav-i/api-keys so the surviving section page stays reachable (minimal href change, not a nav restructure — flagged in PR). - lib/address_book.py — geocode.py + netsyms_api.py still consume it (untouched). Out-of-band follow-up after merge: delete the stale /opt/recon/data/contacts.db (frozen 2026-04-28; data, not code). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * cleanup: pull the entire /nav-i/* subtree (api-keys page is a weaker dup of /settings/keys) Completes the contacts cleanup by removing the rest of /nav-i/. The /nav-i/api-keys page was (a) a weaker duplicate of /settings/keys for Gemini (it lacked remove + reload-from-.env), and (b) a write-only-to-dead-files surface for TomTom + Google Places: it wrote /opt/recon/.env, but the live navi-traffic (:8421) and navi-places (:8425) services read their own /etc/navi-backend/<svc>.env and have ignored recon's copy since extractions #1 + #5. End state: no /nav-i/* URLs in recon. Removed: - /nav-i/api-keys route + template (templates/navi/api_keys.html) - all /api/nav-i/api-keys/* endpoints (list/update/test/restart-recon) - lib/api_keys_admin.py (its only importers were those 4 endpoints; _KEY_DEFS/ _read_env/_write_env were private to it) - the now-orphaned NAVI_SUBNAV - the "Nav-I" top-nav entry in base.html (reverses the /nav-i->/nav-i/api-keys repoint from the previous commit, now that the page itself is gone) Kept (Gemini's real home, recon-product): - /settings/keys + /api/keys/* + lib/key_manager.py (KeyManager) — they import key_manager directly, never api_keys_admin, so untouched. Note: TOMTOM_API_KEY now has zero recon .py references. GOOGLE_PLACES_API_KEY still has one (lib/google_places.py), kept in the prior /api/place cleanup as place_detail's dep; its only caller (_enrich_with_google) is unreachable since the /api/place handlers were removed — left in place pending /api/wiki-enrich retirement (out of scope here). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: zvx-echo6 <mj@k7zvx.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c968497b94
commit
d56b1d5f92
9 changed files with 0 additions and 1257 deletions
|
|
@ -21,7 +21,6 @@
|
|||
<a href="/peertube"{% if domain == 'peertube' %} class="active"{% endif %}>PeerTube</a>
|
||||
<a href="/kiwix"{% if domain == 'kiwix' %} class="active"{% endif %}>Kiwix</a>
|
||||
<a href="/search"{% if domain == 'search' %} class="active"{% endif %}>Search</a>
|
||||
<a href="/nav-i"{% if domain == 'navi' %} class="active"{% endif %}>Nav-I</a>
|
||||
<a href="/settings/keys"{% if domain == 'settings' %} class="active"{% endif %}>Settings</a>
|
||||
</div>
|
||||
{% if subnav %}
|
||||
|
|
|
|||
|
|
@ -1,269 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<h3 style="color:var(--orange);margin-bottom:16px;">API Keys</h3>
|
||||
|
||||
<div class="panel" style="margin-bottom:16px;padding:10px 14px;border-left:3px solid var(--orange);">
|
||||
<p class="text-dim" style="font-size:12px;margin:0;">Updating keys does not restart RECON. After updates, click <strong style="color:var(--text-primary);">Restart RECON</strong> below or restart manually from terminal.</p>
|
||||
</div>
|
||||
|
||||
<div id="keys-loading" class="text-dim" style="padding:20px;">Loading keys...</div>
|
||||
<div id="keys-error" style="display:none;padding:12px;color:#ff4444;"></div>
|
||||
|
||||
<table id="keys-table" style="display:none;">
|
||||
<thead>
|
||||
<tr><th>Provider</th><th>Masked Value</th><th>Count</th><th>Last Modified</th><th style="width:200px;">Actions</th></tr>
|
||||
</thead>
|
||||
<tbody id="keys-tbody"></tbody>
|
||||
</table>
|
||||
|
||||
<div id="gemini-detail" style="display:none;margin-top:16px;">
|
||||
<h4 style="color:var(--text-primary);margin-bottom:8px;font-size:13px;">Gemini Keys</h4>
|
||||
<table style="font-size:12px;">
|
||||
<thead>
|
||||
<tr><th>#</th><th>Masked Key</th><th>Calls</th><th>Errors</th><th>Last Used</th><th style="width:200px;">Actions</th></tr>
|
||||
</thead>
|
||||
<tbody id="gemini-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:20px;padding-top:16px;border-top:1px solid var(--border-light);">
|
||||
<button class="btn" onclick="restartRecon(this)" style="border-color:var(--orange);color:var(--orange);">Restart RECON</button>
|
||||
<span id="restart-status" class="text-dim text-xs" style="margin-left:8px;"></span>
|
||||
</div>
|
||||
|
||||
<!-- Update modal -->
|
||||
<div id="update-modal" style="display:none;position:fixed;inset:0;z-index:50;background:rgba(0,0,0,0.6);align-items:center;justify-content:center;">
|
||||
<div style="background:var(--bg-secondary);border:1px solid var(--border-light);padding:24px;max-width:440px;width:90%;">
|
||||
<h4 style="color:var(--orange);margin-bottom:12px;">Update Key</h4>
|
||||
<p class="text-dim" style="margin-bottom:4px;font-size:12px;">Provider: <span id="modal-provider" style="color:var(--text-primary);"></span></p>
|
||||
<p class="text-dim" style="margin-bottom:12px;font-size:12px;">Key: <span id="modal-key-name" style="color:var(--text-primary);font-family:var(--font-mono);"></span></p>
|
||||
<div style="position:relative;">
|
||||
<input id="modal-new-value" type="password" placeholder="Paste new key value..." autocomplete="off" style="width:100%;padding:6px 36px 6px 10px;background:var(--bg-tertiary);border:1px solid var(--border-light);color:var(--text-primary);font-family:var(--font-mono);font-size:13px;">
|
||||
<button onclick="toggleKeyVisibility()" style="position:absolute;right:4px;top:50%;transform:translateY(-50%);background:none;border:none;color:var(--text-dim);cursor:pointer;font-size:11px;padding:4px;" title="Toggle visibility" id="modal-toggle-vis">show</button>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;justify-content:flex-end;margin-top:16px;">
|
||||
<button class="btn" onclick="closeUpdateModal()">Cancel</button>
|
||||
<button class="btn" id="modal-save" onclick="saveKey()" style="border-color:var(--green);color:var(--green);">Save</button>
|
||||
</div>
|
||||
<p id="modal-error" style="display:none;color:#ff4444;font-size:12px;margin-top:8px;"></p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
var pendingUpdate = null; // {name, index, provider}
|
||||
|
||||
async function loadKeys() {
|
||||
try {
|
||||
var resp = await fetch('/api/nav-i/api-keys/list');
|
||||
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
||||
var data = await resp.json();
|
||||
renderKeys(data.keys);
|
||||
} catch(e) {
|
||||
document.getElementById('keys-loading').style.display = 'none';
|
||||
var errEl = document.getElementById('keys-error');
|
||||
errEl.textContent = 'Failed to load keys: ' + e.message;
|
||||
errEl.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
function renderKeys(keys) {
|
||||
document.getElementById('keys-loading').style.display = 'none';
|
||||
document.getElementById('keys-table').style.display = '';
|
||||
|
||||
var tbody = document.getElementById('keys-tbody');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
keys.forEach(function(k) {
|
||||
var tr = document.createElement('tr');
|
||||
tr.id = 'row-' + k.name;
|
||||
|
||||
var masked = k.masked_value || '<span class="text-dim">not set</span>';
|
||||
var countStr = k.count.toString();
|
||||
var mtime = k.last_modified ? k.last_modified.replace('T', ' ').replace('Z', '') : '—';
|
||||
|
||||
tr.innerHTML =
|
||||
'<td style="font-weight:600;">' + k.display_name + '</td>' +
|
||||
'<td><code style="font-size:12px;">' + masked + '</code></td>' +
|
||||
'<td style="text-align:center;">' + countStr + '</td>' +
|
||||
'<td class="text-dim text-xs">' + mtime + '</td>' +
|
||||
'<td>' +
|
||||
(k.provider === 'gemini'
|
||||
? '<button class="btn" onclick="toggleGeminiDetail()">Details</button> '
|
||||
: '<button class="btn" onclick="openUpdateModal(\'' + k.name + '\', null, \'' + k.display_name + '\')">Update</button> ') +
|
||||
'<button class="btn" onclick="testKey(\'' + k.name + '\', null, this)">Test</button>' +
|
||||
'<span class="test-result text-xs" style="margin-left:6px;"></span>' +
|
||||
'</td>';
|
||||
tbody.appendChild(tr);
|
||||
|
||||
// Render Gemini sub-table
|
||||
if (k.provider === 'gemini' && k.keys) {
|
||||
renderGeminiKeys(k.keys);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderGeminiKeys(keys) {
|
||||
var tbody = document.getElementById('gemini-tbody');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
keys.forEach(function(k) {
|
||||
var tr = document.createElement('tr');
|
||||
var lastUsed = k.last_used ? k.last_used.replace('T', ' ').replace('Z', '') : '—';
|
||||
tr.innerHTML =
|
||||
'<td>' + k.index + '</td>' +
|
||||
'<td><code style="font-size:11px;">' + k.masked + '</code></td>' +
|
||||
'<td style="text-align:center;">' + k.calls + '</td>' +
|
||||
'<td style="text-align:center;">' + (k.errors || 0) + '</td>' +
|
||||
'<td class="text-dim text-xs">' + lastUsed + '</td>' +
|
||||
'<td>' +
|
||||
'<button class="btn" onclick="openUpdateModal(\'GEMINI_KEY\', ' + k.index + ', \'Gemini #' + k.index + '\')">Update</button> ' +
|
||||
'<button class="btn" onclick="testKey(\'GEMINI_KEY\', ' + k.index + ', this)">Test</button>' +
|
||||
'<span class="test-result text-xs" style="margin-left:6px;"></span>' +
|
||||
'</td>';
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
function toggleGeminiDetail() {
|
||||
var el = document.getElementById('gemini-detail');
|
||||
el.style.display = el.style.display === 'none' ? '' : 'none';
|
||||
}
|
||||
|
||||
function openUpdateModal(name, index, displayName) {
|
||||
pendingUpdate = {name: name, index: index};
|
||||
document.getElementById('modal-provider').textContent = displayName;
|
||||
document.getElementById('modal-key-name').textContent = name + (index !== null ? ' [' + index + ']' : '');
|
||||
document.getElementById('modal-new-value').value = '';
|
||||
document.getElementById('modal-new-value').type = 'password';
|
||||
document.getElementById('modal-toggle-vis').textContent = 'show';
|
||||
document.getElementById('modal-error').style.display = 'none';
|
||||
document.getElementById('update-modal').style.display = 'flex';
|
||||
document.getElementById('modal-new-value').focus();
|
||||
}
|
||||
|
||||
function closeUpdateModal() {
|
||||
document.getElementById('update-modal').style.display = 'none';
|
||||
pendingUpdate = null;
|
||||
}
|
||||
|
||||
function toggleKeyVisibility() {
|
||||
var inp = document.getElementById('modal-new-value');
|
||||
var btn = document.getElementById('modal-toggle-vis');
|
||||
if (inp.type === 'password') {
|
||||
inp.type = 'text';
|
||||
btn.textContent = 'hide';
|
||||
} else {
|
||||
inp.type = 'password';
|
||||
btn.textContent = 'show';
|
||||
}
|
||||
}
|
||||
|
||||
async function saveKey() {
|
||||
if (!pendingUpdate) return;
|
||||
var newValue = document.getElementById('modal-new-value').value.trim();
|
||||
if (!newValue) {
|
||||
var errEl = document.getElementById('modal-error');
|
||||
errEl.textContent = 'Key value cannot be empty.';
|
||||
errEl.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
var saveBtn = document.getElementById('modal-save');
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.textContent = 'Saving...';
|
||||
|
||||
try {
|
||||
var body = {name: pendingUpdate.name, new_value: newValue};
|
||||
if (pendingUpdate.index !== null) body.index = pendingUpdate.index;
|
||||
|
||||
var resp = await fetch('/api/nav-i/api-keys/update', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
var data = await resp.json();
|
||||
if (data.success) {
|
||||
closeUpdateModal();
|
||||
loadKeys(); // refresh table
|
||||
} else {
|
||||
var errEl = document.getElementById('modal-error');
|
||||
errEl.textContent = data.error || 'Update failed';
|
||||
errEl.style.display = 'block';
|
||||
}
|
||||
} catch(e) {
|
||||
var errEl = document.getElementById('modal-error');
|
||||
errEl.textContent = 'Error: ' + e.message;
|
||||
errEl.style.display = 'block';
|
||||
} finally {
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.textContent = 'Save';
|
||||
}
|
||||
}
|
||||
|
||||
async function testKey(name, index, btn) {
|
||||
var resultSpan = btn.nextElementSibling;
|
||||
resultSpan.textContent = 'testing...';
|
||||
resultSpan.style.color = 'var(--text-dim)';
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
var body = {name: name};
|
||||
if (index !== null) body.index = index;
|
||||
|
||||
var resp = await fetch('/api/nav-i/api-keys/test', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
var data = await resp.json();
|
||||
if (data.success) {
|
||||
resultSpan.innerHTML = '<span style="color:var(--green);">✓</span> Pass — ' + data.latency_ms + 'ms';
|
||||
if (data.note) resultSpan.innerHTML += ' <span class="text-dim">(' + data.note + ')</span>';
|
||||
} else {
|
||||
resultSpan.innerHTML = '<span style="color:#ff4444;">✗</span> Failed: ' + (data.error || 'unknown');
|
||||
}
|
||||
} catch(e) {
|
||||
resultSpan.innerHTML = '<span style="color:#ff4444;">✗</span> Error: ' + e.message;
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function restartRecon(btn) {
|
||||
if (!confirm('Restart RECON service? Active enrichment/embedding workers will be interrupted.')) return;
|
||||
|
||||
var statusEl = document.getElementById('restart-status');
|
||||
btn.disabled = true;
|
||||
statusEl.textContent = 'Restarting...';
|
||||
statusEl.style.color = 'var(--text-dim)';
|
||||
|
||||
try {
|
||||
var resp = await fetch('/api/nav-i/api-keys/restart-recon', {method: 'POST'});
|
||||
var data = await resp.json();
|
||||
if (data.success) {
|
||||
statusEl.innerHTML = '<span style="color:var(--green);">✓</span> Restarted successfully';
|
||||
} else {
|
||||
statusEl.innerHTML = '<span style="color:#ff4444;">✗</span> ' + (data.error || 'Failed');
|
||||
}
|
||||
} catch(e) {
|
||||
statusEl.innerHTML = '<span style="color:#ff4444;">✗</span> ' + e.message;
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Close modal on Escape key
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') closeUpdateModal();
|
||||
});
|
||||
// Close modal on backdrop click
|
||||
document.getElementById('update-modal').addEventListener('click', function(e) {
|
||||
if (e.target === this) closeUpdateModal();
|
||||
});
|
||||
|
||||
// Load on page init
|
||||
loadKeys();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<h3 style="color:var(--orange);margin-bottom:16px;">Deleted Contacts</h3>
|
||||
{% if not contacts %}
|
||||
<p class="text-dim">No deleted contacts.</p>
|
||||
{% else %}
|
||||
<table>
|
||||
<tr><th>Label</th><th>Name</th><th>Category</th><th>Phone</th><th>Deleted At</th><th>Actions</th></tr>
|
||||
{% for c in contacts %}
|
||||
<tr id="row-{{ c.id }}">
|
||||
<td>{{ c.label }}</td>
|
||||
<td>{{ c.name or '' }}</td>
|
||||
<td class="text-dim">{{ c.category or '' }}</td>
|
||||
<td class="text-dim text-xs">{{ c.phone or '' }}</td>
|
||||
<td class="text-dim text-xs">{{ c.deleted_at or '' }}</td>
|
||||
<td>
|
||||
<button class="btn" onclick="restoreContact({{ c.id }}, '{{ c.label }}')">Restore</button>
|
||||
<button class="btn" style="margin-left:4px;color:#ff4444;" onclick="purgeContact({{ c.id }})">Purge</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
<!-- Conflict resolution modal -->
|
||||
<div id="conflict-modal" style="display:none;position:fixed;inset:0;z-index:50;background:rgba(0,0,0,0.6);align-items:center;justify-content:center;">
|
||||
<div style="background:var(--bg-secondary);border:1px solid var(--border-light);padding:24px;max-width:400px;width:90%;">
|
||||
<h4 style="color:var(--orange);margin-bottom:12px;">Label Conflict</h4>
|
||||
<p class="text-dim" style="margin-bottom:16px;">An active contact with the label "<span id="conflict-label" style="color:var(--text-primary);"></span>" already exists. Choose a new label to restore this contact:</p>
|
||||
<input id="conflict-new-label" type="text" placeholder="New label..." style="width:100%;padding:6px 10px;background:var(--bg-tertiary);border:1px solid var(--border-light);color:var(--text-primary);font-family:var(--font-mono);font-size:13px;margin-bottom:16px;">
|
||||
<div style="display:flex;gap:8px;justify-content:flex-end;">
|
||||
<button class="btn" onclick="closeConflictModal()">Cancel</button>
|
||||
<button class="btn" id="conflict-submit" onclick="submitRestoreAs()" style="border-color:var(--green);color:var(--green);">Restore As</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block scripts %}
|
||||
<script>
|
||||
var pendingRestoreId = null;
|
||||
|
||||
async function restoreContact(id, label) {
|
||||
try {
|
||||
var resp = await fetch('/api/contacts/' + id + '/restore', {method: 'POST'});
|
||||
if (resp.ok) {
|
||||
location.reload();
|
||||
} else if (resp.status === 409) {
|
||||
// Home/Work conflict — show modal
|
||||
pendingRestoreId = id;
|
||||
document.getElementById('conflict-label').textContent = label;
|
||||
document.getElementById('conflict-new-label').value = '';
|
||||
var modal = document.getElementById('conflict-modal');
|
||||
modal.style.display = 'flex';
|
||||
document.getElementById('conflict-new-label').focus();
|
||||
} else {
|
||||
var data = await resp.json();
|
||||
alert(data.error || 'Restore failed');
|
||||
}
|
||||
} catch(e) {
|
||||
alert('Error: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function closeConflictModal() {
|
||||
document.getElementById('conflict-modal').style.display = 'none';
|
||||
pendingRestoreId = null;
|
||||
}
|
||||
|
||||
async function submitRestoreAs() {
|
||||
var newLabel = document.getElementById('conflict-new-label').value.trim();
|
||||
if (!newLabel) {
|
||||
document.getElementById('conflict-new-label').style.borderColor = 'var(--red)';
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var resp = await fetch('/api/contacts/' + pendingRestoreId + '/restore-as', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({label: newLabel})
|
||||
});
|
||||
if (resp.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
var data = await resp.json();
|
||||
alert(data.error || 'Restore failed');
|
||||
}
|
||||
} catch(e) {
|
||||
alert('Error: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function purgeContact(id) {
|
||||
if (!confirm('Permanently delete this contact? This cannot be undone.')) return;
|
||||
try {
|
||||
var resp = await fetch('/api/contacts/' + id + '/purge', {method: 'DELETE'});
|
||||
if (resp.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
var data = await resp.json();
|
||||
alert(data.error || 'Purge failed');
|
||||
}
|
||||
} catch(e) {
|
||||
alert('Error: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Close modal on Escape key
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') closeConflictModal();
|
||||
});
|
||||
// Close modal on backdrop click
|
||||
document.getElementById('conflict-modal').addEventListener('click', function(e) {
|
||||
if (e.target === this) closeConflictModal();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<h3 style="color:var(--green);margin-bottom:16px;">Nav-I</h3>
|
||||
<p class="text-dim" style="margin-bottom:24px;">Navi frontend management — contacts, API keys, and configuration.</p>
|
||||
|
||||
<div class="stat-grid">
|
||||
<a href="/deleted-contacts" style="text-decoration:none;">
|
||||
<div class="stat-card" style="cursor:pointer;transition:border-color 0.15s;">
|
||||
<div class="label">Deleted Contacts</div>
|
||||
<div class="value">{{ deleted_count }}</div>
|
||||
<div class="sublabel">awaiting restore or purge</div>
|
||||
</div>
|
||||
</a>
|
||||
<a href="/nav-i/api-keys" style="text-decoration:none;">
|
||||
<div class="stat-card" style="cursor:pointer;transition:border-color 0.15s;">
|
||||
<div class="label">API Keys</div>
|
||||
<div class="value" style="font-size:14px;color:var(--text-dim);margin-top:12px;">Coming soon</div>
|
||||
<div class="sublabel">per-user key management</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Loading…
Add table
Add a link
Reference in a new issue