<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex, nofollow">
<title>Admin Ops — sfpermits.ai</title>
<script nonce="{{ csp_nonce }}" src="/static/htmx.min.js"></script>
{% include "fragments/head_obsidian.html" %}
<style nonce="{{ csp_nonce }}">
/* Tab bar */
.tab-bar {
display: flex; gap: 6px; margin-bottom: 24px; flex-wrap: wrap;
}
.tab-btn {
padding: 7px 16px; border-radius: 6px; font-size: var(--text-sm);
border: 1px solid rgba(255,255,255,0.08); background: var(--bg-elevated);
color: var(--text-secondary); cursor: pointer; font-family: var(--font-body);
text-decoration: none; transition: all 0.15s;
}
.tab-btn:hover { border-color: var(--text-secondary); color: var(--text-primary); }
.tab-btn.active { background: var(--signal-cyan); color: var(--bg-deep); border-color: var(--signal-cyan); font-weight: 600; }
.tab-btn.loading {
color: var(--signal-cyan) !important;
background: var(--bg-elevated) !important;
border-color: var(--signal-cyan) !important;
animation: tab-pulse 0.5s ease-in-out infinite alternate;
}
@keyframes tab-pulse {
from { border-color: rgba(34,211,238,0.3); }
to { border-color: rgba(34,211,238,1); }
}
/* Content area */
#tab-content {
min-height: 200px;
}
.tab-loading {
text-align: center; padding: 60px 0;
color: var(--text-secondary); font-size: var(--text-sm);
}
.tab-loading-spinner {
display: inline-block; width: 20px; height: 20px;
border: 2px solid rgba(255,255,255,0.08); border-top-color: var(--signal-cyan);
border-radius: 50%;
animation: spin 0.7s linear infinite;
margin-right: 8px; vertical-align: middle;
}
@keyframes spin { to { transform: rotate(360deg); } }
</style>
</head>
<body class="obsidian">
{% include 'fragments/nav.html' %}
<main style="padding: var(--space-10) 0 var(--space-16);">
<div class="obs-container">
<div class="glass-card" style="margin-bottom: var(--space-6);">
<h1 style="font-family: var(--font-display); font-size: var(--text-2xl); font-weight: 700; color: var(--text-primary); margin-bottom: var(--space-2);">
<span style="font-size:0.65rem;background:var(--bg-elevated);color:var(--text-secondary);padding:3px 8px;border-radius:8px;border:1px solid rgba(255,255,255,0.08);vertical-align:middle;margin-right:8px; font-family: var(--font-body);">⚙ Admin</span>Operations
</h1>
<p style="color: var(--text-secondary); font-size: var(--text-sm);">Pipeline health, data quality, and admin tools.</p>
</div>
<div class="glass-card">
<div class="tab-bar" id="tab-bar">
<button class="tab-btn" data-tab="pipeline"
hx-get="/admin/ops/fragment/pipeline"
hx-target="#tab-content"
hx-swap="innerHTML"
hx-indicator="find .tab-loading">Pipeline Health</button>
<button class="tab-btn" data-tab="quality"
hx-get="/admin/ops/fragment/quality"
hx-target="#tab-content"
hx-swap="innerHTML">Data Quality</button>
<button class="tab-btn" data-tab="activity"
hx-get="/admin/ops/fragment/activity"
hx-target="#tab-content"
hx-swap="innerHTML">User Activity</button>
<button class="tab-btn" data-tab="feedback"
hx-get="/admin/ops/fragment/feedback"
hx-target="#tab-content"
hx-swap="innerHTML">Feedback</button>
<button class="tab-btn" data-tab="sources"
hx-get="/admin/ops/fragment/sources"
hx-target="#tab-content"
hx-swap="innerHTML">LUCK Sources</button>
<button class="tab-btn" data-tab="regulatory"
hx-get="/admin/ops/fragment/regulatory"
hx-target="#tab-content"
hx-swap="innerHTML">Regulatory Watch</button>
<button class="tab-btn" data-tab="intel"
hx-get="/admin/ops/fragment/intel"
hx-target="#tab-content"
hx-swap="innerHTML">Intelligence</button>
<button class="tab-btn" data-tab="syshealth"
hx-get="/admin/ops/fragment/syshealth"
hx-target="#tab-content"
hx-swap="innerHTML">System Health</button>
</div>
<div id="tab-content">
<div class="tab-loading">
<span class="tab-loading-spinner"></span>
Loading...
</div>
</div>
</div>
</div>
</main>
{% include 'fragments/feedback_widget.html' %}
<script nonce="{{ csp_nonce }}">
(function() {
// 30s client-side timeout for all HTMX requests on this page
htmx.config.timeout = 30000;
var buttons = document.querySelectorAll('.tab-btn');
var activeTab = null;
var contentLoaded = false; // track whether ANY content has loaded
// Hash aliases — allow friendly names in URL hash
var hashAliases = {
'luck': 'sources',
'dq': 'quality',
'watch': 'regulatory',
'intelligence': 'intel'
};
// Helper: extract the trigger element from an HTMX event.
// HTMX 2.0 may put the element at evt.detail.elt OR
// evt.detail.requestConfig.elt depending on the event type.
function getTrigger(evt) {
var d = evt.detail || {};
return d.elt || (d.requestConfig && d.requestConfig.elt) || null;
}
function showError(msg) {
contentLoaded = true;
document.getElementById('tab-content').innerHTML =
'<div class="tab-loading" style="color:var(--signal-red);">' +
(msg || 'Failed to load tab.') +
' <a href="javascript:location.reload()" style="color:var(--signal-cyan);">Reload page</a>' +
'</div>';
buttons.forEach(function(b) { b.classList.remove('loading'); });
}
function activateTab(tabName) {
if (activeTab === tabName) return;
activeTab = tabName;
buttons.forEach(function(btn) {
var isActive = btn.dataset.tab === tabName;
btn.classList.toggle('active', isActive);
if (isActive) btn.classList.remove('loading');
});
history.replaceState(null, '', '#' + tabName);
}
// Tab click handling — HTMX handles the content loading,
// we just handle the active state and loading animation
buttons.forEach(function(btn) {
btn.addEventListener('click', function() {
if (this.classList.contains('active')) return;
buttons.forEach(function(b) { b.classList.remove('active', 'loading'); });
this.classList.add('loading');
contentLoaded = false;
});
});
// After HTMX swaps content, mark the tab as active
document.body.addEventListener('htmx:afterSwap', function(evt) {
contentLoaded = true;
var trigger = getTrigger(evt);
if (trigger && trigger.dataset && trigger.dataset.tab) {
activateTab(trigger.dataset.tab);
}
});
// Handle HTTP errors (4xx, 5xx)
document.body.addEventListener('htmx:responseError', function(evt) {
showError('Tab returned an error (HTTP ' + ((evt.detail.xhr && evt.detail.xhr.status) || '?') + ').');
});
// Handle network errors (connection reset, CORS failures)
document.body.addEventListener('htmx:sendError', function(evt) {
showError('Request failed — server may be busy or unreachable.');
});
// Handle client-side timeout (30s)
document.body.addEventListener('htmx:timeout', function(evt) {
showError('Request timed out (30s). The server may be overloaded.');
});
// ── Fallback safety net ──────────────────────────────────────
// If nothing has loaded after 35s (HTMX timeout is 30s + 5s grace),
// force-show an error. Catches edge cases where HTMX events don't
// fire or the event detail structure doesn't match our handlers.
setTimeout(function() {
if (!contentLoaded) {
showError('Page failed to load. The server did not respond in time.');
}
}, 35000);
// Load initial tab from hash or default to quality.
// Uses htmx.ajax() directly instead of simulating a button click,
// because the inline script runs BEFORE HTMX's DOMContentLoaded
// handler processes hx-get attributes on the buttons. A simulated
// .click() would fire before HTMX is listening, causing a no-op.
var hash = location.hash.replace('#', '') || 'quality';
hash = hashAliases[hash] || hash;
var initialBtn = document.querySelector('.tab-btn[data-tab="' + hash + '"]');
if (!initialBtn) initialBtn = document.querySelector('.tab-btn[data-tab="quality"]');
if (initialBtn) {
initialBtn.classList.add('loading');
// htmx.ajax bypasses element processing — works immediately
htmx.ajax('GET', '/admin/ops/fragment/' + hash, {target: '#tab-content', swap: 'innerHTML'});
}
})();
</script>
</body>
</html>