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:
@@ -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);
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user