{
  "sha": "1b525576bf182fbf18ec4698918136bccf03fdf0",
  "message": "Improve incident database with responsive event cards (#301)",
  "author": {
    "login": "nnneeeooo-nnneeeooo",
    "name": "",
    "email": ""
  },
  "committer": {
    "login": "web-flow",
    "name": "",
    "email": ""
  },
  "created_at": "2026-10-13T13:25:38Z",
  "files": [
    {
      "filename": "static/app.js",
      "previous_filename": "",
      "status": "modified",
      "additions": 6,
      "deletions": 1,
      "changes": 7
    },
    {
      "filename": "static/site.css",
      "previous_filename": "",
      "status": "modified",
      "additions": 65,
      "deletions": 0,
      "changes": 65
    },
    {
      "filename": "templates/incidents.html",
      "previous_filename": "",
      "status": "modified",
      "additions": 37,
      "deletions": 34,
      "changes": 71
    },
    {
      "filename": "tests/test_incident_navigation.py",
      "previous_filename": "",
      "status": "modified",
      "additions": 1,
      "deletions": 1,
      "changes": 2
    }
  ],
  "diff": "@@ -153,14 +153,19 @@\\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+    sevBtns.forEach(function (x) {\\n+      x.classList.toggle(\"active\", x === button);\\n+      x.setAttribute(\"aria-pressed\", String(x === button));\\n+    });\\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+    var emptyEl = document.getElementById(\"inc-empty\");\\n+    if (emptyEl) emptyEl.classList.toggle(\"is-hidden\", n > 0);\\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@@ -406,6 +406,71 @@ a.flash-row:hover .flash-text { color: var(--color-accent); }\\n .incident-article-link:hover,\\n .incident-date-link:hover { color: var(--color-accent); }\\n \\n+/* — incident database: readable summaries and compact supporting details — */\\n+.incidents-page { padding: 36px 32px 56px; }\\n+.incidents-header { max-width: 760px; margin-bottom: 28px; }\\n+.incidents-header h1 { margin: 8px 0 12px; font-size: clamp(30px, 4vw, 44px); line-height: 1.2; }\\n+.incidents-header p { margin: 0; font-size: 14px; line-height: 1.8; }\\n+.incidents-toolbar {\\n+  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;\\n+  gap: 16px; padding: 18px 0; border-top: 1px solid var(--color-divider);\\n+}\\n+.incidents-filters { display: flex; flex-wrap: wrap; gap: 8px; }\\n+.incidents-filters .seg-btn {\\n+  min-height: 40px; padding: 9px 15px; border: 1px solid var(--color-divider);\\n+  border-radius: 6px; font-size: 13px; white-space: nowrap;\\n+}\\n+.incidents-filters .seg-btn.active { border-color: var(--color-accent); }\\n+.incidents-filters .seg-btn:focus-visible,\\n+.incident-card a:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 4px; }\\n+.incidents-count { font-size: 13px; color: color-mix(in srgb, var(--color-text) 68%, transparent); }\\n+.incidents-count strong { color: var(--color-text); font-size: 20px; margin-right: 4px; }\\n+.incidents-list { display: grid; gap: 16px; }\\n+.incident-card {\\n+  display: grid; grid-template-columns: 100px minmax(0, 1fr); gap: 24px;\\n+  padding: 24px; border: 1px solid color-mix(in srgb, var(--color-text) 18%, transparent);\\n+  border-radius: 10px; background: color-mix(in srgb, var(--color-bg) 65%, var(--color-surface));\\n+  box-shadow: 0 2px 6px color-mix(in srgb, var(--color-text) 3%, transparent);\\n+}\\n+.incident-date { display: flex; flex-direction: column; align-items: flex-start; gap: 8px; }\\n+.incident-label, .incident-details dt {\\n+  font-size: 11px; font-weight: 500; color: color-mix(in srgb, var(--color-text) 65%, transparent);\\n+}\\n+.incident-date-link, .incident-date-value { font-size: 21px; font-weight: 800; line-height: 1.3; }\\n+.incident-date .tag { white-space: nowrap; font-size: 11px; }\\n+.incident-body { min-width: 0; }\\n+.incident-summary { margin: 0 0 20px; font-size: 18px; font-weight: 700; line-height: 1.8; overflow-wrap: anywhere; }\\n+.incident-summary .incident-article-link { text-decoration: none; }\\n+.incident-go { color: var(--color-accent); white-space: nowrap; }\\n+.incident-details { display: grid; grid-template-columns: 1fr 1fr 1fr 1.5fr; gap: 20px; margin: 0; }\\n+.incident-details div { min-width: 0; }\\n+.incident-details dt { margin-bottom: 5px; }\\n+.incident-details dd { margin: 0; font-size: 13px; line-height: 1.7; overflow-wrap: anywhere; }\\n+.incident-footer {\\n+  display: flex; flex-wrap: wrap; justify-content: space-between; gap: 10px 24px;\\n+  margin-top: 20px; padding-top: 14px; border-top: 1px solid color-mix(in srgb, var(--color-text) 12%, transparent);\\n+  font-size: 12px; line-height: 1.7;\\n+}\\n+.incident-footer .incident-label { margin-right: 8px; }\\n+.incident-source { overflow-wrap: anywhere; }\\n+.incidents-note { margin: 24px 0 0; font-size: 12px; line-height: 1.8; }\\n+.incidents-empty { margin: 0; padding: 40px 20px; text-align: center; border: 1px dashed var(--color-divider); border-radius: 10px; }\\n+@media (max-width: 900px) {\\n+  .incident-details { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px 24px; }\\n+}\\n+@media (max-width: 600px) {\\n+  .incidents-page { padding: 28px 16px 40px; }\\n+  .incidents-header { margin-bottom: 20px; }\\n+  .incidents-toolbar { gap: 12px; }\\n+  .incidents-filters { gap: 6px; width: 100%; }\\n+  .incidents-filters .seg-btn { padding: 9px 12px; }\\n+  .incident-card { grid-template-columns: minmax(0, 1fr); gap: 16px; padding: 20px 16px; }\\n+  .incident-date { flex-direction: row; flex-wrap: wrap; align-items: center; gap: 10px; }\\n+  .incident-date-link, .incident-date-value { font-size: 18px; }\\n+  .incident-summary { font-size: 17px; margin-bottom: 16px; }\\n+  .incident-footer { flex-direction: column; gap: 6px; margin-top: 16px; }\\n+}\\n+\\n /* — Taiwan civil-flight radar — */\\n .radar-page { padding: 24px 28px 32px; }\\n .radar-header {\\n@@ -1,41 +1,44 @@\\n {% extends \"base.html\" %}\\n {% block content %}\\n-  <div style=\"padding:28px 20px 64px;\">\\n-    <span class=\"card-kicker\">{{ t.incKicker }}</span>\\n-    <h2 style=\"margin:8px 0 6px;\">{{ t.incTitle }}</h2>\\n-    <p class=\"text-muted\" style=\"font-size:14px;max-width:640px;\">{{ t.incSub }}</p>\\n-    <div style=\"display:flex;justify-content:space-between;align-items:center;margin:18px 0 0;flex-wrap:wrap;gap:10px;\">\\n-      <span class=\"seg seg-sev\">\\n+  <section class=\"incidents-page\" aria-labelledby=\"incidents-title\">\\n+    <header class=\"incidents-header\">\\n+      <span class=\"card-kicker\">{{ t.incKicker }}</span>\\n+      <h1 id=\"incidents-title\">{{ t.incTitle }}</h1>\\n+      <p class=\"text-muted\">{{ t.incSub }}</p>\\n+    </header>\\n+    <div class=\"incidents-toolbar\">\\n+      <div class=\"incidents-filters\" role=\"group\" aria-label=\"{{ t.thSev }}\">\\n         {% for key, label in sev_seg %}\\n-        <button type=\"button\" class=\"seg-btn{% if key == 'all' %} active{% endif %}\" data-filter-sev=\"{{ key }}\">{{ label }}</button>\\n+        <button type=\"button\" class=\"seg-btn{% if key == 'all' %} active{% endif %}\" data-filter-sev=\"{{ key }}\" aria-pressed=\"{{ 'true' if key == 'all' else 'false' }}\">{{ label }}</button>\\n         {% endfor %}\\n-      </span>\\n-      <span class=\"text-muted\" style=\"font-size:12.5px;\"><span id=\"inc-count\">{{ count }}</span> {{ t.incCountLabel }}</span>\\n+      </div>\\n+      <span class=\"incidents-count\" role=\"status\"><strong id=\"inc-count\">{{ count }}</strong> {{ t.incCountLabel }}</span>\\n     </div>\\n-    <div style=\"overflow-x:auto;\">\\n-      <table class=\"table\" style=\"margin-top:14px;\">\\n-        <thead><tr>\\n-          <th>{{ t.thDate }}</th><th>{{ t.thSev }}</th><th>{{ t.thType }}</th><th>{{ t.thOp }}</th><th>{{ t.thPhase }}</th><th>{{ t.thLoc }}</th><th>{{ t.thDesc }}</th><th>{{ t.thStatus }}</th><th>{{ t.thSrc }}</th>\\n-        </tr></thead>\\n-        <tbody>\\n-          {% for i in rows %}\\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\">{% 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-          {% else %}\\n-          <tr><td colspan=\"9\" class=\"text-muted\">{{ t.awaiting }}</td></tr>\\n-          {% endfor %}\\n-        </tbody>\\n-      </table>\\n+    <div class=\"incidents-list\">\\n+      {% for i in rows %}\\n+      <article data-sev=\"{{ i.sev_key }}\" data-weekly-serious=\"{{ 'true' if i.weekly_serious else 'false' }}\" class=\"incident-card\">\\n+        <div class=\"incident-date\">\\n+          <span class=\"incident-label\">{{ t.thDate }}</span>\\n+          {% if i.url %}<a class=\"incident-date-link\" href=\"{{ i.url }}\" aria-label=\"{{ t.incReadEvent }}：{{ i.desc }}\">{{ i.d }}</a>{% else %}<span class=\"incident-date-value\">{{ i.d }}</span>{% endif %}\\n+          <span class=\"tag {{ i.sev_class }}\">{{ i.sev_label }}</span>\\n+        </div>\\n+        <div class=\"incident-body\">\\n+          <h2 class=\"incident-summary\">{% if i.url %}<a class=\"incident-article-link\" href=\"{{ i.url }}\">{{ i.desc }}<span class=\"incident-go\" aria-hidden=\"true\"> ↗</span></a>{% else %}{{ i.desc }}{% endif %}</h2>\\n+          <dl class=\"incident-details\">\\n+            <div><dt>{{ t.thType }}</dt><dd>{{ i.ac }}</dd></div>\\n+            <div><dt>{{ t.thOp }}</dt><dd>{{ i.op }}</dd></div>\\n+            <div><dt>{{ t.thPhase }}</dt><dd>{{ i.ph }}</dd></div>\\n+            <div><dt>{{ t.thLoc }}</dt><dd>{{ i.loc }}</dd></div>\\n+          </dl>\\n+          <div class=\"incident-footer\">\\n+            <span class=\"incident-status\"><span class=\"incident-label\">{{ t.thStatus }}</span> {{ i.st }}</span>\\n+            <span class=\"incident-source\"><span class=\"incident-label\">{{ t.thSrc }}</span> {{ i.src }}</span>\\n+          </div>\\n+        </div>\\n+      </article>\\n+      {% endfor %}\\n+      <p id=\"inc-empty\" class=\"incidents-empty{% if rows %} is-hidden{% endif %}\" role=\"status\">{{ t.awaiting }}</p>\\n     </div>\\n-    <p class=\"text-muted\" style=\"font-size:12px;margin-top:14px;\">{{ t.incNote }}</p>\\n-  </div>\\n+    <p class=\"incidents-note text-muted\">{{ t.incNote }}</p>\\n+  </section>\\n {% endblock %}\\n@@ -27,7 +27,7 @@ def main() -> None:\\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+        r'<article data-sev=\"(?:acc|ser)\" data-weekly-serious=\"true\"[^>]*>.*?</article>',\\n         incidents,\\n         re.DOTALL,\\n     )"
}
