<!-- Account Settings Fragment — user profile, watches, points, plan analyses -->
<!-- Loaded inline for non-admin users; loaded via HTMX for admin tab bar -->
<div class="card">
<h2>Profile</h2>
<div class="info-row">
<span class="info-label">Email</span>
<span>{{ user.email }}</span>
</div>
<div class="info-row">
<span class="info-label">Name</span>
<span>{{ user.display_name or '—' }}</span>
</div>
<div class="info-row">
<span class="info-label">Role</span>
<span>{{ user.role or '—' }}</span>
</div>
<div class="info-row">
<span class="info-label">Firm</span>
<span>{{ user.firm_name or '—' }}</span>
</div>
<div class="info-row">
<span class="info-label">Verified</span>
<span>{{ 'Yes' if user.email_verified else 'No' }}</span>
</div>
<div class="info-row">
<span class="info-label">Primary Address</span>
<span id="primary-addr-display">
{% if user.primary_street_number and user.primary_street_name %}
<span style="display:flex;align-items:center;gap:8px;">
{{ user.primary_street_number }} {{ user.primary_street_name }}
<button class="btn-unwatch"
hx-post="/account/primary-address/clear"
hx-target="#primary-addr-display"
hx-swap="innerHTML"
title="Remove primary address">✕</button>
</span>
{% else %}
<span style="color:var(--text-muted);font-style:italic;">Not set — search for your address to save it</span>
{% endif %}
</span>
</div>
</div>
<div class="card">
<h2>Email Preferences</h2>
<p style="color:var(--text-muted);font-size:0.85rem;margin-bottom:16px;">
Receive your morning brief by email. Only sent when there's something new.
</p>
<form method="POST" action="/account/brief-frequency"
hx-post="/account/brief-frequency"
hx-target="#freq-status"
hx-swap="innerHTML"
style="display:flex;align-items:center;gap:12px;">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<select name="brief_frequency" style="padding:8px 12px;background:var(--surface-2);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:0.9rem;">
<option value="none" {{ 'selected' if (user.brief_frequency or 'none') == 'none' else '' }}>No emails</option>
<option value="daily" {{ 'selected' if user.brief_frequency == 'daily' else '' }}>Daily brief</option>
<option value="weekly" {{ 'selected' if user.brief_frequency == 'weekly' else '' }}>Weekly brief (Mondays)</option>
</select>
<button type="submit" style="padding:8px 16px;background:var(--accent);color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:0.85rem;">Save</button>
<span id="freq-status" style="font-size:0.85rem;"></span>
</form>
<div style="margin-top:16px;padding-top:16px;border-top:1px solid var(--border);">
<form method="POST" action="/account/notify-permit-changes"
hx-post="/account/notify-permit-changes"
hx-target="#notify-status"
hx-swap="innerHTML"
style="display:flex;align-items:flex-start;gap:10px;">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="checkbox" name="notify_permit_changes" id="notify_permit_changes"
value="1"
{% if user.notify_permit_changes %}checked{% endif %}
style="margin-top:3px;accent-color:var(--accent);width:16px;height:16px;flex-shrink:0;">
<label for="notify_permit_changes" style="font-size:0.9rem;color:var(--text);cursor:pointer;flex:1;">
Email me when watched permits change status
<span style="display:block;font-size:0.8rem;color:var(--text-muted);margin-top:2px;">
Sent overnight. Up to 10 individual emails, or a digest if there are more.
</span>
</label>
<button type="submit" style="padding:6px 14px;background:var(--accent);color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:0.82rem;flex-shrink:0;">Save</button>
</form>
<span id="notify-status" style="font-size:0.85rem;display:block;margin-top:6px;"></span>
</div>
</div>
<!-- Voice & Style Preferences -->
<div class="card">
<h2>Voice & Style</h2>
<p style="color:var(--text-muted); font-size:0.85rem; margin-bottom:12px;">
Tell me how you want AI responses to sound. These instructions apply to every response I generate for you.
</p>
<form hx-post="/account/voice-style"
hx-target="#voice-status"
hx-swap="innerHTML"
style="display:flex; flex-direction:column; gap:10px;">
<textarea name="voice_style" rows="4"
placeholder="e.g. Be warm and direct. Use bullet points for action items. Start with a one-line summary. Sign off as 'Amy'. Avoid jargon when talking to homeowners..."
style="width:100%; background:var(--surface-2); border:1px solid var(--border); border-radius:8px; color:var(--text); padding:10px; font-family:inherit; font-size:0.9rem; resize:vertical;">{{ user.voice_style or '' }}</textarea>
<div style="display:flex; align-items:center; gap:10px;">
<button type="submit" style="padding:8px 16px; background:var(--accent); color:#fff; border:none; border-radius:6px; cursor:pointer; font-size:0.85rem;">Save Style</button>
<span id="voice-status" style="font-size:0.85rem;"></span>
</div>
</form>
<div style="margin-top:14px; padding-top:12px; border-top:1px solid var(--border);">
<a href="/account/voice-calibration"
style="color:var(--accent); text-decoration:none; font-size:0.88rem; font-weight:500;">
Voice Calibration →
</a>
<span style="color:var(--text-muted); font-size:0.82rem; margin-left:8px;">
{% if cal_stats %}
{{ cal_stats.calibrated }} of {{ cal_stats.total }} scenarios calibrated
{% else %}
Teach me your voice for different situations
{% endif %}
</span>
</div>
</div>
<!-- Portfolio Import -->
<div class="card">
<h2>Import Portfolio</h2>
<p style="color: var(--text-muted); font-size: 0.85rem; margin-bottom: 12px;">
Find and watch all your permits automatically by searching the permit database.
</p>
<form hx-post="/portfolio/discover" hx-target="#discover-results" hx-indicator="#discover-spinner"
style="display: flex; gap: 8px; flex-wrap: wrap; align-items: flex-end;">
<div>
<label style="font-size: 0.75rem; color: var(--text-muted); display: block; margin-bottom: 2px;">Name</label>
<input type="text" name="name" placeholder="e.g. Amy Lee"
style="background: var(--surface-2); border: 1px solid var(--border); color: var(--text); padding: 8px 12px; border-radius: 6px; font-size: 0.9rem; width: 180px;">
</div>
<div>
<label style="font-size: 0.75rem; color: var(--text-muted); display: block; margin-bottom: 2px;">Firm (optional)</label>
<input type="text" name="firm" placeholder="e.g. 3S LLC"
style="background: var(--surface-2); border: 1px solid var(--border); color: var(--text); padding: 8px 12px; border-radius: 6px; font-size: 0.9rem; width: 180px;">
</div>
<button type="submit" class="badge badge-btn"
style="background: var(--accent); color: white; border-color: var(--accent); padding: 8px 16px; font-size: 0.85rem;">
Find My Permits
</button>
<span id="discover-spinner" class="htmx-indicator" style="color: var(--accent); font-size: 0.85rem; margin-left: 8px;">
Searching permits...
</span>
</form>
<div id="discover-results"></div>
</div>
<div class="card" id="watch-list-card">
<h2>Watch List (<span id="watch-count">{{ watches | length }}</span>)</h2>
{% if watches %}
<input type="text" class="watch-search" id="watch-search"
placeholder="Search watched items..." autocomplete="off"
oninput="filterWatches(this.value)">
<ul class="watch-list" id="watch-list">
{% for w in watches %}
<li class="watch-item" data-search="{{ (w.label or '') | lower }} {{ (w.street_number or '') | lower }} {{ (w.street_name or '') | lower }} {{ (w.permit_number or '') | lower }} {{ (w.neighborhood or '') | lower }} {{ (w.watch_type or '') | lower }} {{ (w.block or '') }}/{{ (w.lot or '') }} {{ (w.tags or '') | lower }}">
<div class="watch-meta">
<span class="watch-label" id="watch-label-{{ w.watch_id }}">
{{ w.label or [w.street_number, w.street_name] | select | join(' ') or w.permit_number or ([w.block, w.lot] | select | join('/')) or w.neighborhood or ('Entity ' ~ w.entity_id) }}
</span>
<span class="watch-type">{{ w.watch_type }}</span>
{% with watch=w %}{% include 'fragments/tag_editor.html' %}{% endwith %}
</div>
<div class="watch-actions">
<button class="btn-edit" title="Edit label"
onclick="editWatch({{ w.watch_id }}, this)">
✎
</button>
<button class="btn-unwatch"
hx-post="/watch/remove"
hx-target="closest .watch-item"
hx-swap="outerHTML"
hx-vals='{"watch_id": "{{ w.watch_id }}"}'>
✕
</button>
</div>
</li>
{% endfor %}
</ul>
{% else %}
<p class="empty">No watched items yet. Search for permits, addresses, or people and click "Watch" to track changes.</p>
{% endif %}
<!-- Add watch form -->
<form class="watch-add-form" method="POST" action="/watch/add"
hx-post="/watch/add"
hx-target="#watch-add-status"
hx-swap="innerHTML"
onsubmit="setTimeout(function(){ location.reload(); }, 800);">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="text" name="street_number" placeholder="Street #" style="width:70px;" autocomplete="off">
<input type="text" name="street_name" placeholder="Street name" style="flex:1;min-width:140px;" autocomplete="off">
<input type="hidden" name="watch_type" value="address">
<input type="text" name="label" placeholder="Label (optional)" style="width:140px;" autocomplete="off">
<button type="submit">+ Add Address</button>
<span id="watch-add-status" style="font-size:0.8rem;"></span>
</form>
</div>
<div class="card">
<h2>Points
<span style="float:right;font-size:1.3rem;color:var(--success);">{{ total_points }}</span>
</h2>
{% if points_history %}
<div style="margin-top:12px;">
{% for entry in points_history %}
<div class="info-row">
<span>
<span style="color:var(--success);font-weight:600;">+{{ entry.points }}</span>
<span style="color:var(--text-muted);font-size:0.85rem;margin-left:8px;">{{ entry.reason_label }}</span>
</span>
<span class="info-label" style="font-size:0.8rem;">
{{ entry.created_at.strftime('%b %d') if entry.created_at else '' }}
</span>
</div>
{% endfor %}
</div>
{% else %}
<p class="empty" style="padding:8px 0;">
No points yet. Submit feedback to earn points!
</p>
{% endif %}
</div>
<div class="card">
<h2>Plan Analyses
{% if recent_analyses %}
<span style="float:right;font-size:0.85rem;color:var(--text-muted);font-weight:400;">{{ recent_analyses|length }} recent</span>
{% endif %}
</h2>
{% if recent_analyses %}
<div style="margin-top:12px;">
{% for job in recent_analyses %}
<div class="info-row">
<span>
<span style="font-weight:500;">{{ job.filename }}</span>
<br>
<span style="color:var(--text-muted);font-size:0.8rem;">
{{ job.file_size_mb }} MB
{% if job.property_address %} · {{ job.property_address }}{% endif %}
</span>
</span>
<span style="display:flex;align-items:center;gap:8px;">
<span class="status-badge status-{{ job.status }}">{{ job.status }}</span>
{% if job.status == 'completed' and job.session_id %}
<a href="/plan-jobs/{{ job.job_id }}/results" style="color:var(--accent);text-decoration:none;font-size:0.85rem;">View</a>
{% endif %}
</span>
</div>
{% endfor %}
</div>
<div style="margin-top:16px;">
<a href="/account/analyses" style="color:var(--accent);text-decoration:none;font-size:0.9rem;">
View all analyses →
</a>
</div>
{% else %}
<p class="empty">
No plan analyses yet.
<a href="/" style="color:var(--accent);">Upload a plan set</a> to get started.
</p>
{% endif %}
</div>
<script nonce="{{ csp_nonce }}">
/* Watch list search/filter */
function filterWatches(query) {
var q = query.toLowerCase().trim();
var items = document.querySelectorAll('#watch-list .watch-item');
var visible = 0;
items.forEach(function(item) {
var data = item.getAttribute('data-search') || '';
if (!q || data.indexOf(q) !== -1) {
item.classList.remove('hidden');
visible++;
} else {
item.classList.add('hidden');
}
});
var countEl = document.getElementById('watch-count');
if (countEl) countEl.textContent = q ? visible + '/' + items.length : items.length;
}
/* Inline watch label edit */
function editWatch(watchId, btn) {
var labelEl = document.getElementById('watch-label-' + watchId);
if (!labelEl) return;
var currentText = labelEl.textContent.trim();
/* Replace label with input */
labelEl.innerHTML = '<input type="text" class="watch-edit-input" value="' +
currentText.replace(/"/g, '"') + '" onkeydown="if(event.key===\'Enter\')saveWatch(' +
watchId + ',this)" onblur="saveWatch(' + watchId + ',this)">';
var input = labelEl.querySelector('input');
input.focus();
input.select();
}
function saveWatch(watchId, input) {
var label = input.value.trim();
if (!label) {
/* Revert on empty */
location.reload();
return;
}
var formData = new FormData();
formData.append('watch_id', watchId);
formData.append('label', label);
fetch('/watch/edit', {
method: 'POST',
headers: { 'X-CSRFToken': document.querySelector('meta[name="csrf-token"]')?.content || '' },
body: formData
})
.then(function(resp) { return resp.text(); })
.then(function(html) {
var labelEl = document.getElementById('watch-label-' + watchId);
if (labelEl) labelEl.innerHTML = label;
/* Update search data */
var li = labelEl.closest('.watch-item');
if (li) {
var old = li.getAttribute('data-search') || '';
li.setAttribute('data-search', label.toLowerCase() + ' ' + old);
}
});
}
</script>