diff --git a/lib/api.py b/lib/api.py index c95b39a..86e3fc8 100644 --- a/lib/api.py +++ b/lib/api.py @@ -82,7 +82,6 @@ KNOWLEDGE_SUBNAV = [ {'href': '/upload', 'label': 'Upload'}, {'href': '/web-ingest', 'label': 'Web Ingest'}, {'href': '/failures', 'label': 'Failures'}, - {'href': '/deleted-contacts', 'label': 'Deleted Contacts'}, ] PEERTUBE_SUBNAV = [ @@ -102,6 +101,12 @@ SETTINGS_SUBNAV = [ {'href': '/settings/health', 'label': 'Service Health'}, ] +NAVI_SUBNAV = [ + {'href': '/nav-i', 'label': 'Overview'}, + {'href': '/deleted-contacts', 'label': 'Deleted Contacts'}, + {'href': '/nav-i/api-keys', 'label': 'API Keys'}, +] + def _format_source_citation(payload): """Format a human-readable citation from a search result payload.""" @@ -335,11 +340,29 @@ def deleted_contacts_page(): user_id = get_user_id() or "anonymous" db = ContactsDB() contacts = db.list_deleted(user_id) - return render_template("knowledge/deleted_contacts.html", - domain="knowledge", subnav=KNOWLEDGE_SUBNAV, active_page="/deleted-contacts", + return render_template("navi/deleted_contacts.html", + domain="navi", subnav=NAVI_SUBNAV, active_page="/deleted-contacts", contacts=contacts) +@app.route("/nav-i") +def navi_landing_page(): + from .auth import get_user_id + from .contacts import ContactsDB + user_id = get_user_id() or "anonymous" + db = ContactsDB() + deleted_count = len(db.list_deleted(user_id)) + return render_template("navi/landing.html", + domain="navi", subnav=NAVI_SUBNAV, active_page="/nav-i", + deleted_count=deleted_count) + + +@app.route("/nav-i/api-keys") +def navi_api_keys_page(): + return render_template("navi/api_keys.html", + domain="navi", subnav=NAVI_SUBNAV, active_page="/nav-i/api-keys") + + @app.route('/peertube') def peertube_dashboard(): return render_template('peertube/dashboard.html', diff --git a/lib/contacts.py b/lib/contacts.py index fd7c451..f2782db 100644 --- a/lib/contacts.py +++ b/lib/contacts.py @@ -178,6 +178,25 @@ class ContactsDB: conn.commit() return self.get(user_id, contact_id), None + def restore_as(self, user_id, contact_id, new_label): + """Restore a soft-deleted contact with a new label (for Home/Work conflict resolution).""" + conn = self._get_conn() + row = self.get(user_id, contact_id, include_deleted=True) + if not row or not row.get('deleted_at'): + return None, 'not_found' + if not new_label or not new_label.strip(): + return None, 'invalid_label' + now = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%fZ') + try: + conn.execute( + "UPDATE contacts SET deleted_at = NULL, deleted_by = NULL, label = ?, updated_at = ? WHERE id = ? AND user_id = ?", + (new_label.strip(), now, contact_id, user_id) + ) + conn.commit() + except sqlite3.IntegrityError: + return None, 'conflict' + return self.get(user_id, contact_id), None + def purge(self, user_id, contact_id): conn = self._get_conn() row = self.get(user_id, contact_id, include_deleted=True) diff --git a/lib/contacts_api.py b/lib/contacts_api.py index 4e50605..0e4506b 100644 --- a/lib/contacts_api.py +++ b/lib/contacts_api.py @@ -102,6 +102,24 @@ def restore_contact(contact_id): return jsonify(contact) +@contacts_bp.route('/api/contacts//restore-as', methods=['POST']) +@require_auth +def restore_as_contact(contact_id): + db = _get_db() + data = request.get_json(force=True) + new_label = data.get('label', '').strip() + if not new_label: + return jsonify({'error': 'label is required'}), 400 + contact, err = db.restore_as(request.user_id, contact_id, new_label) + if err == 'not_found': + return jsonify({'error': 'Not found'}), 404 + if err == 'invalid_label': + return jsonify({'error': 'Invalid label'}), 400 + if err == 'conflict': + return jsonify({'error': 'Label conflict'}), 409 + return jsonify(contact) + + @contacts_bp.route('/api/contacts//purge', methods=['DELETE']) @require_auth def purge_contact(contact_id): diff --git a/templates/base.html b/templates/base.html index 49b1a21..4c06892 100644 --- a/templates/base.html +++ b/templates/base.html @@ -21,6 +21,7 @@ PeerTube Kiwix Search + Nav-I Settings {% if subnav %} diff --git a/templates/navi/api_keys.html b/templates/navi/api_keys.html new file mode 100644 index 0000000..341c6d7 --- /dev/null +++ b/templates/navi/api_keys.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} +{% block content %} +

API Keys

+
+

Per-user API key management is coming soon.

+

This will allow generating keys for programmatic access to the Navi contacts API.

+
+{% endblock %} diff --git a/templates/navi/deleted_contacts.html b/templates/navi/deleted_contacts.html new file mode 100644 index 0000000..0847fab --- /dev/null +++ b/templates/navi/deleted_contacts.html @@ -0,0 +1,116 @@ +{% extends "base.html" %} +{% block content %} +

Deleted Contacts

+{% if not contacts %} +

No deleted contacts.

+{% else %} + + + {% for c in contacts %} + + + + + + + + + {% endfor %} +
LabelNameCategoryPhoneDeleted AtActions
{{ c.label }}{{ c.name or '' }}{{ c.category or '' }}{{ c.phone or '' }}{{ c.deleted_at or '' }} + + +
+{% endif %} + + + +{% endblock %} +{% block scripts %} + +{% endblock %} diff --git a/templates/navi/landing.html b/templates/navi/landing.html new file mode 100644 index 0000000..131f3af --- /dev/null +++ b/templates/navi/landing.html @@ -0,0 +1,22 @@ +{% extends "base.html" %} +{% block content %} +

Nav-I

+

Navi frontend management — contacts, API keys, and configuration.

+ + +{% endblock %}