From 1d0cf4828b5b236041a7f3447494ae1fdb274c05 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Tue, 7 Apr 2026 06:55:15 -0400 Subject: [PATCH 1/3] fix(briefing): fetch full article content via trafilatura in discuss endpoint --- src/fabledassistant/routes/briefing.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/fabledassistant/routes/briefing.py b/src/fabledassistant/routes/briefing.py index 4c623ef..4e6da05 100644 --- a/src/fabledassistant/routes/briefing.py +++ b/src/fabledassistant/routes/briefing.py @@ -453,7 +453,8 @@ async def discuss_article(item_id: int): if get_buffer(conv_id) is not None: return jsonify({"error": "Generation already in progress"}), 409 - article_content = item.content or "" + from fabledassistant.services.rss import _fetch_full_article + article_content = await _fetch_full_article(item.url) or item.content or "" # Store synthetic assistant message with read_article tool result synthetic_tool_calls = [{ From 814f44c3fb9f40667c226a826b70fae0132cee1d Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Tue, 7 Apr 2026 06:59:06 -0400 Subject: [PATCH 2/3] fix(push): fix VAPID key format and add regenerate endpoint + UI button --- frontend/src/views/SettingsView.vue | 38 ++++++++++++++++++++++++++++ src/fabledassistant/routes/push.py | 14 ++++++++-- src/fabledassistant/services/push.py | 38 ++++++++++++++++++++++++---- 3 files changed, 83 insertions(+), 7 deletions(-) diff --git a/frontend/src/views/SettingsView.vue b/frontend/src/views/SettingsView.vue index 75e4b03..0c50c89 100644 --- a/frontend/src/views/SettingsView.vue +++ b/frontend/src/views/SettingsView.vue @@ -428,6 +428,28 @@ const notifySecurityAlerts = ref(true); const savingNotifications = ref(false); const notificationsSaved = ref(false); +// VAPID key reset (admin) +const vapidResetting = ref(false); +const vapidResetMsg = ref(""); +const vapidResetError = ref(false); + +async function resetVapidKeys() { + if (!confirm("This will regenerate VAPID keys and clear all push subscriptions. You will need to re-enable notifications afterwards. Continue?")) return; + vapidResetting.value = true; + vapidResetMsg.value = ""; + vapidResetError.value = false; + try { + await apiPost("/api/push/reset-vapid", {}); + vapidResetMsg.value = "Keys regenerated. Please re-enable notifications in this browser."; + pushStore.checkSubscription(); + } catch { + vapidResetError.value = true; + vapidResetMsg.value = "Reset failed — check server logs."; + } finally { + vapidResetting.value = false; + } +} + // CalDAV settings (per-user) const caldav = ref({ caldav_url: "", @@ -1882,6 +1904,22 @@ function formatUserDate(iso: string): string { Notifications are blocked. Allow them in your browser site settings to re-enable.

