Files
FabledScribe/src/fabledassistant/routes/settings.py
T
bvandeusen 91bafb641f refactor: Phase 8 — backend deletion (chat / voice / push / journal / curator)
Mega-commit. Strips all server-side LLM machinery now that Phase 7 has
removed the corresponding UI surfaces and the MCP HTTP endpoint is the
sole assistant interface.

Deleted (services/):
  chat, generation_buffer, generation_log, generation_task, llm, tools/
  (entire package), stt, tts, voice_config, voice_library, push,
  journal_closeout, journal_pipeline, journal_prep, journal_scheduler,
  journal_search, curator, curator_scheduler, consolidation,
  tag_suggestions, research, weather, article_fetcher, pending_actions,
  moments, assist, wikipedia.

Deleted (routes/):
  chat, voice, push, journal, quick_capture, fable_mcp_dist.

Deleted (models/):
  conversation, generation_tool_log, push_subscription,
  pending_curator_action, moment, weather_cache.

Deleted (tests/):
  test_generation_log, test_journal_*, test_consolidation, test_lookup_tool,
  test_notes_consolidation_trigger, test_record_moment_guards,
  test_research_pipeline, test_tools_*, test_tool_use_fixes,
  test_voice_library, test_weather_service, test_calendar_tool_tz,
  test_wikipedia.

Deleted (top-level):
  fable-mcp/ (legacy standalone stdio package — wheel-build pipeline
  also removed from Dockerfile).

app.py:
  - blueprint registrations for the 6 deleted routes
  - startup hook trimmed: no more Ollama warmup, KV-cache priming,
    journal/curator schedulers, voice model loading
  - shutdown hook simplified
  - httpx import dropped (was for Ollama calls)

pyproject.toml:
  - removed deps: pywebpush, feedparser, html2text, trafilatura
  - removed [voice] extras entirely
  - description updated for the MCP-first architecture

Dockerfile:
  - removed faster-whisper / piper-tts install steps
  - removed bundled piper voice download stage
  - removed fable-mcp wheel build stage

Surviving-file edits:
  - services/auth.py: drop Conversation table claim on first-user setup
  - services/backup.py: drop conversation / push-subscription export+restore;
    v1/v2 restore now silently skip pre-pivot conversation data
  - services/notes.py: drop maybe_consolidate trigger on task done/cancelled;
    drop _maybe_trigger_project_summary (LLM auto-summary)
  - services/projects.py: drop generate_project_summary + backfill_project_summaries
    (both LLM-driven)
  - services/user_profile.py: drop append_observations / consolidate /
    clear_learned_data (curator-tied) and build_profile_context
    (was LLM system-prompt builder)
  - services/notifications.py: stub out _fire_push_notif (was send_push_notification)
  - services/event_scheduler.py: drop event-reminder push + chat-retention
    cleanup job; keep CalDAV pull-sync + reminders job (in-app)
  - services/diagnostics.py: _curator_busy() always False
  - routes/notes.py: drop /assist, /assist/stream, /suggest-tags endpoints
  - routes/tasks.py: drop /<id>/consolidate endpoint
  - routes/settings.py: drop /models, KV-cache-prime-on-save, journal-schedule
    timezone hook, and the SearXNG search-test endpoint; inline _is_private_url
    (was in services/llm.py)
  - routes/admin.py: drop /voice, /voice/reload endpoints
  - routes/profile.py: drop /consolidate, /observations (GET, DELETE)
  - models/__init__.py: drop the 6 dead model imports

Frontend cascade:
  - stores/push.ts: deleted entirely (no callers after Phase 7)
  - stores/settings.ts: drop checkVoiceStatus + voice-status state
  - views/SettingsView.vue: drop Locations section + journalConfig state
    (was tied to /api/journal/config); drop JournalConfig + journal/voice
    api/client imports
  - frontend/api/client.ts: orphaned voice/journal/profile-observation/
    fable-mcp-dist exports are left as dead but harmless (call them and
    they 404; type-check is clean).

