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);
|
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.
|
// Sidebar widgets — see WeatherTab/StocksTab/BookmarksTab in the admin panel.
|
||||||
app.get('/api/weather', async () => settingsDb.getSettings().weather);
|
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
|
nitter_media_mode TEXT NOT NULL DEFAULT 'proxy', -- self-host | proxy | direct
|
||||||
fxtwitter_base_url TEXT NOT NULL DEFAULT 'https://api.fxtwitter.com',
|
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)
|
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_location_name TEXT,
|
||||||
weather_latitude REAL,
|
weather_latitude REAL,
|
||||||
weather_longitude 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_league_name TEXT');
|
||||||
db.exec('ALTER TABLE global_settings ADD COLUMN poe2_updated_at 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
|
// 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.
|
// 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,
|
nitterMediaMode: row.nitter_media_mode,
|
||||||
fxtwitterBaseUrl: row.fxtwitter_base_url,
|
fxtwitterBaseUrl: row.fxtwitter_base_url,
|
||||||
telegramMediaMode: row.telegram_media_mode,
|
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: {
|
retention: {
|
||||||
publishedArticleMaxAgeDays: row.published_article_max_age_days,
|
publishedArticleMaxAgeDays: row.published_article_max_age_days,
|
||||||
rawItemMaxAgeDays: row.raw_item_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 ?? {}) },
|
retention: { ...current.retention, ...(patch.retention ?? {}) },
|
||||||
selectedModels: { ...current.selectedModels, ...(patch.selectedModels ?? {}) },
|
selectedModels: { ...current.selectedModels, ...(patch.selectedModels ?? {}) },
|
||||||
weather: { ...current.weather, ...(patch.weather ?? {}) },
|
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
|
// Named params (rather than positional `?`) so this list can be reordered or
|
||||||
// extended without the column list and the bound-values list silently drifting
|
// 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,
|
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,
|
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,
|
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,
|
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,
|
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,
|
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,
|
$nitter_media_mode: merged.nitterMediaMode,
|
||||||
$fxtwitter_base_url: merged.fxtwitterBaseUrl,
|
$fxtwitter_base_url: merged.fxtwitterBaseUrl,
|
||||||
$telegram_media_mode: merged.telegramMediaMode,
|
$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,
|
$published_article_max_age_days: merged.retention.publishedArticleMaxAgeDays,
|
||||||
$raw_item_max_age_days: merged.retention.rawItemMaxAgeDays,
|
$raw_item_max_age_days: merged.retention.rawItemMaxAgeDays,
|
||||||
$storage_cap_enabled: merged.retention.storageCapEnabled ? 1 : 0,
|
$storage_cap_enabled: merged.retention.storageCapEnabled ? 1 : 0,
|
||||||
|
|||||||
@@ -280,6 +280,13 @@ export interface GlobalSettings {
|
|||||||
fxtwitterBaseUrl: string;
|
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. */
|
/** 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';
|
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: {
|
retention: {
|
||||||
publishedArticleMaxAgeDays: number | null;
|
publishedArticleMaxAgeDays: number | null;
|
||||||
rawItemMaxAgeDays: number | null;
|
rawItemMaxAgeDays: number | null;
|
||||||
|
|||||||
@@ -118,6 +118,13 @@ export interface AdminPoe2Settings {
|
|||||||
updatedAt: string | null;
|
updatedAt: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AdminWidgetsEnabled {
|
||||||
|
weather: boolean;
|
||||||
|
stocks: boolean;
|
||||||
|
bookmarks: boolean;
|
||||||
|
poe2: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AdminSettings {
|
export interface AdminSettings {
|
||||||
mergeStrictness: 1 | 2 | 3 | 4 | 5;
|
mergeStrictness: 1 | 2 | 3 | 4 | 5;
|
||||||
defaultPollIntervalMinutes: number;
|
defaultPollIntervalMinutes: number;
|
||||||
@@ -132,6 +139,7 @@ export interface AdminSettings {
|
|||||||
nitterMediaMode: 'self-host' | 'proxy' | 'direct';
|
nitterMediaMode: 'self-host' | 'proxy' | 'direct';
|
||||||
fxtwitterBaseUrl: string;
|
fxtwitterBaseUrl: string;
|
||||||
telegramMediaMode: 'self-host' | 'proxy';
|
telegramMediaMode: 'self-host' | 'proxy';
|
||||||
|
widgets: AdminWidgetsEnabled;
|
||||||
retention: RetentionSettings;
|
retention: RetentionSettings;
|
||||||
categoryPriority: CategoryPriority[];
|
categoryPriority: CategoryPriority[];
|
||||||
weather: AdminWeatherSettings;
|
weather: AdminWeatherSettings;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { getBackendUrl } from './config';
|
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<T>(path: string, fetchFn: typeof fetch = fetch): Promise<T> {
|
async function get<T>(path: string, fetchFn: typeof fetch = fetch): Promise<T> {
|
||||||
// credentials: 'include' so the private-access cookie (see lib/privateAccess.ts)
|
// credentials: 'include' so the private-access cookie (see lib/privateAccess.ts)
|
||||||
@@ -57,3 +57,7 @@ export function getBookmarks(fetchFn?: typeof fetch): Promise<Bookmark[]> {
|
|||||||
export function getPoe2(fetchFn?: typeof fetch): Promise<Poe2Data> {
|
export function getPoe2(fetchFn?: typeof fetch): Promise<Poe2Data> {
|
||||||
return get<Poe2Data>('/api/poe2', fetchFn);
|
return get<Poe2Data>('/api/poe2', fetchFn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getWidgetsEnabled(fetchFn?: typeof fetch): Promise<WidgetsEnabled> {
|
||||||
|
return get<WidgetsEnabled>('/api/widgets', fetchFn);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
|
// Minimized by default — Weather/Stocks/Bookmarks/PoE2 stacked at full height would
|
||||||
|
// make the consolidated "Widgets" tab unwieldy as more get added over time. Enabled
|
||||||
|
// state is independent of expanded state: disabling a widget only hides it from the
|
||||||
|
// sidebar (see Sidebar.svelte's widgetsEnabled gate), it doesn't stop the admin from
|
||||||
|
// expanding this section to keep configuring it.
|
||||||
|
let {
|
||||||
|
title,
|
||||||
|
enabled,
|
||||||
|
onToggle,
|
||||||
|
children
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
enabled: boolean;
|
||||||
|
onToggle: () => void;
|
||||||
|
children: Snippet;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let expanded = $state(false);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-head">
|
||||||
|
<button class="head-btn" onclick={() => (expanded = !expanded)} aria-expanded={expanded}>
|
||||||
|
<span class="chevron" class:open={expanded}>▸</span>
|
||||||
|
<span class="title">{title}</span>
|
||||||
|
{#if !enabled}<span class="disabled-tag">Hidden</span>{/if}
|
||||||
|
</button>
|
||||||
|
<label class="enable-toggle" title={enabled ? 'Hide from sidebar' : 'Show in sidebar'}>
|
||||||
|
<input type="checkbox" checked={enabled} onchange={onToggle} />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{#if expanded}
|
||||||
|
<div class="section-body">
|
||||||
|
{@render children()}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.section {
|
||||||
|
background: var(--surface-1);
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.section-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
}
|
||||||
|
.head-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-primary);
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.chevron {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
transition: transform 0.15s ease;
|
||||||
|
}
|
||||||
|
.chevron.open {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
.disabled-tag {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--text-muted);
|
||||||
|
border: 0.5px solid var(--border);
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
}
|
||||||
|
.enable-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.enable-toggle input {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
.section-body {
|
||||||
|
padding: 14px;
|
||||||
|
padding-top: 0;
|
||||||
|
border-top: 0.5px solid var(--border);
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.section-body > :global(*:first-child) {
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { AdminSettings, AdminStockTicker, AdminBookmark, AdminPoe2Entry } from '$lib/adminTypes';
|
||||||
|
import { updateSettings } from '$lib/adminApi';
|
||||||
|
import WidgetSection from './WidgetSection.svelte';
|
||||||
|
import WeatherTab from './WeatherTab.svelte';
|
||||||
|
import StocksTab from './StocksTab.svelte';
|
||||||
|
import BookmarksTab from './BookmarksTab.svelte';
|
||||||
|
import Poe2Tab from './Poe2Tab.svelte';
|
||||||
|
|
||||||
|
let {
|
||||||
|
settings,
|
||||||
|
stockTickers,
|
||||||
|
bookmarks,
|
||||||
|
poe2Watchlist
|
||||||
|
}: {
|
||||||
|
settings: AdminSettings;
|
||||||
|
stockTickers: AdminStockTicker[];
|
||||||
|
bookmarks: AdminBookmark[];
|
||||||
|
poe2Watchlist: AdminPoe2Entry[];
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
// Local copy so each checkbox flips immediately — same idiom as BookmarksTab's
|
||||||
|
// per-row "Private" toggle, just for widget visibility instead.
|
||||||
|
let widgets = $state({ ...settings.widgets });
|
||||||
|
|
||||||
|
async function toggle(key: keyof typeof widgets) {
|
||||||
|
widgets[key] = !widgets[key];
|
||||||
|
await updateSettings({ widgets });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<p class="hint">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<WidgetSection title="Weather" enabled={widgets.weather} onToggle={() => toggle('weather')}>
|
||||||
|
<WeatherTab {settings} />
|
||||||
|
</WidgetSection>
|
||||||
|
|
||||||
|
<WidgetSection title="Stocks" enabled={widgets.stocks} onToggle={() => toggle('stocks')}>
|
||||||
|
<StocksTab tickers={stockTickers} />
|
||||||
|
</WidgetSection>
|
||||||
|
|
||||||
|
<WidgetSection title="Bookmarks" enabled={widgets.bookmarks} onToggle={() => toggle('bookmarks')}>
|
||||||
|
<BookmarksTab {bookmarks} />
|
||||||
|
</WidgetSection>
|
||||||
|
|
||||||
|
<WidgetSection title="PoE2" enabled={widgets.poe2} onToggle={() => toggle('poe2')}>
|
||||||
|
<Poe2Tab {settings} watchlist={poe2Watchlist} />
|
||||||
|
</WidgetSection>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin: 0 0 14px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,12 +1,24 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { tick } from 'svelte';
|
import { tick } from 'svelte';
|
||||||
import type { Weather, StockTicker, Bookmark, Poe2Data } from '$lib/types';
|
import type { Weather, StockTicker, Bookmark, Poe2Data, WidgetsEnabled } from '$lib/types';
|
||||||
import WeatherWidget from './WeatherWidget.svelte';
|
import WeatherWidget from './WeatherWidget.svelte';
|
||||||
import StocksWidget from './StocksWidget.svelte';
|
import StocksWidget from './StocksWidget.svelte';
|
||||||
import BookmarksWidget from './BookmarksWidget.svelte';
|
import BookmarksWidget from './BookmarksWidget.svelte';
|
||||||
import Poe2Widget from './Poe2Widget.svelte';
|
import Poe2Widget from './Poe2Widget.svelte';
|
||||||
|
|
||||||
let { weather, stocks, bookmarks, poe2 }: { weather: Weather; stocks: StockTicker[]; bookmarks: Bookmark[]; poe2: Poe2Data } = $props();
|
let {
|
||||||
|
weather,
|
||||||
|
stocks,
|
||||||
|
bookmarks,
|
||||||
|
poe2,
|
||||||
|
widgetsEnabled
|
||||||
|
}: {
|
||||||
|
weather: Weather;
|
||||||
|
stocks: StockTicker[];
|
||||||
|
bookmarks: Bookmark[];
|
||||||
|
poe2: Poe2Data;
|
||||||
|
widgetsEnabled: WidgetsEnabled;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
// Weather + Stocks + PoE2 + Bookmarks stacked can be taller than the viewport. Plain
|
// Weather + Stocks + PoE2 + Bookmarks stacked can be taller than the viewport. Plain
|
||||||
// `position: sticky` alone can only pin a box at a constant offset — it can't reveal
|
// `position: sticky` alone can only pin a box at a constant offset — it can't reveal
|
||||||
@@ -55,11 +67,13 @@
|
|||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
// Widget data changing the sidebar's natural height needs a remeasure, not just a
|
// Widget data changing the sidebar's natural height needs a remeasure, not just a
|
||||||
// scroll-position update.
|
// scroll-position update. widgetsEnabled changes it too — a disabled widget is
|
||||||
|
// removed from the flow entirely, not just emptied.
|
||||||
void weather;
|
void weather;
|
||||||
void stocks;
|
void stocks;
|
||||||
void bookmarks;
|
void bookmarks;
|
||||||
void poe2;
|
void poe2;
|
||||||
|
void widgetsEnabled;
|
||||||
remeasure();
|
remeasure();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -76,10 +90,10 @@
|
|||||||
<div class="sidebar-track" bind:this={trackEl} style:height="{trackHeight}px">
|
<div class="sidebar-track" bind:this={trackEl} style:height="{trackHeight}px">
|
||||||
<aside class="sidebar-viewport" style:height="{viewportHeight}px">
|
<aside class="sidebar-viewport" style:height="{viewportHeight}px">
|
||||||
<div class="sidebar-content" bind:this={contentEl} style:transform="translateY(-{progress}px)">
|
<div class="sidebar-content" bind:this={contentEl} style:transform="translateY(-{progress}px)">
|
||||||
<WeatherWidget {weather} />
|
{#if widgetsEnabled.weather}<WeatherWidget {weather} />{/if}
|
||||||
<StocksWidget {stocks} />
|
{#if widgetsEnabled.stocks}<StocksWidget {stocks} />{/if}
|
||||||
<Poe2Widget {poe2} />
|
{#if widgetsEnabled.poe2}<Poe2Widget {poe2} />{/if}
|
||||||
<BookmarksWidget {bookmarks} />
|
{#if widgetsEnabled.bookmarks}<BookmarksWidget {bookmarks} />{/if}
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -171,3 +171,11 @@ export interface Poe2Data {
|
|||||||
updatedAt: string | null;
|
updatedAt: string | null;
|
||||||
entries: Poe2WatchlistEntry[];
|
entries: Poe2WatchlistEntry[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Per-widget sidebar visibility, admin-toggled from the consolidated "Widgets" tab. */
|
||||||
|
export interface WidgetsEnabled {
|
||||||
|
weather: boolean;
|
||||||
|
stocks: boolean;
|
||||||
|
bookmarks: boolean;
|
||||||
|
poe2: boolean;
|
||||||
|
}
|
||||||
|
|||||||
@@ -129,7 +129,7 @@
|
|||||||
{@render children()}
|
{@render children()}
|
||||||
</div>
|
</div>
|
||||||
{#if showSidebar}
|
{#if showSidebar}
|
||||||
<Sidebar weather={data.weather} stocks={data.stocks} bookmarks={data.bookmarks} poe2={data.poe2} />
|
<Sidebar weather={data.weather} stocks={data.stocks} bookmarks={data.bookmarks} poe2={data.poe2} widgetsEnabled={data.widgetsEnabled} />
|
||||||
{/if}
|
{/if}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { LayoutLoad } from './$types';
|
import type { LayoutLoad } from './$types';
|
||||||
import { getCategories, getEvents, getWeather, getStocks, getBookmarks, getPoe2 } from '$lib/api';
|
import { getCategories, getEvents, getWeather, getStocks, getBookmarks, getPoe2, getWidgetsEnabled } from '$lib/api';
|
||||||
import { getPrivateAccessStatus } from '$lib/privateAccess';
|
import { getPrivateAccessStatus } from '$lib/privateAccess';
|
||||||
|
|
||||||
// Named so the layout can be re-fetched on its own (see +layout.svelte's periodic
|
// Named so the layout can be re-fetched on its own (see +layout.svelte's periodic
|
||||||
@@ -7,14 +7,15 @@ import { getPrivateAccessStatus } from '$lib/privateAccess';
|
|||||||
// own pagination state, which a blanket invalidateAll() would reset every refresh.
|
// own pagination state, which a blanket invalidateAll() would reset every refresh.
|
||||||
export const load: LayoutLoad = async ({ fetch, data, depends }) => {
|
export const load: LayoutLoad = async ({ fetch, data, depends }) => {
|
||||||
depends('app:sidebar');
|
depends('app:sidebar');
|
||||||
const [categories, events, privateAccess, weather, stocks, bookmarks, poe2] = await Promise.all([
|
const [categories, events, privateAccess, weather, stocks, bookmarks, poe2, widgetsEnabled] = await Promise.all([
|
||||||
getCategories(fetch),
|
getCategories(fetch),
|
||||||
getEvents(fetch),
|
getEvents(fetch),
|
||||||
getPrivateAccessStatus(fetch),
|
getPrivateAccessStatus(fetch),
|
||||||
getWeather(fetch),
|
getWeather(fetch),
|
||||||
getStocks(fetch),
|
getStocks(fetch),
|
||||||
getBookmarks(fetch),
|
getBookmarks(fetch),
|
||||||
getPoe2(fetch)
|
getPoe2(fetch),
|
||||||
|
getWidgetsEnabled(fetch)
|
||||||
]);
|
]);
|
||||||
// Tracked events are a displayed category like any other (see MergeTab/EventsTab) —
|
// Tracked events are a displayed category like any other (see MergeTab/EventsTab) —
|
||||||
// only active ones show up as browsable, same as a paused/disabled category wouldn't.
|
// only active ones show up as browsable, same as a paused/disabled category wouldn't.
|
||||||
@@ -26,6 +27,7 @@ export const load: LayoutLoad = async ({ fetch, data, depends }) => {
|
|||||||
weather,
|
weather,
|
||||||
stocks,
|
stocks,
|
||||||
bookmarks,
|
bookmarks,
|
||||||
poe2
|
poe2,
|
||||||
|
widgetsEnabled
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,10 +5,7 @@
|
|||||||
import ModelsTab from '$lib/components/admin/ModelsTab.svelte';
|
import ModelsTab from '$lib/components/admin/ModelsTab.svelte';
|
||||||
import RetentionTab from '$lib/components/admin/RetentionTab.svelte';
|
import RetentionTab from '$lib/components/admin/RetentionTab.svelte';
|
||||||
import EventsTab from '$lib/components/admin/EventsTab.svelte';
|
import EventsTab from '$lib/components/admin/EventsTab.svelte';
|
||||||
import WeatherTab from '$lib/components/admin/WeatherTab.svelte';
|
import WidgetsTab from '$lib/components/admin/WidgetsTab.svelte';
|
||||||
import StocksTab from '$lib/components/admin/StocksTab.svelte';
|
|
||||||
import BookmarksTab from '$lib/components/admin/BookmarksTab.svelte';
|
|
||||||
import Poe2Tab from '$lib/components/admin/Poe2Tab.svelte';
|
|
||||||
import ConnectionsTab from '$lib/components/admin/ConnectionsTab.svelte';
|
import ConnectionsTab from '$lib/components/admin/ConnectionsTab.svelte';
|
||||||
import LogsTab from '$lib/components/admin/LogsTab.svelte';
|
import LogsTab from '$lib/components/admin/LogsTab.svelte';
|
||||||
|
|
||||||
@@ -20,10 +17,7 @@
|
|||||||
{ id: 'models', label: 'Models' },
|
{ id: 'models', label: 'Models' },
|
||||||
{ id: 'retention', label: 'Retention' },
|
{ id: 'retention', label: 'Retention' },
|
||||||
{ id: 'events', label: 'Tracked events' },
|
{ id: 'events', label: 'Tracked events' },
|
||||||
{ id: 'weather', label: 'Weather' },
|
{ id: 'widgets', label: 'Widgets' },
|
||||||
{ id: 'stocks', label: 'Stocks' },
|
|
||||||
{ id: 'bookmarks', label: 'Bookmarks' },
|
|
||||||
{ id: 'poe2', label: 'PoE2' },
|
|
||||||
{ id: 'connections', label: 'Connections' },
|
{ id: 'connections', label: 'Connections' },
|
||||||
{ id: 'logs', label: 'Logs' }
|
{ id: 'logs', label: 'Logs' }
|
||||||
];
|
];
|
||||||
@@ -53,14 +47,8 @@
|
|||||||
<RetentionTab settings={data.settings} />
|
<RetentionTab settings={data.settings} />
|
||||||
{:else if active === 'events'}
|
{:else if active === 'events'}
|
||||||
<EventsTab events={data.events} sources={data.sources} />
|
<EventsTab events={data.events} sources={data.sources} />
|
||||||
{:else if active === 'weather'}
|
{:else if active === 'widgets'}
|
||||||
<WeatherTab settings={data.settings} />
|
<WidgetsTab settings={data.settings} stockTickers={data.stockTickers} bookmarks={data.bookmarks} poe2Watchlist={data.poe2Watchlist} />
|
||||||
{:else if active === 'stocks'}
|
|
||||||
<StocksTab tickers={data.stockTickers} />
|
|
||||||
{:else if active === 'bookmarks'}
|
|
||||||
<BookmarksTab bookmarks={data.bookmarks} />
|
|
||||||
{:else if active === 'poe2'}
|
|
||||||
<Poe2Tab settings={data.settings} watchlist={data.poe2Watchlist} />
|
|
||||||
{:else if active === 'connections'}
|
{:else if active === 'connections'}
|
||||||
<ConnectionsTab settings={data.settings} aiStatus={data.aiStatus} telegramStatus={data.telegramStatus} />
|
<ConnectionsTab settings={data.settings} aiStatus={data.aiStatus} telegramStatus={data.telegramStatus} />
|
||||||
{:else if active === 'logs'}
|
{:else if active === 'logs'}
|
||||||
|
|||||||
Reference in New Issue
Block a user