news-digest: add world + local headlines sections
Two new dense headline rails above the existing reddit/tech cards.
Designed for high-volume "what happened" coverage where the title
is the deliverable — no LLM summarization, ~15 items per section,
6-column-collapsing grid (title / source / time).
Digest pipeline:
* fetch_miniflux_headlines(category) — flat list per category, dedup
by lowercased title (different feeds syndicate the same wire stories)
* 8h look-back window (vs 12h for tech/reddit) since headlines move
faster
* cap of 15 per section (DIGEST_MINIFLUX_HEADLINES_MAX)
Frontend:
* .headline element parallels .item for the hide-button machinery
(both have data-id, both honored by app.js)
* dense 3-col layout collapses to 1-col on narrow screens
* jumpnav now numbers world=01, local=02, reddit=03, tech=04
Setup:
* seed-headlines.py — one-shot script (lives in the image at
/app/seed-headlines.py). Creates the World + Local categories in
miniflux, subscribes a curated feed list, and renames each feed
to a short display title (BBC vs "BBC News", "LA Times" vs "California").
Idempotent — reruns only add new feeds.
* Default world: BBC, NPR, Al Jazeera. Default local: LA Times Local,
LA Times CA, Voice of OC. (OC Register blocks miniflux; left out.)
* entrypoint.sh now syncs templates/{style.css,app.js,favicon.svg}
to /output on container start so frontend asset updates land
without a manual copy after rebuild.
This commit is contained in:
@@ -30,9 +30,10 @@
|
||||
const TRAY_COUNT = document.getElementById("hidden-tray-count");
|
||||
const TRAY_TOGGLE = TRAY && TRAY.querySelector(".hidden-tray-toggle");
|
||||
|
||||
/** Map<id, HTMLElement> — every .item on this page, keyed by data-id */
|
||||
/** Map<id, HTMLElement> — every hideable element on this page, keyed by
|
||||
* data-id. Both .item (cards) and .headline (compact rows) qualify. */
|
||||
const itemsById = new Map();
|
||||
document.querySelectorAll(".item[data-id]").forEach((el) => {
|
||||
document.querySelectorAll(".item[data-id], .headline[data-id]").forEach((el) => {
|
||||
itemsById.set(el.dataset.id, el);
|
||||
});
|
||||
|
||||
@@ -76,7 +77,7 @@
|
||||
src.classList.toggle("is-empty", visible === 0);
|
||||
});
|
||||
document.querySelectorAll(".desk").forEach((desk) => {
|
||||
const visibleItems = desk.querySelectorAll(".item:not(.is-hidden)").length;
|
||||
const visibleItems = desk.querySelectorAll(".item:not(.is-hidden), .headline:not(.is-hidden)").length;
|
||||
const badge = desk.querySelector(".desk-count");
|
||||
if (badge) badge.textContent = `${visibleItems} items`;
|
||||
desk.classList.toggle("is-empty", visibleItems === 0);
|
||||
@@ -130,7 +131,7 @@
|
||||
}
|
||||
|
||||
function titleOf(el) {
|
||||
const t = el.querySelector(".item-title");
|
||||
const t = el.querySelector(".item-title, .headline-title");
|
||||
return (t && t.textContent.trim()) || "(untitled)";
|
||||
}
|
||||
|
||||
@@ -187,7 +188,7 @@
|
||||
document.addEventListener("click", (ev) => {
|
||||
const btn = ev.target.closest(".item-hide");
|
||||
if (!btn) return;
|
||||
const item = btn.closest(".item[data-id]");
|
||||
const item = btn.closest("[data-id]");
|
||||
if (!item) return;
|
||||
ev.preventDefault();
|
||||
hideItem(item.dataset.id);
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">DESK</span>
|
||||
<span class="meta-value">SUBREDDITS · TECH FEEDS</span>
|
||||
<span class="meta-value">WORLD · LOCAL · SUBREDDITS · TECH FEEDS</span>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">CURATED BY</span>
|
||||
@@ -43,22 +43,76 @@
|
||||
{# ────── nav strip ────── #}
|
||||
<nav class="jumpnav" aria-label="sections">
|
||||
<ol>
|
||||
{% if world_headlines %}
|
||||
<li><a href="#world"><span class="jump-num">01</span> WORLD</a></li>
|
||||
{% endif %}
|
||||
{% if local_headlines %}
|
||||
<li><a href="#local"><span class="jump-num">02</span> LOCAL</a></li>
|
||||
{% endif %}
|
||||
{% if reddit_sources %}
|
||||
<li><a href="#reddit"><span class="jump-num">01</span> REDDIT</a></li>
|
||||
<li><a href="#reddit"><span class="jump-num">03</span> REDDIT</a></li>
|
||||
{% endif %}
|
||||
{% if tech_sources %}
|
||||
<li><a href="#tech"><span class="jump-num">02</span> TECH FEEDS</a></li>
|
||||
<li><a href="#tech"><span class="jump-num">04</span> TECH FEEDS</a></li>
|
||||
{% endif %}
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<main class="brief">
|
||||
|
||||
{# ────── WORLD HEADLINES ────── #}
|
||||
{% if world_headlines %}
|
||||
<section id="world" class="desk desk-headlines">
|
||||
<header class="desk-head">
|
||||
<span class="desk-num">01</span>
|
||||
<h2 class="desk-title">World</h2>
|
||||
<span class="desk-sub">top of last {{ generated_at.hour < 14 and "8" or "8" }} hours · headlines</span>
|
||||
<span class="desk-count">{{ world_total }} items</span>
|
||||
</header>
|
||||
<ol class="headlines-list">
|
||||
{% for h in world_headlines %}
|
||||
<li class="headline" data-id="{{ h.id }}">
|
||||
<button class="item-hide" type="button" title="Hide this headline" aria-label="Hide this headline">×</button>
|
||||
<h3 class="headline-title">
|
||||
<a href="{{ h.url }}" target="_blank" rel="noopener">{{ h.title }}</a>
|
||||
</h3>
|
||||
<span class="headline-source">{{ h.source }}</span>
|
||||
<span class="headline-time">{{ h.posted_at|humanago }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{# ────── LOCAL HEADLINES ────── #}
|
||||
{% if local_headlines %}
|
||||
<section id="local" class="desk desk-headlines">
|
||||
<header class="desk-head">
|
||||
<span class="desk-num">02</span>
|
||||
<h2 class="desk-title">Local</h2>
|
||||
<span class="desk-sub">orange county · greater LA · last 8 hours</span>
|
||||
<span class="desk-count">{{ local_total }} items</span>
|
||||
</header>
|
||||
<ol class="headlines-list">
|
||||
{% for h in local_headlines %}
|
||||
<li class="headline" data-id="{{ h.id }}">
|
||||
<button class="item-hide" type="button" title="Hide this headline" aria-label="Hide this headline">×</button>
|
||||
<h3 class="headline-title">
|
||||
<a href="{{ h.url }}" target="_blank" rel="noopener">{{ h.title }}</a>
|
||||
</h3>
|
||||
<span class="headline-source">{{ h.source }}</span>
|
||||
<span class="headline-time">{{ h.posted_at|humanago }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{# ────── REDDIT SECTION ────── #}
|
||||
{% if reddit_sources %}
|
||||
<section id="reddit" class="desk">
|
||||
<header class="desk-head">
|
||||
<span class="desk-num">01</span>
|
||||
<span class="desk-num">03</span>
|
||||
<h2 class="desk-title">Reddit</h2>
|
||||
<span class="desk-sub">top of last {{ generated_at.hour < 14 and "12" or "12" }} hours · score-filtered</span>
|
||||
<span class="desk-count">{{ reddit_total }} items</span>
|
||||
@@ -127,7 +181,7 @@
|
||||
{% if tech_sources %}
|
||||
<section id="tech" class="desk">
|
||||
<header class="desk-head">
|
||||
<span class="desk-num">02</span>
|
||||
<span class="desk-num">04</span>
|
||||
<h2 class="desk-title">Tech Feeds</h2>
|
||||
<span class="desk-sub">non-reddit · last {{ generated_at.hour < 14 and "12" or "12" }} hours</span>
|
||||
<span class="desk-count">{{ tech_total }} items</span>
|
||||
@@ -184,7 +238,7 @@
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% if not reddit_sources and not tech_sources %}
|
||||
{% if not reddit_sources and not tech_sources and not world_headlines and not local_headlines %}
|
||||
<section class="desk empty">
|
||||
<p class="empty-msg">
|
||||
No items cleared the filters in the last window.<br>
|
||||
|
||||
@@ -490,6 +490,71 @@ a:hover { color: var(--accent); }
|
||||
.item[data-tag="meme"] { opacity: .58; }
|
||||
.item[data-tag="other"] { opacity: .85; }
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
headlines — dense list (world / local).
|
||||
Different rhythm from .item cards: one row per story, source on the
|
||||
right, title-led. Volume-driven sections where the deliverable is
|
||||
"what happened" not "what someone said about what happened".
|
||||
------------------------------------------------------------------------- */
|
||||
|
||||
.headlines-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.headline {
|
||||
position: relative; /* anchors the absolute-positioned × button */
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto auto;
|
||||
align-items: baseline;
|
||||
gap: 16px;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid var(--rule);
|
||||
}
|
||||
.headline:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.headline-title {
|
||||
margin: 0;
|
||||
font-family: var(--font-display);
|
||||
font-size: 17px;
|
||||
line-height: 1.35;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.005em;
|
||||
color: var(--fg);
|
||||
font-variation-settings: "opsz" 24;
|
||||
/* Title takes the leftmost column; allow it to wrap on narrow screens. */
|
||||
min-width: 0;
|
||||
}
|
||||
.headline-title a { color: inherit; }
|
||||
.headline-title a:hover { color: var(--accent); }
|
||||
|
||||
.headline-source {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
letter-spacing: .12em;
|
||||
color: var(--fg-faint);
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.headline-time {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10.5px;
|
||||
color: var(--fg-faint);
|
||||
font-variant-numeric: tabular-nums;
|
||||
min-width: 32px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Hidden state — same mechanism as .item, applies to .headline too. */
|
||||
.headline.is-hidden { display: none; }
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
archive page — list of all editions
|
||||
------------------------------------------------------------------------- */
|
||||
@@ -789,4 +854,17 @@ a:hover { color: var(--accent); }
|
||||
bigger tap target too. */
|
||||
.item-hide { opacity: 1; width: 28px; height: 28px; font-size: 20px; }
|
||||
.hidden-tray { padding: 12px 14px; }
|
||||
|
||||
/* Headlines: stack source + time below title on narrow screens so
|
||||
the title gets the full width. */
|
||||
.headline {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 4px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
.headline-source,
|
||||
.headline-time {
|
||||
grid-column: 1 / 2;
|
||||
}
|
||||
.headline-time { text-align: left; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user