From 3ac32dc3bced893424de8960098fd6a491a0a0db Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Fri, 17 Apr 2026 13:58:50 -0400 Subject: [PATCH] feat: add rss_enabled user setting to toggle RSS functionality RSS is now off by default. When disabled: - Scheduler skips RSS feed sync during compilation slot - Briefing pipeline skips RSS item gathering - RSS LLM tools (get_rss_items, add_rss_feed) are hidden - API routes return empty results for feeds/news - Frontend hides News nav link, RSS Feeds and News Preferences in settings - Briefing view hides news sidebar section Toggle in Settings > Briefing > RSS / News. Co-Authored-By: Claude Opus 4.6 --- frontend/src/components/AppHeader.vue | 6 +++-- frontend/src/stores/settings.ts | 5 ++++ frontend/src/views/BriefingView.vue | 4 ++- frontend/src/views/SettingsView.vue | 24 +++++++++++++++-- src/fabledassistant/routes/briefing.py | 15 +++++++++++ .../services/briefing_pipeline.py | 26 ++++++++++++------- .../services/briefing_scheduler.py | 6 +++-- .../services/tools/_registry.py | 3 +++ src/fabledassistant/services/tools/rss.py | 2 ++ 9 files changed, 75 insertions(+), 16 deletions(-) diff --git a/frontend/src/components/AppHeader.vue b/frontend/src/components/AppHeader.vue index 4be5a93..d366490 100644 --- a/frontend/src/components/AppHeader.vue +++ b/frontend/src/components/AppHeader.vue @@ -5,6 +5,7 @@ import { useTheme } from "@/composables/useTheme"; import { useShortcuts } from "@/composables/useShortcuts"; import { useAuthStore } from "@/stores/auth"; import { useChatStore } from "@/stores/chat"; +import { useSettingsStore } from "@/stores/settings"; import AppLogo from "@/components/AppLogo.vue"; import NotificationBell from "@/components/NotificationBell.vue"; @@ -12,6 +13,7 @@ const { theme, toggleTheme } = useTheme(); const { toggleShortcuts } = useShortcuts(); const authStore = useAuthStore(); const chatStore = useChatStore(); +const settingsStore = useSettingsStore(); const router = useRouter(); const route = useRoute(); @@ -78,7 +80,7 @@ router.afterEach(() => { Chat Briefing Calendar - News + News Projects @@ -128,7 +130,7 @@ router.afterEach(() => { Briefing Calendar Projects - News + News Shared
Settings diff --git a/frontend/src/stores/settings.ts b/frontend/src/stores/settings.ts index 4d07108..7d41ffb 100644 --- a/frontend/src/stores/settings.ts +++ b/frontend/src/stores/settings.ts @@ -16,6 +16,10 @@ export const useSettingsStore = defineStore("settings", () => { () => settings.value.default_model || "" ); + const rssEnabled = computed( + () => settings.value.rss_enabled === "true" + ); + // Voice status — checked once on login, refreshable from Settings const voiceEnabled = ref(false); const voiceSttReady = ref(false); @@ -62,6 +66,7 @@ export const useSettingsStore = defineStore("settings", () => { loading, assistantName, defaultModel, + rssEnabled, voiceEnabled, voiceSttReady, voiceTtsReady, diff --git a/frontend/src/views/BriefingView.vue b/frontend/src/views/BriefingView.vue index bffdde8..27f5fec 100644 --- a/frontend/src/views/BriefingView.vue +++ b/frontend/src/views/BriefingView.vue @@ -2,6 +2,7 @@ import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue' import { useBackgroundRefresh } from '@/composables/useBackgroundRefresh' import { useChatStore } from '@/stores/chat' +import { useSettingsStore } from '@/stores/settings' import ChatPanel from '@/components/ChatPanel.vue' import WeatherCard from '@/components/WeatherCard.vue' import BriefingSetupWizard from '@/components/BriefingSetupWizard.vue' @@ -33,6 +34,7 @@ interface WeatherData { } const chatStore = useChatStore() +const settingsStore = useSettingsStore() // Setup wizard const showWizard = ref(false) @@ -293,7 +295,7 @@ onMounted(async () => { -
+
Today's News
{{ newsItems.length }} items diff --git a/frontend/src/views/SettingsView.vue b/frontend/src/views/SettingsView.vue index 987081b..5bdcff1 100644 --- a/frontend/src/views/SettingsView.vue +++ b/frontend/src/views/SettingsView.vue @@ -387,6 +387,14 @@ async function saveBriefingSettings() { } } +async function toggleRss() { + try { + await store.updateSettings({ rss_enabled: store.rssEnabled ? "false" : "true" }); + } catch { + toastStore.show("Failed to update RSS setting", "error"); + } +} + async function addFeed() { if (!newFeedUrl.value.trim() || addingFeed.value) return; addingFeed.value = true; @@ -2209,8 +2217,20 @@ function formatUserDate(iso: string): string {

- +
+

RSS / News

+
+ +

Subscribe to RSS/Atom feeds and include news in your briefings.

+
+
+ + +

RSS Feeds

@@ -2252,7 +2272,7 @@ function formatUserDate(iso: string): string {
-
+

News Preferences

Tell the briefing what topics you care about. Topics are matched against diff --git a/src/fabledassistant/routes/briefing.py b/src/fabledassistant/routes/briefing.py index 7b3e6da..0cb56c9 100644 --- a/src/fabledassistant/routes/briefing.py +++ b/src/fabledassistant/routes/briefing.py @@ -54,11 +54,17 @@ async def put_config(): return jsonify({"ok": True}) +async def _rss_enabled() -> bool: + return (await get_setting(g.user.id, "rss_enabled", "false")).lower() == "true" + + # ── RSS Feeds ───────────────────────────────────────────────────────────────── @briefing_bp.route("/feeds", methods=["GET"]) @_REQUIRE async def list_feeds(): + if not await _rss_enabled(): + return jsonify([]) async with async_session() as session: result = await session.execute( select(RssFeed).where(RssFeed.user_id == g.user.id).order_by(RssFeed.id) @@ -70,6 +76,8 @@ async def list_feeds(): @briefing_bp.route("/feeds", methods=["POST"]) @_REQUIRE async def add_feed(): + if not await _rss_enabled(): + return jsonify({"error": "RSS is disabled"}), 403 data = await request.get_json() url = (data.get("url") or "").strip() if not url: @@ -120,6 +128,8 @@ async def delete_feed(feed_id: int): @briefing_bp.route("/feeds/refresh", methods=["POST"]) @_REQUIRE async def refresh_feeds(): + if not await _rss_enabled(): + return jsonify({"feeds_refreshed": 0, "new_items": 0}) results = await rss_svc.refresh_all_feeds(g.user.id) total_new = sum(results.values()) return jsonify({"feeds_refreshed": len(results), "new_items": total_new}) @@ -128,6 +138,8 @@ async def refresh_feeds(): @briefing_bp.route("/feeds/recent", methods=["GET"]) @_REQUIRE async def recent_items(): + if not await _rss_enabled(): + return jsonify({"items": []}) limit = min(int(request.args.get("limit", 20)), 100) items = await rss_svc.get_recent_items(g.user.id, limit=limit) return jsonify({"items": items}) @@ -454,6 +466,9 @@ async def list_news(): offset — pagination offset (default 0) feed_id — optional integer filter by feed """ + if not await _rss_enabled(): + return jsonify({"items": [], "total": 0}) + from sqlalchemy import text as _text days = min(int(request.args.get("days", 2)), 90) diff --git a/src/fabledassistant/services/briefing_pipeline.py b/src/fabledassistant/services/briefing_pipeline.py index 02cd6ca..516200e 100644 --- a/src/fabledassistant/services/briefing_pipeline.py +++ b/src/fabledassistant/services/briefing_pipeline.py @@ -20,18 +20,26 @@ SLOT_NAMES = ("compilation", "morning", "midday", "afternoon") # ── External data gather ────────────────────────────────────────────────────── async def _gather_external(user_id: int) -> dict: - """Collect RSS items and weather.""" - from fabledassistant.services.rss import get_recent_items + """Collect RSS items (when enabled) and weather.""" from fabledassistant.services.weather import get_cached_weather - rss_items, weather = await asyncio.gather( - get_recent_items(user_id, limit=20), - get_cached_weather(user_id), - return_exceptions=True, - ) + rss_on = (await get_setting(user_id, "rss_enabled", "false")).lower() == "true" + rss_items: list = [] + if rss_on: + from fabledassistant.services.rss import get_recent_items + try: + rss_items = await get_recent_items(user_id, limit=20) + except Exception: + pass + + try: + weather = await get_cached_weather(user_id) + except Exception: + weather = [] + return { - "rss_items": rss_items if not isinstance(rss_items, Exception) else [], - "weather": weather if not isinstance(weather, Exception) else [], + "rss_items": rss_items, + "weather": weather, } diff --git a/src/fabledassistant/services/briefing_scheduler.py b/src/fabledassistant/services/briefing_scheduler.py index 62826c5..fdaee41 100644 --- a/src/fabledassistant/services/briefing_scheduler.py +++ b/src/fabledassistant/services/briefing_scheduler.py @@ -362,10 +362,12 @@ async def _run_slot_for_user(user_id: int, slot: str) -> None: # Refresh external data first try: import json - from fabledassistant.services.rss import refresh_all_feeds config_raw = await get_setting(user_id, "briefing_config", "{}") config = json.loads(config_raw) if isinstance(config_raw, str) else {} - await refresh_all_feeds(user_id) + rss_on = (await get_setting(user_id, "rss_enabled", "false")).lower() == "true" + if rss_on: + from fabledassistant.services.rss import refresh_all_feeds + await refresh_all_feeds(user_id) from fabledassistant.services import weather as wx for key, loc in config.get("locations", {}).items(): if loc.get("lat") and loc.get("lon"): diff --git a/src/fabledassistant/services/tools/_registry.py b/src/fabledassistant/services/tools/_registry.py index 3271383..d47842d 100644 --- a/src/fabledassistant/services/tools/_registry.py +++ b/src/fabledassistant/services/tools/_registry.py @@ -84,6 +84,9 @@ async def _check_requires(user_id: int, requires: str) -> bool: return await is_caldav_configured(user_id) if requires == "searxng": return Config.searxng_enabled() + if requires == "rss": + from fabledassistant.services.settings import get_setting + return (await get_setting(user_id, "rss_enabled", "false")).lower() == "true" return True diff --git a/src/fabledassistant/services/tools/rss.py b/src/fabledassistant/services/tools/rss.py index 434d301..3627abd 100644 --- a/src/fabledassistant/services/tools/rss.py +++ b/src/fabledassistant/services/tools/rss.py @@ -18,6 +18,7 @@ logger = logging.getLogger(__name__) }, read_only=True, briefing=True, + requires="rss", ) async def get_rss_items_tool(*, user_id, arguments, **_ctx): from fabledassistant.services.rss import get_recent_items @@ -35,6 +36,7 @@ async def get_rss_items_tool(*, user_id, arguments, **_ctx): "category": {"type": "string", "description": "Optional category label (e.g. 'news', 'tech', 'reddit'). Omit if unsure."}, }, required=["url"], + requires="rss", ) async def add_rss_feed_tool(*, user_id, arguments, **_ctx): import asyncio as _asyncio