Dashboard
{#if error}
{error}
{:else if !stats}
Loading…
{:else}
{#if section === 'overview' || section === 'content' || section === 'audience'}
Window
{#each [7, 30, 90] as d (d)}
{/each}
{/if}
{#if section === 'overview'}
{#if (stats.attention ?? []).length}
{#each stats.attention as a}
{a.text}
{/each}
{:else}
✓ All clear — nothing needs attention right now.
{/if}
Pulse
{stats.visitors.today}Visitors today
{stats.visitors.d7}Visitors (7d)
{stats.visitors.d30}Visitors ({range}d)
{stats.content.served}Articles live
{stats.content.accepted_7d}Fresh (7d)
{stats.content.latest_brief_size}In today's brief
{healthy}/{sources.length}Sources healthy
{stats.accounts.total}Accounts
{:else if section === 'content'}
Corpus
{stats.content.served}Articles live
{stats.content.rejected}Filtered out
{stats.content.added_24h}Ingested (24h)
{stats.content.added_7d}Ingested (7d)
{stats.content.accepted_7d}Fresh & live (7d)
{stats.content.latest_brief_size}In today's brief
Coverage
{coverage(stats.content.with_image, stats.content.served)}%Articles with image ({stats.content.with_image}/{stats.content.served})
{coverage(stats.content.summaries, stats.content.served)}%Summaries ({stats.content.summaries}/{stats.content.served})
{coverage(stats.content.summaries_with_image, stats.content.summaries)}%Summary pages w/ image
{stats.content.brief_with_image}/{stats.content.latest_brief_size}Brief w/ image
{stats.content.recent_enrich_fail}Image misses (24h)
Most-opened articles
{#if stats.top_articles.length}
{#each stats.top_articles as a (a.id)}
-
{a.title}
{a.opens}
{/each}
{:else}No opens yet.
{/if}
Popular groupings
{#if stats.top_groupings.length}
{#each stats.top_groupings as g (g.tag)}
-
{g.tag.replace('-', ' ')}
{g.opens}
{/each}
{:else}No data yet.
{/if}
Popular topics
{#if stats.top_topics.length}
{#each stats.top_topics as t (t.topic)}
-
{t.topic}
{t.opens}
{/each}
{:else}No data yet.
{/if}
{:else if section === 'sources'}
{#if pendingCandidates.length}
Candidate queue ({pendingCandidates.length})
{/if}
Sources
{healthy} healthy · {resting} resting · {flagged} flagged · {paused} paused · {retired} retired · {sources.length} total
{#each [['all', 'All'], ['healthy', 'Healthy'], ['resting', 'Resting'], ['flagged', 'Flagged'], ['paused', 'Paused'], ['retired', 'Retired']] as [key, label] (key)}
{/each}
| Source | Served | Accept | Dup |
Last success | Next poll | Fails | Status | Actions |
{#each shownSources as s (s.id)}
{@const st = sstatus(s)}
0} class:flag={s.review_flag} class:paused={st !== 'active'}>
|
{s.name}{#if s.category}{s.category}{/if}
{#if s.review_flag && s.review_reason}“{s.review_reason}”{/if}
|
{s.served} |
{s.acceptance_rate != null ? s.acceptance_rate + '%' : '—'} |
{s.duplicate_rate != null ? s.duplicate_rate + '%' : '—'} |
{s.last_success_at ? fwhen(s.last_success_at) : '—'} |
{st === 'active' ? fwhen(s.next_due_at) : '—'} |
{s.failures || ''} |
{#if st === 'retired'}retired
{:else if st === 'paused'}paused
{:else if rateLimited(s)}rate-limited · rests until {fwhen(s.retry_after_at)}{#if s.review_flag} · review{/if}
{:else if s.failures > 0}⚠ resting{#if s.review_flag} · review{/if}
{:else if s.review_flag}⚑ review
{:else}✓ ok{/if}
{#if !s.content_visible}· hidden{/if}
|
{#if st === 'retired'}
{:else}
{/if}
|
{/each}
“served” = accepted, non-duplicate articles live · accept/dup % is of all ingested · pausing stops polling but keeps existing articles live · times in your local zone
{:else if section === 'audience'}
Visitors
{stats.visitors.today}Today
{stats.visitors.d7}Last 7 days
{stats.visitors.d30}Last {range} days
Returning visitors ({range}d)
{stats.retention.new}New (1 day)
{stats.retention['2-3']}2–3 days
{stats.retention['4-7']}4–7 days
{stats.retention['8+']}8+ days
Accounts
{stats.accounts.total}Total
{stats.accounts.new_today}New today
{stats.accounts.new_7d}New this week
{stats.accounts.active_7d}Active this week
Reading funnel
{stats.funnel.summary_viewed}Summaries read
{stats.funnel.source_click}→ Source (from summary)
{stats.funnel.source_rate}%Summary → source rate
{stats.funnel.full_story}Straight to source
Emotional mix & friction
{stats.emotional_mix.not_today}Not today
{stats.emotional_mix.less_like_this}Less like this
{stats.emotional_mix.hide_topic}Hide topic
{stats.replace.used}Replaces
{stats.paywall.paywalled_source_open}Paywalled opens
Sharing
{#each Object.entries(stats.shares) as [kind, n]}
{n}{SHARE_LABEL[kind] ?? kind}
{/each}
Daily trend
{#if stats.daily.length}
{@const dmax = Math.max(1, ...stats.daily.map((d) => Math.max(d.opens, d.visits)))}
{#each stats.daily as d (d.day)}
{/each}
visits opens
{:else}No data yet.
{/if}
{:else if section === 'feedback'}
Feedback {#if feedback.length}({unreadCount} unread / {feedback.length}){/if}
{#if feedback.length}
{#each fbCats as cat (cat)}
{/each}
{#if shownFeedback.length}
{#each shownFeedback as f (f.id)}
-
{#if !f.read_at}{/if}
{f.category}
{fdate(f.created_at)}
{#if f.contact_email}{f.contact_email}{:else}anonymous{/if}
{f.message}
{#if f.replies?.length}
{#each f.replies as r (r.id)}
↪ Replied · {fwhen(r.sent_at)}
{#if r.message_html}
{@html r.message_html}
{:else}
{r.message}
{/if}
{/each}
{/if}
{#if replyingId === f.id}
{#if replyErr}
{replyErr}
{/if}
{/if}
{#if f.contact_email && replyingId !== f.id}
{:else if !f.contact_email}
No reply address
{/if}
{/each}
{:else}Nothing in this filter.
{/if}
{:else}No feedback yet.
{/if}
{/if}
{/if}