{
  "sha": "399f7f209f89ca355b77b4fe872cd46c52ac1ff7",
  "redacted": false,
  "message": "fix: clarify flash dates, hide duplicate ticker links and correct civil news categories (#382)",
  "author": {
    "login": "nnneeeooo-nnneeeooo",
    "name": "CHI HSUN YANG",
    "email": "125815713+nnneeeooo-nnneeeooo@users.noreply.github.com"
  },
  "committer": {
    "login": "web-flow",
    "name": "GitHub",
    "email": "noreply@github.com"
  },
  "created_at": "2026-10-24T11:24:52Z",
  "files": [
    {
      "filename": "data/articles/2026-10-21T16.json",
      "previous_filename": "",
      "status": "modified",
      "additions": 1,
      "deletions": 1,
      "changes": 2
    },
    {
      "filename": "data/articles/2026-10-22T10.json",
      "previous_filename": "",
      "status": "modified",
      "additions": 1,
      "deletions": 1,
      "changes": 2
    },
    {
      "filename": "pipeline/CONTRACTS.md",
      "previous_filename": "",
      "status": "modified",
      "additions": 5,
      "deletions": 0,
      "changes": 5
    },
    {
      "filename": "pipeline/build.py",
      "previous_filename": "",
      "status": "modified",
      "additions": 1,
      "deletions": 1,
      "changes": 2
    },
    {
      "filename": "templates/base.html",
      "previous_filename": "",
      "status": "modified",
      "additions": 2,
      "deletions": 2,
      "changes": 4
    },
    {
      "filename": "tests/test_news_chronology.py",
      "previous_filename": "",
      "status": "modified",
      "additions": 44,
      "deletions": 1,
      "changes": 45
    }
  ],
  "diff": "diff --git a/data/articles/2026-10-21T16.json b/data/articles/2026-10-21T16.json\\nindex 149d5728e..b2ec7eceb 100644\\n--- a/data/articles/2026-10-21T16.json\\n+++ b/data/articles/2026-10-21T16.json\\n@@ -730,7 +730,7 @@\\n     {\\n       \"id\": \"a-20261021-1637-japan-airlines-cancels-flights-at-nine-a\",\\n       \"publishedUtc\": \"2026-10-21T16:37Z\",\\n-      \"cat\": \"mil\",\\n+      \"cat\": \"ops\",\\n       \"primarySource\": \"Simple Flying\",\\n       \"image\": null,\\n       \"sourceImageCandidates\": [\\ndiff --git a/data/articles/2026-10-22T10.json b/data/articles/2026-10-22T10.json\\nindex 5574c9844..84f4c48d5 100644\\n--- a/data/articles/2026-10-22T10.json\\n+++ b/data/articles/2026-10-22T10.json\\n@@ -3,7 +3,7 @@\\n     {\\n       \"id\": \"a-20261022-1054-taiwan-caa-says-all-domestic-flights-wil\",\\n       \"publishedUtc\": \"2026-10-22T10:54Z\",\\n-      \"cat\": \"mil\",\\n+      \"cat\": \"ops\",\\n       \"primarySource\": \"CAA Taiwan\",\\n       \"image\": null,\\n       \"sourceImageCandidates\": [],\\ndiff --git a/pipeline/CONTRACTS.md b/pipeline/CONTRACTS.md\\nindex bd740cdc6..bf4b08de2 100644\\n--- a/pipeline/CONTRACTS.md\\n+++ b/pipeline/CONTRACTS.md\\n@@ -241,6 +241,11 @@ Rare-aircraft monitor state, committed only when changed:\\n \\n Most recent first, max 10 kept:\\n \\n+Public flash and ticker labels use Taipei time for today's entries and\\n+`MM/DD` for older entries, matching the news feed's date policy. The second\\n+marquee pass is a visual duplicate only: it is hidden from assistive\\n+technology and its links are excluded from keyboard navigation.\\n+\\n ```jsonc\\n [ {\"timeUtc\": \"2026-07-26T05:47Z\", \"hot\": true, \"pinned\": true,\\n    \"zh\": \"...\", \"en\": \"...\",\\ndiff --git a/pipeline/build.py b/pipeline/build.py\\nindex d51243670..c057bffa3 100644\\n--- a/pipeline/build.py\\n+++ b/pipeline/build.py\\n@@ -2256,7 +2256,7 @@ def prep_flashes(raw, known_ids):\\n             # A hidden/archived article must not leave an orphan ticker item.\\n             continue\\n         out.append({\\n-            \"time\": clock_12(dt.astimezone(TPE)),\\n+            \"time\": _honest_time_label(dt, False),\\n             \"hot\": bool(f.get(\"hot\")),\\n             \"pinned\": bool(f.get(\"pinned\")),\\n             \"zh\": zh, \"en\": en,\\ndiff --git a/templates/base.html b/templates/base.html\\nindex f5021babc..b0a562bd4 100644\\n--- a/templates/base.html\\n+++ b/templates/base.html\\n@@ -158,10 +158,10 @@ gtag('config', '{{ ga4_measurement_id }}', {\\n     <div class=\"ticker-window\">\\n       <div class=\"marquee-track\">\\n         {% for pass in range(2) %}\\n-        <span class=\"ticker-pass\">\\n+        <span class=\"ticker-pass\"{% if pass == 1 %} aria-hidden=\"true\"{% endif %}>\\n           {%- for it in ticker_items -%}\\n           {%- if it.url -%}\\n-          <a class=\"ticker-item\" href=\"{{ it.url }}\"{% if it.external %} target=\"_blank\" rel=\"noopener\"{% endif %}><b class=\"ticker-time{% if it.hot %} hot{% endif %}\">{{ it.time }}</b>{% if it.priority|default(false) %}<span class=\"ticker-pin\">{{ t.priorityLabel }}</span>{% endif %}&#8195;{{ it.text }}<span class=\"ticker-go\">↗</span></a>\\n+          <a class=\"ticker-item\" href=\"{{ it.url }}\"{% if pass == 1 %} tabindex=\"-1\"{% endif %}{% if it.external %} target=\"_blank\" rel=\"noopener\"{% endif %}><b class=\"ticker-time{% if it.hot %} hot{% endif %}\">{{ it.time }}</b>{% if it.priority|default(false) %}<span class=\"ticker-pin\">{{ t.priorityLabel }}</span>{% endif %}&#8195;{{ it.text }}<span class=\"ticker-go\">↗</span></a>\\n           {%- else -%}\\n           <span class=\"ticker-item\"><b class=\"ticker-time{% if it.hot %} hot{% endif %}\">{{ it.time }}</b>{% if it.priority|default(false) %}<span class=\"ticker-pin\">{{ t.priorityLabel }}</span>{% endif %}&#8195;{{ it.text }}</span>\\n           {%- endif -%}\\ndiff --git a/tests/test_news_chronology.py b/tests/test_news_chronology.py\\nindex 7159cf662..9d35308da 100644\\n--- a/tests/test_news_chronology.py\\n+++ b/tests/test_news_chronology.py\\n@@ -2,9 +2,13 @@\\n import json\\n import sys\\n import tempfile\\n+from datetime import datetime, timezone\\n from pathlib import Path\\n from unittest.mock import patch\\n \\n+from bs4 import BeautifulSoup\\n+from jinja2 import Environment, FileSystemLoader, StrictUndefined\\n+\\n sys.path.insert(0, str(Path(__file__).resolve().parents[1] / \"pipeline\"))\\n import build\\n \\n@@ -52,6 +56,45 @@ def test_news_chronology():\\n     assert build.prep_article(revised)[\"news_dt\"] == collected[1][\"news_dt\"]\\n \\n \\n+def test_flash_dates_and_marquee_accessibility():\\n+    # Taipei has crossed midnight while UTC is still on the previous day.\\n+    now = datetime(2026, 10, 24, 16, 30, tzinfo=timezone.utc)\\n+    rows = [\\n+        {\"timeUtc\": \"2026-10-24T16:15Z\", \"articleId\": \"today\",\\n+         \"zh\": \"今日快訊\", \"en\": \"Today's flash\", \"hot\": True},\\n+        {\"timeUtc\": \"2026-10-24T15:59Z\", \"articleId\": \"yesterday\",\\n+         \"zh\": \"昨日快訊\", \"en\": \"Yesterday's flash\"},\\n+        {\"timeUtc\": \"2026-10-22T10:54Z\", \"articleId\": \"older\",\\n+         \"zh\": \"舊快訊\", \"en\": \"Older flash\"},\\n+    ]\\n+    with patch.object(build, \"now_utc\", return_value=now):\\n+        flashes = build.prep_flashes(rows, {\"today\", \"yesterday\", \"older\"})\\n+    assert [f[\"time\"] for f in flashes] == [\"12:15 AM\", \"10/24\", \"10/22\"]\\n+    env = Environment(loader=FileSystemLoader(build.TEMPLATES_DIR),\\n+                      undefined=StrictUndefined, autoescape=True)\\n+    for lang in (\"zh\", \"en\"):\\n+        ticker = build.flash_view(flashes, lang, {\"today\"})\\n+        assert ticker[0][\"hot\"] and ticker[0][\"priority\"]\\n+        assert ticker[1][\"url\"] == build.page_url(lang, \"news/yesterday/\")\\n+        context = build.base_ctx(\\n+            lang, \"home\", \"\", title=\"Test\", description=\"Test\", ticker=ticker,\\n+            build={\"date\": \"2026-10-25\", \"utc_hm\": \"4:30 PM\",\\n+                   \"tpe_hm\": \"12:30 AM\", \"stamp\": \"2026-10-24 4:30 PM\"})\\n+        soup = BeautifulSoup(env.get_template(\"base.html\").render(**context),\\n+                             \"html.parser\")\\n+        original, duplicate = soup.select(\".ticker-pass\")\\n+        assert not original.has_attr(\"aria-hidden\")\\n+        assert duplicate[\"aria-hidden\"] == \"true\"\\n+        assert len(original.select(\"a\")) == len(duplicate.select(\"a\")) == 3\\n+        assert all(a.get(\"tabindex\") is None for a in original.select(\"a\"))\\n+        assert all(a.get(\"tabindex\") == \"-1\" for a in duplicate.select(\"a\"))\\n+        assert [a[\"href\"] for a in original.select(\"a\")] == [\\n+            a[\"href\"] for a in duplicate.select(\"a\")]\\n+        assert [t.text for t in original.select(\".ticker-time\")] == [\\n+            \"12:15 AM\", \"10/24\", \"10/22\"]\\n+\\n+\\n if __name__ == \"__main__\":\\n     test_news_chronology()\\n-    print(\"PASS news chronology, source-time fallback and search timestamps\")\\n+    test_flash_dates_and_marquee_accessibility()\\n+    print(\"PASS news chronology, flash dates and marquee accessibility\")"
}
