diff --git a/stacks/news-digest/digest.py b/stacks/news-digest/digest.py index 4ffffcb..9826f00 100644 --- a/stacks/news-digest/digest.py +++ b/stacks/news-digest/digest.py @@ -353,6 +353,54 @@ def write_output(html: str, generated_at: datetime) -> None: index_tmp.write_text(html, encoding="utf-8") index_tmp.rename(index) log(f"wrote {index} (and archive {archive.name})") + write_archive_index(generated_at) + +ARCHIVE_FNAME_RE = re.compile(r"^edition-(\d{4}-\d{2}-\d{2})-(am|pm)\.html$") + +def write_archive_index(generated_at: datetime) -> None: + """Render /output/archive.html — list every edition-*.html in + OUTPUT_DIR, newest-first. Cheap (re-runs every digest fire); + template loads from the same TEMPLATE_DIR.""" + editions = [] + for p in OUTPUT_DIR.glob("edition-*.html"): + m = ARCHIVE_FNAME_RE.match(p.name) + if not m: + continue + date_str, ed = m.group(1), m.group(2) + try: + date = datetime.strptime(date_str, "%Y-%m-%d").date() + except ValueError: + continue + editions.append({ + "filename": p.name, + "date": date, + "edition": "morning" if ed == "am" else "evening", + "edition_short": ed.upper(), + "date_long": date.strftime("%A %B %-d, %Y"), + # Sort key: date descending, then PM before AM (within a day, + # PM is the most recent edition). + "_sort": (date, 1 if ed == "pm" else 0), + }) + editions.sort(key=lambda e: e["_sort"], reverse=True) + + env = Environment( + loader=FileSystemLoader(str(TEMPLATE_DIR)), + autoescape=select_autoescape(["html"]), + trim_blocks=True, + lstrip_blocks=True, + ) + tpl = env.get_template("archive.html.j2") + html = tpl.render( + editions=editions, + generated_at=generated_at, + total=len(editions), + ) + + out = OUTPUT_DIR / "archive.html" + out_tmp = out.with_suffix(".html.tmp") + out_tmp.write_text(html, encoding="utf-8") + out_tmp.rename(out) + log(f"wrote {out} ({len(editions)} editions indexed)") # ── main ───────────────────────────────────────────────────────────── diff --git a/stacks/news-digest/templates/archive.html.j2 b/stacks/news-digest/templates/archive.html.j2 new file mode 100644 index 0000000..3272e01 --- /dev/null +++ b/stacks/news-digest/templates/archive.html.j2 @@ -0,0 +1,83 @@ + + + + + + Daily Digest · Archive + + + + + +
+
+
+ ∞ +

All Editions

+ newest first +
+ + {% if editions %} +
    + {% for e in editions %} +
  1. + + {{ e.date_long }} + + {{ e.edition_short }} + + → + +
  2. + {% endfor %} +
+ {% else %} +

No editions on file yet. Cron fires twice daily at 0800 / 2000.

+ {% endif %} +
+
+ + + + diff --git a/stacks/news-digest/templates/digest.html.j2 b/stacks/news-digest/templates/digest.html.j2 index 9d479d6..749f971 100644 --- a/stacks/news-digest/templates/digest.html.j2 +++ b/stacks/news-digest/templates/digest.html.j2 @@ -17,6 +17,7 @@
№ {{ generated_at.strftime("%j") }} {{ edition|upper }} EDITION + ARCHIVE →
@@ -92,19 +93,24 @@

{{ it.tldr }}

{% endif %}
@@ -150,16 +156,21 @@ {% endif %}
diff --git a/stacks/news-digest/templates/style.css b/stacks/news-digest/templates/style.css index 3857c3a..f313c7b 100644 --- a/stacks/news-digest/templates/style.css +++ b/stacks/news-digest/templates/style.css @@ -131,6 +131,14 @@ a:hover { color: var(--accent); } gap: 18px; font-family: var(--font-mono); } +.archive-link { + color: var(--fg-dim); + font-size: 11px; + letter-spacing: .14em; + border-bottom: 1px dotted var(--fg-faint); + padding-bottom: 1px; +} +.archive-link:hover { color: var(--accent); border-color: var(--accent-dim); } .edition-num { color: var(--fg-faint); font-size: 11px; @@ -400,26 +408,132 @@ a:hover { color: var(--accent); } .item-foot { display: flex; flex-wrap: wrap; - align-items: baseline; - gap: 8px; + align-items: center; + gap: 16px; font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .06em; color: var(--fg-faint); } -.item-link { - color: var(--fg-dim); - border-bottom: 1px dotted var(--fg-faint); - padding-bottom: 1px; +/* Action chips — explicit "go here" buttons. Reddit items get TWO of + these when the post links externally: SOURCE (the linked content) + and REDDIT THREAD (the discussion). Non-Reddit items get just SOURCE. */ +.item-actions { + display: flex; + flex-wrap: wrap; + gap: 8px; } -.item-link:hover { color: var(--accent); border-color: var(--accent-dim); } -.item-meta { font-variant-numeric: tabular-nums; } +.action { + display: inline-flex; + align-items: baseline; + gap: 6px; + padding: 4px 10px; + border: 1px solid var(--rule-bright); + border-radius: 2px; + font-family: var(--font-mono); + font-size: 10.5px; + font-weight: 600; + letter-spacing: .08em; + text-transform: uppercase; + color: var(--fg-dim); + transition: border-color 120ms, color 120ms, background 120ms; +} +.action-arrow { + color: var(--accent-dim); + font-size: 11px; +} +.action-label { color: var(--fg); } +.action-domain { + color: var(--fg-dim); + font-weight: 500; + letter-spacing: .04em; + text-transform: lowercase; +} +.action:hover { + border-color: var(--accent); + background: rgba(66, 220, 209, 0.08); +} +.action:hover .action-arrow, +.action:hover .action-label, +.action:hover .action-domain { color: var(--accent); } + +.action-source { /* default styling above is "source" */ } +.action-thread .action-arrow { color: var(--aus-bright-blue); } +.action-thread:hover { border-color: var(--aus-bright-blue); background: rgba(164, 196, 255, 0.08); } +.action-thread:hover .action-arrow, +.action-thread:hover .action-label { color: var(--aus-bright-blue); } + +.item-meta-row { + display: flex; + align-items: baseline; + gap: 8px; + margin-left: auto; /* pushes timestamp to the right */ +} +.item-meta { font-variant-numeric: tabular-nums; color: var(--fg-faint); } .sep { color: var(--rule-bright); } /* Visual de-emphasize purely meme/skip items if any slip through. */ .item[data-tag="meme"] { opacity: .58; } .item[data-tag="other"] { opacity: .85; } +/* ------------------------------------------------------------------------- + archive page — list of all editions + ------------------------------------------------------------------------- */ + +.archive-list { + list-style: none; + margin: 0; + padding: 0; + display: grid; + gap: 0; + border: 1px solid var(--rule); + border-radius: 2px; + overflow: hidden; +} +.archive-item + .archive-item { border-top: 1px solid var(--rule); } + +.archive-row { + display: grid; + grid-template-columns: 1fr auto auto; + align-items: center; + gap: 24px; + padding: 16px 20px; + background: var(--surface); + transition: background 120ms; +} +.archive-row:hover { + background: var(--surface-hi); + color: var(--fg); +} + +.archive-date { + font-family: var(--font-display); + font-size: 17px; + font-weight: 500; + letter-spacing: -0.005em; + color: var(--fg); + font-variation-settings: "opsz" 36; +} + +.archive-edition { + font-family: var(--font-mono); + font-size: 10.5px; + font-weight: 700; + letter-spacing: .14em; + padding: 3px 9px; + border: 1px solid currentColor; + border-radius: 2px; +} +.archive-edition-am { color: var(--aus-yellow); } +.archive-edition-pm { color: var(--aus-bright-cyan); } + +.archive-arrow { + font-family: var(--font-mono); + color: var(--fg-faint); + font-size: 14px; +} +.archive-row:hover .archive-arrow { color: var(--accent); } + /* ------------------------------------------------------------------------- empty state + colophon ------------------------------------------------------------------------- */