PageSourceSearch

https://warriorlifepodcast.com/demo/live-decodes.js?v=3f144db95188

js warriorlifepodcast.com collected 2026-10-09 01:09:47 UTC 36,280 bytes, 995 lines download raw bytes

1/* DCC_CONTINUOUS_FT8_DECODER */
2(() => {
3  "use strict";
4
5  const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
6  let requesting = false;
7  let latestData = null;
8  let lastMapSignature = "";
9  let lastMappedRoster = [];
10  let lastMapUpdateAt = 0;
11  let mapViewMode = "auto";
12  let demoView = null;
13  let hideWorked = false;
14  let hideCq = false;
15  let clearFrequencySelected = false;
16  let clearFrequencyRequesting = false;
17  let clearFrequencyButton = null;
18  let lastLayoutSignature = "";
19  const mapSignalFilters = {
20    strong: true,
21    workable: true,
22    weak: true
23  };
24
25  const MAP_WIDTH = 1000;
26  const MAP_HEIGHT = 500;
27  const MAP_RETENTION_MS = 120000;
28
29  // Tell the radio-status overlay that this script is the sole owner of the
30  // live map.  Without this flag the two refresh loops alternate the label
31  // between the grid count and "WAITING FOR GRIDS" several times per second.
32  window.dccLiveDecoderOwnsMap = true;
33
34  function bindDemoSelection(element, station) {
35    if (window.dccDemoMode !== true) return;
36    element.dataset.demoCall = station.call || "";
37    element.addEventListener("click", () => window.dispatchEvent(
38      new CustomEvent("dcc-demo-select", {detail: station})
39    ));
40    element.addEventListener("dblclick", () => window.dispatchEvent(
41      new CustomEvent("dcc-demo-answer-cq", {detail: station})
42    ));
43  }
44
45  function enableDemoNavigation(map, svg) {
46    const controls = document.createElement("div");
47    controls.className = "dcc-demo-map-navigation";
48    const current = () => {
49      const [x, y, width, height] = svg.getAttribute("viewBox").split(/\s+/).map(Number);
50      return {x, y, width, height};
51    };
52    const apply = view => {
53      demoView = {...view, x: Math.max(0, Math.min(MAP_WIDTH - view.width, view.x)),
54        y: Math.max(0, Math.min(MAP_HEIGHT - view.height, view.y))};
55      lastMapSignature = "";
56      renderMap(latestData?.roster, latestData?.status);
57    };
58    for (const [label, factor, title] of [["+", .75, "Zoom in"], ["−", 1 / .75, "Zoom out"]]) {
59      const button = document.createElement("button");
60      button.type = "button"; button.textContent = label; button.setAttribute("aria-label", title);
61      button.addEventListener("click", () => {
62        const view = current();
63        const scale = Math.max(150 / view.width, Math.min(MAP_WIDTH / view.width, MAP_HEIGHT / view.height, factor));
64        const width = view.width * scale, height = view.height * scale;
65        apply({x: view.x + (view.width - width) / 2, y: view.y + (view.height - height) / 2, width, height});
66      });
67      controls.appendChild(button);
68    }
69    map.appendChild(controls);
70    let drag;
71    map.addEventListener("pointerdown", event => {
72      if (event.button !== 0 || event.target.closest("button, input, a")) return;
73      drag = {x: event.clientX, y: event.clientY, view: current(), bounds: map.getBoundingClientRect()};
74      map.setPointerCapture(event.pointerId);
75    });
76    map.addEventListener("pointermove", event => {
77      if (!drag) return;
78      apply({...drag.view,
79        x: drag.view.x - (event.clientX - drag.x) / drag.bounds.width * drag.view.width,
80        y: drag.view.y - (event.clientY - drag.y) / drag.bounds.height * drag.view.height});
81    });
82    for (const event of ["pointerup", "pointercancel", "lostpointercapture"]) map.addEventListener(event, () => { drag = null; });
83  }
84
85  function decoderSummary(status) {
86    const mode = status?.mode || "Digital";
87    if (status?.demo) return `DEMO ${status.mode || "FT8"} • ${status.last_message_count || 0} decoded • cycle ${status.cycles_completed || 0}`;
88    if (status?.state === "engine_unavailable") return `${status.mode || "Decoder"}: ${status.last_error || "engine setup needed"}`;
89    if (!status || !status.enabled) return `${mode} decoder disabled`;
90
91    const count = Number(status.last_message_count || 0);
92
93    if (status.state === "error") return `${mode} decoder error`;
94    if (status.state === "audio_disconnected") {
95      return `${mode} decoder waiting for audio`;
96    }
97    if (status.state === "synchronizing") {
98      return `${mode} decoder synchronizing`;
99    }
100    if (status.state === "decoding") return `${mode} decoding cycle`;
101    if (status.state === "receiving") return `${mode} receiving • ${status.total_messages || 0} messages`;
102    if (status.cycles_completed > 0) {
103      return `${mode} live • ${count} last cycle`;
104    }
105
106    return `${mode} collecting first cycle`;
107  }
108
109  function formatDt(value) {
110    if (value == null) return "--";
111    const number = Number(value);
112    if (!Number.isFinite(number)) return "--";
113    return number.toFixed(1);
114  }
115
116  function createDecodeRow(row) {
117    const button = document.createElement("button");
118    button.className = "decode-row dcc-live-decode-row";
119    button.dataset.dccLive = "true";
120    button.dataset.digitalMode = row.mode || "FT8";
121    bindDemoSelection(button, row);
122    button.title = window.dccDemoMode === true ? `Simulated ${row.mode || "FT8"} decode — click for station details` : `Live receive-only ${row.mode || "FT8"} decode`;
123    button.classList.add(`dcc-row-signal-${signalLevel(row.db)}`);
124    if (window.dccDemoMode && row.active_contact) button.classList.add("dcc-qso-row");
125    else if (row.is_cq) button.classList.add("dcc-cq-row");
126
127    const values = [
128      row.utc || "------",
129      row.db != null && Number.isFinite(Number(row.db)) ? String(row.db) : "--",
130      formatDt(row.dt),
131      row.audio_hz != null && Number.isFinite(Number(row.audio_hz))
132        ? String(row.audio_hz)
133        : "--",
134      row.message || ""
135    ];
136
137    values.forEach((value, index) => {
138      const cell = document.createElement("span");
139      cell.textContent = value;
140
141      if (index === 1) {
142        cell.className = `dcc-db-${signalLevel(row.db)}`;
143      }
144
145      button.appendChild(cell);
146    });
147
148    return button;
149  }
150
151  function renderDecodePanel(panel, rows, emptyText) {
152    if (!panel) return;
153
154    panel.classList.add("dcc-live-decode-panel");
155    const body = panel.querySelector(".decode-body");
156    if (!body) return;
157
158    // Keep unchanged demo rows mounted so an RX selection does not interrupt
159    // the second click of a CQ double-click in Band Activity.
160    if (window.dccDemoMode) {
161      const signature = JSON.stringify([rows, rows?.length ? "" : emptyText]);
162      if (body.dccDemoSignature === signature) return;
163      body.dccDemoSignature = signature;
164    }
165
166    body.querySelectorAll("[data-dcc-live]").forEach(
167      element => element.remove()
168    );
169
170    const selected = Array.isArray(rows) ? rows.slice(0, window.dccDemoMode ? 80 : 40) : [];
171
172    if (!selected.length) {
173      const empty = document.createElement("div");
174      empty.className = "dcc-empty-decodes";
175      empty.dataset.dccLive = "true";
176      empty.textContent = emptyText;
177      body.appendChild(empty);
178      return;
179    }
180
181    let lastPeriod;
182    selected.forEach(row => {
183      const period = row.cycle_utc ?? row.period_index ?? (Number.isFinite(row.received_at) ? Math.floor((row.received_at - 13.2) / 15) : row.utc);
184      if (period !== lastPeriod) {
185        const separator = document.createElement("div");
186        separator.className = "dcc-decode-period"; separator.dataset.dccLive = "true";
187        separator.textContent = `— ${row.utc || "------"} UTC · ${row.mode || "FT8"} ${["JS8","PSK31","RTTY"].includes(row.mode) ? "receive text" : "receive period"} —`;
188        body.appendChild(separator); lastPeriod = period;
189      }
190      body.appendChild(createDecodeRow(row));
191    });
192  }
193
194  function statusClass(status) {
195    if (status === "CQ") return "status-grid";
196    return "status-worked";
197  }
198
199  function hasDistance(value) {
200    return value !== null
201      && value !== undefined
202      && value !== ""
203      && Number.isFinite(Number(value));
204  }
205
206  function createRosterRow(station) {
207    const button = document.createElement("button");
208    button.className = "roster-row dcc-live-roster-row";
209    button.dataset.dccLive = "true";
210    bindDemoSelection(button, station);
211    button.title = station.message || "Live decoded station";
212    button.classList.add(`dcc-row-signal-${signalLevel(station.db)}`);
213    if (window.dccDemoMode && station.active_contact) button.classList.add("dcc-qso-row");
214    else if (station.status === "CQ") button.classList.add("dcc-cq-row");
215    if (station.worked) button.classList.add("dcc-worked-row");
216
217    const distance = hasDistance(station.distance_miles)
218      ? `${Number(station.distance_miles).toLocaleString()} mi`
219      : "--";
220
221    const values = [
222      station.call || "--",
223      station.grid || "--",
224      Number.isFinite(Number(station.db)) ? String(station.db) : "--",
225      distance,
226      station.status || "Decoded",
227      station.seen || "Now"
228    ];
229
230    values.forEach((value, index) => {
231      const cell = document.createElement(index === 0 ? "strong" : "span");
232      cell.textContent = value;
233
234      if (index === 2) {
235        cell.className = `dcc-db-${signalLevel(station.db)}`;
236      }
237      if (index === 4) cell.className = statusClass(value);
238
239      button.appendChild(cell);
240    });
241
242    return button;
243  }
244
245  function renderRoster(roster) {
246    const panel = document.querySelector(".roster-panel");
247    const table = panel?.querySelector(".roster-table");
248    if (!panel || !table) return;
249
250    panel.classList.add("dcc-live-roster-panel");
251    table.querySelectorAll("[data-dcc-live]").forEach(
252      element => element.remove()
253    );
254
255    const selected = Array.isArray(roster) ? roster.slice(0, 50) : [];
256    selected.forEach(station => {
257      table.appendChild(createRosterRow(station));
258    });
259
260    if (!selected.length) {
261      const empty = document.createElement("div");
262      empty.className = "dcc-empty-roster";
263      empty.dataset.dccLive = "true";
264      empty.textContent = "Waiting for decoded callsigns";
265      table.appendChild(empty);
266    }
267
268    const count = panel.querySelector(".roster-title em");
269    if (count) count.textContent = String(selected.length);
270  }
271
272  function preference(name, fallback = false) {
273    try {
274      const value = window.localStorage.getItem(name);
275      return value === null ? fallback : value === "true";
276    } catch {
277      return fallback;
278    }
279  }
280
281  function savePreference(name, value) {
282    try {
283      window.localStorage.setItem(name, String(Boolean(value)));
284    } catch {
285      // Private-browsing storage failures should not stop the live display.
286    }
287  }
288
289  function createCheckboxLabel(text, className, checked) {
290    const label = document.createElement("label");
291    label.className = className;
292    const input = document.createElement("input");
293    input.type = "checkbox";
294    input.checked = checked;
295    label.append(input, document.createTextNode(text));
296    return {label, input};
297  }
298
299  function ensureLogControls() {
300    const panel = document.querySelector(".roster-panel");
301    const controls = panel?.querySelector(".roster-title > div");
302    if (!panel || !controls || controls.dataset.dccLogControls === "true") {
303      return;
304    }
305    controls.dataset.dccLogControls = "true";
306
307    const oldWorked = Array.from(controls.querySelectorAll("label"))
308      .find(label => label.textContent.trim().toLowerCase() === "worked");
309    if (oldWorked) oldWorked.style.display = "none";
310
311    hideWorked = preference("dcc-hide-worked", false);
312    hideCq = preference("dcc-hide-cq", false);
313    const hideCqControl = createCheckboxLabel(
314      "Hide CQ",
315      "dcc-hide-cq-control",
316      hideCq
317    );
318    hideCqControl.input.addEventListener("change", () => {
319      hideCq = hideCqControl.input.checked;
320      savePreference("dcc-hide-cq", hideCq);
321      lastMappedRoster = [];
322      lastMapSignature = "";
323      render(latestData);
324    });
325
326    const hideControl = createCheckboxLabel(
327      "Hide worked",
328      "dcc-hide-worked-control",
329      hideWorked
330    );
331    hideControl.input.addEventListener("change", () => {
332      hideWorked = hideControl.input.checked;
333      savePreference("dcc-hide-worked", hideWorked);
334      lastMappedRoster = [];
335      lastMapSignature = "";
336      render(latestData);
337    });
338
339    const input = document.createElement("input");
340    input.type = "file";
341    input.accept = ".adi,.adif,text/plain";
342    input.className = "dcc-adif-file-input";
343
344    const importButton = document.createElement("button");
345    importButton.type = "button";
346    importButton.className = "dcc-adif-button";
347    importButton.textContent = "Import .adi";
348    importButton.addEventListener("click", () => input.click());
349
350    input.addEventListener("change", async () => {
351      const file = input.files?.[0];
352      if (!file) return;
353      const originalText = importButton.textContent;
354      importButton.disabled = true;
355      importButton.textContent = "Importing…";
356      try {
357        const response = await fetch("/api/v1/log/adif/import", {
358          method: "POST",
359          headers: {"Content-Type": "text/plain; charset=utf-8"},
360          body: await file.text()
361        });
362        const result = await response.json();
363        if (!response.ok) {
364          throw new Error(result.detail || "ADIF import failed");
365        }
366        importButton.textContent = `Added ${result.imported}`;
367        await fetchDecodes();
368      } catch (error) {
369        importButton.textContent = "Import failed";
370        importButton.title = error.message || "ADIF import failed";
371      } finally {
372        input.value = "";
373        importButton.disabled = false;
374        window.setTimeout(() => {
375          importButton.textContent = originalText;
376        }, 3500);
377      }
378    });
379
380    const exportLink = document.createElement("a");
381    exportLink.className = "dcc-adif-button";
382    exportLink.href = "/api/v1/log/adif/export";
383    exportLink.download = "digital-command-center-log.adi";
384    exportLink.textContent = "Export .adi";
385
386    const filterButton = Array.from(controls.querySelectorAll("button"))
387      .find(button => button.textContent.trim().toLowerCase().startsWith("filter"));
388    controls.insertBefore(hideCqControl.label, filterButton || null);
389    controls.insertBefore(hideControl.label, filterButton || null);
390    controls.insertBefore(importButton, filterButton || null);
391    controls.insertBefore(exportLink, filterButton || null);
392    panel.appendChild(input);
393  }
394
395  function gridToLatLon(grid) {
396    const value = String(grid || "").trim().toUpperCase();
397    if (!/^[A-R]{2}\d{2}(?:[A-X]{2})?$/.test(value)) return null;
398
399    let longitude = (value.charCodeAt(0) - 65) * 20 - 180;
400    let latitude = (value.charCodeAt(1) - 65) * 10 - 90;
401    longitude += Number(value[2]) * 2;
402    latitude += Number(value[3]);
403
404    if (value.length === 6) {
405      longitude += (value.charCodeAt(4) - 65) / 12 + 1 / 24;
406      latitude += (value.charCodeAt(5) - 65) / 24 + 1 / 48;
407    } else {
408      longitude += 1;
409      latitude += 0.5;
410    }
411
412    return {latitude, longitude};
413  }
414
415  function worldMapPosition(grid) {
416    const location = gridToLatLon(grid);
417    if (!location) return null;
418
419    return {
420      x: (location.longitude + 180) / 360 * MAP_WIDTH,
421      y: (90 - location.latitude) / 180 * MAP_HEIGHT
422    };
423  }
424
425  function signalLevel(db) {
426    const level = Number(db);
427    if (level >= -10) return "strong";
428    if (level >= -18) return "workable";
429    return "weak";
430  }
431
432  function signalOpacity(seconds) {
433    const age = Number(seconds);
434    if (!Number.isFinite(age) || age <= 60) return 1;
435    return Math.max(0.28, 1 - (age - 60) / 1740 * 0.72);
436  }
437
438  function automaticViewBox(points) {
439    if (window.dccDemoMode === true && demoView) return demoView;
440    if (mapViewMode === "world" || !points.length) {
441      return {x: 0, y: 0, width: MAP_WIDTH, height: MAP_HEIGHT};
442    }
443
444    const xs = points.map(point => point.x);
445    const ys = points.map(point => point.y);
446    const minimumX = Math.min(...xs);
447    const maximumX = Math.max(...xs);
448    const minimumY = Math.min(...ys);
449    const maximumY = Math.max(...ys);
450    const width = Math.min(
451      MAP_WIDTH,
452      Math.max(300, (maximumX - minimumX) * 1.65 + 100)
453    );
454    const height = Math.min(
455      MAP_HEIGHT,
456      Math.max(190, (maximumY - minimumY) * 1.8 + 80)
457    );
458    const centerX = (minimumX + maximumX) / 2;
459    const centerY = (minimumY + maximumY) / 2;
460
461    return {
462      x: Math.max(0, Math.min(MAP_WIDTH - width, centerX - width / 2)),
463      y: Math.max(0, Math.min(MAP_HEIGHT - height, centerY - height / 2)),
464      width,
465      height
466    };
467  }
468
469  function svgElement(name, attributes = {}) {
470    const element = document.createElementNS(SVG_NAMESPACE, name);
471    Object.entries(attributes).forEach(([key, value]) => {
472      element.setAttribute(key, String(value));
473    });
474    return element;
475  }
476
477  function bindMapControls(map) {
478    if (map.dataset.dccControlsBound === "true") return;
479    map.dataset.dccControlsBound = "true";
480
481    const header = map.closest(".map-tabs")?.querySelector(".intel-header");
482    const checkboxes = Array.from(
483      header?.querySelectorAll('input[type="checkbox"]') || []
484    );
485    const [gridControl, signalControl] = checkboxes;
486
487    if (gridControl) {
488      const updateGrid = () => {
489        map.classList.toggle("dcc-hide-map-grid", !gridControl.checked);
490      };
491      gridControl.addEventListener("change", updateGrid);
492      updateGrid();
493    }
494
495    if (signalControl) {
496      const updateSignals = () => {
497        map.classList.toggle("dcc-hide-map-signals", !signalControl.checked);
498      };
499      signalControl.addEventListener("change", updateSignals);
500      updateSignals();
501    }
502  }
503
504  function updateTxAudioDisplay(audioHz, selected = clearFrequencySelected) {
505    const frequency = Math.max(300, Math.min(3000, Number(audioHz) || 1500));
506    clearFrequencySelected = Boolean(selected);
507    window.dccSelectedTxAudioHz = Math.round(frequency);
508    const labels = Array.from(
509      document.querySelectorAll(".sequence-options > span")
510    );
511    const label = labels.find(item => /TX\s+\d+\s*Hz/i.test(item.textContent));
512    if (label) {
513      label.textContent = clearFrequencySelected
514        ? `TX ${window.dccSelectedTxAudioHz} Hz • CLEAR`
515        : `TX ${window.dccSelectedTxAudioHz} Hz`;
516    }
517
518    const waterfall = document.querySelector(".waterfall");
519    if (!waterfall) return;
520    let marker = waterfall.querySelector(".dcc-clear-frequency-marker");
521    if (!marker) {
522      marker = document.createElement("i");
523      marker.className = "dcc-clear-frequency-marker";
524      marker.title = "Selected clear FT8 transmit audio frequency";
525      waterfall.appendChild(marker);
526    }
527    marker.hidden = !clearFrequencySelected;
528    marker.style.left = `${(window.dccSelectedTxAudioHz - 100) / 3400 * 100}%`;
529  }
530
531  function resetClearFrequency() {
532    clearFrequencySelected = false;
533    updateTxAudioDisplay(1500, false);
534    if (clearFrequencyButton) {
535      clearFrequencyButton.textContent = "Find clear";
536      clearFrequencyButton.title = "Find and hold one clear FT8 audio frequency";
537    }
538  }
539
540  async function findClearFrequency() {
541    if (clearFrequencyRequesting) return;
542    clearFrequencyRequesting = true;
543    const button = clearFrequencyButton;
544    if (button) {
545      button.disabled = true;
546      button.textContent = "Searching…";
547    }
548    try {
549      const response = await fetch("/api/v1/audio/clear-frequency", {
550        cache: "no-store"
551      });
552      if (!response.ok) throw new Error("Clear-frequency API unavailable");
553      const result = await response.json();
554      const audioHz = result.recommendation?.audio_hz;
555      if (result.connected && Number.isFinite(Number(audioHz))) {
556        updateTxAudioDisplay(audioHz, true);
557        if (button) {
558          button.textContent = `Clear ${window.dccSelectedTxAudioHz} Hz`;
559          button.title = "Press again to search for another clear frequency";
560        }
561      } else if (button) {
562        button.textContent = "No audio";
563      }
564    } catch {
565      if (button) button.textContent = "Try again";
566    } finally {
567      clearFrequencyRequesting = false;
568      if (button) button.disabled = false;
569    }
570  }
571
572  function ensureMapFilterControls(map) {
573    const header = map.closest(".map-tabs")?.querySelector(".intel-header");
574    const controls = header?.querySelector(":scope > div:last-child");
575    if (!controls || controls.dataset.dccMapFilters === "true") return;
576    controls.dataset.dccMapFilters = "true";
577
578    const group = document.createElement("span");
579    group.className = "dcc-map-level-controls";
580    [
581      ["strong", "Strong"],
582      ["workable", "Workable"],
583      ["weak", "Weak"]
584    ].forEach(([level, text]) => {
585      const control = createCheckboxLabel(
586        text,
587        `dcc-map-level-filter dcc-filter-${level}`,
588        preference(`dcc-map-${level}`, true)
589      );
590      mapSignalFilters[level] = control.input.checked;
591      control.input.addEventListener("change", () => {
592        mapSignalFilters[level] = control.input.checked;
593        savePreference(`dcc-map-${level}`, control.input.checked);
594        lastMappedRoster = [];
595        lastMapSignature = "";
596        renderMap(latestData?.roster, latestData?.status);
597      });
598      group.appendChild(control.label);
599    });
600
601    clearFrequencyButton = document.createElement("button");
602    clearFrequencyButton.type = "button";
603    clearFrequencyButton.className = "dcc-find-clear-button";
604    clearFrequencyButton.textContent = "Find clear";
605    clearFrequencyButton.title = "Find and hold one clear FT8 audio frequency";
606    clearFrequencyButton.addEventListener("click", findClearFrequency);
607    if (window.dccDemoMode === true) {
608      clearFrequencyButton.disabled = true;
609      clearFrequencyButton.title = "Available with live receive audio; disabled in the demo";
610    }
611    group.appendChild(clearFrequencyButton);
612    controls.appendChild(group);
613    updateTxAudioDisplay(window.dccSelectedTxAudioHz || 1500, false);
614  }
615
616  function ensureWorldMap(map) {
617    let svg = map.querySelector(":scope > svg.dcc-world-map");
618    if (!svg) {
619      svg = svgElement("svg", {
620        class: "dcc-world-map",
621        viewBox: `0 0 ${MAP_WIDTH} ${MAP_HEIGHT}`,
622        preserveAspectRatio: "none",
623        role: "img",
624        "aria-label": "Live FT8 Maidenhead grid map"
625      });
626
627      const baseMap = svgElement("image", {
628        href: "./world-map.svg?v=0.3.0",
629        x: 0,
630        y: 0,
631        width: MAP_WIDTH,
632        height: MAP_HEIGHT,
633        preserveAspectRatio: "none"
634      });
635      baseMap.classList.add("dcc-map-base");
636      svg.appendChild(baseMap);
637
638      const gridLayer = svgElement("g", {class: "dcc-map-grid-layer"});
639      for (let longitude = 0; longitude <= MAP_WIDTH; longitude += MAP_WIDTH / 24) {
640        gridLayer.appendChild(svgElement("line", {
641          x1: longitude,
642          y1: 0,
643          x2: longitude,
644          y2: MAP_HEIGHT
645        }));
646      }
647      for (let latitude = 0; latitude <= MAP_HEIGHT; latitude += MAP_HEIGHT / 12) {
648        gridLayer.appendChild(svgElement("line", {
649          x1: 0,
650          y1: latitude,
651          x2: MAP_WIDTH,
652          y2: latitude
653        }));
654      }
655      svg.appendChild(gridLayer);
656      svg.appendChild(svgElement("g", {class: "dcc-map-route-layer"}));
657      map.prepend(svg);
658
659      const viewToggle = document.createElement("button");
660      viewToggle.type = "button";
661      viewToggle.className = "dcc-map-view-toggle";
662      viewToggle.textContent = "AUTO";
663      viewToggle.title = "Switch between automatic regional view and full world";
664      viewToggle.addEventListener("click", () => {
665        demoView = null;
666        mapViewMode = mapViewMode === "auto" ? "world" : "auto";
667        viewToggle.textContent = mapViewMode.toUpperCase();
668        lastMapSignature = "";
669        renderMap(latestData?.roster, latestData?.status);
670      });
671      map.appendChild(viewToggle);
672      if (window.dccDemoMode === true) enableDemoNavigation(map, svg);
673    }
674
675    bindMapControls(map);
676    ensureMapFilterControls(map);
677    return svg;
678  }
679
680  function markerPosition(element, position, view) {
681    element.style.left = `${(position.x - view.x) / view.width * 100}%`;
682    element.style.top = `${(position.y - view.y) / view.height * 100}%`;
683  }
684
685  function createHomeMarker(homeCall, homeGrid, position, view) {
686    const marker = document.createElement("div");
687    marker.className = "dcc-world-marker dcc-home-marker";
688    marker.dataset.dccLiveMap = "true";
689    marker.innerHTML = "<i></i><strong></strong><small></small>";
690    marker.querySelector("strong").textContent = homeCall || "HOME";
691    marker.querySelector("small").textContent = homeGrid;
692    markerPosition(marker, position, view);
693    return marker;
694  }
695
696  function createStationMarker(station, position, view, labelOffset) {
697    const marker = document.createElement("button");
698    bindDemoSelection(marker, station);
699    const level = signalLevel(station.db);
700    marker.type = "button";
701    marker.className = `dcc-world-marker dcc-station-marker dcc-signal-${level}`;
702    marker.dataset.dccLiveMap = "true";
703    marker.style.opacity = String(signalOpacity(station.seen_seconds));
704    marker.innerHTML = "<i></i><strong></strong><small></small>";
705    marker.querySelector("strong").textContent = station.call || "--";
706    marker.querySelector("small").textContent = station.grid || "--";
707    marker.style.setProperty("--dcc-label-shift-x", `${labelOffset.x}px`);
708    marker.style.setProperty("--dcc-label-shift-y", `${labelOffset.y}px`);
709    marker.title = `${station.call} ${station.grid} ${station.db} dB`;
710    markerPosition(marker, position, view);
711    marker.addEventListener("click", () => {
712      const label = document.querySelector(".signal-map .dcc-demo-label");
713      const distance = hasDistance(station.distance_miles)
714        ? `${Number(station.distance_miles).toLocaleString()} MI`
715        : "DISTANCE --";
716      if (label) {
717        label.textContent = `${station.call} • ${station.grid} • ${station.db} dB • ${distance}`;
718      }
719    });
720    return marker;
721  }
722
723  function labelOffsets(mapped, homePosition, view, map) {
724    const width = Math.max(600, map.clientWidth || 900);
725    const height = Math.max(150, map.clientHeight || 240);
726    const project = position => ({
727      x: (position.x - view.x) / view.width * width,
728      y: (position.y - view.y) / view.height * height
729    });
730    const candidates = [
731      {x: 0, y: 0},
732      {x: -44, y: -24},
733      {x: 44, y: -24},
734      {x: -52, y: 22},
735      {x: 52, y: 22},
736      {x: 0, y: 38},
737      {x: -70, y: 0},
738      {x: 70, y: 0}
739    ];
740    const occupied = [project(homePosition)];
741
742    return mapped.map(item => {
743      const base = project(item.position);
744      const offset = candidates.find(candidate => {
745        const x = base.x + candidate.x;
746        const y = base.y + candidate.y;
747        return !occupied.some(point =>
748          Math.abs(point.x - x) < 74 && Math.abs(point.y - y) < 28
749        );
750      }) || candidates[candidates.length - 1];
751      occupied.push({x: base.x + offset.x, y: base.y + offset.y});
752      return {...item, labelOffset: offset};
753    });
754  }
755
756  function createRoute(home, destination, station) {
757    const midpointX = (home.x + destination.x) / 2;
758    const distance = Math.abs(destination.x - home.x);
759    const midpointY = Math.max(
760      5,
761      (home.y + destination.y) / 2 - Math.max(18, Math.min(90, distance * 0.18))
762    );
763    const path = svgElement("path", {
764      d: `M ${home.x} ${home.y} Q ${midpointX} ${midpointY} ${destination.x} ${destination.y}`,
765      class: `dcc-map-route dcc-signal-${signalLevel(station.db)}`
766    });
767    path.dataset.dccLiveMap = "true";
768    path.style.opacity = String(signalOpacity(station.seen_seconds));
769    return path;
770  }
771
772  function renderMap(roster, status) {
773    const map = document.querySelector(".signal-map");
774    if (!map) return;
775
776    map.classList.add("dcc-live-signal-map");
777    const svg = ensureWorldMap(map);
778    const originalGrid = map.querySelector(".home-node small")?.textContent || "";
779    const originalCall = map.querySelector(".home-node")?.textContent
780      .replace(originalGrid, "")
781      .trim();
782    const homeGrid = String(status?.station_grid || originalGrid)
783      .trim()
784      .toUpperCase();
785    const homeCall = String(status?.station_call || originalCall || "HOME")
786      .trim()
787      .toUpperCase();
788    const homePosition = worldMapPosition(homeGrid);
789    if (!homePosition) return;
790
791    let mapped = (Array.isArray(roster) ? roster : [])
792      .filter(station => Number(station.seen_seconds || 0) <= 1800)
793      .filter(station => !hideWorked || !station.worked)
794      .filter(station => !hideCq || station.status !== "CQ")
795      .filter(station => mapSignalFilters[signalLevel(station.db)])
796      .map(station => ({
797        station,
798        position: worldMapPosition(station.grid)
799      }))
800      .filter(item => item.position)
801      .slice(0, 12);
802
803    if (mapped.length) {
804      lastMappedRoster = mapped;
805      lastMapUpdateAt = Date.now();
806    } else if (
807      lastMappedRoster.length
808      && Date.now() - lastMapUpdateAt <= MAP_RETENTION_MS
809    ) {
810      mapped = lastMappedRoster;
811    } else {
812      lastMappedRoster = [];
813    }
814
815    const view = automaticViewBox([
816      homePosition,
817      ...mapped.map(item => item.position)
818    ]);
819    mapped = labelOffsets(mapped, homePosition, view, map);
820    const signature = JSON.stringify({
821      mode: mapViewMode,
822      hideWorked,
823      hideCq,
824      filters: mapSignalFilters,
825      homeCall,
826      homeGrid,
827      view,
828      stations: mapped.map(({station, position}) => ({
829        call: station.call || "",
830        grid: station.grid || "",
831        db: station.db,
832        age: Math.floor(Number(station.seen_seconds || 0) / 60),
833        x: position.x,
834        y: position.y
835      }))
836    });
837    const existingPaths = svg.querySelectorAll(
838      ".dcc-map-route-layer > [data-dcc-live-map]"
839    ).length;
840    const existingMarkers = map.querySelectorAll(
841      ":scope > .dcc-world-marker[data-dcc-live-map]"
842    ).length;
843
844    const label = map.querySelector(".dcc-demo-label");
845    const mapPrefix = status?.demo ? "DEMO MAP" : "LIVE MAP";
846    const mapSummary = mapped.length
847      ? `${mapPrefix} • ${mapViewMode.toUpperCase()} • ${mapped.length} GRID${mapped.length === 1 ? "" : "S"}`
848      : `${mapPrefix} • ${mapViewMode.toUpperCase()} • WAITING FOR GRIDS`;
849
850    if (
851      signature === lastMapSignature
852      && existingPaths === mapped.length
853      && existingMarkers === mapped.length + 1
854    ) {
855      if (label && !label.textContent.includes(" dB • ")) {
856        label.textContent = mapSummary;
857      }
858      return;
859    }
860
861    svg.setAttribute(
862      "viewBox",
863      `${view.x} ${view.y} ${view.width} ${view.height}`
864    );
865    svg.querySelectorAll("[data-dcc-live-map]").forEach(
866      element => element.remove()
867    );
868    map.querySelectorAll(":scope > [data-dcc-live-map]").forEach(
869      element => element.remove()
870    );
871
872    const routeLayer = svg.querySelector(".dcc-map-route-layer");
873    mapped.forEach(({station, position, labelOffset}) => {
874      routeLayer.appendChild(createRoute(homePosition, position, station));
875      map.appendChild(createStationMarker(station, position, view, labelOffset));
876    });
877    map.appendChild(createHomeMarker(homeCall, homeGrid, homePosition, view));
878
879    if (label) label.textContent = mapSummary;
880    const viewToggle = map.querySelector(".dcc-map-view-toggle");
881    if (viewToggle) viewToggle.textContent = demoView ? "MANUAL" : mapViewMode.toUpperCase();
882    lastMapSignature = signature;
883  }
884
885  function renderStatus(status) {
886    const summary = decoderSummary(status);
887    window.dccDecoderSummary = summary;
888
889    const items = document.querySelectorAll(".receive-status > span");
890    if (items[3]) items[3].textContent = summary;
891
892    const waterfallLabel = document.querySelector(
893      ".waterfall .dcc-demo-label"
894    );
895    if (waterfallLabel) waterfallLabel.textContent = "LIVE FT8 DECODER";
896
897    document.body.classList.toggle(
898      "dcc-decoder-error",
899      status?.state === "error"
900    );
901  }
902
903  function render(data) {
904    if (!data) return;
905    document.body.dataset.dccDigitalMode = data.status?.mode || "FT8";
906    if (window.dccDemoMode) hideWorked = window.dccDemoStation?.hideWorked === true;
907
908    const panels = Array.from(document.querySelectorAll(".decode-panel"));
909    const bandPanel = panels.find(panel =>
910      panel.querySelector(".section-title")?.textContent
911        .trim()
912        .toUpperCase()
913        .includes("BAND ACTIVITY")
914    );
915    const rxPanel = panels.find(panel =>
916      panel.querySelector(".section-title")?.textContent
917        .trim()
918        .toUpperCase()
919        .includes("RX FREQUENCY")
920    );
921
922    const visibleDecodes = (Array.isArray(data.decodes) ? data.decodes : [])
923      .filter(row => !window.dccDemoMode || !hideWorked || !row.worked)
924      .filter(row => !hideCq || !row.is_cq);
925    const visibleLatest = (Array.isArray(data.latest_cycle) ? data.latest_cycle : [])
926      .filter(row => !window.dccDemoMode || !hideWorked || !row.worked)
927      .filter(row => !hideCq || !row.is_cq);
928
929    renderDecodePanel(
930      bandPanel,
931      visibleDecodes,
932      data.status?.last_error || `Waiting for ${data.status?.mode || "FT8"} messages`
933    );
934    renderDecodePanel(
935      rxPanel,
936      visibleLatest,
937      window.dccDemoMode ? `No messages near RX ${window.dccDemoStation?.rxHz || 0} Hz yet (±50 Hz). Select a station or move the green marker.` : `No messages near RX ${data.status?.rx_hz || 1500} Hz yet`
938    );
939    if (window.dccDemoMode !== true) ensureLogControls();
940    const visibleRoster = (Array.isArray(data.roster) ? data.roster : [])
941      .filter(station => !hideWorked || !station.worked)
942      .filter(station => !hideCq || station.status !== "CQ");
943    renderRoster(visibleRoster);
944    renderMap(data.roster, data.status);
945    renderStatus(data.status);
946
947    const layoutSignature = JSON.stringify({
948      bandRows: Math.min(8, visibleDecodes.length),
949      rxRows: Math.min(8, visibleLatest.length),
950      rosterRows: Math.min(8, visibleRoster.length)
951    });
952    if (layoutSignature !== lastLayoutSignature) {
953      lastLayoutSignature = layoutSignature;
954      window.dispatchEvent(new window.Event("dcc-layout-change"));
955    }
956  }
957
958  async function fetchDecodes() {
959    if (requesting) return;
960    requesting = true;
961
962    try {
963      const response = await fetch("/api/v1/decodes?limit=80", {
964        cache: "no-store"
965      });
966      if (!response.ok) throw new Error("Decoder API unavailable");
967
968      latestData = await response.json();
969      render(latestData);
970    } catch {
971      window.dccDecoderSummary = "FT8 decoder API disconnected";
972      renderStatus({enabled: true, state: "error"});
973    } finally {
974      requesting = false;
975    }
976  }
977
978  // Reuse the real map in the demo without starting API polling or log controls.
979  if (window.dccDemoMode === true) {
980    window.dccRenderDemoDecodes = data => {
981      latestData = data;
982      render(data);
983    };
984    window.dccRenderDemoMap = (roster, status) => {
985      latestData = {roster, status: {...status, demo: true}};
986      renderMap(latestData.roster, latestData.status);
987    };
988    return;
989  }
990
991  fetchDecodes();
992  window.addEventListener("dcc-band-change", resetClearFrequency);
993  setInterval(fetchDecodes, 1000);
994  setInterval(() => render(latestData), 400);
995})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.