{
  "sha": "151cff5874870c847d9741255ba422ab2b3e1ae7",
  "redacted": false,
  "message": "feat(radar): show matching aircraft photos and yellow markers (#384)",
  "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-24T13:30:19Z",
  "files": [
    {
      "filename": ".github/workflows/radar-snapshot.yml",
      "previous_filename": "",
      "status": "modified",
      "additions": 4,
      "deletions": 0,
      "changes": 4
    },
    {
      "filename": "docs/open-source-scouting.md",
      "previous_filename": "",
      "status": "modified",
      "additions": 12,
      "deletions": 0,
      "changes": 12
    },
    {
      "filename": "pipeline/build.py",
      "previous_filename": "",
      "status": "modified",
      "additions": 1,
      "deletions": 0,
      "changes": 1
    },
    {
      "filename": "static/radar-photos.js",
      "previous_filename": "",
      "status": "added",
      "additions": 89,
      "deletions": 0,
      "changes": 89
    },
    {
      "filename": "static/radar.css",
      "previous_filename": "",
      "status": "modified",
      "additions": 9,
      "deletions": 6,
      "changes": 15
    },
    {
      "filename": "static/radar.js",
      "previous_filename": "",
      "status": "modified",
      "additions": 39,
      "deletions": 7,
      "changes": 46
    },
    {
      "filename": "templates/radar.html",
      "previous_filename": "",
      "status": "modified",
      "additions": 1,
      "deletions": 0,
      "changes": 1
    },
    {
      "filename": "tests/test_radar_photos.js",
      "previous_filename": "",
      "status": "added",
      "additions": 69,
      "deletions": 0,
      "changes": 69
    }
  ],
  "diff": "diff --git a/.github/workflows/radar-snapshot.yml b/.github/workflows/radar-snapshot.yml\\nindex de6d499e5..1e1807cb6 100644\\n--- a/.github/workflows/radar-snapshot.yml\\n+++ b/.github/workflows/radar-snapshot.yml\\n@@ -18,7 +18,9 @@ on:\\n       - \"static/radar.js\"\\n       - \"static/radar.css\"\\n       - \"static/radar-aircraft.js\"\\n+      - \"static/radar-photos.js\"\\n       - \"tests/test_radar_aircraft.js\"\\n+      - \"tests/test_radar_photos.js\"\\n       - \"templates/radar.html\"\\n       - \"tests/test_radar.py\"\\n       - \"tests/test_radar_snapshot.py\"\\n@@ -34,6 +36,7 @@ on:\\n       - \"static/radar.js\"\\n       - \"static/radar.css\"\\n       - \"static/radar-aircraft.js\"\\n+      - \"static/radar-photos.js\"\\n       - \"templates/radar.html\"\\n \\n jobs:\\n@@ -55,6 +58,7 @@ jobs:\\n       - name: Run radar regression tests\\n         run: |\\n           node tests/test_radar_aircraft.js\\n+          node tests/test_radar_photos.js\\n           python tests/test_radar_snapshot.py\\n           python tests/test_build_resilience.py\\n           python tests/test_radar.py\\ndiff --git a/docs/open-source-scouting.md b/docs/open-source-scouting.md\\nindex 86ca44767..623609cd1 100644\\n--- a/docs/open-source-scouting.md\\n+++ b/docs/open-source-scouting.md\\n@@ -2,6 +2,18 @@\\n \\n 更新日期：2026-10-23\\n \\n+## 雷達對應實機照片（2026-10-24）\\n+\\n+查證官方 Photo API 文件與三個開源專案；不複製第三方實作。\\n+\\n+| 專案 | 最近推送 | Stars | 語言／授權 | 比較與導入成本 |\\n+| --- | --- | --- | --- | --- |\\n+| [tar1090](https://github.com/wiedehopf/tar1090) | 2026-10-19 | 1,941 | JavaScript／NOASSERTION | 成熟 ADS-B 介面，參考選取航機再顯示資訊的流程；整套導入過重。 |\\n+| [SkyWatcher](https://github.com/Sperryfreak01/SkyWatcher) | 2026-04-24 | 0 | JavaScript／未宣告授權 | 近期功能包含按 hex 查詢照片與攝影者署名；僅比較 API 整合方式，不使用其程式碼。 |\\n+| [contrails](https://github.com/gauravdraj/contrails) | 2026-10-21 | 0 | JavaScript／MIT | 輕量 plane spotting、照片 metadata 快取；不導入其 Worker 與代理層。 |\\n+\\n+採用原生瀏覽器 fetch 與現有 Leaflet popup，僅點選航機時依完整六位 ICAO hex 查詢照片。依 [Planespotters 官方規範](https://www.planespotters.net/photo/api) 保留 API 回傳的原始縮圖／原圖連結 URL、顯示攝影者文字署名；縮圖直接由顯示它的瀏覽器載入，不下載、重存或代理圖片。metadata 僅記憶體快取一小時、無照片十五分鐘；查無照片與失敗均不改用同機型其他實機。官方 hex 端點依已知現役實機查詢，未知 hex 由其資料庫對應註冊號；未加入機型 fallback 參數。\\n+\\n ## 民航雷達介面與機型剪影（2026-10-24）\\n \\n 透過 GitHub 搜尋及官方 API 比較，僅借鑑地圖優先、機型剪影與選取資訊的互動概念，沒有複製第三方程式碼或圖示。\\ndiff --git a/pipeline/build.py b/pipeline/build.py\\nindex 37b89f575..1b317fedf 100644\\n--- a/pipeline/build.py\\n+++ b/pipeline/build.py\\n@@ -107,6 +107,7 @@ def static_asset_version() -> str:\\n         \"radar.css\",\\n         \"radar.js\",\\n         \"radar-aircraft.js\",\\n+        \"radar-photos.js\",\\n     ):\\n         digest.update((STATIC_DIR / name).read_bytes())\\n     return digest.hexdigest()[:10]\\ndiff --git a/static/radar-photos.js b/static/radar-photos.js\\nnew file mode 100644\\nindex 000000000..4dba7a692\\n--- /dev/null\\n+++ b/static/radar-photos.js\\n@@ -0,0 +1,89 @@\\n+/* Planespotters metadata is cached in memory; images load directly from the\\n+ * unchanged provider URL. Never substitute a photo from a different airframe. */\\n+(function (root) {\\n+  \"use strict\";\\n+  const cache = new Map();\\n+  const pending = new Map();\\n+  function safeUrl(value, hosts, prefix) {\\n+    if (typeof value !== \"string\") return false;\\n+    try {\\n+      const url = new URL(value);\\n+      return url.protocol === \"https:\" && !url.username && !url.password &&\\n+        !url.port && hosts.includes(url.hostname) && url.pathname.startsWith(prefix);\\n+    } catch (_) { return false; }\\n+  }\\n+  function normalize(payload) {\\n+    const photo = payload && Array.isArray(payload.photos) && payload.photos[0];\\n+    if (!photo) return null;\\n+    const src = (photo.thumbnail_large || photo.thumbnail || {}).src;\\n+    if (!safeUrl(src, [\"t.plnspttrs.net\", \"cdn.planespotters.net\"], \"/\") ||\\n+        !safeUrl(photo.link, [\"www.planespotters.net\"], \"/photo/\") ||\\n+        typeof photo.photographer !== \"string\" || !photo.photographer.trim()) return null;\\n+    return { src, link: photo.link, photographer: photo.photographer };\\n+  }\\n+  async function fetchPhoto(hex) {\\n+    const key = String(hex || \"\").toLowerCase();\\n+    if (!/^[a-f0-9]{6}$/.test(key)) return null;\\n+    const cached = cache.get(key);\\n+    if (cached && cached.expires > Date.now()) return cached.photo;\\n+    if (pending.has(key)) return pending.get(key);\\n+    const request = (async () => {\\n+      const controller = new AbortController();\\n+      const timeout = setTimeout(() => controller.abort(), 8000);\\n+      try {\\n+        const response = await fetch(`https://api.planespotters.net/pub/photos/hex/${key}`, {\\n+          mode: \"cors\", credentials: \"omit\", signal: controller.signal,\\n+        });\\n+        if (!response.ok) throw new Error(`Photo HTTP ${response.status}`);\\n+        const payload = await response.json();\\n+        if (!payload || !Array.isArray(payload.photos)) throw new Error(\"Invalid photo response\");\\n+        const photo = normalize(payload);\\n+        if (cache.size >= 100) cache.delete(cache.keys().next().value);\\n+        cache.set(key, { photo, expires: Date.now() + (photo ? 3600000 : 900000) });\\n+        return photo;\\n+      } finally { clearTimeout(timeout); }\\n+    })();\\n+    pending.set(key, request);\\n+    try { return await request; } finally { pending.delete(key); }\\n+  }\\n+  async function render(container, hex, label, copy, onLayout = () => {}) {\\n+    if (container.dataset.requested) return;\\n+    container.dataset.requested = \"true\";\\n+    container.setAttribute(\"aria-busy\", \"true\");\\n+    container.textContent = copy.photoLoading;\\n+    try {\\n+      const photo = await fetchPhoto(hex);\\n+      if (!photo) { container.textContent = copy.photoMissing; return; }\\n+      const link = document.createElement(\"a\");\\n+      link.href = photo.link;\\n+      link.target = \"_blank\";\\n+      link.rel = \"noopener\";\\n+      link.setAttribute(\"aria-label\", `${copy.photoOriginal} · ${photo.photographer}`);\\n+      const img = document.createElement(\"img\");\\n+      img.alt = `${label} · ${copy.photoAircraft}`;\\n+      img.decoding = \"async\";\\n+      img.addEventListener(\"error\", () => {\\n+        cache.delete(String(hex).toLowerCase());\\n+        container.textContent = copy.photoError;\\n+        onLayout();\\n+      }, { once: true });\\n+      img.addEventListener(\"load\", () => {\\n+        container.setAttribute(\"aria-busy\", \"false\");\\n+        onLayout();\\n+      }, { once: true });\\n+      img.src = photo.src;\\n+      link.appendChild(img);\\n+      const credit = document.createElement(\"span\");\\n+      credit.className = \"radar-photo-credit\";\\n+      credit.textContent = `© ${photo.photographer} / Planespotters.net`;\\n+      container.replaceChildren(link, credit);\\n+    } catch (_) { container.textContent = copy.photoError; }\\n+    finally {\\n+      container.setAttribute(\"aria-busy\", \"false\");\\n+      onLayout();\\n+    }\\n+  }\\n+  const api = { normalize, fetchPhoto, render };\\n+  if (typeof module !== \"undefined\" && module.exports) module.exports = api;\\n+  else root.SkyticalPhotos = api;\\n+})(typeof window === \"undefined\" ? this : window);\\ndiff --git a/static/radar.css b/static/radar.css\\nindex 8b425eca2..40e0763d2 100644\\n--- a/static/radar.css\\n+++ b/static/radar.css\\n@@ -52,14 +52,12 @@\\n .radar-list-item .radar-callsign-flight { color: var(--color-text); }\\n .radar-list-item > .text-muted, .radar-empty { color: var(--radar-muted); font-size: 11px; }\\n .radar-list-item .radar-list-telemetry { color: var(--color-text); font-size: 11px; font-variant-numeric: tabular-nums; }\\n-.radar-plane-wrap .radar-plane { width: 100%; height: 100%; display: grid; place-items: center; color: var(--radar-accent); transform: rotate(var(--heading)); filter: drop-shadow(0 1px 1px #201e1d80); }\\n-.radar-plane svg { display: block; width: var(--aircraft-size); height: var(--aircraft-size); fill: currentColor; stroke: #fff; stroke-width: 1; stroke-linejoin: round; }\\n-.radar-plane-wrap .radar-plane.ground { color: #73706d; }\\n-.radar-plane-wrap .radar-plane.selected { color: var(--color-text); filter: drop-shadow(0 0 3px #fff); }\\n+.radar-plane-wrap .radar-plane { width: 100%; height: 100%; display: grid; place-items: center; color: #ffd34e; transform: rotate(var(--heading)); filter: drop-shadow(0 1px 1px #201e1d80); }\\n+.radar-plane svg { display: block; width: var(--aircraft-size); height: var(--aircraft-size); fill: currentColor; stroke: #68501a; stroke-width: 1; stroke-linejoin: round; }\\n+.radar-plane-wrap .radar-plane.ground { color: #ffd34e; }\\n+.radar-plane-wrap .radar-plane.selected { color: #ffe99c; filter: drop-shadow(0 0 3px #fff); }\\n .radar-map .leaflet-popup-content-wrapper { border-radius: 10px; padding: 3px; }\\n .radar-map .leaflet-popup-content { margin: 14px 18px; }\\n-.radar-detail-aircraft { background: var(--color-surface); border-radius: 6px; display: grid; place-items: center; height: 82px; margin-bottom: 12px; color: var(--color-text); }\\n-.radar-detail-aircraft svg { width: 68px; height: 68px; fill: currentColor; transform: rotate(38deg); }\\n .radar-popup-title { font-size: 22px; color: var(--color-accent); }\\n .radar-popup-title .radar-callsign-carrier { color: var(--color-accent); }\\n .radar-popup-row { padding: 5px 0; border-bottom: 1px solid var(--color-divider); }\\n@@ -101,3 +99,8 @@\\n .radar-popup-title .radar-callsign-flight { color: var(--color-text); }\\n [data-theme=\"dark\"] .radar-state.live, [data-theme=\"8bit\"] .radar-state.live { color: #79dfba; }\\n [data-theme=\"8bit\"] .radar-page { --radar-on-accent: var(--color-bg); }\\n+\\n+.radar-aircraft-photo { width: 260px; min-height: 150px; margin-bottom: 12px; background: var(--color-surface); color: var(--radar-muted); border-radius: 6px; overflow: hidden; font-size: 12px; display: flex; flex-direction: column; justify-content: center; text-align: center; }\\n+.radar-aircraft-photo a { display: block; }\\n+.radar-aircraft-photo img { display: block; width: 100%; height: 165px; object-fit: contain; }\\n+.radar-photo-credit { display: block; padding: 6px 8px; font-size: 10px; line-height: 1.4; }\\ndiff --git a/static/radar.js b/static/radar.js\\nindex b17ed34ac..b33fb5cdf 100644\\n--- a/static/radar.js\\n+++ b/static/radar.js\\n@@ -22,6 +22,9 @@\\n       cooldown: (seconds) => `請於 ${seconds} 秒後再手動更新`,\\n       callsign: \"航班代碼\", airline: \"航空公司\", type: \"機型\",\\n       route: \"起迄點\",\\n+      photoLoading: \"正在載入實機照片…\", photoMissing: \"暫無這架飛機的照片\",\\n+      photoError: \"實機照片暫時無法載入\", photoOriginal: \"查看原始照片\",\\n+      photoAircraft: \"對應實機照片\",\\n       altitude: \"高度\", speed: \"地速\", heading: \"航向\",\\n       vertical: \"垂直速率\", freshness: \"位置新鮮度\",\\n       ground: \"地面\", unknown: \"未提供\", feet: \"呎\", knots: \"節\",\\n@@ -38,6 +41,9 @@\\n       cooldown: (seconds) => `Manual refresh available in ${seconds}s`,\\n       callsign: \"Flight code\", airline: \"Airline\", type: \"Aircraft\",\\n       route: \"Route\",\\n+      photoLoading: \"Loading aircraft photo…\", photoMissing: \"No photo available for this aircraft\",\\n+      photoError: \"Aircraft photo is temporarily unavailable\", photoOriginal: \"View original photo\",\\n+      photoAircraft: \"Photo of this aircraft\",\\n       altitude: \"Altitude\", speed: \"Ground speed\", heading: \"Heading\",\\n       vertical: \"Vertical rate\", freshness: \"Position age\",\\n       ground: \"Ground\", unknown: \"Not reported\", feet: \"ft\", knots: \"kt\",\\n@@ -426,10 +432,10 @@\\n     box.className = \"radar-popup\";\\n     const title = document.createElement(\"strong\");\\n     title.className = \"radar-popup-title\";\\n-    const silhouette = document.createElement(\"div\");\\n-    silhouette.className = \"radar-detail-aircraft\";\\n-    silhouette.innerHTML = aircraftSymbols.svg(row.typeCode);\\n-    box.appendChild(silhouette);\\n+    const photo = document.createElement(\"div\");\\n+    photo.className = \"radar-aircraft-photo\";\\n+    photo.textContent = copy.photoLoading;\\n+    box.appendChild(photo);\\n     renderCallsign(title, displayCallsign(row) || copy.noCallsign);\\n     box.appendChild(title);\\n     const route = formatRoute(row);\\n@@ -462,6 +468,21 @@\\n     });\\n   }\\n \\n+  function fitFlightPopup(marker) {\\n+    if (!marker.isPopupOpen()) return;\\n+    const popup = marker.getPopup();\\n+    popup.update();\\n+    const card = popup.getElement().getBoundingClientRect();\\n+    const bounds = map.getContainer().getBoundingClientRect();\\n+    const top = Math.max(bounds.top, 0) + 16;\\n+    const bottom = Math.min(bounds.bottom, window.innerHeight) - 16;\\n+    const left = Math.max(bounds.left, 0) + 8;\\n+    const right = Math.min(bounds.right, window.innerWidth) - 8;\\n+    const dx = card.left < left ? card.left - left : Math.max(0, card.right - right);\\n+    const dy = card.top < top ? card.top - top : Math.max(0, card.bottom - bottom);\\n+    if (dx || dy) map.panBy([dx, dy], { animate: false });\\n+  }\\n+\\n   function rowMatches(row, query, airborneOnly) {\\n     if (airborneOnly && row.altitude === \"ground\") return false;\\n     if (!query) return true;\\n@@ -506,7 +527,11 @@\\n       const tooltip = document.createElement(\"span\");\\n       tooltip.textContent = visibleCallsign || row.typeCode || copy.noCallsign;\\n       marker.bindTooltip(tooltip, { direction: \"top\", offset: [0, -10] });\\n-      marker.bindPopup(popupFor(row), { minWidth: 230 });\\n+      marker.bindPopup(popupFor(row), {\\n+        minWidth: 260,\\n+        maxHeight: Math.max(200, Math.min(440, window.innerHeight - 240)),\\n+        autoPan: false,\\n+      });\\n \\n       const item = document.createElement(\"button\");\\n       item.type = \"button\";\\n@@ -525,10 +550,17 @@\\n       telemetry.textContent = `${formatAltitude(row)}  /  ${formatNumber(row.speed, copy.knots)}`;\\n       item.appendChild(telemetry);\\n       selections.push({ row, marker, item });\\n-      marker.on(\"popupopen\", () => selectFlight(row.key));\\n+      marker.on(\"popupopen\", () => {\\n+        selectFlight(row.key);\\n+        const photo = marker.getPopup().getContent().querySelector(\".radar-aircraft-photo\");\\n+        void window.SkyticalPhotos.render(photo, row.key,\\n+          [visibleCallsign, row.typeCode].filter(Boolean).join(\" · \"), copy,\\n+          () => fitFlightPopup(marker));\\n+        fitFlightPopup(marker);\\n+      });\\n       item.addEventListener(\"click\", () => {\\n         selectFlight(row.key);\\n-        map.setView([row.lat, row.lon], Math.max(map.getZoom(), 9));\\n+        map.setView([row.lat, row.lon], Math.max(map.getZoom(), 9), { animate: false });\\n         marker.openPopup();\\n       });\\n       listEl.appendChild(item);\\ndiff --git a/templates/radar.html b/templates/radar.html\\nindex c4bbbcc8b..8b5e5bfb2 100644\\n--- a/templates/radar.html\\n+++ b/templates/radar.html\\n@@ -95,5 +95,6 @@\\n         integrity=\"sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=\"\\n         crossorigin=\"\" defer></script>\\n <script src=\"{{ base }}/assets/radar-aircraft.js?v={{ asset_version }}\" defer></script>\\n+<script src=\"{{ base }}/assets/radar-photos.js?v={{ asset_version }}\" defer></script>\\n <script src=\"{{ base }}/assets/radar.js?v={{ asset_version }}\" defer></script>\\n {% endblock %}\\ndiff --git a/tests/test_radar_photos.js b/tests/test_radar_photos.js\\nnew file mode 100644\\nindex 000000000..9704a8da1\\n--- /dev/null\\n+++ b/tests/test_radar_photos.js\\n@@ -0,0 +1,69 @@\\n+\"use strict\";\\n+const assert = require(\"node:assert/strict\");\\n+const photos = require(\"../static/radar-photos.js\");\\n+const payload = { photos: [{\\n+  thumbnail_large: { src: \"https://t.plnspttrs.net/123/photo_280.jpg?original=1\" },\\n+  link: \"https://www.planespotters.net/photo/123/airframe?utm_source=api\",\\n+  photographer: \"Test photographer\",\\n+}] };\\n+\\n+(async () => {\\n+  const normalized = photos.normalize(payload);\\n+  assert.equal(normalized.src, payload.photos[0].thumbnail_large.src);\\n+  assert.equal(normalized.link, payload.photos[0].link);\\n+  assert.equal(photos.normalize({ photos: [] }), null);\\n+  for (const src of [\"javascript:alert(1)\", \"http://t.plnspttrs.net/a.jpg\",\\n+    \"https://t.plnspttrs.net.evil.test/a.jpg\", \"https://user@t.plnspttrs.net/a.jpg\"]) {\\n+    assert.equal(photos.normalize({ photos: [{ ...payload.photos[0], thumbnail_large: { src } }] }), null);\\n+  }\\n+  assert.equal(photos.normalize({ photos: [{ ...payload.photos[0], photographer: \"\" }] }), null);\\n+  assert.equal(photos.normalize({ photos: [{ ...payload.photos[0], link: \"https://evil.test/photo/123\" }] }), null);\\n+\\n+  let calls = [];\\n+  global.fetch = async (url, options) => {\\n+    calls.push(url);\\n+    assert.equal(options.credentials, \"omit\");\\n+    return { ok: true, json: async () => payload };\\n+  };\\n+  const [a, b] = await Promise.all([photos.fetchPhoto(\"789275\"), photos.fetchPhoto(\"789275\")]);\\n+  assert.equal(calls.length, 1, \"Concurrent opens must share one request\");\\n+  assert.equal(calls[0], \"https://api.planespotters.net/pub/photos/hex/789275\");\\n+  assert.deepEqual(a, b);\\n+  await photos.fetchPhoto(\"789275\");\\n+  assert.equal(calls.length, 1, \"Cached metadata must avoid another request\");\\n+  await photos.fetchPhoto(\"ABC123\");\\n+  assert.ok(calls[1].endsWith(\"/abc123\"));\\n+  await photos.fetchPhoto(\"~abc123\");\\n+  assert.equal(calls.length, 2, \"Invalid identities must never reach the provider\");\\n+\\n+  global.fetch = async () => ({ ok: true, json: async () => ({ photos: [] }) });\\n+  assert.equal(await photos.fetchPhoto(\"123456\"), null, \"Missing photo must not use a different airframe\");\\n+  global.fetch = async () => ({ ok: false, status: 429 });\\n+  await assert.rejects(photos.fetchPhoto(\"111111\"), /429/);\\n+  global.fetch = async () => ({ ok: true, json: async () => payload });\\n+  assert.ok(await photos.fetchPhoto(\"111111\"), \"Temporary failures must remain retryable\");\\n+\\n+  class Element {\\n+    constructor() { this.dataset = {}; this.events = {}; }\\n+    setAttribute() {}\\n+    appendChild(child) { this.child = child; }\\n+    replaceChildren(...children) { this.children = children; }\\n+    addEventListener(event, handler) { this.events[event] = handler; }\\n+  }\\n+  global.document = { createElement: () => new Element() };\\n+  const copy = { photoLoading: \"loading\", photoMissing: \"missing\", photoError: \"error\",\\n+    photoOriginal: \"original\", photoAircraft: \"aircraft\" };\\n+  const container = new Element();\\n+  await photos.render(container, \"789275\", \"CX844\", copy);\\n+  const [link, credit] = container.children;\\n+  assert.equal(link.href, normalized.link);\\n+  assert.equal(link.child.src, normalized.src);\\n+  assert.equal(link.rel, \"noopener\");\\n+  assert.ok(credit.textContent.includes(\"Test photographer\"));\\n+  link.child.events.error();\\n+  assert.equal(container.textContent, \"error\", \"Broken image must become a clear status\");\\n+  const missing = new Element();\\n+  await photos.render(missing, \"123456\", \"missing\", copy);\\n+  assert.equal(missing.textContent, \"missing\");\\n+  console.log(\"radar photos: exact identity, cache, URL safety, credit and failure states passed\");\\n+})().catch((error) => { console.error(error); process.exitCode = 1; });"
}