+
diff --git a/src/fabledassistant/routes/push.py b/src/fabledassistant/routes/push.py index c827420..6179518 100644 --- a/src/fabledassistant/routes/push.py +++ b/src/fabledassistant/routes/push.py @@ -3,9 +3,9 @@ import logging from quart import Blueprint, jsonify, request -from fabledassistant.auth import login_required, get_current_user_id +from fabledassistant.auth import login_required, get_current_user_id, admin_required from fabledassistant.config import Config -from fabledassistant.services.push import delete_subscription, save_subscription, vapid_enabled +from fabledassistant.services.push import delete_subscription, regenerate_vapid_keys, save_subscription, vapid_enabled logger = logging.getLogger(__name__) @@ -42,3 +42,13 @@ async def unsubscribe(): return jsonify({"error": "endpoint is required"}), 400 await delete_subscription(uid, endpoint) return "", 204 + + +@push_bp.route("/reset-vapid", methods=["POST"]) +@admin_required +async def reset_vapid(): + """Regenerate VAPID keys and clear all push subscriptions.""" + ok = await regenerate_vapid_keys() + if ok: + return jsonify({"publicKey": Config.VAPID_PUBLIC_KEY}), 200 + return jsonify({"error": "Key regeneration failed"}), 500 diff --git a/src/fabledassistant/services/push.py b/src/fabledassistant/services/push.py index 87256ad..b6c7708 100644 --- a/src/fabledassistant/services/push.py +++ b/src/fabledassistant/services/push.py @@ -62,11 +62,14 @@ def ensure_vapid_keys() -> None: v = Vapid01() v.generate_keys() - private_pem = v.private_key.private_bytes( - serialization.Encoding.PEM, + # pywebpush expects the private key as a base64url-encoded DER blob + # (passed to Vapid.from_string → from_der), NOT a PEM string. + private_der = v.private_key.private_bytes( + serialization.Encoding.DER, serialization.PrivateFormat.TraditionalOpenSSL, serialization.NoEncryption(), - ).decode() + ) + private_b64 = base64.urlsafe_b64encode(private_der).rstrip(b"=").decode() pub_bytes = v.public_key.public_bytes( serialization.Encoding.X962, @@ -76,15 +79,40 @@ def ensure_vapid_keys() -> None: # Persist so they survive container restarts. _VAPID_KEYS_FILE.parent.mkdir(parents=True, exist_ok=True) - _VAPID_KEYS_FILE.write_text(json.dumps({"private_key": private_pem, "public_key": public_b64})) + _VAPID_KEYS_FILE.write_text(json.dumps({"private_key": private_b64, "public_key": public_b64})) - Config.VAPID_PRIVATE_KEY = private_pem + Config.VAPID_PRIVATE_KEY = private_b64 Config.VAPID_PUBLIC_KEY = public_b64 logger.info("Generated new VAPID keys and saved to %s", _VAPID_KEYS_FILE) except Exception: logger.warning("Failed to generate VAPID keys — push notifications will be unavailable", exc_info=True) +async def regenerate_vapid_keys() -> bool: + """Delete existing VAPID keys, clear all push subscriptions, and generate a fresh pair. + + All existing browser subscriptions are invalidated when keys rotate, so they + must be cleared — users will need to re-enable notifications. + """ + if _VAPID_KEYS_FILE.exists(): + _VAPID_KEYS_FILE.unlink() + Config.VAPID_PRIVATE_KEY = "" + Config.VAPID_PUBLIC_KEY = "" + + # Clear all push subscriptions — they are bound to the old public key. + async with async_session() as session: + await session.execute(delete(PushSubscription)) + await session.commit() + + ensure_vapid_keys() + enabled = vapid_enabled() + if enabled: + logger.info("VAPID keys regenerated successfully") + else: + logger.error("VAPID key regeneration failed") + return enabled + + async def save_subscription(user_id: int, subscription_json: dict, user_agent: str | None = None) -> PushSubscription: """Upsert a push subscription by endpoint.""" endpoint = subscription_json.get("endpoint", "") From d3170e5545016ca7d4fca8f231bd67b7cd8f6d79 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Tue, 7 Apr 2026 08:13:40 -0400 Subject: [PATCH 3/3] fix(chat): fetch full article content in from-article endpoint --- src/fabledassistant/routes/chat.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/fabledassistant/routes/chat.py b/src/fabledassistant/routes/chat.py index 9e83e5a..8bfa2b7 100644 --- a/src/fabledassistant/routes/chat.py +++ b/src/fabledassistant/routes/chat.py @@ -533,8 +533,9 @@ async def create_conversation_from_article(item_id: int): conv_title = (item.title or "Article discussion")[:80] conv = await create_conversation(uid, title=conv_title, conversation_type="chat") + from fabledassistant.services.rss import _fetch_full_article source = feed_title or "News" - content_body = (item.content or "").strip() + content_body = (await _fetch_full_article(item.url) if item.url else None) or (item.content or "").strip() seeded_text = f"**{source}**\n\n**{item.title}**" if content_body: seeded_text += f"\n\n{content_body}"