Add contacts/phone book system with per-user scoping

New files:
- lib/auth.py: Authentik forward-auth helpers (get_user_id, @require_auth)
- lib/contacts.py: ContactsDB with CRUD, soft delete, restore, purge, find_nearby
- lib/contacts_api.py: Flask Blueprint with 9 API endpoints at /api/contacts
- templates/knowledge/deleted_contacts.html: Dashboard recovery page

Modified:
- lib/api.py: Register contacts_bp, add KNOWLEDGE_SUBNAV entry, /deleted-contacts route
- config/profiles: has_contacts feature flag (true for home, false for pi profiles)

Separate SQLite DB at data/contacts.db. Per-user isolation via X-Authentik-Username.
Home/Work labels enforced unique per user. Haversine proximity queries (75m default).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matt 2026-04-22 05:29:54 +00:00
commit a4288c0cd8
8 changed files with 423 additions and 0 deletions

View file

@ -0,0 +1,56 @@
{% extends "base.html" %}
{% block content %}
<h3 style="color:#ffa500;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 }})">Restore</button>
<button class="btn" style="margin-left:4px;color:#ff4444;" onclick="purgeContact({{ c.id }})">Purge</button>
</td>
</tr>
{% endfor %}
</table>
{% endif %}
{% endblock %}
{% block scripts %}
<script>
async function restoreContact(id) {
try {
var resp = await fetch('/api/contacts/' + id + '/restore', {method: 'POST'});
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);
}
}
</script>
{% endblock %}