2026-10-20 TUE UTC 12:42 PM TPE 8:42 PM 每小時自動更新 中文 EN
LIVE 快訊
← 返回網站更新紀錄 Site Changelog

提交原始紀錄

首頁本週嚴重事件可點擊並列出對應事件 (#131) 將首頁統計卡連到事故資料庫本週篩選,並補上事件文章連結、中英文介面、手機版排列及回歸測試。

SHA
4ed4256904c10d238d4ecd1f168c3e764c37ae35
提交時間
作者
nnneeeooo-nnneeeooo
提交者
web-flow

變更檔案 (6)

  • pipeline/build.py modified · +32 / −5
  • static/app.js modified · +25 / −9
  • static/site.css modified · +18 / −0
  • templates/home.html modified · +2 / −2
  • templates/incidents.html modified · +3 / −3
  • tests/test_incident_navigation.py added · +45 / −0

完整差異

@@ -109,6 +109,8 @@ def static_asset_version() -> str:\n         "incKicker": "Incident Database", "incTitle": "事故資料庫",\n         "incSub": "自動彙整全球民航事故與事件,資料來自各國調查機關與專業追報來源,每小時更新。",\n         "incCountLabel": "筆紀錄",\n+        "incWeeklySerious": "本週嚴重事件",\n+        "incReadEvent": "閱讀事件",\n         "incNote": "* 分級依 ICAO Annex 13:事故(Accident)、嚴重事件(Serious incident)、事件(Incident)。",\n         "thDate": "日期", "thSev": "等級", "thType": "機型", "thOp": "營運人",\n         "thPhase": "飛行階段", "thLoc": "地點", "thDesc": "摘要", "thStatus": "狀態", "thSrc": "來源",\n@@ -182,7 +184,7 @@ def static_asset_version() -> str:\n                    "🌐 國際航空產業動態", "🚄 地面與海運交通"],\n         "brSevs": {"fatal": "致命", "serious": "嚴重",\n                    "significant": "重要", "routine": "一般"},\n-        "sevs": ["全部", "事故", "嚴重事件", "事件"],\n+        "sevs": ["本週嚴重事件", "全部", "事故", "嚴重事件", "事件"],\n         "themeOpts": ["亮", "暗"],\n         # production-only keys (not in the design dictionary)\n         "siteName": "SKYTICAL",\n@@ -224,6 +226,8 @@ def static_asset_version() -> str:\n         "incKicker": "Incident Database", "incTitle": "Incident database",\n         "incSub": "Automatically aggregated civil-aviation accidents and incidents, from state investigators and specialist trackers. Updated hourly.",\n         "incCountLabel": "records",\n+        "incWeeklySerious": "Serious events this week",\n+        "incReadEvent": "Read event",\n         "incNote": "* Classes per ICAO Annex 13: Accident, Serious incident, Incident.",\n         "thDate": "Date", "thSev": "Class", "thType": "Aircraft", "thOp": "Operator",\n         "thPhase": "Phase", "thLoc": "Location", "thDesc": "Summary", "thStatus": "Status", "thSrc": "Sources",\n@@ -306,7 +310,7 @@ def static_asset_version() -> str:\n                    "🚄 Ground & maritime transport"],\n         "brSevs": {"fatal": "Fatal", "serious": "Serious",\n                    "significant": "Significant", "routine": "Routine"},\n-        "sevs": ["All", "Accident", "Serious", "Incident"],\n+        "sevs": ["This week", "All", "Accident", "Serious", "Incident"],\n         "themeOpts": ["Light", "Dark"],\n         # production-only keys (not in the design dictionary)\n         "siteName": "SKYTICAL",\n@@ -1560,24 +1564,39 @@ def prep_incidents(raw):\n     return [i for i in raw if isinstance(i, dict) and i.get("date")]\n \n \n-def inc_view(incidents, lang: str):\n+def _is_weekly_serious_incident(incident, cutoff) -> bool:\n+    if incident.get("sev") not in ("acc", "ser"):\n+        return False\n+    try:\n+        return parse_iso(str(incident.get("date"))) >= cutoff\n+    except (TypeError, ValueError):\n+        return False\n+\n+\n+def inc_view(incidents, lang: str, weekly_cutoff, published_ids):\n     rows = []\n     for i in incidents:\n         date = str(i.get("date") or "")\n         disp = date[5:] if re.match(r"\d{4}-\d{2}-\d{2}\Z", date) else date\n         sev = i.get("sev") if isinstance(i.get("sev"), str) else ""\n         srcs = i.get("sources")\n         src = " · ".join(str(s) for s in srcs) if isinstance(srcs, list) else str(srcs or "")\n+        article_id = str(i.get("articleId") or "")\n+        article_url = None\n+        if article_id in published_ids and _ID_RE.fullmatch(article_id):\n+            article_url = page_url(lang, f"news/{article_id}/")\n         rows.append({\n             "d": disp,\n             "sev_key": sev,\n+            "weekly_serious": _is_weekly_serious_incident(i, weekly_cutoff),\n             "sev_label": _bi(SEV.get(sev), lang, sev),\n             "sev_class": SEV.get(sev, {}).get("cls", "tag-neutral"),\n             "ac": str(i.get("aircraft") or "—"),\n             "op": _bi(i.get("operator"), lang, "—"),\n             "ph": _bi(i.get("phase"), lang, "—"),\n             "loc": _bi(i.get("location"), lang, "—"),\n             "desc": _bi(i.get("desc"), lang, ""),\n+            "url": article_url,\n             "st": _bi(ST.get(i.get("status")), lang, str(i.get("status") or "")),\n             "src": src,\n         })\n@@ -2687,6 +2706,7 @@ def base_ctx(lang, page, sub, *, title, description, ticker, build,\n             if BASE_PATH else "/search-aliases.json"\n         ),\n         "radar_url": page_url(lang, "radar/"),\n+        "incidents_week_url": page_url(lang, "incidents/") + "?filter=week",\n         "about_url": page_url(lang, "about/"),\n         "policy_url": page_url(lang, "editorial-policy/"),\n         "changelog_url": page_url(lang, "changelog/"),\n@@ -2917,7 +2937,7 @@ def main() -> int:\n         "stamp": f"{now:%Y-%m-%d} {clock_12(now)}",\n     }\n     cat_keys = ["all", "safety", "reg", "biz", "ops", "mil"]\n-    sev_keys = ["all", "acc", "ser", "inc"]\n+    sev_keys = ["week", "all", "acc", "ser", "inc"]\n     pages = 0\n     failed = 0\n \n@@ -3106,7 +3126,14 @@ def main() -> int:\n                 failed += 1\n                 print(f"build: article {a['id']} ({lang}) failed: {exc}")\n \n-        rows = inc_view(incidents, lang)\n+        try:\n+            weekly_cutoff = (\n+                parse_iso(str(stats_raw.get("updatedUtc")))\n+                - timedelta(days=7)\n+            )\n+        except (TypeError, ValueError):\n+            weekly_cutoff = now - timedelta(days=7)\n+        rows = inc_view(incidents, lang, weekly_cutoff, published_ids)\n         ctx = base_ctx(lang, "incidents", "incidents/",\n                        title=f"{t['siteName']} — {t['incTitle']}",\n                        description=t["incSub"], ticker=ticker, build=build)\n@@ -141,17 +141,33 @@\n   /* — incidents: severity filter over rows ([data-sev]) + live record count — */\n   var sevBtns = Array.prototype.slice.call(document.querySelectorAll("[data-filter-sev]"));\n   var countEl = document.getElementById("inc-count");\n+  function applyIncidentFilter(v, button, updateUrl) {\n+    var n = 0;\n+    sevBtns.forEach(function (x) { x.classList.toggle("active", x === button); });\n+    Array.prototype.forEach.call(document.querySelectorAll("[data-sev]"), function (row) {\n+      var matches = v === "all" ||\n+        (v === "week" ? row.dataset.weeklySerious === "true" : row.dataset.sev === v);\n+      row.classList.toggle("is-hidden", !matches);\n+      if (matches) n += 1;\n+    });\n+    if (countEl) countEl.textContent = String(n);\n+    if (updateUrl && window.history && window.history.replaceState) {\n+      var url = new URL(window.location.href);\n+      if (v === "all") url.searchParams.delete("filter");\n+      else url.searchParams.set("filter", v);\n+      window.history.replaceState(null, "", url.pathname + url.search + url.hash);\n+    }\n+  }\n   sevBtns.forEach(function (b) {\n     b.addEventListener("click", function () {\n-      var v = b.dataset.filterSev;\n-      var n = 0;\n-      sevBtns.forEach(function (x) { x.classList.toggle("active", x === b); });\n-      Array.prototype.forEach.call(document.querySelectorAll("[data-sev]"), function (row) {\n-        var hide = v !== "all" && row.dataset.sev !== v;\n-        row.classList.toggle("is-hidden", hide);\n-        if (!hide) n += 1;\n-      });\n-      if (countEl) countEl.textContent = String(n);\n+      applyIncidentFilter(b.dataset.filterSev, b, true);\n     });\n   });\n+  if (sevBtns.length) {\n+    var requestedFilter = new URLSearchParams(window.location.search).get("filter") || "all";\n+    var initialButton = sevBtns.find(function (b) {\n+      return b.dataset.filterSev === requestedFilter;\n+    }) || sevBtns.find(function (b) { return b.dataset.filterSev === "all"; });\n+    applyIncidentFilter(initialButton.dataset.filterSev, initialButton, false);\n+  }\n })();\n@@ -337,6 +337,14 @@ a.flash-row:hover { color: inherit; }\n   color: inherit;\n   background: color-mix(in srgb, var(--color-accent) 7%, transparent);\n }\n+.incident-article-link,\n+.incident-date-link {\n+  color: inherit;\n+  text-decoration-color: color-mix(in srgb, var(--color-accent) 55%, transparent);\n+  text-underline-offset: 3px;\n+}\n+.incident-article-link:hover,\n+.incident-date-link:hover { color: var(--color-accent); }\n \n /* — Taiwan civil-flight radar — */\n .radar-page { padding: 24px 28px 32px; }\n@@ -642,6 +650,16 @@ a.flash-row:hover { color: inherit; }\n   .seg-cat .seg-btn:nth-child(n + 4) {\n     border-top: 1px solid var(--color-divider);\n   }\n+  .seg-sev {\n+    display: grid;\n+    grid-template-columns: repeat(3, minmax(0, 1fr));\n+    width: 100%;\n+  }\n+  .seg-sev .seg-btn { justify-content: center; }\n+  .seg-sev .seg-btn:nth-child(4) { border-left: 0; }\n+  .seg-sev .seg-btn:nth-child(n + 4) {\n+    border-top: 1px solid var(--color-divider);\n+  }\n   .changelog-page { padding: 24px 14px 48px; }\n   .changelog-summary { grid-template-columns: 1fr; }\n   .changelog-summary > div + div {\n@@ -101,10 +101,10 @@ <h6 style="margin:0 0 12px;">{{ t.today }}</h6>\n           <div style="font-size:24px;font-weight:800;letter-spacing:-.02em;">{{ stats.delay }}</div>\n           <div class="text-muted" style="font-size:11px;">{{ t.statDelay }}</div>\n         </div>\n-        <div style="padding:12px;border-right:1px solid var(--color-divider);">\n+        <a class="radar-stat-link" href="{{ incidents_week_url }}" aria-label="{{ t.incWeeklySerious }}:{{ stats.serious }} {{ t.incCountLabel }}" style="padding:12px;border-right:1px solid var(--color-divider);">\n           <div style="font-size:24px;font-weight:800;letter-spacing:-.02em;">{{ stats.serious }}</div>\n           <div class="text-muted" style="font-size:11px;">{{ t.statSerious }}</div>\n-        </div>\n+        </a>\n         <div style="padding:12px;">\n           <div style="font-size:24px;font-weight:800;letter-spacing:-.02em;">{{ stats.notams }}</div>\n           <div class="text-muted" style="font-size:11px;">{{ t.statNotam }}</div>\n@@ -19,14 +19,14 @@ <h2 style="margin:8px 0 6px;">{{ t.incTitle }}</h2>\n         </tr></thead>\n         <tbody>\n           {% for i in rows %}\n-          <tr data-sev="{{ i.sev_key }}">\n-            <td style="font-weight:700;white-space:nowrap;">{{ i.d }}</td>\n+          <tr data-sev="{{ i.sev_key }}" data-weekly-serious="{{ 'true' if i.weekly_serious else 'false' }}">\n+            <td style="font-weight:700;white-space:nowrap;">{% if i.url %}<a class="incident-date-link" href="{{ i.url }}" aria-label="{{ t.incReadEvent }}:{{ i.desc }}">{{ i.d }}</a>{% else %}{{ i.d }}{% endif %}</td>\n             <td><span class="tag {{ i.sev_class }}">{{ i.sev_label }}</span></td>\n             <td style="white-space:nowrap;">{{ i.ac }}</td>\n             <td>{{ i.op }}</td>\n             <td>{{ i.ph }}</td>\n             <td>{{ i.loc }}</td>\n-            <td class="text-muted">{{ i.desc }}</td>\n+            <td class="text-muted">{% if i.url %}<a class="incident-article-link" href="{{ i.url }}">{{ i.desc }}</a>{% else %}{{ i.desc }}{% endif %}</td>\n             <td style="white-space:nowrap;">{{ i.st }}</td>\n             <td class="text-muted" style="font-size:12px;white-space:nowrap;">{{ i.src }}</td>\n           </tr>\n@@ -0,0 +1,45 @@\n+"""Offline regression checks for weekly serious-incident navigation."""\n+from __future__ import annotations\n+\n+import json\n+import re\n+import sys\n+from pathlib import Path\n+\n+ROOT = Path(__file__).resolve().parent.parent\n+sys.path.insert(0, str(ROOT / "pipeline"))\n+\n+import build  # noqa: E402\n+\n+\n+def main() -> None:\n+    assert build.main() == 0\n+\n+    stats = json.loads(\n+        (ROOT / "data" / "stats.json").read_text(encoding="utf-8"))\n+    expected = stats["seriousThisWeek"]\n+    home = (ROOT / "site" / "index.html").read_text(encoding="utf-8")\n+    en_home = (ROOT / "site" / "en" / "index.html").read_text(encoding="utf-8")\n+    incidents = (ROOT / "site" / "incidents" / "index.html").read_text(encoding="utf-8")\n+    script = (ROOT / "static" / "app.js").read_text(encoding="utf-8")\n+\n+    assert 'href="/incidents/?filter=week"' in home\n+    assert 'href="/en/incidents/?filter=week"' in en_home\n+    assert 'data-filter-sev="week"' in incidents\n+    weekly_rows = re.findall(\n+        r'<tr data-sev="(?:acc|ser)" data-weekly-serious="true">.*?</tr>',\n+        incidents,\n+        re.DOTALL,\n+    )\n+    assert len(weekly_rows) == expected\n+    assert weekly_rows and all(\n+        'class="incident-article-link"' in row for row in weekly_rows)\n+    assert all('class="incident-date-link"' in row for row in weekly_rows)\n+    assert 'new URLSearchParams(window.location.search)' in script\n+    assert 'row.dataset.weeklySerious === "true"' in script\n+\n+    print("test_incident_navigation: OK")\n+\n+\n+if __name__ == "__main__":\n+    main()

你發現了隱藏主題!

8-bit 主題已解鎖。重新整理後,就能在主題選單選擇 8-bit。