▍ humdrum codex / soft

TUI News page: 2x2 grid of the four news briefs, one per quadrant

1ed966def06aa687b465245b368765f4f31af3c7
humdrum <me@humdrum.me> · 2026-07-08 17:15

parent 816c2461

TUI News page: 2x2 grid of the four news briefs, one per quadrant

Replace the News page's headlines/briefs/feeds/hacker-news mix with a 2x2 grid
of the four screamer brief categories (Politics/Tech/Sports/Culture), each in
its own quadrant with the category name as the panel title. Drop the now-dead
render_feeds/render_briefs helpers. Feeds still flows to the printed paper.

TASK-037

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

2 files changed

- → News-page-2x2-grid-of-the-four-briefs-only.md +24 −0
@@ -0,0 +1,24 @@
+---
+id: TASK-037
+title: 'News page: 2x2 grid of the four briefs only'
+status: To Do
+assignee: []
+created_date: '2026-07-09 00:15'
+labels:
+  - feature
+dependencies: []
+priority: medium
+ordinal: 37000
+---
+
+## Description
+
+<!-- SECTION:DESCRIPTION:BEGIN -->
+Replace News page (headlines/briefs/feeds/hackernews) with a 2x2 grid of the four screamer brief categories, one per quadrant. Panel titles = category names. Removed dead render_feeds/render_briefs; feeds still feeds the paper.
+<!-- SECTION:DESCRIPTION:END -->
+
+## Acceptance Criteria
+<!-- AC:BEGIN -->
+- [ ] #1 News page shows 4 brief-category quadrants, titled by category
+- [ ] #2 No headlines/feeds/hackernews on News page
+<!-- AC:END -->
tui/dashboard.py +31 −54
@@ -395,27 +395,6 @@         return Text("No headlines.", style="dim italic")
     return t
 
 
-def render_feeds(ps: list[dict], cap: int = 12) -> Text:
-    t, seen, n = Text(), set(), 0
-    for p in ps:
-        for item in p.get("items", []):
-            if n >= cap:
-                break
-            title = item.get("title", "?")
-            if title in seen:
-                continue
-            seen.add(title)
-            n += 1
-            url = item.get("link", "")
-            t.append("• ", style="dim")
-            t.append(f"{title}\n", style=_link(url) if url else "")
-            if src := item.get("source") or item.get("feedTitle"):
-                t.append(f"  {src}\n", style="dim")
-    if n == 0:
-        return Text("No feed items.", style="dim italic")
-    return t
-
-
 def render_hackernews(p: dict, cap: int = 10) -> Text:
     stories = p.get("stories", [])[:cap]
     if not stories:
@@ -542,19 +521,19 @@         t.append("\n")
     return t
 
 
-def render_briefs(p: dict) -> Text:
-    cats = [c for c in p.get("categories", []) if c.get("stories")]
-    if not cats:
-        return Text("No briefs.", style="dim italic")
+def render_brief_category(cat: dict) -> Text:
+    """One brief category for a News-page quadrant — the panel border carries
+    the category name, so this just lists its stories (title + full summary)."""
+    stories = cat.get("stories", [])
+    if not stories:
+        return Text("No stories.", style="dim italic")
     t = Text()
-    for cat in cats:
-        t.append(f"{cat.get('name', '?').upper()}\n", style="bold")
-        for s in cat.get("stories", [])[:3]:
-            url = s.get("link") or ""
-            t.append("  • ", style="dim")
-            t.append(f"{s.get('title', '?')}\n", style=_link(url) if url else "")
-            if summ := s.get("summary"):
-                t.append(f"    {summ[:120]}\n", style="dim")
+    for s in stories:
+        url = s.get("link") or ""
+        t.append("• ", style="dim")
+        t.append(f"{s.get('title', '?')}\n", style=_link(url) if url else "")
+        if summ := s.get("summary"):
+            t.append(f"  {summ}\n\n", style="dim")
     return t
 
 
@@ -1019,10 +998,7 @@     #sports     { width: 2.5fr; }
     #news       { width: 2fr; }
     #hackernews { width: 1fr; }
 
-    #np-news       { width: 1.25fr; }
-    #np-briefs     { width: 1fr; }
-    #np-feeds      { width: 1fr; }
-    #np-hackernews { width: 1fr; }
+    #brief-0, #brief-1, #brief-2, #brief-3 { width: 1fr; }
 
     #sys-cpu, #sys-memdisk, #sys-net, #sys-power { width: 1fr; }
 
@@ -1095,12 +1071,14 @@                     yield DashPanel("Scores", "sports")
                     yield DashPanel("Headlines", "news")
                     yield DashPanel("Hacker News", "hackernews")
             with Vertical(id="page-news"):
+                # 2×2 of the four news briefs from screamer — one per quadrant.
+                # Titles are set per-refresh from the brief category names.
                 with Horizontal(classes="row"):
-                    yield DashPanel("Headlines", "np-news")
-                    yield DashPanel("Briefs", "np-briefs")
+                    yield DashPanel("Brief", "brief-0")
+                    yield DashPanel("Brief", "brief-1")
                 with Horizontal(classes="row"):
-                    yield DashPanel("Feeds", "np-feeds")
-                    yield DashPanel("Hacker News", "np-hackernews")
+                    yield DashPanel("Brief", "brief-2")
+                    yield DashPanel("Brief", "brief-3")
             with Vertical(id="page-system"):
                 with Horizontal(classes="row"):
                     yield DashPanel("CPU", "sys-cpu")
@@ -1142,27 +1120,26 @@         )
         panel("sports").set_content(
             render_sports(p) if (p := payload(sources, "sports")) else no_data
         )
-        feed_ps = payloads(sources, "feeds")
-        panel("np-feeds").set_content(
-            render_feeds(feed_ps, cap=24) if feed_ps else no_data
-        )
         news_ps = payloads(sources, "news")
         panel("news").set_content(
             render_news(news_ps) if news_ps else no_data
-        )
-        panel("np-news").set_content(
-            render_news(news_ps, cap=24) if news_ps else no_data
         )
         hn_p = payload(sources, "hackernews")
         panel("hackernews").set_content(
             render_hackernews(hn_p) if hn_p else no_data
         )
-        panel("np-hackernews").set_content(
-            render_hackernews(hn_p, cap=20) if hn_p else no_data
-        )
-        panel("np-briefs").set_content(
-            render_briefs(p) if (p := payload(sources, "briefs")) else no_data
-        )
+        # News page: one brief category per quadrant (config order).
+        briefs_p = payload(sources, "briefs")
+        brief_cats = (briefs_p or {}).get("categories", [])
+        for i in range(4):
+            pnl = panel(f"brief-{i}")
+            if i < len(brief_cats):
+                cat = brief_cats[i]
+                pnl.border_title = cat.get("name", "Brief")
+                pnl.set_content(render_brief_category(cat))
+            else:
+                pnl.border_title = "—"
+                pnl.set_content(no_data)
         panel("uptime").set_content(
             render_uptime(p) if (p := payload(sources, "uptime")) else no_data
         )