From e6ac6cd061d19a833c72d55e6ee5f717f198628b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 26 Jul 2026 17:09:26 +0000 Subject: [PATCH] Consolidate Weather/Stocks/Bookmarks/PoE2 into a single admin "Widgets" tab Each widget now has an independent enable/disable checkbox that gates its visibility in the sidebar (new global_settings.widgets columns + GET /api/widgets), and is collapsed by default behind a WidgetSection wrapper so the tab stays manageable as more widgets get added. The four existing tab components (WeatherTab/StocksTab/BookmarksTab/Poe2Tab) are reused unmodified as each section's expanded content. --- backend/src/api/public.ts | 5 + backend/src/storage/db/index.ts | 10 ++ backend/src/storage/db/settings.ts | 15 ++- backend/src/storage/db/types.ts | 7 ++ frontend/src/lib/adminTypes.ts | 8 ++ frontend/src/lib/api.ts | 6 +- .../lib/components/admin/WidgetSection.svelte | 103 ++++++++++++++++++ .../lib/components/admin/WidgetsTab.svelte | 59 ++++++++++ .../src/lib/components/sidebar/Sidebar.svelte | 28 +++-- frontend/src/lib/types.ts | 8 ++ frontend/src/routes/+layout.svelte | 2 +- frontend/src/routes/+layout.ts | 10 +- .../src/routes/admin/settings/+page.svelte | 20 +--- 13 files changed, 251 insertions(+), 30 deletions(-) create mode 100644 frontend/src/lib/components/admin/WidgetSection.svelte create mode 100644 frontend/src/lib/components/admin/WidgetsTab.svelte diff --git a/backend/src/api/public.ts b/backend/src/api/public.ts index 9765fb4..453c77e 100644 --- a/backend/src/api/public.ts +++ b/backend/src/api/public.ts @@ -60,6 +60,11 @@ export async function registerPublicRoutes(app: FastifyInstance) { return categories.filter((c) => !c.isPrivate); }); + // Per-widget sidebar visibility — see the admin panel's consolidated "Widgets" tab. + // Each widget keeps polling/config regardless of this; it only gates whether the + // sidebar renders it at all. + app.get('/api/widgets', async () => settingsDb.getSettings().widgets); + // Sidebar widgets — see WeatherTab/StocksTab/BookmarksTab in the admin panel. app.get('/api/weather', async () => settingsDb.getSettings().weather); diff --git a/backend/src/storage/db/index.ts b/backend/src/storage/db/index.ts index 8d5b415..449d020 100644 --- a/backend/src/storage/db/index.ts +++ b/backend/src/storage/db/index.ts @@ -185,6 +185,10 @@ export function migrate() { nitter_media_mode TEXT NOT NULL DEFAULT 'proxy', -- self-host | proxy | direct fxtwitter_base_url TEXT NOT NULL DEFAULT 'https://api.fxtwitter.com', telegram_media_mode TEXT NOT NULL DEFAULT 'self-host', -- self-host | proxy (no "direct" — Telegram has no public hotlinkable media URL) + widget_weather_enabled INTEGER NOT NULL DEFAULT 1, + widget_stocks_enabled INTEGER NOT NULL DEFAULT 1, + widget_bookmarks_enabled INTEGER NOT NULL DEFAULT 1, + widget_poe2_enabled INTEGER NOT NULL DEFAULT 1, weather_location_name TEXT, weather_latitude REAL, weather_longitude REAL, @@ -348,6 +352,12 @@ export function migrate() { db.exec('ALTER TABLE global_settings ADD COLUMN poe2_league_name TEXT'); db.exec('ALTER TABLE global_settings ADD COLUMN poe2_updated_at TEXT'); } + if (!hasColumn('global_settings', 'widget_weather_enabled')) { + db.exec('ALTER TABLE global_settings ADD COLUMN widget_weather_enabled INTEGER NOT NULL DEFAULT 1'); + db.exec('ALTER TABLE global_settings ADD COLUMN widget_stocks_enabled INTEGER NOT NULL DEFAULT 1'); + db.exec('ALTER TABLE global_settings ADD COLUMN widget_bookmarks_enabled INTEGER NOT NULL DEFAULT 1'); + db.exec('ALTER TABLE global_settings ADD COLUMN widget_poe2_enabled INTEGER NOT NULL DEFAULT 1'); + } // Seed a handful of sensible default tickers so the Stocks widget isn't empty on a // fresh install — the admin can remove/replace any of them via the Stocks tab. diff --git a/backend/src/storage/db/settings.ts b/backend/src/storage/db/settings.ts index b220a8f..d734acd 100644 --- a/backend/src/storage/db/settings.ts +++ b/backend/src/storage/db/settings.ts @@ -16,6 +16,12 @@ function rowToSettings(row: any): GlobalSettings { nitterMediaMode: row.nitter_media_mode, fxtwitterBaseUrl: row.fxtwitter_base_url, telegramMediaMode: row.telegram_media_mode, + widgets: { + weather: !!row.widget_weather_enabled, + stocks: !!row.widget_stocks_enabled, + bookmarks: !!row.widget_bookmarks_enabled, + poe2: !!row.widget_poe2_enabled + }, retention: { publishedArticleMaxAgeDays: row.published_article_max_age_days, rawItemMaxAgeDays: row.raw_item_max_age_days, @@ -58,7 +64,8 @@ export function updateSettings(patch: Partial): GlobalSettings { retention: { ...current.retention, ...(patch.retention ?? {}) }, selectedModels: { ...current.selectedModels, ...(patch.selectedModels ?? {}) }, weather: { ...current.weather, ...(patch.weather ?? {}) }, - poe2: { ...current.poe2, ...(patch.poe2 ?? {}) } + poe2: { ...current.poe2, ...(patch.poe2 ?? {}) }, + widgets: { ...current.widgets, ...(patch.widgets ?? {}) } }; // Named params (rather than positional `?`) so this list can be reordered or // extended without the column list and the bound-values list silently drifting @@ -71,6 +78,8 @@ export function updateSettings(patch: Partial): GlobalSettings { follow_up_min_hours_since_last=$follow_up_min_hours_since_last, follow_up_min_new_sources=$follow_up_min_new_sources, ai_service_host=$ai_service_host, ai_service_port=$ai_service_port, selected_models=$selected_models, nitter_media_mode=$nitter_media_mode, fxtwitter_base_url=$fxtwitter_base_url, telegram_media_mode=$telegram_media_mode, + widget_weather_enabled=$widget_weather_enabled, widget_stocks_enabled=$widget_stocks_enabled, + widget_bookmarks_enabled=$widget_bookmarks_enabled, widget_poe2_enabled=$widget_poe2_enabled, published_article_max_age_days=$published_article_max_age_days, raw_item_max_age_days=$raw_item_max_age_days, storage_cap_enabled=$storage_cap_enabled, storage_cap_value=$storage_cap_value, storage_cap_unit=$storage_cap_unit, weather_location_name=$weather_location_name, weather_latitude=$weather_latitude, weather_longitude=$weather_longitude, @@ -93,6 +102,10 @@ export function updateSettings(patch: Partial): GlobalSettings { $nitter_media_mode: merged.nitterMediaMode, $fxtwitter_base_url: merged.fxtwitterBaseUrl, $telegram_media_mode: merged.telegramMediaMode, + $widget_weather_enabled: merged.widgets.weather ? 1 : 0, + $widget_stocks_enabled: merged.widgets.stocks ? 1 : 0, + $widget_bookmarks_enabled: merged.widgets.bookmarks ? 1 : 0, + $widget_poe2_enabled: merged.widgets.poe2 ? 1 : 0, $published_article_max_age_days: merged.retention.publishedArticleMaxAgeDays, $raw_item_max_age_days: merged.retention.rawItemMaxAgeDays, $storage_cap_enabled: merged.retention.storageCapEnabled ? 1 : 0, diff --git a/backend/src/storage/db/types.ts b/backend/src/storage/db/types.ts index 5717ad8..9feb5f2 100644 --- a/backend/src/storage/db/types.ts +++ b/backend/src/storage/db/types.ts @@ -280,6 +280,13 @@ export interface GlobalSettings { fxtwitterBaseUrl: string; /** How Telegram message media (attached photos/videos, channel avatars) is served — see pipeline/publish.ts's resolveTelegramMedia. No "direct" option: Telegram has no public hotlinkable media URL, bytes only come from the authenticated MTProto session. */ telegramMediaMode: 'self-host' | 'proxy'; + /** Per-widget sidebar visibility — see admin/settings' consolidated "Widgets" tab. Each widget keeps polling/config regardless (disabling doesn't pause its poller), this only gates whether GET /api/widgets tells the sidebar to render it. */ + widgets: { + weather: boolean; + stocks: boolean; + bookmarks: boolean; + poe2: boolean; + }; retention: { publishedArticleMaxAgeDays: number | null; rawItemMaxAgeDays: number | null; diff --git a/frontend/src/lib/adminTypes.ts b/frontend/src/lib/adminTypes.ts index 6550513..2795d58 100644 --- a/frontend/src/lib/adminTypes.ts +++ b/frontend/src/lib/adminTypes.ts @@ -118,6 +118,13 @@ export interface AdminPoe2Settings { updatedAt: string | null; } +export interface AdminWidgetsEnabled { + weather: boolean; + stocks: boolean; + bookmarks: boolean; + poe2: boolean; +} + export interface AdminSettings { mergeStrictness: 1 | 2 | 3 | 4 | 5; defaultPollIntervalMinutes: number; @@ -132,6 +139,7 @@ export interface AdminSettings { nitterMediaMode: 'self-host' | 'proxy' | 'direct'; fxtwitterBaseUrl: string; telegramMediaMode: 'self-host' | 'proxy'; + widgets: AdminWidgetsEnabled; retention: RetentionSettings; categoryPriority: CategoryPriority[]; weather: AdminWeatherSettings; diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index e0afe7d..dba92e5 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -1,5 +1,5 @@ import { getBackendUrl } from './config'; -import type { MergedArticle, Tag, TrackedEventPublic, Category, Weather, StockTicker, Bookmark, Poe2Data } from './types'; +import type { MergedArticle, Tag, TrackedEventPublic, Category, Weather, StockTicker, Bookmark, Poe2Data, WidgetsEnabled } from './types'; async function get(path: string, fetchFn: typeof fetch = fetch): Promise { // credentials: 'include' so the private-access cookie (see lib/privateAccess.ts) @@ -57,3 +57,7 @@ export function getBookmarks(fetchFn?: typeof fetch): Promise { export function getPoe2(fetchFn?: typeof fetch): Promise { return get('/api/poe2', fetchFn); } + +export function getWidgetsEnabled(fetchFn?: typeof fetch): Promise { + return get('/api/widgets', fetchFn); +} diff --git a/frontend/src/lib/components/admin/WidgetSection.svelte b/frontend/src/lib/components/admin/WidgetSection.svelte new file mode 100644 index 0000000..ca697a3 --- /dev/null +++ b/frontend/src/lib/components/admin/WidgetSection.svelte @@ -0,0 +1,103 @@ + + +
+
+ + +
+ {#if expanded} +
+ {@render children()} +
+ {/if} +
+ + diff --git a/frontend/src/lib/components/admin/WidgetsTab.svelte b/frontend/src/lib/components/admin/WidgetsTab.svelte new file mode 100644 index 0000000..ffeeb42 --- /dev/null +++ b/frontend/src/lib/components/admin/WidgetsTab.svelte @@ -0,0 +1,59 @@ + + +

+ Each widget can be shown or hidden from the sidebar independently. Hiding one only affects + whether it's visible on the site — its own settings and data below keep working either way. +

+ + toggle('weather')}> + + + + toggle('stocks')}> + + + + toggle('bookmarks')}> + + + + toggle('poe2')}> + + + + diff --git a/frontend/src/lib/components/sidebar/Sidebar.svelte b/frontend/src/lib/components/sidebar/Sidebar.svelte index 250d620..c64a1da 100644 --- a/frontend/src/lib/components/sidebar/Sidebar.svelte +++ b/frontend/src/lib/components/sidebar/Sidebar.svelte @@ -1,12 +1,24 @@