Add 1/2/3-column layout option to the Bookmarks widget
The bookmarks list can now render as 1 (default), 2, or 3 columns instead of always being a single-column list, admin-configurable via a pill picker on the Widgets tab. Column count lives in widget_kv (same idiom as weather's config) and rides along in the public GET /api/widget/bookmarks response so the sidebar picks it up without a separate request.
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { randomUUID } from 'node:crypto';
|
import { randomUUID } from 'node:crypto';
|
||||||
import { db } from '../../storage/db/index.js';
|
import { db } from '../../storage/db/index.js';
|
||||||
|
import { getKv, setKv } from '../../storage/db/widgetKv.js';
|
||||||
import type { Bookmark } from '../../storage/db/types.js';
|
import type { Bookmark } from '../../storage/db/types.js';
|
||||||
|
|
||||||
function rowToBookmark(row: any): Bookmark {
|
function rowToBookmark(row: any): Bookmark {
|
||||||
@@ -44,3 +45,14 @@ export function updateBookmark(id: string, patch: { name?: string; url?: string;
|
|||||||
export function deleteBookmark(id: string) {
|
export function deleteBookmark(id: string) {
|
||||||
db.prepare('DELETE FROM widget_bookmarks_items WHERE id = ?').run(id);
|
db.prepare('DELETE FROM widget_bookmarks_items WHERE id = ?').run(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// How many columns the sidebar/admin panel lays the bookmark list out in — stored in
|
||||||
|
// widget_kv rather than a bespoke table since it's a single scalar, same idiom as weather's
|
||||||
|
// config (see widgets/weather/db.ts).
|
||||||
|
export function getColumns(): 1 | 2 | 3 {
|
||||||
|
return getKv<{ columns: 1 | 2 | 3 }>('bookmarks', 'config')?.columns ?? 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setColumns(columns: 1 | 2 | 3) {
|
||||||
|
setKv('bookmarks', 'config', { columns });
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,14 +31,25 @@ export const bookmarksPlugin: WidgetPlugin = {
|
|||||||
registerPublicRoutes(app) {
|
registerPublicRoutes(app) {
|
||||||
app.get('/api/widget/bookmarks', async (req) => {
|
app.get('/api/widget/bookmarks', async (req) => {
|
||||||
const bookmarks = bookmarksDb.listBookmarks();
|
const bookmarks = bookmarksDb.listBookmarks();
|
||||||
if (hasPrivateAccess(req)) return bookmarks;
|
const items = hasPrivateAccess(req) ? bookmarks : bookmarks.filter((b) => !b.isPrivate);
|
||||||
return bookmarks.filter((b) => !b.isPrivate);
|
return { items, columns: bookmarksDb.getColumns() };
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
registerAdminRoutes(app) {
|
registerAdminRoutes(app) {
|
||||||
app.get('/api/admin/widget/bookmarks', async () => bookmarksDb.listBookmarks());
|
app.get('/api/admin/widget/bookmarks', async () => bookmarksDb.listBookmarks());
|
||||||
|
|
||||||
|
app.get('/api/admin/widget/bookmarks/config', async () => ({ columns: bookmarksDb.getColumns() }));
|
||||||
|
|
||||||
|
app.patch('/api/admin/widget/bookmarks/config', async (req, reply) => {
|
||||||
|
const { columns } = req.body as { columns?: number };
|
||||||
|
if (columns !== 1 && columns !== 2 && columns !== 3) {
|
||||||
|
return reply.code(400).send({ error: 'columns must be 1, 2, or 3' });
|
||||||
|
}
|
||||||
|
bookmarksDb.setColumns(columns);
|
||||||
|
return { columns };
|
||||||
|
});
|
||||||
|
|
||||||
app.post('/api/admin/widget/bookmarks', async (req, reply) => {
|
app.post('/api/admin/widget/bookmarks', async (req, reply) => {
|
||||||
const { name, url, isPrivate } = req.body as { name?: string; url?: string; isPrivate?: boolean };
|
const { name, url, isPrivate } = req.body as { name?: string; url?: string; isPrivate?: boolean };
|
||||||
if (!name || !name.trim() || !url || !url.trim()) {
|
if (!name || !name.trim() || !url || !url.trim()) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import type {
|
|||||||
GeocodeResult,
|
GeocodeResult,
|
||||||
AdminStockTicker,
|
AdminStockTicker,
|
||||||
AdminBookmark,
|
AdminBookmark,
|
||||||
|
AdminBookmarksConfig,
|
||||||
Poe2BrowseEntry,
|
Poe2BrowseEntry,
|
||||||
AdminPoe2Entry,
|
AdminPoe2Entry,
|
||||||
AdminWeatherSettings,
|
AdminWeatherSettings,
|
||||||
@@ -239,6 +240,16 @@ export const updateBookmark = (id: string, patch: { name?: string; url?: string;
|
|||||||
export const deleteBookmark = (id: string, fetchFn?: typeof fetch) =>
|
export const deleteBookmark = (id: string, fetchFn?: typeof fetch) =>
|
||||||
request<void>(`/api/admin/widget/bookmarks/${id}`, { method: 'DELETE' }, fetchFn);
|
request<void>(`/api/admin/widget/bookmarks/${id}`, { method: 'DELETE' }, fetchFn);
|
||||||
|
|
||||||
|
export const getBookmarksConfig = (fetchFn?: typeof fetch) =>
|
||||||
|
request<AdminBookmarksConfig>('/api/admin/widget/bookmarks/config', {}, fetchFn);
|
||||||
|
|
||||||
|
export const updateBookmarksConfig = (columns: 1 | 2 | 3, fetchFn?: typeof fetch) =>
|
||||||
|
request<AdminBookmarksConfig>(
|
||||||
|
'/api/admin/widget/bookmarks/config',
|
||||||
|
{ method: 'PATCH', body: JSON.stringify({ columns }) },
|
||||||
|
fetchFn
|
||||||
|
);
|
||||||
|
|
||||||
// PoE2 — league is always auto-detected, never admin-set (see widgets/poe2/poll.ts).
|
// PoE2 — league is always auto-detected, never admin-set (see widgets/poe2/poll.ts).
|
||||||
export const browsePoe2Currencies = (fetchFn?: typeof fetch) =>
|
export const browsePoe2Currencies = (fetchFn?: typeof fetch) =>
|
||||||
request<Poe2BrowseEntry[]>('/api/admin/widget/poe2/browse', {}, fetchFn);
|
request<Poe2BrowseEntry[]>('/api/admin/widget/poe2/browse', {}, fetchFn);
|
||||||
|
|||||||
@@ -95,6 +95,11 @@ export interface AdminBookmark {
|
|||||||
isPrivate: boolean;
|
isPrivate: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Response from GET/PATCH /api/admin/widget/bookmarks/config — how many columns the sidebar/widget layout uses. */
|
||||||
|
export interface AdminBookmarksConfig {
|
||||||
|
columns: 1 | 2 | 3;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Poe2BrowseEntry {
|
export interface Poe2BrowseEntry {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { getBackendUrl } from './config';
|
import { getBackendUrl } from './config';
|
||||||
import type { MergedArticle, Tag, TrackedEventPublic, Category, Weather, StockTicker, Bookmark, Poe2Data, WidgetsEnabled } from './types';
|
import type { MergedArticle, Tag, TrackedEventPublic, Category, Weather, StockTicker, BookmarksFeed, 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)
|
||||||
@@ -54,8 +54,8 @@ export function getStocks(fetchFn?: typeof fetch): Promise<StockTicker[]> {
|
|||||||
return get<StockTicker[]>('/api/widget/stocks', fetchFn);
|
return get<StockTicker[]>('/api/widget/stocks', fetchFn);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getBookmarks(fetchFn?: typeof fetch): Promise<Bookmark[]> {
|
export function getBookmarks(fetchFn?: typeof fetch): Promise<BookmarksFeed> {
|
||||||
return get<Bookmark[]>('/api/widget/bookmarks', fetchFn);
|
return get<BookmarksFeed>('/api/widget/bookmarks', fetchFn);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPoe2(fetchFn?: typeof fetch): Promise<Poe2Data> {
|
export function getPoe2(fetchFn?: typeof fetch): Promise<Poe2Data> {
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { AdminBookmark } from '$lib/adminTypes';
|
import type { AdminBookmark, AdminBookmarksConfig } from '$lib/adminTypes';
|
||||||
import { addBookmark, updateBookmark, deleteBookmark } from '$lib/adminApi';
|
import { addBookmark, updateBookmark, deleteBookmark, updateBookmarksConfig } from '$lib/adminApi';
|
||||||
|
|
||||||
let { bookmarks: initial }: { bookmarks: AdminBookmark[] } = $props();
|
let { bookmarks: initial, config }: { bookmarks: AdminBookmark[]; config: AdminBookmarksConfig } = $props();
|
||||||
let bookmarks = $state([...initial]);
|
let bookmarks = $state([...initial]);
|
||||||
|
let columns = $state(config.columns);
|
||||||
let showAdd = $state(false);
|
let showAdd = $state(false);
|
||||||
let newBookmark = $state({ name: '', url: '', isPrivate: false });
|
let newBookmark = $state({ name: '', url: '', isPrivate: false });
|
||||||
|
|
||||||
|
async function setColumns(n: 1 | 2 | 3) {
|
||||||
|
columns = n;
|
||||||
|
await updateBookmarksConfig(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnOptions: (1 | 2 | 3)[] = [1, 2, 3];
|
||||||
|
|
||||||
let editingId = $state<string | null>(null);
|
let editingId = $state<string | null>(null);
|
||||||
let editForm = $state({ name: '', url: '' });
|
let editForm = $state({ name: '', url: '' });
|
||||||
|
|
||||||
@@ -47,6 +55,14 @@
|
|||||||
|
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<span class="count">{bookmarks.length} bookmarks</span>
|
<span class="count">{bookmarks.length} bookmarks</span>
|
||||||
|
<div class="columns-picker">
|
||||||
|
<span class="field-label">Columns</span>
|
||||||
|
<div class="pill-row">
|
||||||
|
{#each columnOptions as n}
|
||||||
|
<button class="pill" class:active={columns === n} onclick={() => setColumns(n)}>{n}</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<button class="add-btn" onclick={() => (showAdd = !showAdd)}>+ New bookmark</button>
|
<button class="add-btn" onclick={() => (showAdd = !showAdd)}>+ New bookmark</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -102,12 +118,40 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
.count {
|
.count {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
.columns-picker {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.field-label {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
.pill-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.pill {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
border: 0.5px solid var(--border);
|
||||||
|
background: var(--surface-2);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
.pill.active {
|
||||||
|
background: var(--pill-bg);
|
||||||
|
color: var(--pill-text);
|
||||||
|
border-color: var(--pill-bg);
|
||||||
|
}
|
||||||
.add-btn {
|
.add-btn {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
AdminSettings,
|
AdminSettings,
|
||||||
AdminStockTicker,
|
AdminStockTicker,
|
||||||
AdminBookmark,
|
AdminBookmark,
|
||||||
|
AdminBookmarksConfig,
|
||||||
AdminPoe2Entry,
|
AdminPoe2Entry,
|
||||||
AdminWeatherSettings,
|
AdminWeatherSettings,
|
||||||
InstalledWidget,
|
InstalledWidget,
|
||||||
@@ -20,6 +21,7 @@
|
|||||||
settings,
|
settings,
|
||||||
stockTickers,
|
stockTickers,
|
||||||
bookmarks,
|
bookmarks,
|
||||||
|
bookmarksConfig,
|
||||||
poe2Watchlist,
|
poe2Watchlist,
|
||||||
weatherConfig,
|
weatherConfig,
|
||||||
poe2,
|
poe2,
|
||||||
@@ -28,6 +30,7 @@
|
|||||||
settings: AdminSettings;
|
settings: AdminSettings;
|
||||||
stockTickers: AdminStockTicker[];
|
stockTickers: AdminStockTicker[];
|
||||||
bookmarks: AdminBookmark[];
|
bookmarks: AdminBookmark[];
|
||||||
|
bookmarksConfig: AdminBookmarksConfig;
|
||||||
poe2Watchlist: AdminPoe2Entry[];
|
poe2Watchlist: AdminPoe2Entry[];
|
||||||
weatherConfig: AdminWeatherSettings;
|
weatherConfig: AdminWeatherSettings;
|
||||||
poe2: Poe2Data;
|
poe2: Poe2Data;
|
||||||
@@ -138,7 +141,7 @@
|
|||||||
{:else if key === 'stocks'}
|
{:else if key === 'stocks'}
|
||||||
<StocksTab tickers={stockTickers} />
|
<StocksTab tickers={stockTickers} />
|
||||||
{:else if key === 'bookmarks'}
|
{:else if key === 'bookmarks'}
|
||||||
<BookmarksTab {bookmarks} />
|
<BookmarksTab {bookmarks} config={bookmarksConfig} />
|
||||||
{:else if key === 'poe2'}
|
{:else if key === 'poe2'}
|
||||||
<Poe2Tab {poe2} watchlist={poe2Watchlist} />
|
<Poe2Tab {poe2} watchlist={poe2Watchlist} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Bookmark } from '$lib/types';
|
import type { Bookmark } from '$lib/types';
|
||||||
|
|
||||||
let { bookmarks }: { bookmarks: Bookmark[] } = $props();
|
let { bookmarks, columns = 1 }: { bookmarks: Bookmark[]; columns?: 1 | 2 | 3 } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="widget">
|
<div class="widget">
|
||||||
<span class="title">Bookmarks</span>
|
<span class="title">Bookmarks</span>
|
||||||
{#if bookmarks.length > 0}
|
{#if bookmarks.length > 0}
|
||||||
<div class="list">
|
<div class="list" class:grid={columns > 1} style:grid-template-columns={columns > 1 ? `repeat(${columns}, 1fr)` : undefined}>
|
||||||
{#each bookmarks as bookmark (bookmark.id)}
|
{#each bookmarks as bookmark (bookmark.id)}
|
||||||
<a class="row" href={bookmark.url} target="_blank" rel="noopener noreferrer">{bookmark.name}</a>
|
<a class="row" href={bookmark.url} target="_blank" rel="noopener noreferrer">{bookmark.name}</a>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -33,6 +33,10 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
.list.grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
.row {
|
.row {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
padding: 6px 0;
|
padding: 6px 0;
|
||||||
@@ -42,6 +46,15 @@
|
|||||||
.row:first-child {
|
.row:first-child {
|
||||||
border-top: none;
|
border-top: none;
|
||||||
}
|
}
|
||||||
|
.list.grid .row {
|
||||||
|
border-top: none;
|
||||||
|
padding: 6px 8px;
|
||||||
|
background: var(--surface-2);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
.row:hover {
|
.row:hover {
|
||||||
color: var(--text-accent);
|
color: var(--text-accent);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { tick } from 'svelte';
|
import { tick } from 'svelte';
|
||||||
import type { Weather, StockTicker, Bookmark, Poe2Data, WidgetsEnabled } from '$lib/types';
|
import type { Weather, StockTicker, BookmarksFeed, 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';
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
}: {
|
}: {
|
||||||
weather: Weather;
|
weather: Weather;
|
||||||
stocks: StockTicker[];
|
stocks: StockTicker[];
|
||||||
bookmarks: Bookmark[];
|
bookmarks: BookmarksFeed;
|
||||||
poe2: Poe2Data;
|
poe2: Poe2Data;
|
||||||
widgetsEnabled: WidgetsEnabled;
|
widgetsEnabled: WidgetsEnabled;
|
||||||
} = $props();
|
} = $props();
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
{:else if key === 'poe2' && widgetsEnabled.poe2}
|
{:else if key === 'poe2' && widgetsEnabled.poe2}
|
||||||
<Poe2Widget {poe2} />
|
<Poe2Widget {poe2} />
|
||||||
{:else if key === 'bookmarks' && widgetsEnabled.bookmarks}
|
{:else if key === 'bookmarks' && widgetsEnabled.bookmarks}
|
||||||
<BookmarksWidget {bookmarks} />
|
<BookmarksWidget bookmarks={bookmarks.items} columns={bookmarks.columns} />
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
{#each widgetsEnabled.pluggable as w (w.id)}
|
{#each widgetsEnabled.pluggable as w (w.id)}
|
||||||
|
|||||||
@@ -159,6 +159,12 @@ export interface Bookmark {
|
|||||||
isPrivate: boolean;
|
isPrivate: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Response from GET /api/widget/bookmarks — items plus the admin-configured sidebar layout width. */
|
||||||
|
export interface BookmarksFeed {
|
||||||
|
items: Bookmark[];
|
||||||
|
columns: 1 | 2 | 3;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Poe2WatchlistEntry {
|
export interface Poe2WatchlistEntry {
|
||||||
id: string;
|
id: string;
|
||||||
baseName: string;
|
baseName: string;
|
||||||
|
|||||||
@@ -48,6 +48,7 @@
|
|||||||
settings={data.settings}
|
settings={data.settings}
|
||||||
stockTickers={data.stockTickers}
|
stockTickers={data.stockTickers}
|
||||||
bookmarks={data.bookmarks}
|
bookmarks={data.bookmarks}
|
||||||
|
bookmarksConfig={data.bookmarksConfig}
|
||||||
poe2Watchlist={data.poe2Watchlist}
|
poe2Watchlist={data.poe2Watchlist}
|
||||||
weatherConfig={data.weatherConfig}
|
weatherConfig={data.weatherConfig}
|
||||||
poe2={data.poe2}
|
poe2={data.poe2}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
getLogs,
|
getLogs,
|
||||||
getStockTickers,
|
getStockTickers,
|
||||||
getAdminBookmarks,
|
getAdminBookmarks,
|
||||||
|
getBookmarksConfig,
|
||||||
getPoe2Watchlist,
|
getPoe2Watchlist,
|
||||||
getWeatherConfig,
|
getWeatherConfig,
|
||||||
listWidgets
|
listWidgets
|
||||||
@@ -21,19 +22,31 @@ const EMPTY_MODELS: ModelCatalog = { embedding: [], image: [], synthesis: [] };
|
|||||||
|
|
||||||
export const load: PageLoad = async ({ fetch }) => {
|
export const load: PageLoad = async ({ fetch }) => {
|
||||||
try {
|
try {
|
||||||
const [settings, sources, events, logs, stockTickers, bookmarks, poe2Watchlist, weatherConfig, poe2, installedWidgets] =
|
const [
|
||||||
await Promise.all([
|
settings,
|
||||||
getSettings(fetch),
|
sources,
|
||||||
getSources(fetch),
|
events,
|
||||||
getEvents(fetch),
|
logs,
|
||||||
getLogs({}, fetch),
|
stockTickers,
|
||||||
getStockTickers(fetch),
|
bookmarks,
|
||||||
getAdminBookmarks(fetch),
|
bookmarksConfig,
|
||||||
getPoe2Watchlist(fetch),
|
poe2Watchlist,
|
||||||
getWeatherConfig(fetch),
|
weatherConfig,
|
||||||
getPoe2(fetch),
|
poe2,
|
||||||
listWidgets(fetch)
|
installedWidgets
|
||||||
]);
|
] = await Promise.all([
|
||||||
|
getSettings(fetch),
|
||||||
|
getSources(fetch),
|
||||||
|
getEvents(fetch),
|
||||||
|
getLogs({}, fetch),
|
||||||
|
getStockTickers(fetch),
|
||||||
|
getAdminBookmarks(fetch),
|
||||||
|
getBookmarksConfig(fetch),
|
||||||
|
getPoe2Watchlist(fetch),
|
||||||
|
getWeatherConfig(fetch),
|
||||||
|
getPoe2(fetch),
|
||||||
|
listWidgets(fetch)
|
||||||
|
]);
|
||||||
|
|
||||||
// The AI service (Ollama) may not be running yet — that shouldn't take down the
|
// The AI service (Ollama) may not be running yet — that shouldn't take down the
|
||||||
// whole settings page, just leave the Models/Connections tabs showing "unreachable".
|
// whole settings page, just leave the Models/Connections tabs showing "unreachable".
|
||||||
@@ -59,6 +72,7 @@ export const load: PageLoad = async ({ fetch }) => {
|
|||||||
logs,
|
logs,
|
||||||
stockTickers,
|
stockTickers,
|
||||||
bookmarks,
|
bookmarks,
|
||||||
|
bookmarksConfig,
|
||||||
poe2Watchlist,
|
poe2Watchlist,
|
||||||
weatherConfig,
|
weatherConfig,
|
||||||
poe2,
|
poe2,
|
||||||
|
|||||||
Reference in New Issue
Block a user