From 269f78c32b784f8745ba0a187f529c8d9828ac48 Mon Sep 17 00:00:00 2001 From: "jampick@microsoft.com" Date: Wed, 8 Apr 2026 10:10:42 -0700 Subject: [PATCH] Add clickable links to history items in web UI Store the original article URL from RSS feeds in history entries so users can click through to read the full story from the Pi web UI. Links also persist through quiet-hours and retry queues. Fixes #74 Co-Authored-By: Claude Opus 4.6 (1M context) --- pi/webapp/templates/history.html | 12 +++++++++++- printpulse/watch.py | 6 ++++++ tests/test_watch.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/pi/webapp/templates/history.html b/pi/webapp/templates/history.html index 23fbaa0..eb6711f 100644 --- a/pi/webapp/templates/history.html +++ b/pi/webapp/templates/history.html @@ -125,6 +125,16 @@ font-size: 0.9em; } + .history-title a { + color: var(--primary); + text-decoration: underline; + } + + .history-title a:hover { + color: var(--bg); + background-color: var(--primary); + } + .history-meta { color: var(--help); font-size: 0.75em; @@ -195,7 +205,7 @@

[ PRINTPULSE ]

{% for item in items %}
-
> {{ item.title }}
+
> {% if item.link %}{{ item.title }}{% else %}{{ item.title }}{% endif %}
{% if item.source %}{{ item.source }} — {% endif %}{{ item.timestamp }}
diff --git a/printpulse/watch.py b/printpulse/watch.py index dc60ace..f741909 100644 --- a/printpulse/watch.py +++ b/printpulse/watch.py @@ -68,6 +68,7 @@ def fetch_new_items(feed_url: str, max_items: int = 3) -> list[dict]: "id": entry_id, "title": title, "summary": entry.get("summary", ""), + "link": getattr(entry, "link", ""), "_entry": entry, # raw feedparser entry for image extraction "_source": feed_title, }) @@ -107,6 +108,7 @@ def _append_history(items: list[dict]): history.append({ "title": item["title"], "source": item.get("_source", ""), + "link": item.get("link", ""), "timestamp": now_str, }) # Trim to max @@ -144,6 +146,7 @@ def _add_to_retry(item: dict): "id": item.get("id", ""), "title": item.get("title", ""), "summary": item.get("summary", ""), + "link": item.get("link", ""), "_source": item.get("_source", ""), "attempts": 1, }) @@ -183,6 +186,7 @@ def _enqueue_quiet_items(items: list[dict]): "id": item.get("id", ""), "title": item.get("title", ""), "summary": item.get("summary", ""), + "link": item.get("link", ""), "_source": item.get("_source", ""), }) existing_ids.add(item.get("id")) @@ -326,6 +330,7 @@ def run_watch_loop(feed_urls: list[str], interval: int, max_prints: int, fake_item = { "id": q_item["id"], "title": title, "summary": q_item.get("summary", ""), + "link": q_item.get("link", ""), "_source": source, } try: @@ -361,6 +366,7 @@ def run_watch_loop(feed_urls: list[str], interval: int, max_prints: int, fake_item = { "id": r_item["id"], "title": title, "summary": r_item.get("summary", ""), + "link": r_item.get("link", ""), "_source": r_item.get("_source", ""), } try: diff --git a/tests/test_watch.py b/tests/test_watch.py index 31f30b3..b2203b9 100644 --- a/tests/test_watch.py +++ b/tests/test_watch.py @@ -9,11 +9,13 @@ from printpulse.watch import ( QUIET_QUEUE_FILE, + _append_history, _enqueue_quiet_items, _filter_quiet_queue_latest, _is_in_quiet_hours, _load_quiet_queue, _save_quiet_queue, + load_history, ) @@ -125,6 +127,12 @@ def test_enqueue_adds_new_items(self): assert queue[0]["id"] == "a1" assert queue[1]["id"] == "a2" + def test_enqueue_preserves_link(self): + items = [{"id": "link1", "title": "Story", "summary": "", "link": "https://example.com/story", "_source": "Feed"}] + _enqueue_quiet_items(items) + queue = _load_quiet_queue() + assert queue[0]["link"] == "https://example.com/story" + def test_enqueue_skips_duplicates(self): item = {"id": "dup", "title": "Dupe Story", "summary": "", "_source": "X"} _enqueue_quiet_items([item]) @@ -195,3 +203,24 @@ def test_empty_source_treated_as_one_group(self): result = _filter_quiet_queue_latest(queue) assert len(result) == 1 assert result[0]["id"] == "2" + + +class TestHistoryLink: + """Test that history entries persist the article link.""" + + @pytest.fixture(autouse=True) + def _use_tmp_history(self, monkeypatch, tmp_path): + monkeypatch.setattr("printpulse.watch.HISTORY_FILE", str(tmp_path / "history.json")) + + def test_append_history_stores_link(self): + items = [{"title": "Breaking News", "_source": "BBC", "link": "https://bbc.com/story"}] + _append_history(items) + history = load_history() + assert len(history) == 1 + assert history[0]["link"] == "https://bbc.com/story" + + def test_append_history_missing_link_defaults_empty(self): + items = [{"title": "No Link Item", "_source": "Manual"}] + _append_history(items) + history = load_history() + assert history[0]["link"] == ""