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 { useShortcuts } from "@/composables/useShortcuts";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
import { useChatStore } from "@/stores/chat";
|
import { useChatStore } from "@/stores/chat";
|
||||||
|
import { useSettingsStore } from "@/stores/settings";
|
||||||
import AppLogo from "@/components/AppLogo.vue";
|
import AppLogo from "@/components/AppLogo.vue";
|
||||||
import NotificationBell from "@/components/NotificationBell.vue";
|
import NotificationBell from "@/components/NotificationBell.vue";
|
||||||
|
|
||||||
@@ -12,6 +13,7 @@ const { theme, toggleTheme } = useTheme();
|
|||||||
const { toggleShortcuts } = useShortcuts();
|
const { toggleShortcuts } = useShortcuts();
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
const chatStore = useChatStore();
|
const chatStore = useChatStore();
|
||||||
|
const settingsStore = useSettingsStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const route = useRoute();
|
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="/chat" :class="['nav-link', { 'router-link-active': isChatActive }]">Chat</router-link>
|
||||||
<router-link to="/briefing" class="nav-link">Briefing</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="/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>
|
<router-link to="/projects" class="nav-link">Projects</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -128,7 +130,7 @@ router.afterEach(() => {
|
|||||||
<router-link to="/briefing" class="nav-link">Briefing</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="/calendar" class="nav-link">Calendar</router-link>
|
||||||
<router-link to="/projects" class="nav-link">Projects</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>
|
<router-link to="/shared" class="nav-link">Shared</router-link>
|
||||||
<div class="mobile-divider"></div>
|
<div class="mobile-divider"></div>
|
||||||
<router-link to="/settings" class="nav-link">Settings</router-link>
|
<router-link to="/settings" class="nav-link">Settings</router-link>
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ export const useSettingsStore = defineStore("settings", () => {
|
|||||||
() => settings.value.default_model || ""
|
() => settings.value.default_model || ""
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const rssEnabled = computed(
|
||||||
|
() => settings.value.rss_enabled === "true"
|
||||||
|
);
|
||||||
|
|
||||||
// Voice status — checked once on login, refreshable from Settings
|
// Voice status — checked once on login, refreshable from Settings
|
||||||
const voiceEnabled = ref(false);
|
const voiceEnabled = ref(false);
|
||||||
const voiceSttReady = ref(false);
|
const voiceSttReady = ref(false);
|
||||||
@@ -62,6 +66,7 @@ export const useSettingsStore = defineStore("settings", () => {
|
|||||||
loading,
|
loading,
|
||||||
assistantName,
|
assistantName,
|
||||||
defaultModel,
|
defaultModel,
|
||||||
|
rssEnabled,
|
||||||
voiceEnabled,
|
voiceEnabled,
|
||||||
voiceSttReady,
|
voiceSttReady,
|
||||||
voiceTtsReady,
|
voiceTtsReady,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||||
import { useBackgroundRefresh } from '@/composables/useBackgroundRefresh'
|
import { useBackgroundRefresh } from '@/composables/useBackgroundRefresh'
|
||||||
import { useChatStore } from '@/stores/chat'
|
import { useChatStore } from '@/stores/chat'
|
||||||
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
import ChatPanel from '@/components/ChatPanel.vue'
|
import ChatPanel from '@/components/ChatPanel.vue'
|
||||||
import WeatherCard from '@/components/WeatherCard.vue'
|
import WeatherCard from '@/components/WeatherCard.vue'
|
||||||
import BriefingSetupWizard from '@/components/BriefingSetupWizard.vue'
|
import BriefingSetupWizard from '@/components/BriefingSetupWizard.vue'
|
||||||
@@ -33,6 +34,7 @@ interface WeatherData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const chatStore = useChatStore()
|
const chatStore = useChatStore()
|
||||||
|
const settingsStore = useSettingsStore()
|
||||||
|
|
||||||
// Setup wizard
|
// Setup wizard
|
||||||
const showWizard = ref(false)
|
const showWizard = ref(false)
|
||||||
@@ -293,7 +295,7 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- News section (scrollable) -->
|
<!-- News section (scrollable) -->
|
||||||
<div class="news-section">
|
<div v-if="settingsStore.rssEnabled" class="news-section">
|
||||||
<div class="panel-label-row">
|
<div class="panel-label-row">
|
||||||
<div class="panel-label">Today's News</div>
|
<div class="panel-label">Today's News</div>
|
||||||
<span v-if="newsItems.length" class="news-count">{{ newsItems.length }} items</span>
|
<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() {
|
async function addFeed() {
|
||||||
if (!newFeedUrl.value.trim() || addingFeed.value) return;
|
if (!newFeedUrl.value.trim() || addingFeed.value) return;
|
||||||
addingFeed.value = true;
|
addingFeed.value = true;
|
||||||
@@ -2209,8 +2217,20 @@ function formatUserDate(iso: string): string {
|
|||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- RSS Feeds -->
|
<!-- RSS toggle -->
|
||||||
<section class="settings-section full-width">
|
<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 class="briefing-feeds-header">
|
||||||
<div>
|
<div>
|
||||||
<h2>RSS Feeds</h2>
|
<h2>RSS Feeds</h2>
|
||||||
@@ -2252,7 +2272,7 @@ function formatUserDate(iso: string): string {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- News Preferences -->
|
<!-- News Preferences -->
|
||||||
<section class="settings-section full-width">
|
<section v-if="store.rssEnabled" class="settings-section full-width">
|
||||||
<h2>News Preferences</h2>
|
<h2>News Preferences</h2>
|
||||||
<p class="section-desc">
|
<p class="section-desc">
|
||||||
Tell the briefing what topics you care about. Topics are matched against
|
Tell the briefing what topics you care about. Topics are matched against
|
||||||
|
|||||||
@@ -54,11 +54,17 @@ async def put_config():
|
|||||||
return jsonify({"ok": True})
|
return jsonify({"ok": True})
|
||||||
|
|
||||||
|
|
||||||
|
async def _rss_enabled() -> bool:
|
||||||
|
return (await get_setting(g.user.id, "rss_enabled", "false")).lower() == "true"
|
||||||
|
|
||||||
|
|
||||||
# ── RSS Feeds ─────────────────────────────────────────────────────────────────
|
# ── RSS Feeds ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@briefing_bp.route("/feeds", methods=["GET"])
|
@briefing_bp.route("/feeds", methods=["GET"])
|
||||||
@_REQUIRE
|
@_REQUIRE
|
||||||
async def list_feeds():
|
async def list_feeds():
|
||||||
|
if not await _rss_enabled():
|
||||||
|
return jsonify([])
|
||||||
async with async_session() as session:
|
async with async_session() as session:
|
||||||
result = await session.execute(
|
result = await session.execute(
|
||||||
select(RssFeed).where(RssFeed.user_id == g.user.id).order_by(RssFeed.id)
|
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"])
|
@briefing_bp.route("/feeds", methods=["POST"])
|
||||||
@_REQUIRE
|
@_REQUIRE
|
||||||
async def add_feed():
|
async def add_feed():
|
||||||
|
if not await _rss_enabled():
|
||||||
|
return jsonify({"error": "RSS is disabled"}), 403
|
||||||
data = await request.get_json()
|
data = await request.get_json()
|
||||||
url = (data.get("url") or "").strip()
|
url = (data.get("url") or "").strip()
|
||||||
if not url:
|
if not url:
|
||||||
@@ -120,6 +128,8 @@ async def delete_feed(feed_id: int):
|
|||||||
@briefing_bp.route("/feeds/refresh", methods=["POST"])
|
@briefing_bp.route("/feeds/refresh", methods=["POST"])
|
||||||
@_REQUIRE
|
@_REQUIRE
|
||||||
async def refresh_feeds():
|
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)
|
results = await rss_svc.refresh_all_feeds(g.user.id)
|
||||||
total_new = sum(results.values())
|
total_new = sum(results.values())
|
||||||
return jsonify({"feeds_refreshed": len(results), "new_items": total_new})
|
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"])
|
@briefing_bp.route("/feeds/recent", methods=["GET"])
|
||||||
@_REQUIRE
|
@_REQUIRE
|
||||||
async def recent_items():
|
async def recent_items():
|
||||||
|
if not await _rss_enabled():
|
||||||
|
return jsonify({"items": []})
|
||||||
limit = min(int(request.args.get("limit", 20)), 100)
|
limit = min(int(request.args.get("limit", 20)), 100)
|
||||||
items = await rss_svc.get_recent_items(g.user.id, limit=limit)
|
items = await rss_svc.get_recent_items(g.user.id, limit=limit)
|
||||||
return jsonify({"items": items})
|
return jsonify({"items": items})
|
||||||
@@ -454,6 +466,9 @@ async def list_news():
|
|||||||
offset — pagination offset (default 0)
|
offset — pagination offset (default 0)
|
||||||
feed_id — optional integer filter by feed
|
feed_id — optional integer filter by feed
|
||||||
"""
|
"""
|
||||||
|
if not await _rss_enabled():
|
||||||
|
return jsonify({"items": [], "total": 0})
|
||||||
|
|
||||||
from sqlalchemy import text as _text
|
from sqlalchemy import text as _text
|
||||||
|
|
||||||
days = min(int(request.args.get("days", 2)), 90)
|
days = min(int(request.args.get("days", 2)), 90)
|
||||||
|
|||||||
@@ -20,18 +20,26 @@ SLOT_NAMES = ("compilation", "morning", "midday", "afternoon")
|
|||||||
# ── External data gather ──────────────────────────────────────────────────────
|
# ── External data gather ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
async def _gather_external(user_id: int) -> dict:
|
async def _gather_external(user_id: int) -> dict:
|
||||||
"""Collect RSS items and weather."""
|
"""Collect RSS items (when enabled) and weather."""
|
||||||
from fabledassistant.services.rss import get_recent_items
|
|
||||||
from fabledassistant.services.weather import get_cached_weather
|
from fabledassistant.services.weather import get_cached_weather
|
||||||
|
|
||||||
rss_items, weather = await asyncio.gather(
|
rss_on = (await get_setting(user_id, "rss_enabled", "false")).lower() == "true"
|
||||||
get_recent_items(user_id, limit=20),
|
rss_items: list = []
|
||||||
get_cached_weather(user_id),
|
if rss_on:
|
||||||
return_exceptions=True,
|
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 {
|
return {
|
||||||
"rss_items": rss_items if not isinstance(rss_items, Exception) else [],
|
"rss_items": rss_items,
|
||||||
"weather": weather if not isinstance(weather, Exception) else [],
|
"weather": weather,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -362,10 +362,12 @@ async def _run_slot_for_user(user_id: int, slot: str) -> None:
|
|||||||
# Refresh external data first
|
# Refresh external data first
|
||||||
try:
|
try:
|
||||||
import json
|
import json
|
||||||
from fabledassistant.services.rss import refresh_all_feeds
|
|
||||||
config_raw = await get_setting(user_id, "briefing_config", "{}")
|
config_raw = await get_setting(user_id, "briefing_config", "{}")
|
||||||
config = json.loads(config_raw) if isinstance(config_raw, str) else {}
|
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
|
from fabledassistant.services import weather as wx
|
||||||
for key, loc in config.get("locations", {}).items():
|
for key, loc in config.get("locations", {}).items():
|
||||||
if loc.get("lat") and loc.get("lon"):
|
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)
|
return await is_caldav_configured(user_id)
|
||||||
if requires == "searxng":
|
if requires == "searxng":
|
||||||
return Config.searxng_enabled()
|
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
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ logger = logging.getLogger(__name__)
|
|||||||
},
|
},
|
||||||
read_only=True,
|
read_only=True,
|
||||||
briefing=True,
|
briefing=True,
|
||||||
|
requires="rss",
|
||||||
)
|
)
|
||||||
async def get_rss_items_tool(*, user_id, arguments, **_ctx):
|
async def get_rss_items_tool(*, user_id, arguments, **_ctx):
|
||||||
from fabledassistant.services.rss import get_recent_items
|
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."},
|
"category": {"type": "string", "description": "Optional category label (e.g. 'news', 'tech', 'reddit'). Omit if unsure."},
|
||||||
},
|
},
|
||||||
required=["url"],
|
required=["url"],
|
||||||
|
requires="rss",
|
||||||
)
|
)
|
||||||
async def add_rss_feed_tool(*, user_id, arguments, **_ctx):
|
async def add_rss_feed_tool(*, user_id, arguments, **_ctx):
|
||||||
import asyncio as _asyncio
|
import asyncio as _asyncio
|
||||||
|
|||||||
Reference in New Issue
Block a user