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 <noreply@anthropic.com>
This commit is contained in:
@@ -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(() => {
|
||||
<router-link to="/chat" :class="['nav-link', { 'router-link-active': isChatActive }]">Chat</router-link>
|
||||
<router-link to="/briefing" class="nav-link">Briefing</router-link>
|
||||
<router-link to="/calendar" class="nav-link">Calendar</router-link>
|
||||
<router-link to="/news" class="nav-link">News</router-link>
|
||||
<router-link v-if="settingsStore.rssEnabled" to="/news" class="nav-link">News</router-link>
|
||||
<router-link to="/projects" class="nav-link">Projects</router-link>
|
||||
</div>
|
||||
</div>
|
||||
@@ -128,7 +130,7 @@ router.afterEach(() => {
|
||||
<router-link to="/briefing" class="nav-link">Briefing</router-link>
|
||||
<router-link to="/calendar" class="nav-link">Calendar</router-link>
|
||||
<router-link to="/projects" class="nav-link">Projects</router-link>
|
||||
<router-link to="/news" class="nav-link">News</router-link>
|
||||
<router-link v-if="settingsStore.rssEnabled" to="/news" class="nav-link">News</router-link>
|
||||
<router-link to="/shared" class="nav-link">Shared</router-link>
|
||||
<div class="mobile-divider"></div>
|
||||
<router-link to="/settings" class="nav-link">Settings</router-link>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 () => {
|
||||
</div>
|
||||
|
||||
<!-- News section (scrollable) -->
|
||||
<div class="news-section">
|
||||
<div v-if="settingsStore.rssEnabled" class="news-section">
|
||||
<div class="panel-label-row">
|
||||
<div class="panel-label">Today's News</div>
|
||||
<span v-if="newsItems.length" class="news-count">{{ newsItems.length }} items</span>
|
||||
|
||||
@@ -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 {
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- RSS Feeds -->
|
||||
<!-- RSS toggle -->
|
||||
<section class="settings-section full-width">
|
||||
<h2>RSS / News</h2>
|
||||
<div class="checkbox-field">
|
||||
<label>
|
||||
<input type="checkbox" :checked="store.rssEnabled" @change="toggleRss" />
|
||||
Enable RSS feeds
|
||||
</label>
|
||||
<p class="field-hint">Subscribe to RSS/Atom feeds and include news in your briefings.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- RSS Feeds -->
|
||||
<section v-if="store.rssEnabled" class="settings-section full-width">
|
||||
<div class="briefing-feeds-header">
|
||||
<div>
|
||||
<h2>RSS Feeds</h2>
|
||||
@@ -2252,7 +2272,7 @@ function formatUserDate(iso: string): string {
|
||||
</section>
|
||||
|
||||
<!-- News Preferences -->
|
||||
<section class="settings-section full-width">
|
||||
<section v-if="store.rssEnabled" class="settings-section full-width">
|
||||
<h2>News Preferences</h2>
|
||||
<p class="section-desc">
|
||||
Tell the briefing what topics you care about. Topics are matched against
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user