<!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>My Account — sfpermits.ai</title>
{% include "fragments/head_obsidian.html" %}
<script nonce="{{ csp_nonce }}" src="/static/htmx.min.js"></script>
<style nonce="{{ csp_nonce }}">
/* ── Account page — obsidian design system ──────────────────────────── */
main { padding: var(--space-10) 0 var(--space-16); }
/* Page header */
.account-header {
margin-bottom: var(--space-8);
}
.account-header h1 {
font-family: var(--sans);
font-size: var(--text-2xl);
font-weight: 300;
color: var(--text-primary);
margin-bottom: var(--space-1);
}
.account-header .subtitle {
font-family: var(--mono);
font-size: var(--text-sm);
font-weight: 300;
color: var(--text-secondary);
}
/* Impersonate banner */
.impersonate-banner {
background: rgba(251, 191, 36, 0.08);
border: 1px solid rgba(251, 191, 36, 0.25);
color: var(--signal-amber);
text-align: center;
padding: var(--space-2) var(--space-4);
font-family: var(--sans);
font-size: var(--text-sm);
}
.impersonate-banner form { display: inline; }
.impersonate-banner button {
background: none;
border: 1px solid rgba(251, 191, 36, 0.4);
color: var(--signal-amber);
padding: 2px var(--space-2);
border-radius: var(--radius-sm);
cursor: pointer;
font-family: var(--mono);
font-size: var(--text-xs);
margin-left: var(--space-2);
transition: border-color 0.2s, background 0.2s;
}
.impersonate-banner button:hover {
background: rgba(251, 191, 36, 0.1);
}
/* ── Tabs — token underline pattern ─────────────────────────────────── */
.tab-bar {
display: flex;
gap: var(--space-6);
border-bottom: 1px solid var(--glass-border);
margin-bottom: var(--space-6);
}
.tab-btn {
font-family: var(--mono);
font-size: var(--text-sm);
font-weight: 400;
color: var(--text-tertiary);
background: none;
border: none;
padding: var(--space-3) 0;
cursor: pointer;
position: relative;
transition: color 0.2s;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: var(--space-2);
}
.tab-btn:hover { color: var(--text-secondary); }
.tab-btn.active {
color: var(--text-primary);
}
.tab-btn.active::after {
content: '';
position: absolute;
bottom: -1px;
left: 0;
right: 0;
height: 2px;
background: var(--accent);
border-radius: 1px;
}
.tab-admin-badge {
font-family: var(--mono);
font-size: var(--text-xs);
font-weight: 400;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--signal-amber);
background: rgba(251, 191, 36, 0.10);
padding: 1px 5px;
border-radius: 3px;
}
.tab-divider {
width: 1px;
height: 20px;
background: var(--glass-border);
align-self: center;
flex-shrink: 0;
}
#tab-content { min-height: 200px; }
/* Tab panel visibility */
.tab-panel { display: none; }
.tab-panel.active { display: block; }
/* ── Info rows ───────────────────────────────────────────────────────── */
.info-row {
display: flex;
justify-content: space-between;
padding: var(--space-3) 0;
border-bottom: 1px solid var(--glass-border);
}
.info-row:last-child { border-bottom: none; }
.info-label {
font-family: var(--sans);
font-size: var(--text-sm);
color: var(--text-secondary);
}
.info-value {
font-family: var(--mono);
font-size: var(--text-sm);
font-weight: 300;
color: var(--text-primary);
}
/* ── Watch list ──────────────────────────────────────────────────────── */
.watch-list { list-style: none; }
.watch-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--space-3) 0;
border-bottom: 1px solid var(--glass-border);
}
.watch-item:last-child { border-bottom: none; }
.watch-item.hidden { display: none; }
.watch-meta {
display: flex;
flex-direction: column;
gap: 3px;
}
.watch-label {
font-family: var(--mono);
font-size: var(--text-sm);
font-weight: 400;
color: var(--text-primary);
}
.watch-type {
font-family: var(--mono);
font-size: var(--text-xs);
color: var(--text-tertiary);
text-transform: uppercase;
letter-spacing: 0.06em;
}
.watch-actions {
display: flex;
gap: var(--space-2);
align-items: center;
flex-shrink: 0;
}
.btn-unwatch {
background: none;
border: 1px solid var(--glass-border);
color: var(--text-secondary);
padding: 3px var(--space-2);
border-radius: var(--radius-sm);
cursor: pointer;
font-family: var(--mono);
font-size: var(--text-xs);
transition: border-color 0.2s, color 0.2s;
}
.btn-unwatch:hover {
color: var(--signal-red);
border-color: rgba(248, 113, 113, 0.4);
}
.btn-edit {
background: none;
border: 1px solid var(--glass-border);
color: var(--text-secondary);
padding: 3px var(--space-2);
border-radius: var(--radius-sm);
cursor: pointer;
font-family: var(--mono);
font-size: var(--text-xs);
transition: border-color 0.2s, color 0.2s;
}
.btn-edit:hover {
color: var(--accent);
border-color: var(--accent-ring);
}
.watch-search {
width: 100%;
margin-bottom: var(--space-4);
}
.watch-edit-input {
padding: 3px var(--space-2);
background: var(--glass);
border: 1px solid var(--accent-ring);
border-radius: var(--radius-sm);
color: var(--text-primary);
font-family: var(--mono);
font-size: var(--text-xs);
width: 200px;
outline: none;
}
.watch-add-form {
display: flex;
gap: var(--space-2);
align-items: center;
flex-wrap: wrap;
margin-top: var(--space-4);
padding-top: var(--space-4);
border-top: 1px solid var(--glass-border);
}
/* ── Tag pills ───────────────────────────────────────────────────────── */
.tag-pill {
display: inline-block;
background: var(--accent-glow);
color: var(--accent);
padding: 1px var(--space-2);
border-radius: var(--radius-full);
font-family: var(--mono);
font-size: var(--text-xs);
border: 1px solid var(--accent-ring);
margin-right: 3px;
}
.tag-editor { margin-top: var(--space-1); }
/* ── Status badges for plan analysis ─────────────────────────────────── */
.status-badge {
font-family: var(--mono);
font-size: var(--text-xs);
font-weight: 400;
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 1px 6px;
border-radius: 3px;
white-space: nowrap;
}
.status-completed { color: var(--signal-green); background: rgba(52, 211, 153, 0.10); }
.status-processing { color: var(--signal-blue); background: rgba(96, 165, 250, 0.10); }
.status-pending { color: var(--signal-amber); background: rgba(251, 191, 36, 0.10); }
.status-failed { color: var(--signal-red); background: rgba(248, 113, 113, 0.10); }
.status-stale { color: var(--signal-amber); background: rgba(251, 191, 36, 0.08); }
/* ── Empty state ─────────────────────────────────────────────────────── */
.empty {
font-family: var(--sans);
font-size: var(--text-sm);
color: var(--text-tertiary);
font-style: italic;
}
/* ── HTMX loading ────────────────────────────────────────────────────── */
.htmx-indicator { display: none; }
.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator { display: inline; }
.tab-loading {
text-align: center;
padding: var(--space-16) 0;
font-family: var(--sans);
color: var(--text-tertiary);
font-size: var(--text-sm);
}
.tab-loading-spinner {
display: inline-block;
width: 18px; height: 18px;
border: 2px solid var(--glass-border);
border-top-color: var(--accent);
border-radius: var(--radius-full);
animation: spin 0.7s linear infinite;
margin-right: var(--space-2);
vertical-align: middle;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* .card = glass-card alias for account fragments using legacy class name */
.card {
background: var(--obsidian-mid);
border: 1px solid var(--glass-border);
border-radius: var(--radius-md);
padding: var(--space-6);
margin-bottom: var(--space-6);
transition: border-color 0.3s;
}
.card:hover { border-color: var(--glass-hover); }
.card h2, .glass-card h2 {
font-family: var(--sans);
font-size: var(--text-lg);
font-weight: 300;
margin-bottom: var(--space-4);
color: var(--text-primary);
}
.admin-section { margin-top: var(--space-8); }
</style>
</head>
<body class="obsidian" hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
{% if session.get('impersonating') %}
<div class="impersonate-banner">
Viewing as {{ session.impersonating }}
<form method="POST" action="/auth/stop-impersonate">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button type="submit">Stop impersonating</button>
</form>
</div>
{% endif %}
{% set active_page = 'account' %}
{% include 'fragments/nav.html' %}
<main>
<div class="obs-container">
<div class="account-header">
<h1>My Account</h1>
<p class="subtitle">{{ user.email }}</p>
</div>
{% if user.is_admin %}
<!-- Admin users: tab bar with visual separator between Settings and Admin -->
<nav class="tab-bar tab-bar-divided" id="tab-bar" role="tablist">
<button class="tab-btn active" data-tab="settings"
role="tab" aria-selected="true"
hx-get="/account/fragment/settings"
hx-target="#tab-content-settings"
hx-swap="innerHTML"
onclick="switchTab('settings')">Settings</button>
<span class="tab-divider" aria-hidden="true"></span>
<button class="tab-btn" data-tab="admin"
role="tab" aria-selected="false"
hx-get="/account/fragment/admin"
hx-target="#tab-content-admin"
hx-swap="innerHTML"
onclick="switchTab('admin')">Admin<span class="tab-admin-badge">Admin</span></button>
</nav>
<!-- Settings tab: pre-rendered server-side -->
<div id="tab-content-settings" class="tab-panel active" role="tabpanel">
{% include 'fragments/account_settings.html' %}
</div>
<!-- Admin tab: pre-rendered server-side -->
<div id="tab-content-admin" class="tab-panel" role="tabpanel">
{% include 'fragments/account_admin.html' %}
</div>
{% else %}
<!-- Non-admin users: settings content rendered directly (no tab bar needed) -->
{% include 'fragments/account_settings.html' %}
{% endif %}
</div>
</main>
{% include 'fragments/feedback_widget.html' %}
{% if user.is_admin %}
<script nonce="{{ csp_nonce }}">
(function() {
var activeTab = 'settings';
// Read hash on load to restore tab state
var hash = location.hash.replace('#', '');
if (hash === 'settings' || hash === 'admin') {
activeTab = hash;
}
function switchTab(tabName) {
// Update button states
document.querySelectorAll('.tab-btn').forEach(function(btn) {
btn.classList.toggle('active', btn.dataset.tab === tabName);
btn.setAttribute('aria-selected', btn.dataset.tab === tabName ? 'true' : 'false');
});
// Update panel visibility
document.querySelectorAll('.tab-panel').forEach(function(panel) {
panel.classList.remove('active');
});
var panel = document.getElementById('tab-content-' + tabName);
if (panel) panel.classList.add('active');
// Persist to URL hash
history.replaceState(null, '', '#' + tabName);
activeTab = tabName;
}
// Make switchTab globally accessible (called from onclick)
window.switchTab = switchTab;
// Restore active tab from hash on page load
if (activeTab !== 'settings') {
switchTab(activeTab);
}
// After HTMX swaps fragment content, keep the panel visible
document.body.addEventListener('htmx:afterSwap', function(evt) {
var d = evt.detail || {};
var trigger = d.elt || (d.requestConfig && d.requestConfig.elt) || null;
if (trigger && trigger.dataset && trigger.dataset.tab) {
var tabName = trigger.dataset.tab;
// Ensure the correct panel stays visible after HTMX refresh
document.querySelectorAll('.tab-panel').forEach(function(panel) {
panel.classList.remove('active');
});
var panel = document.getElementById('tab-content-' + tabName);
if (panel) panel.classList.add('active');
}
});
})();
</script>
{% endif %}
</body>
</html>