From 9e13e95f353e2441eb67cf81ed1822addacd521a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 26 Jul 2026 19:05:24 +0000 Subject: [PATCH] Simplify Widgets tab: state button instead of checkbox, add drag-free up/down reordering Drops the checkbox/tooltip/hint-paragraph combo for a single click-to-toggle Active/Disabled badge (same pattern as EventsTab's Active/Paused). Widget order is now admin-sortable via up/down arrows and persisted as widgetOrder, with Sidebar.svelte rendering widgets in that exact sequence instead of a fixed hardcoded order. --- backend/src/api/public.ts | 11 ++- backend/src/storage/db/index.ts | 6 ++ backend/src/storage/db/settings.ts | 3 + backend/src/storage/db/types.ts | 4 +- frontend/src/lib/adminTypes.ts | 1 + .../lib/components/admin/WidgetSection.svelte | 74 +++++++++---------- .../lib/components/admin/WidgetsTab.svelte | 74 ++++++++++--------- .../src/lib/components/sidebar/Sidebar.svelte | 15 +++- frontend/src/lib/types.ts | 3 +- 9 files changed, 109 insertions(+), 82 deletions(-) diff --git a/backend/src/api/public.ts b/backend/src/api/public.ts index b5c4cf9..e2a0898 100644 --- a/backend/src/api/public.ts +++ b/backend/src/api/public.ts @@ -62,10 +62,13 @@ 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); + // Per-widget enable flags + display order — see the admin panel's consolidated + // "Widgets" tab. Weather/Stocks/PoE2's backend pollers are also gated on these + // flags (see scheduler.ts); Sidebar.svelte renders in exactly this order. + app.get('/api/widgets', async () => { + const { widgets, widgetOrder } = settingsDb.getSettings(); + return { ...widgets, order: widgetOrder }; + }); // 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 c8b182f..7be16c5 100644 --- a/backend/src/storage/db/index.ts +++ b/backend/src/storage/db/index.ts @@ -188,6 +188,7 @@ export function migrate() { widget_stocks_enabled INTEGER NOT NULL DEFAULT 1, widget_bookmarks_enabled INTEGER NOT NULL DEFAULT 1, widget_poe2_enabled INTEGER NOT NULL DEFAULT 1, + widget_order TEXT NOT NULL DEFAULT '["weather","stocks","poe2","bookmarks"]', -- JSON array, admin-sortable via the Widgets tab weather_location_name TEXT, weather_latitude REAL, weather_longitude REAL, @@ -363,6 +364,11 @@ export function migrate() { 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'); } + if (!hasColumn('global_settings', 'widget_order')) { + db.exec( + `ALTER TABLE global_settings ADD COLUMN widget_order TEXT NOT NULL DEFAULT '["weather","stocks","poe2","bookmarks"]'` + ); + } // 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 a7a4b92..9b0469c 100644 --- a/backend/src/storage/db/settings.ts +++ b/backend/src/storage/db/settings.ts @@ -21,6 +21,7 @@ function rowToSettings(row: any): GlobalSettings { bookmarks: !!row.widget_bookmarks_enabled, poe2: !!row.widget_poe2_enabled }, + widgetOrder: JSON.parse(row.widget_order), retention: { publishedArticleMaxAgeDays: row.published_article_max_age_days, rawItemMaxAgeDays: row.raw_item_max_age_days, @@ -79,6 +80,7 @@ export function updateSettings(patch: Partial): GlobalSettings { 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, + widget_order=$widget_order, 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, @@ -104,6 +106,7 @@ export function updateSettings(patch: Partial): GlobalSettings { $widget_stocks_enabled: merged.widgets.stocks ? 1 : 0, $widget_bookmarks_enabled: merged.widgets.bookmarks ? 1 : 0, $widget_poe2_enabled: merged.widgets.poe2 ? 1 : 0, + $widget_order: JSON.stringify(merged.widgetOrder), $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 572fe81..406db07 100644 --- a/backend/src/storage/db/types.ts +++ b/backend/src/storage/db/types.ts @@ -281,13 +281,15 @@ 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. */ + /** Per-widget enable flags — see admin/settings' consolidated "Widgets" tab. Weather/Stocks/PoE2's backend pollers (scheduler.ts) are gated on these too, not just sidebar visibility; Bookmarks has no poller so its flag only affects the sidebar. */ widgets: { weather: boolean; stocks: boolean; bookmarks: boolean; poe2: boolean; }; + /** Sidebar widget display order, admin-sortable via the Widgets tab's up/down arrows — mirrored exactly by Sidebar.svelte. */ + widgetOrder: ('weather' | 'stocks' | 'bookmarks' | 'poe2')[]; retention: { publishedArticleMaxAgeDays: number | null; rawItemMaxAgeDays: number | null; diff --git a/frontend/src/lib/adminTypes.ts b/frontend/src/lib/adminTypes.ts index ba1b649..d0a1063 100644 --- a/frontend/src/lib/adminTypes.ts +++ b/frontend/src/lib/adminTypes.ts @@ -139,6 +139,7 @@ export interface AdminSettings { fxtwitterBaseUrl: string; telegramMediaMode: 'self-host' | 'proxy'; widgets: AdminWidgetsEnabled; + widgetOrder: ('weather' | 'stocks' | 'bookmarks' | 'poe2')[]; retention: RetentionSettings; categoryPriority: CategoryPriority[]; weather: AdminWeatherSettings; diff --git a/frontend/src/lib/components/admin/WidgetSection.svelte b/frontend/src/lib/components/admin/WidgetSection.svelte index 63b39ec..2f237cd 100644 --- a/frontend/src/lib/components/admin/WidgetSection.svelte +++ b/frontend/src/lib/components/admin/WidgetSection.svelte @@ -1,26 +1,23 @@ -

- Each widget can be enabled or disabled independently. Disabling Weather, Stocks, or PoE2 - hides it from the sidebar and stops its backend poller — no more outbound - requests until it's turned back on, at which point it polls again immediately rather than - waiting out its normal schedule. You can still edit a disabled widget's settings below; they - just won't fetch anything new until it's re-enabled. Bookmarks has no poller, so its toggle - only affects sidebar visibility. -

- - toggle('weather')}> - - - - toggle('stocks')}> - - - - toggle('bookmarks')} hasBackendPoller={false}> - - - - toggle('poe2')}> - - - - +{#each widgetOrder as key, i (key)} + toggle(key)} + canMoveUp={i > 0} + canMoveDown={i < widgetOrder.length - 1} + onMoveUp={() => move(i, -1)} + onMoveDown={() => move(i, 1)} + > + {#if key === 'weather'} + + {:else if key === 'stocks'} + + {:else if key === 'bookmarks'} + + {:else if key === 'poe2'} + + {/if} + +{/each} diff --git a/frontend/src/lib/components/sidebar/Sidebar.svelte b/frontend/src/lib/components/sidebar/Sidebar.svelte index c64a1da..8dcda03 100644 --- a/frontend/src/lib/components/sidebar/Sidebar.svelte +++ b/frontend/src/lib/components/sidebar/Sidebar.svelte @@ -90,10 +90,17 @@ diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index 400fe59..7a9d9b1 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -173,10 +173,11 @@ export interface Poe2Data { entries: Poe2WatchlistEntry[]; } -/** Per-widget sidebar visibility, admin-toggled from the consolidated "Widgets" tab. */ +/** Per-widget sidebar visibility + display order, admin-set from the consolidated "Widgets" tab. */ export interface WidgetsEnabled { weather: boolean; stocks: boolean; bookmarks: boolean; poe2: boolean; + order: ('weather' | 'stocks' | 'bookmarks' | 'poe2')[]; }