Combine Merge and Sources admin tabs, add explanation to Poll interval

Category priority and Sources now live at the top of the combined tab, each
collapsed by default behind the same CollapsibleSection pattern used for the
Widgets tab. Merge strictness and the rest of the synthesis settings sit below,
uncollapsed. The Poll interval panel's hint now explains that it's a fallback
only — every source sets its own poll interval, so this global default has no
effect in current usage.
This commit is contained in:
Claude
2026-07-26 17:24:11 +00:00
parent e6ac6cd061
commit e7367bbb86
3 changed files with 154 additions and 80 deletions
@@ -0,0 +1,69 @@
<script lang="ts">
import type { Snippet } from 'svelte';
// Same collapsed-by-default shell as WidgetSection, minus the enable/disable
// checkbox — for sections that are always "on" (Category priority, Sources) and
// just need to stay out of the way until expanded.
let {
title,
defaultExpanded = false,
children
}: {
title: string;
defaultExpanded?: boolean;
children: Snippet;
} = $props();
let expanded = $state(defaultExpanded);
</script>
<div class="section">
<button class="section-head" onclick={() => (expanded = !expanded)} aria-expanded={expanded}>
<span class="chevron" class:open={expanded}>▸</span>
<span class="title">{title}</span>
</button>
{#if expanded}
<div class="section-body">
{@render children()}
</div>
{/if}
</div>
<style>
.section {
background: var(--surface-1);
border-radius: 12px;
margin-bottom: 14px;
overflow: hidden;
}
.section-head {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
background: transparent;
border: none;
padding: 12px 14px;
font-size: 13px;
font-weight: 500;
color: var(--text-primary);
text-align: left;
}
.chevron {
display: inline-block;
font-size: 10px;
color: var(--text-muted);
transition: transform 0.15s ease;
}
.chevron.open {
transform: rotate(90deg);
}
.section-body {
padding: 14px;
padding-top: 0;
border-top: 0.5px solid var(--border);
}
.section-body > :global(*:first-child) {
margin-top: 14px;
}
</style>
@@ -1,9 +1,11 @@
<script lang="ts">
import type { AdminSettings } from '$lib/adminTypes';
import type { AdminSettings, AdminSource } from '$lib/adminTypes';
import { updateSettings, createCategory, deleteCategory } from '$lib/adminApi';
import SaveStatus from './SaveStatus.svelte';
import CollapsibleSection from './CollapsibleSection.svelte';
import SourcesTab from './SourcesTab.svelte';
let { settings }: { settings: AdminSettings } = $props();
let { settings, sources }: { settings: AdminSettings; sources: AdminSource[] } = $props();
let local = $state({ ...settings, categoryPriority: [...settings.categoryPriority] });
let status = $state<'idle' | 'saving' | 'saved' | 'error'>('idle');
@@ -89,78 +91,7 @@
}
</script>
<div class="panel">
<div class="head">
<span class="panel-title">Merge strictness</span>
<SaveStatus {status} />
</div>
<p class="hint">How similar articles must be before they're combined into one story.</p>
<div class="slider-row">
<span class="end">Loose</span>
<input
type="range"
min="1"
max="5"
step="1"
bind:value={local.mergeStrictness}
oninput={scheduleSave}
/>
<span class="end">Strict</span>
<span class="value">{local.mergeStrictness}</span>
</div>
</div>
<div class="grid-2">
<div class="panel">
<span class="panel-title">Poll interval</span>
<p class="hint">How often each source is checked for new items.</p>
<select bind:value={local.defaultPollIntervalMinutes} onchange={scheduleSave}>
<option value={5}>Every 5 minutes</option>
<option value={15}>Every 15 minutes</option>
<option value={60}>Every hour</option>
</select>
</div>
<div class="panel">
<span class="panel-title">Hold before publish</span>
<p class="hint">Wait window to gather more sources before finalizing a story.</p>
<select bind:value={local.holdBeforePublishMinutes} onchange={scheduleSave}>
<option value={0}>Publish immediately</option>
<option value={30}>Wait 30 minutes</option>
<option value={120}>Wait 2 hours</option>
</select>
</div>
</div>
<div class="panel">
<span class="panel-title">Follow-up articles</span>
<p class="hint">
Instead of editing a published article, a distinct follow-up is created once enough new
corroborating sources arrive after enough time has passed.
</p>
<div class="grid-2">
<div>
<label class="field-label" for="followup-hours">Minimum time since last article</label>
<select id="followup-hours" bind:value={local.followUpMinHoursSinceLast} onchange={scheduleSave}>
<option value={1}>1 hour</option>
<option value={6}>6 hours</option>
<option value={12}>12 hours</option>
<option value={24}>24 hours</option>
</select>
</div>
<div>
<label class="field-label" for="followup-sources">Minimum new sources</label>
<select id="followup-sources" bind:value={local.followUpMinNewSources} onchange={scheduleSave}>
<option value={1}>1</option>
<option value={2}>2</option>
<option value={3}>3</option>
<option value={4}>4</option>
</select>
</div>
</div>
</div>
<div class="panel">
<span class="panel-title">Category priority</span>
<CollapsibleSection title="Category priority">
<p class="hint">
Synthesis queue processes higher-ranked categories first. Nothing is dropped — lower
categories just wait longer when the queue is busy. This list also drives the site's nav —
@@ -225,6 +156,84 @@
{addingCategory ? 'Adding…' : '+ Add'}
</button>
</div>
</CollapsibleSection>
<CollapsibleSection title="Sources">
<SourcesTab {sources} categories={local.categoryPriority} />
</CollapsibleSection>
<div class="panel">
<div class="head">
<span class="panel-title">Merge strictness</span>
<SaveStatus {status} />
</div>
<p class="hint">How similar articles must be before they're combined into one story.</p>
<div class="slider-row">
<span class="end">Loose</span>
<input
type="range"
min="1"
max="5"
step="1"
bind:value={local.mergeStrictness}
oninput={scheduleSave}
/>
<span class="end">Strict</span>
<span class="value">{local.mergeStrictness}</span>
</div>
</div>
<div class="grid-2">
<div class="panel">
<span class="panel-title">Poll interval</span>
<p class="hint">
Fallback only — every source above sets its own poll interval, so this value has no
effect unless a source somehow has none set (not currently possible through this admin
panel or the API).
</p>
<select bind:value={local.defaultPollIntervalMinutes} onchange={scheduleSave}>
<option value={5}>Every 5 minutes</option>
<option value={15}>Every 15 minutes</option>
<option value={60}>Every hour</option>
</select>
</div>
<div class="panel">
<span class="panel-title">Hold before publish</span>
<p class="hint">Wait window to gather more sources before finalizing a story.</p>
<select bind:value={local.holdBeforePublishMinutes} onchange={scheduleSave}>
<option value={0}>Publish immediately</option>
<option value={30}>Wait 30 minutes</option>
<option value={120}>Wait 2 hours</option>
</select>
</div>
</div>
<div class="panel">
<span class="panel-title">Follow-up articles</span>
<p class="hint">
Instead of editing a published article, a distinct follow-up is created once enough new
corroborating sources arrive after enough time has passed.
</p>
<div class="grid-2">
<div>
<label class="field-label" for="followup-hours">Minimum time since last article</label>
<select id="followup-hours" bind:value={local.followUpMinHoursSinceLast} onchange={scheduleSave}>
<option value={1}>1 hour</option>
<option value={6}>6 hours</option>
<option value={12}>12 hours</option>
<option value={24}>24 hours</option>
</select>
</div>
<div>
<label class="field-label" for="followup-sources">Minimum new sources</label>
<select id="followup-sources" bind:value={local.followUpMinNewSources} onchange={scheduleSave}>
<option value={1}>1</option>
<option value={2}>2</option>
<option value={3}>3</option>
<option value={4}>4</option>
</select>
</div>
</div>
</div>
<div class="panel">
@@ -1,7 +1,6 @@
<script lang="ts">
import type { PageData } from './$types';
import MergeTab from '$lib/components/admin/MergeTab.svelte';
import SourcesTab from '$lib/components/admin/SourcesTab.svelte';
import ModelsTab from '$lib/components/admin/ModelsTab.svelte';
import RetentionTab from '$lib/components/admin/RetentionTab.svelte';
import EventsTab from '$lib/components/admin/EventsTab.svelte';
@@ -12,8 +11,7 @@
let { data }: { data: PageData } = $props();
const tabs = [
{ id: 'merge', label: 'Merge' },
{ id: 'sources', label: 'Sources' },
{ id: 'merge', label: 'Sources & Merge' },
{ id: 'models', label: 'Models' },
{ id: 'retention', label: 'Retention' },
{ id: 'events', label: 'Tracked events' },
@@ -38,9 +36,7 @@
<div class="content">
{#if active === 'merge'}
<MergeTab settings={data.settings} />
{:else if active === 'sources'}
<SourcesTab sources={data.sources} categories={data.settings.categoryPriority} />
<MergeTab settings={data.settings} sources={data.sources} />
{:else if active === 'models'}
<ModelsTab settings={data.settings} models={data.models} aiStatus={data.aiStatus} />
{:else if active === 'retention'}