Pre-existing v1 backups that contained conversations/messages still
restore — those tables are silently dropped from the import path.
Anyone pulling the new image with a populated database will need the
Phase 9 migration to drop the dead tables (coming next).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 17:47:18 -04:00

132 lines
4.3 KiB
Python

"""User settings + integrations (CalDAV, SearXNG status).
Chat-model picker endpoints (/models), KV-cache priming, and journal-schedule
hooks were removed in Phase 8 alongside the chat/journal subsystems.
"""
import ipaddress
import logging
import socket
from urllib.parse import urlparse
from quart import Blueprint, jsonify, request
from fabledassistant.auth import login_required, get_current_user_id
from fabledassistant.config import Config
from fabledassistant.services.caldav import CALDAV_SETTING_KEYS, get_caldav_config, test_connection
from fabledassistant.services.settings import delete_setting, get_all_settings, get_setting, set_settings_batch
logger = logging.getLogger(__name__)
def _is_private_url(url: str) -> bool:
"""SSRF-blocking helper: returns True for URLs that resolve to private,
loopback, or link-local addresses. Inlined here after services/llm.py
(the original home) was removed in Phase 8."""
try:
host = urlparse(url).hostname
if not host:
return True
# Resolve to all addresses; reject if any is private/loopback/link-local.
infos = socket.getaddrinfo(host, None)
for family, *_rest, sockaddr in infos:
ip_str = sockaddr[0]
try:
ip = ipaddress.ip_address(ip_str)
except ValueError:
continue
if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved:
return True
except Exception:
# Conservative: if we can't resolve, treat as private (reject).
return True
return False
settings_bp = Blueprint("settings", __name__, url_prefix="/api/settings")
@settings_bp.route("", methods=["GET"])
@login_required
async def get_settings_route():
uid = get_current_user_id()
settings = await get_all_settings(uid)
return jsonify(settings)
@settings_bp.route("", methods=["PUT"])
@login_required
async def update_settings_route():
uid = get_current_user_id()
data = await request.get_json()
if not isinstance(data, dict):
return jsonify({"error": "Expected a JSON object"}), 400
to_save = {}
for k, v in data.items():
str_v = str(v)
if not str_v:
await delete_setting(uid, k)
else:
to_save[k] = str_v
if to_save:
await set_settings_batch(uid, to_save)
settings = await get_all_settings(uid)
return jsonify(settings)
@settings_bp.route("/caldav", methods=["GET"])
@login_required
async def get_caldav():
uid = get_current_user_id()
config = await get_caldav_config(uid)
if config.get("caldav_password"):
config["caldav_password"] = "********"
return jsonify(config)
@settings_bp.route("/caldav", methods=["PUT"])
@login_required
async def update_caldav():
uid = get_current_user_id()
data = await request.get_json()
# Validate CalDAV URL before saving — block internal/private addresses
if "caldav_url" in data:
url = str(data.get("caldav_url") or "").strip()
if url:
parsed_scheme = url.split("://")[0].lower() if "://" in url else ""
if parsed_scheme not in ("http", "https"):
return jsonify({"error": "CalDAV URL must use http or https"}), 400
if _is_private_url(url):
return jsonify({"error": "CalDAV URL must not point to an internal or private address"}), 400
settings_to_save = {}
for key in CALDAV_SETTING_KEYS:
if key in data:
if key == "caldav_password" and data[key] == "********":
continue
settings_to_save[key] = str(data[key])
if settings_to_save:
await set_settings_batch(uid, settings_to_save)
return jsonify({"status": "ok"})
@settings_bp.route("/caldav/test", methods=["POST"])
@login_required
async def test_caldav():
uid = get_current_user_id()
result = await test_connection(uid)
return jsonify(result)
@settings_bp.route("/search", methods=["GET"])
@login_required
async def test_search():
"""Report SearXNG configuration status (used by the Integrations tab)."""
if not Config.searxng_enabled():
return jsonify({"configured": False, "results": [], "searxng_url": ""})
return jsonify({"configured": True, "results": [], "searxng_url": Config.SEARXNG_URL})