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.
This commit is contained in:
Claude
2026-07-26 17:09:26 +00:00
parent 24bf5afb07
commit e6ac6cd061
13 changed files with 251 additions and 30 deletions
+5
View File
@@ -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);
+10
View File
@@ -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.
+14 -1
View File
@@ -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>): 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>): 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>): 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,
+7
View File
@@ -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;