MissionChief OpsMap Command

Tactical map overlay for MissionChief: triage rings, needs-action mode, stale mission warnings, resource drain panel, coverage circles, and local building icon overrides by type.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

Advertisement:

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

Advertisement:

// ==UserScript==
// @name         MissionChief OpsMap Command
// @namespace    Conroy1988
// @version      1.0.2
// @description  Tactical map overlay for MissionChief: triage rings, needs-action mode, stale mission warnings, resource drain panel, coverage circles, and local building icon overrides by type.
// @author       Conroy1988
// @copyright    2026, Conroy1988
// @license      MIT
// @match        https://www.missionchief.co.uk/*
// @match        https://police.missionchief.co.uk/*
// @match        https://www.missionchief.com/*
// @match        https://www.leitstellenspiel.de/*
// @match        https://www.meldkamerspel.com/*
// @grant        none
// @run-at       document-idle
// @noframes
// ==/UserScript==

/*
MIT License

Copyright (c) 2026 Conroy1988

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

(function OpsMapCommand() {
  'use strict';

  const APP = 'MC_OPSMAP_COMMAND';
  const VERSION = '1.0.2';
  const CFG_KEY = `${APP}_CONFIG_V1`;

  const DEFAULT_CONFIG = {
    enabled: true,
    triageRings: true,
    badges: true,
    needsActionMode: false,
    hardHideHandled: false,
    dimHandledOpacity: 0.18,
    staleMissionMinutes: 60,
    veryStaleMissionMinutes: 120,
    scanIntervalMs: 3000,
    apiRefreshMs: 60000,
    apiEnabled: true,
    resourceDrainPanel: true,
    coverageEnabled: false,
    coverageMode: 'all',
    coverageRadiusKm: 12,
    coverageCircleLimit: 250,
    buildingIconOverrideEnabled: true,
    iconOverrides: {},
    panelCollapsed: false,
    debug: false
  };

  const SERVICE_PATTERNS = {
    fire: /\b(fire|pump|engine|ladder|aerial|platform|rescue support|heavy rescue|foam|water carrier|water tanker|hazmat|cbrn|welfare|incident command|fire officer|operational team leader|otl|prv|srv|technical rescue|rescue\s?pump)\b/i,
    ems: /\b(ems|ambulance|paramedic|hems|hart|critical care|doctor|patient|patients|rescue station|medical|casualty|iccu|mcu|mass casualty|ambulance officer|control unit)\b/i,
    police: /\b(police|arv|armed response|dog|dogs|public order|riot|traffic|custody|prisoner|prisoners|cell|cells|detention|prison|transport officer)\b/i,
    specialist: /\b(search and rescue|sar|mountain rescue|coastguard|coastal|bomb|disposal|hgv recovery|recovery|dive|boat|drone|railway|airport|foam|hazmat|cbrn|welfare|command|iccu|prv|srv|hart)\b/i
  };

  const MISSING_PATTERNS = /\b(missing|needed|required|requires|required vehicles|required personnel|missing vehicles|missing personnel|not enough|no suitable|unavailable|still required)\b/i;
  const BLOCKED_PATTERNS = /\b(no suitable|not enough|unavailable|cannot|can't|failed|missing personnel|wrong training|requires training|no free|none available|blocked)\b/i;
  const NO_UNITS_PATTERNS = /\b(no vehicles assigned|no units assigned|not dispatched|no units|nothing assigned|unassigned)\b/i;
  const HANDLED_PATTERNS = /\b(finished|completed|cleared|all patients treated|mission accomplished)\b/i;

  const state = {
    config: loadConfig(),
    panel: null,
    badgeLayer: null,
    observer: null,
    scanTimer: null,
    badgeTimer: null,
    apiTimer: null,
    scanQueued: false,
    isScanning: false,
    missions: new Map(),
    missionMarkers: new Map(),
    buildings: new Map(),
    vehicles: [],
    vehicleSummary: null,
    apiLastRefresh: 0,
    apiError: '',
    leafletMap: null,
    coverageLayers: [],
    lastStats: null
  };

  function loadConfig() {
    try {
      const raw = localStorage.getItem(CFG_KEY);
      if (!raw) return { ...DEFAULT_CONFIG };
      return deepMerge({ ...DEFAULT_CONFIG }, JSON.parse(raw) || {});
    } catch {
      return { ...DEFAULT_CONFIG };
    }
  }

  function saveConfig() {
    try {
      localStorage.setItem(CFG_KEY, JSON.stringify(state.config));
    } catch (error) {
      console.warn(`[${APP}] Could not save config`, error);
    }
  }

  function deepMerge(base, extra) {
    Object.keys(extra || {}).forEach(key => {
      if (
        extra[key] &&
        typeof extra[key] === 'object' &&
        !Array.isArray(extra[key]) &&
        base[key] &&
        typeof base[key] === 'object' &&
        !Array.isArray(base[key])
      ) {
        base[key] = deepMerge({ ...base[key] }, extra[key]);
      } else {
        base[key] = extra[key];
      }
    });
    return base;
  }

  function log(...args) {
    if (state.config.debug) console.log(`[${APP}]`, ...args);
  }

  function safeText(value) {
    return String(value == null ? '' : value).replace(/\s+/g, ' ').trim();
  }

  function normalise(value) {
    return safeText(value).toLowerCase();
  }

  function clamp(value, min, max) {
    return Math.min(max, Math.max(min, value));
  }

  function cssEscape(value) {
    if (window.CSS && typeof window.CSS.escape === 'function') return window.CSS.escape(String(value));
    return String(value).replace(/["\\]/g, '\\$&');
  }

  function injectStyles() {
    if (document.getElementById(`${APP}_STYLE`)) return;

    const css = `
      #mc-opsmap-panel {
        position: fixed;
        top: 74px;
        right: 12px;
        width: 330px;
        max-height: calc(100vh - 96px);
        overflow: auto;
        z-index: 2147482600;
        background: rgba(16, 18, 22, 0.94);
        color: #f2f5f8;
        border: 1px solid rgba(255,255,255,0.16);
        border-radius: 12px;
        box-shadow: 0 12px 34px rgba(0,0,0,0.36);
        font: 12px/1.35 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
        backdrop-filter: blur(7px);
      }

      #mc-opsmap-panel.mc-ops-collapsed {
        width: auto;
        overflow: visible;
      }

      #mc-opsmap-panel * { box-sizing: border-box; }

      .mc-ops-head {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 8px;
        padding: 10px 11px;
        border-bottom: 1px solid rgba(255,255,255,0.12);
        cursor: move;
        user-select: none;
      }

      .mc-ops-title {
        font-weight: 800;
        letter-spacing: 0.2px;
        font-size: 13px;
      }

      .mc-ops-version {
        color: rgba(242,245,248,0.58);
        font-weight: 600;
        margin-left: 4px;
      }

      .mc-ops-actions {
        display: flex;
        align-items: center;
        gap: 5px;
      }

      .mc-ops-btn {
        border: 1px solid rgba(255,255,255,0.16);
        background: rgba(255,255,255,0.08);
        color: #f2f5f8;
        border-radius: 7px;
        padding: 4px 7px;
        cursor: pointer;
        font-size: 11px;
      }

      .mc-ops-btn:hover { background: rgba(255,255,255,0.16); }
      .mc-ops-btn:active { transform: translateY(1px); }

      .mc-ops-body { padding: 10px 11px 12px; }
      #mc-opsmap-panel.mc-ops-collapsed .mc-ops-body { display: none; }

      .mc-ops-section {
        border-top: 1px solid rgba(255,255,255,0.10);
        padding-top: 9px;
        margin-top: 9px;
      }

      .mc-ops-section:first-child {
        border-top: 0;
        padding-top: 0;
        margin-top: 0;
      }

      .mc-ops-row {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 8px;
        margin: 7px 0;
      }

      .mc-ops-row label {
        display: flex;
        align-items: center;
        gap: 7px;
        min-width: 0;
      }

      .mc-ops-row input[type="checkbox"] {
        margin: 0;
        width: 14px;
        height: 14px;
      }

      .mc-ops-row input[type="number"],
      .mc-ops-row select {
        width: 96px;
        background: rgba(255,255,255,0.10);
        color: #f2f5f8;
        border: 1px solid rgba(255,255,255,0.15);
        border-radius: 7px;
        padding: 4px 6px;
        font-size: 11px;
      }

      .mc-ops-subtitle {
        color: rgba(242,245,248,0.68);
        font-weight: 700;
        text-transform: uppercase;
        letter-spacing: 0.7px;
        font-size: 10px;
        margin-bottom: 6px;
      }

      .mc-ops-grid {
        display: grid;
        grid-template-columns: repeat(4, 1fr);
        gap: 6px;
      }

      .mc-ops-stat {
        border-radius: 9px;
        background: rgba(255,255,255,0.08);
        border: 1px solid rgba(255,255,255,0.10);
        padding: 6px;
        text-align: center;
      }

      .mc-ops-stat strong {
        display: block;
        font-size: 15px;
        line-height: 1.1;
      }

      .mc-ops-stat span {
        color: rgba(242,245,248,0.64);
        font-size: 10px;
      }

      .mc-ops-muted { color: rgba(242,245,248,0.58); }
      .mc-ops-warn { color: #ffd166; }
      .mc-ops-danger { color: #ff6b6b; }
      .mc-ops-good { color: #7be495; }
      .mc-ops-blue { color: #6db7ff; }
      .mc-ops-purple { color: #d6a3ff; }

      #mc-opsmap-badges {
        position: absolute;
        inset: 0;
        z-index: 1000;
        pointer-events: none;
        overflow: hidden;
      }

      .mc-ops-badge {
        position: absolute;
        min-width: 17px;
        height: 17px;
        padding: 0 5px;
        display: flex;
        align-items: center;
        justify-content: center;
        border-radius: 99px;
        border: 1px solid rgba(255,255,255,0.84);
        color: white;
        font-size: 9px;
        line-height: 1;
        font-weight: 900;
        text-shadow: 0 1px 1px rgba(0,0,0,0.8);
        box-shadow: 0 2px 8px rgba(0,0,0,0.34);
      }

      .mc-ops-badge-green { background: #1a9f55; }
      .mc-ops-badge-yellow { background: #d9a300; }
      .mc-ops-badge-orange { background: #e66a00; }
      .mc-ops-badge-red { background: #d93025; }
      .mc-ops-badge-blue { background: #0b74d1; }
      .mc-ops-badge-purple { background: #8d3ed6; }
      .mc-ops-badge-black { background: #16181d; }

      .leaflet-marker-icon.mc-ops-ring-green,
      img.mc-ops-ring-green { filter: drop-shadow(0 0 0 #1a9f55) drop-shadow(0 0 5px #1a9f55) !important; }
      .leaflet-marker-icon.mc-ops-ring-yellow,
      img.mc-ops-ring-yellow { filter: drop-shadow(0 0 0 #f6c343) drop-shadow(0 0 6px #f6c343) !important; }
      .leaflet-marker-icon.mc-ops-ring-orange,
      img.mc-ops-ring-orange { filter: drop-shadow(0 0 0 #ff8c1a) drop-shadow(0 0 7px #ff8c1a) !important; }
      .leaflet-marker-icon.mc-ops-ring-red,
      img.mc-ops-ring-red { filter: drop-shadow(0 0 0 #ff3232) drop-shadow(0 0 8px #ff3232) !important; }
      .leaflet-marker-icon.mc-ops-ring-blue,
      img.mc-ops-ring-blue { filter: drop-shadow(0 0 0 #2196f3) drop-shadow(0 0 7px #2196f3) !important; }
      .leaflet-marker-icon.mc-ops-ring-purple,
      img.mc-ops-ring-purple { filter: drop-shadow(0 0 0 #b25cff) drop-shadow(0 0 7px #b25cff) !important; }
      .leaflet-marker-icon.mc-ops-ring-black,
      img.mc-ops-ring-black { filter: drop-shadow(0 0 0 #111) drop-shadow(0 0 8px #111) !important; }

      .mc-ops-needs-action-dim {
        opacity: var(--mc-ops-dim-opacity, 0.18) !important;
        filter: grayscale(1) opacity(0.65) !important;
      }

      .mc-ops-needs-action-hide {
        display: none !important;
      }

      .mc-ops-mission-panel-red { box-shadow: inset 4px 0 0 #d93025 !important; }
      .mc-ops-mission-panel-yellow { box-shadow: inset 4px 0 0 #f6c343 !important; }
      .mc-ops-mission-panel-blue { box-shadow: inset 4px 0 0 #2196f3 !important; }
      .mc-ops-mission-panel-purple { box-shadow: inset 4px 0 0 #b25cff !important; }
      .mc-ops-mission-panel-orange { box-shadow: inset 4px 0 0 #ff8c1a !important; }
      .mc-ops-mission-panel-green { box-shadow: inset 4px 0 0 #1a9f55 !important; }

      .mc-ops-float-note {
        margin-top: 8px;
        border-radius: 8px;
        padding: 7px 8px;
        background: rgba(255,255,255,0.07);
        color: rgba(242,245,248,0.72);
      }
    `;

    const style = document.createElement('style');
    style.id = `${APP}_STYLE`;
    style.textContent = css;
    document.head.appendChild(style);
  }

  function createElement(tag, attrs = {}, children = []) {
    const node = document.createElement(tag);
    Object.entries(attrs).forEach(([key, value]) => {
      if (key === 'class') node.className = value;
      else if (key === 'text') node.textContent = value;
      else if (key === 'html') node.innerHTML = value;
      else if (key.startsWith('on') && typeof value === 'function') node.addEventListener(key.slice(2), value);
      else if (value !== undefined && value !== null) node.setAttribute(key, String(value));
    });
    children.forEach(child => node.appendChild(typeof child === 'string' ? document.createTextNode(child) : child));
    return node;
  }

  function getMapContainer() {
    return document.querySelector('.leaflet-container') || document.getElementById('map');
  }

  function ensureBadgeLayer() {
    const mapContainer = getMapContainer();
    let layer = document.getElementById('mc-opsmap-badges');

    if (!mapContainer) {
      if (layer) layer.remove();
      state.badgeLayer = null;
      return null;
    }

    if (!layer) {
      layer = document.createElement('div');
      layer.id = 'mc-opsmap-badges';
    }

    if (layer.parentElement !== mapContainer) {
      layer.remove();
      mapContainer.appendChild(layer);
    }

    state.badgeLayer = layer;
    return layer;
  }

  function injectPanel() {
    if (document.getElementById('mc-opsmap-panel')) {
      state.panel = document.getElementById('mc-opsmap-panel');
      return;
    }

    const panel = createElement('div', {
      id: 'mc-opsmap-panel',
      class: state.config.panelCollapsed ? 'mc-ops-collapsed' : ''
    });

    const title = createElement('div', { class: 'mc-ops-title' }, [
      'OpsMap Command ',
      createElement('span', { class: 'mc-ops-version', text: `v${VERSION}` })
    ]);

    const collapseButton = createElement('button', { class: 'mc-ops-btn', type: 'button', text: state.config.panelCollapsed ? 'Open' : 'Min' });
    collapseButton.addEventListener('click', () => {
      state.config.panelCollapsed = !state.config.panelCollapsed;
      saveConfig();
      panel.classList.toggle('mc-ops-collapsed', state.config.panelCollapsed);
      collapseButton.textContent = state.config.panelCollapsed ? 'Open' : 'Min';
    });

    const refreshButton = createElement('button', { class: 'mc-ops-btn', type: 'button', text: 'Scan' });
    refreshButton.addEventListener('click', () => {
      queueScan(true);
      refreshApiData(true);
    });

    const head = createElement('div', { class: 'mc-ops-head' }, [
      title,
      createElement('div', { class: 'mc-ops-actions' }, [refreshButton, collapseButton])
    ]);

    const body = createElement('div', { class: 'mc-ops-body' });

    body.append(
      createElement('div', { class: 'mc-ops-section' }, [
        createElement('div', { class: 'mc-ops-subtitle', text: 'Mission state' }),
        createElement('div', { class: 'mc-ops-grid', id: 'mc-ops-stat-grid' }, [
          statBox('0', 'Open', 'mc-ops-muted'),
          statBox('0', 'Action', 'mc-ops-warn'),
          statBox('0', 'Blocked', 'mc-ops-danger'),
          statBox('0', 'Patients', 'mc-ops-blue')
        ]),
        createElement('div', { class: 'mc-ops-float-note', id: 'mc-ops-main-alert', text: 'Waiting for first scan…' })
      ]),
      createElement('div', { class: 'mc-ops-section' }, [
        createElement('div', { class: 'mc-ops-subtitle', text: 'Controls' }),
        checkboxRow('Enabled', 'enabled'),
        checkboxRow('Triage rings', 'triageRings'),
        checkboxRow('Map badges', 'badges'),
        checkboxRow('Needs Action mode', 'needsActionMode'),
        checkboxRow('Hard-hide handled missions', 'hardHideHandled'),
        numberRow('Stale warning minutes', 'staleMissionMinutes', 5, 999),
        checkboxRow('Building icon override', 'buildingIconOverrideEnabled')
      ]),
      createElement('div', { class: 'mc-ops-section' }, [
        createElement('div', { class: 'mc-ops-subtitle', text: 'Coverage pressure' }),
        checkboxRow('Coverage circles', 'coverageEnabled'),
        selectRow('Coverage mode', 'coverageMode', [
          ['all', 'All'],
          ['fire', 'Fire'],
          ['ems', 'EMS'],
          ['police', 'Police'],
          ['specialist', 'Specialist']
        ]),
        numberRow('Radius km', 'coverageRadiusKm', 1, 100),
        createElement('div', { id: 'mc-ops-coverage-status', class: 'mc-ops-muted', text: 'Coverage disabled.' })
      ]),
      createElement('div', { class: 'mc-ops-section' }, [
        createElement('div', { class: 'mc-ops-subtitle', text: 'Resource drain' }),
        checkboxRow('Use MissionChief API', 'apiEnabled'),
        checkboxRow('Show resource warnings', 'resourceDrainPanel'),
        createElement('div', { id: 'mc-ops-resource-status', class: 'mc-ops-muted', text: 'No vehicle data loaded yet.' })
      ]),
      createElement('div', { class: 'mc-ops-section' }, [
        createElement('div', { class: 'mc-ops-subtitle', text: 'Building icons' }),
        createElement('div', { class: 'mc-ops-row' }, [
          createElement('button', { class: 'mc-ops-btn', type: 'button', text: 'List types', onclick: listBuildingTypes }),
          createElement('button', { class: 'mc-ops-btn', type: 'button', text: 'Set icon', onclick: setIconOverride }),
          createElement('button', { class: 'mc-ops-btn', type: 'button', text: 'Clear icon', onclick: clearIconOverride })
        ]),
        createElement('div', { id: 'mc-ops-icon-status', class: 'mc-ops-muted', text: 'Icon overrides are local to this browser.' })
      ]),
      createElement('div', { class: 'mc-ops-section' }, [
        checkboxRow('Debug logging', 'debug'),
        createElement('div', { class: 'mc-ops-muted', text: 'Safe overlay only: this script does not dispatch, cancel, or click units.' })
      ])
    );

    panel.append(head, body);
    document.body.appendChild(panel);
    state.panel = panel;

    makePanelDraggable(panel, head);
  }

  function statBox(value, label, cls) {
    return createElement('div', { class: `mc-ops-stat ${cls || ''}` }, [
      createElement('strong', { text: value }),
      createElement('span', { text: label })
    ]);
  }

  function checkboxRow(label, key) {
    const input = createElement('input', { type: 'checkbox' });
    input.checked = !!state.config[key];
    input.addEventListener('change', () => {
      state.config[key] = input.checked;
      saveConfig();
      queueScan(true);
      if (['coverageEnabled', 'coverageMode'].includes(key)) drawCoverage();
      if (key === 'apiEnabled') refreshApiData(true);
    });

    return createElement('div', { class: 'mc-ops-row' }, [
      createElement('label', {}, [input, createElement('span', { text: label })])
    ]);
  }

  function numberRow(label, key, min, max) {
    const input = createElement('input', { type: 'number', min, max, value: state.config[key] });
    input.addEventListener('change', () => {
      const value = clamp(Number(input.value || state.config[key]), min, max);
      input.value = String(value);
      state.config[key] = value;
      saveConfig();
      queueScan(true);
      if (key === 'coverageRadiusKm') drawCoverage();
    });

    return createElement('div', { class: 'mc-ops-row' }, [
      createElement('label', {}, [createElement('span', { text: label })]),
      input
    ]);
  }

  function selectRow(label, key, options) {
    const select = createElement('select');
    options.forEach(([value, text]) => {
      const option = createElement('option', { value, text });
      option.selected = state.config[key] === value;
      select.appendChild(option);
    });

    select.addEventListener('change', () => {
      state.config[key] = select.value;
      saveConfig();
      queueScan(true);
      drawCoverage();
    });

    return createElement('div', { class: 'mc-ops-row' }, [
      createElement('label', {}, [createElement('span', { text: label })]),
      select
    ]);
  }

  function makePanelDraggable(panel, handle) {
    let dragging = false;
    let startX = 0;
    let startY = 0;
    let startRight = 0;
    let startTop = 0;

    handle.addEventListener('mousedown', event => {
      if (event.target.closest('button')) return;
      dragging = true;
      startX = event.clientX;
      startY = event.clientY;
      startRight = Number.parseFloat(panel.style.right || '12');
      startTop = Number.parseFloat(panel.style.top || '74');
      event.preventDefault();
    });

    document.addEventListener('mousemove', event => {
      if (!dragging) return;
      panel.style.right = `${Math.max(4, startRight - (event.clientX - startX))}px`;
      panel.style.top = `${Math.max(4, startTop + (event.clientY - startY))}px`;
    });

    document.addEventListener('mouseup', () => {
      dragging = false;
    });
  }

  function queueScan(immediate = false) {
    if (state.scanQueued && !immediate) return;
    state.scanQueued = true;

    window.setTimeout(() => {
      state.scanQueued = false;
      scanNow();
    }, immediate ? 20 : 250);
  }

  function scanNow() {
    if (state.isScanning) return;
    state.isScanning = true;

    try {
      if (!state.config.enabled) {
        cleanupVisuals();
        updateStatsPanel(emptyStats(), 'Disabled.');
        return;
      }

      ensureBadgeLayer();

      const missions = scanMissionPanels();
      state.missions = missions;

      applyMissionPanelStyles(missions);
      applyMissionMarkerStyles(missions);
      applyBuildingIconOverrides();
      updateBadges();

      const stats = calculateStats(missions);
      state.lastStats = stats;

      updateStatsPanel(stats);
      drawCoverage();
    } catch (error) {
      console.warn(`[${APP}] scan failed`, error);
      updateMainAlert(`Scan error: ${error.message || error}`, 'danger');
    } finally {
      state.isScanning = false;
    }
  }

  function emptyStats() {
    return {
      total: 0,
      action: 0,
      blocked: 0,
      patients: 0,
      prisoners: 0,
      stale: 0,
      handled: 0,
      fire: 0,
      ems: 0,
      police: 0,
      specialist: 0,
      red: 0,
      yellow: 0,
      orange: 0,
      blue: 0,
      purple: 0,
      green: 0,
      black: 0
    };
  }

  function scanMissionPanels() {
    const missionLinks = Array.from(document.querySelectorAll('a[href*="/missions/"]'));

    const candidates = [
      ...Array.from(document.querySelectorAll('div[id^="mission_panel_"]')),
      ...Array.from(document.querySelectorAll('.mission_panel')),
      ...Array.from(document.querySelectorAll('[id^="mission_"][class*="panel"]')),
      ...missionLinks
        .map(link => link.closest('.panel, .mission_panel, div[id^="mission_panel_"], li, tr'))
        .filter(Boolean)
    ];

    const seenElements = new Set();
    const result = new Map();

    candidates.forEach(panel => {
      if (!panel || seenElements.has(panel)) return;
      seenElements.add(panel);

      const id = extractMissionId(panel);
      if (!id || result.has(id)) return;

      result.set(id, classifyMissionPanel(panel, id));
    });

    return result;
  }

  function extractMissionId(element) {
    const values = collectCandidateStrings(element, 3);

    for (const value of values) {
      const match = value.match(/(?:mission_panel_|mission_marker_|mission_|missions\/)(\d{3,})/i);
      if (match) return match[1];
    }

    const link = element.querySelector?.('a[href*="/missions/"]');
    const href = link?.getAttribute('href') || '';
    const linkMatch = href.match(/\/missions\/(\d{3,})/i);

    return linkMatch ? linkMatch[1] : '';
  }

  function collectCandidateStrings(element, parentDepth = 1) {
    const values = [];
    let current = element;

    for (let i = 0; current && i <= parentDepth; i += 1) {
      values.push(
        current.id || '',
        typeof current.className === 'string' ? current.className : '',
        current.getAttribute?.('href') || '',
        current.getAttribute?.('src') || '',
        current.getAttribute?.('alt') || '',
        current.getAttribute?.('title') || '',
        current.getAttribute?.('data-id') || '',
        current.getAttribute?.('data-mission-id') || '',
        current.getAttribute?.('data-building-id') || ''
      );
      current = current.parentElement;
    }

    return values.filter(Boolean).map(String);
  }

  function classifyMissionPanel(panel, id) {
    const text = safeText(panel.innerText || panel.textContent || '');
    const classText = `${panel.className || ''}`.toLowerCase();
    const title = getMissionTitle(panel) || `Mission ${id}`;
    const reasons = [];
    const categories = new Set();
    const ageMinutes = parseMissionAgeMinutes(panel, text);

    Object.entries(SERVICE_PATTERNS).forEach(([service, pattern]) => {
      if (pattern.test(text)) categories.add(service);
    });

    const missing = MISSING_PATTERNS.test(text) || /\bmission_panel_red\b/i.test(classText);
    const blocked = BLOCKED_PATTERNS.test(text);
    const noUnits = NO_UNITS_PATTERNS.test(text);
    const handledByText = HANDLED_PATTERNS.test(text);
    const hasPatient = /\b(patient|patients|casualt(?:y|ies)|ambulance|hems|hart|critical care)\b/i.test(text);
    const hasPrisoner = /\b(prisoner|prisoners|custody|cell|cells|detention)\b/i.test(text);
    const isStale = Number.isFinite(ageMinutes) && ageMinutes >= Number(state.config.staleMissionMinutes);
    const veryStale = Number.isFinite(ageMinutes) && ageMinutes >= Number(state.config.veryStaleMissionMinutes);
    const progressPct = readProgressPercent(panel);
    const greenClass = /\b(mission_panel_green|panel-success|success)\b/i.test(classText);
    const warningClass = /\b(mission_panel_yellow|panel-warning|warning)\b/i.test(classText);
    const dangerClass = /\b(mission_panel_red|panel-danger|danger)\b/i.test(classText);

    if (missing) reasons.push('missing requirement');
    if (blocked) reasons.push('blocked requirement');
    if (noUnits) reasons.push('no units assigned');
    if (hasPatient) reasons.push('patient/EMS attention');
    if (hasPrisoner) reasons.push('prisoner/custody attention');
    if (isStale) reasons.push(`stale ${ageMinutes}m`);

    let status = 'green';
    let action = false;

    if (blocked || dangerClass) {
      status = 'red';
      action = true;
    } else if (veryStale && !handledByText) {
      status = 'black';
      action = true;
    } else if ((missing || noUnits || warningClass) && !handledByText) {
      status = 'yellow';
      action = true;
    } else if (hasPrisoner && !handledByText) {
      status = 'purple';
      action = true;
    } else if (hasPatient && !handledByText) {
      status = 'blue';
      action = true;
    } else if (isStale && !handledByText) {
      status = 'orange';
      action = true;
    } else if (greenClass || progressPct >= 98 || handledByText) {
      status = 'green';
      action = false;
    } else if (missing) {
      status = 'yellow';
      action = true;
    }

    return {
      id,
      panel,
      title,
      text,
      status,
      action,
      reasons,
      categories: [...categories],
      ageMinutes,
      progressPct,
      hasPatient,
      hasPrisoner,
      isStale,
      missing,
      blocked,
      noUnits
    };
  }

  function getMissionTitle(panel) {
    const link = panel.querySelector?.('a[href*="/missions/"]');
    const titleElement = panel.querySelector?.('.panel-title, .mission-title, .mission_caption, .caption');
    return safeText(titleElement?.innerText || link?.innerText || panel.getAttribute?.('title') || '').slice(0, 90);
  }

  function readProgressPercent(panel) {
    const progress = panel.querySelector?.('.progress-bar, [role="progressbar"]');
    if (!progress) return NaN;

    const aria = progress.getAttribute('aria-valuenow');
    if (aria && !Number.isNaN(Number(aria))) return Number(aria);

    const width = progress.style?.width || progress.getAttribute('style') || '';
    const match = width.match(/(\d+(?:\.\d+)?)\s*%/);

    return match ? Number(match[1]) : NaN;
  }

  function parseMissionAgeMinutes(panel, text) {
    const candidates = [
      panel.getAttribute?.('data-created-at'),
      panel.getAttribute?.('data-created_at'),
      panel.querySelector?.('[data-created-at]')?.getAttribute('data-created-at'),
      panel.querySelector?.('[data-created_at]')?.getAttribute('data-created_at'),
      panel.querySelector?.('time')?.getAttribute('datetime'),
      panel.querySelector?.('.mission-time, .mission_time, .time')?.getAttribute('title')
    ].filter(Boolean);

    for (const candidate of candidates) {
      const parsed = Date.parse(candidate);
      if (!Number.isNaN(parsed)) return Math.max(0, Math.round((Date.now() - parsed) / 60000));
    }

    const directMinutes = text.match(/\b(\d{1,4})\s*(?:m|min|mins|minute|minutes)\b/i);
    if (directMinutes) return Number(directMinutes[1]);

    const hourMinute = text.match(/\b(\d{1,3})\s*(?:h|hr|hour|hours)\s*(?:(\d{1,2})\s*(?:m|min|mins|minute|minutes))?/i);
    if (hourMinute) return (Number(hourMinute[1]) * 60) + Number(hourMinute[2] || 0);

    return NaN;
  }

  function applyMissionPanelStyles(missions) {
    document.querySelectorAll('.mc-ops-mission-panel-red,.mc-ops-mission-panel-yellow,.mc-ops-mission-panel-blue,.mc-ops-mission-panel-purple,.mc-ops-mission-panel-orange,.mc-ops-mission-panel-green')
      .forEach(panel => panel.classList.remove(
        'mc-ops-mission-panel-red',
        'mc-ops-mission-panel-yellow',
        'mc-ops-mission-panel-blue',
        'mc-ops-mission-panel-purple',
        'mc-ops-mission-panel-orange',
        'mc-ops-mission-panel-green'
      ));

    if (!state.config.triageRings) return;

    missions.forEach(mission => {
      mission.panel?.classList?.add(`mc-ops-mission-panel-${mission.status === 'black' ? 'red' : mission.status}`);
    });
  }

  function calculateStats(missions) {
    const stats = emptyStats();
    stats.total = missions.size;

    missions.forEach(mission => {
      stats[mission.status] = (stats[mission.status] || 0) + 1;
      if (mission.action) stats.action += 1;
      if (mission.status === 'red' || mission.status === 'black' || mission.blocked) stats.blocked += 1;
      if (mission.hasPatient || mission.categories.includes('ems')) stats.patients += 1;
      if (mission.hasPrisoner) stats.prisoners += 1;
      if (mission.isStale) stats.stale += 1;
      if (!mission.action) stats.handled += 1;

      mission.categories.forEach(category => {
        stats[category] = (stats[category] || 0) + 1;
      });
    });

    return stats;
  }

  function updateStatsPanel(stats, customAlert = '') {
    const grid = document.getElementById('mc-ops-stat-grid');

    if (grid) {
      const boxes = grid.querySelectorAll('.mc-ops-stat strong');
      const values = [stats.total, stats.action, stats.blocked, stats.patients];
      boxes.forEach((box, index) => { box.textContent = String(values[index] ?? 0); });
    }

    if (customAlert) {
      updateMainAlert(customAlert);
    } else if (stats.blocked > 0) {
      updateMainAlert(`${stats.blocked} blocked / critical mission${stats.blocked === 1 ? '' : 's'} need attention.`, 'danger');
    } else if (stats.action > 0) {
      updateMainAlert(`${stats.action} mission${stats.action === 1 ? '' : 's'} need action.`, 'warn');
    } else if (stats.total > 0) {
      updateMainAlert('No obvious map action required.', 'good');
    } else {
      updateMainAlert('No mission panels detected on this page.', 'muted');
    }

    updateResourcePanel();
    updateCoveragePanel();
  }

  function updateMainAlert(text, kind = 'muted') {
    const alert = document.getElementById('mc-ops-main-alert');
    if (!alert) return;

    alert.textContent = text;
    alert.className = 'mc-ops-float-note';

    if (kind === 'danger') alert.classList.add('mc-ops-danger');
    else if (kind === 'warn') alert.classList.add('mc-ops-warn');
    else if (kind === 'good') alert.classList.add('mc-ops-good');
    else if (kind === 'blue') alert.classList.add('mc-ops-blue');
    else alert.classList.add('mc-ops-muted');
  }

  function getMissionMarkerEntries() {
    const mapContainer = getMapContainer();
    const entries = [];

    if (!mapContainer) return entries;

    const possibleGlobals = [
      window.mission_markers,
      window.missionMarkers,
      window.mission_marker,
      window.missionMarker
    ].filter(Boolean);

    for (const markerObject of possibleGlobals) {
      if (!markerObject || typeof markerObject !== 'object') continue;

      Object.entries(markerObject).forEach(([id, marker]) => {
        const icon = marker?._icon || marker?.icon || null;
        if (!icon || !isRealMapIcon(icon, mapContainer)) return;
        if (!/^\d+$/.test(String(id))) return;

        entries.push([String(id), icon]);
      });

      if (entries.length) return entries;
    }

    const fallbackIcons = Array.from(mapContainer.querySelectorAll('.leaflet-marker-pane .leaflet-marker-icon'));

    fallbackIcons.forEach(icon => {
      if (!isRealMapIcon(icon, mapContainer)) return;

      const id = extractMarkerMissionId(icon);
      if (!id) return;

      entries.push([id, icon]);
    });

    return entries;
  }

  function isRealMapIcon(icon, mapContainer) {
    if (!icon || !mapContainer || !mapContainer.contains(icon)) return false;
    if (!icon.closest('.leaflet-marker-pane')) return false;
    if (icon.closest('#mission_list, #radio_messages, .mission_panel, div[id^="mission_panel_"], .panel, table, tr, li')) return false;
    return true;
  }

  function applyMissionMarkerStyles(missions) {
    state.missionMarkers.clear();

    getMissionMarkerEntries().forEach(([id, marker]) => {
      removeMissionMarkerClasses(marker);

      if (!missions.has(id)) return;

      const mission = missions.get(id);
      state.missionMarkers.set(id, marker);

      marker.dataset.mcOpsMissionId = id;
      marker.style.setProperty('--mc-ops-dim-opacity', String(state.config.dimHandledOpacity));

      if (state.config.triageRings) {
        marker.classList.add(`mc-ops-ring-${mission.status === 'black' ? 'black' : mission.status}`);
      }

      if (state.config.needsActionMode && !mission.action) {
        marker.classList.add(state.config.hardHideHandled ? 'mc-ops-needs-action-hide' : 'mc-ops-needs-action-dim');
      }
    });
  }

  function removeMissionMarkerClasses(marker) {
    marker.classList.remove(
      'mc-ops-ring-green',
      'mc-ops-ring-yellow',
      'mc-ops-ring-orange',
      'mc-ops-ring-red',
      'mc-ops-ring-blue',
      'mc-ops-ring-purple',
      'mc-ops-ring-black',
      'mc-ops-needs-action-dim',
      'mc-ops-needs-action-hide'
    );
  }

  function extractMarkerMissionId(marker) {
    const values = collectCandidateStrings(marker, 4);

    for (const value of values) {
      const patterns = [
        /(?:mission_marker_|mission_)(\d{3,})/i,
        /\/missions\/(\d{3,})/i,
        /[?&]mission_id=(\d{3,})/i,
        /(?:mission)[^0-9]{0,12}(\d{3,})/i
      ];

      for (const pattern of patterns) {
        const match = value.match(pattern);
        if (match) return match[1];
      }
    }

    return '';
  }

  function updateBadges() {
    const layer = ensureBadgeLayer();

    if (!layer) return;

    if (!state.config.enabled || !state.config.badges) {
      layer.innerHTML = '';
      return;
    }

    const active = new Set();

    state.missionMarkers.forEach((marker, id) => {
      const mission = state.missions.get(id);
      if (!mission) return;
      if (state.config.needsActionMode && !mission.action && state.config.hardHideHandled) return;

      active.add(id);

      let badge = layer.querySelector(`[data-mc-ops-badge-id="${cssEscape(id)}"]`);

      if (!badge) {
        badge = document.createElement('div');
        badge.dataset.mcOpsBadgeId = id;
        layer.appendChild(badge);
      }

      badge.className = `mc-ops-badge mc-ops-badge-${mission.status === 'black' ? 'black' : mission.status}`;
      badge.textContent = getBadgeText(mission);

      positionBadge(marker, badge, layer);
    });

    Array.from(layer.querySelectorAll('.mc-ops-badge')).forEach(badge => {
      if (!active.has(badge.dataset.mcOpsBadgeId)) badge.remove();
    });
  }

  function getBadgeText(mission) {
    if (mission.status === 'red' || mission.status === 'black') return '!';
    if (mission.hasPrisoner) return 'P';
    if (mission.hasPatient || mission.categories.includes('ems')) return 'EMS';
    if (mission.isStale) return 'OLD';
    if (mission.categories.includes('police')) return 'POL';
    if (mission.categories.includes('specialist')) return 'SPEC';
    if (mission.categories.includes('fire')) return 'FIR';
    if (mission.status === 'yellow') return '?';
    return '✓';
  }

  function positionBadge(marker, badge, layer = state.badgeLayer) {
    const mapContainer = getMapContainer();
    if (!mapContainer || !layer || !marker) {
      badge.style.display = 'none';
      return;
    }

    const mapRect = mapContainer.getBoundingClientRect();
    const rect = marker.getBoundingClientRect();

    const outside =
      rect.right < mapRect.left ||
      rect.left > mapRect.right ||
      rect.bottom < mapRect.top ||
      rect.top > mapRect.bottom;

    if (outside || (!rect.width && !rect.height)) {
      badge.style.display = 'none';
      return;
    }

    const x = Math.round(rect.left - mapRect.left + rect.width - 8);
    const y = Math.round(rect.top - mapRect.top - 5);

    badge.style.display = '';
    badge.style.left = `${clamp(x, 0, Math.max(0, mapRect.width - 20))}px`;
    badge.style.top = `${clamp(y, 0, Math.max(0, mapRect.height - 20))}px`;
  }

  function repositionBadges() {
    if (!state.badgeLayer || !state.config.badges) return;

    state.missionMarkers.forEach((marker, id) => {
      const badge = state.badgeLayer.querySelector(`[data-mc-ops-badge-id="${cssEscape(id)}"]`);
      if (badge) positionBadge(marker, badge);
    });
  }

  function cleanupVisuals() {
    document.querySelectorAll('.mc-ops-ring-green,.mc-ops-ring-yellow,.mc-ops-ring-orange,.mc-ops-ring-red,.mc-ops-ring-blue,.mc-ops-ring-purple,.mc-ops-ring-black,.mc-ops-needs-action-dim,.mc-ops-needs-action-hide')
      .forEach(removeMissionMarkerClasses);

    document.querySelectorAll('.mc-ops-mission-panel-red,.mc-ops-mission-panel-yellow,.mc-ops-mission-panel-blue,.mc-ops-mission-panel-purple,.mc-ops-mission-panel-orange,.mc-ops-mission-panel-green')
      .forEach(panel => panel.classList.remove(
        'mc-ops-mission-panel-red',
        'mc-ops-mission-panel-yellow',
        'mc-ops-mission-panel-blue',
        'mc-ops-mission-panel-purple',
        'mc-ops-mission-panel-orange',
        'mc-ops-mission-panel-green'
      ));

    const layer = ensureBadgeLayer();
    if (layer) layer.innerHTML = '';

    clearCoverageLayers();
  }

  async function refreshApiData(force = false) {
    if (!state.config.apiEnabled) {
      state.apiError = '';
      state.vehicleSummary = null;
      updateResourcePanel();
      return;
    }

    const now = Date.now();
    if (!force && now - state.apiLastRefresh < Number(state.config.apiRefreshMs)) return;

    state.apiLastRefresh = now;

    try {
      const [buildings, vehicles] = await Promise.all([
        fetchBuildings().catch(error => {
          log('building API failed', error);
          return [];
        }),
        fetchVehicles().catch(error => {
          log('vehicle API failed', error);
          return [];
        })
      ]);

      state.buildings = new Map(buildings.map(building => [String(building.id), building]));
      state.vehicles = vehicles;
      state.vehicleSummary = summariseVehicles(vehicles);
      state.apiError = '';

      applyBuildingIconOverrides();
      updateResourcePanel();
      updateCoveragePanel();
      drawCoverage();
    } catch (error) {
      state.apiError = error.message || String(error);
      updateResourcePanel();
      updateCoveragePanel();
    }
  }

  async function fetchJson(url) {
    const response = await fetch(url, {
      credentials: 'same-origin',
      headers: { Accept: 'application/json' }
    });

    if (!response.ok) throw new Error(`${url} returned ${response.status}`);
    return response.json();
  }

  async function fetchBuildings() {
    const json = await fetchJson('/api/buildings');

    if (Array.isArray(json)) return json;
    if (Array.isArray(json.result)) return json.result;
    if (Array.isArray(json.buildings)) return json.buildings;

    return [];
  }

  async function fetchVehicles() {
    const vehicles = [];
    let url = '/api/v2/vehicles?limit=10000';
    const visited = new Set();

    for (let page = 0; page < 25 && url && !visited.has(url); page += 1) {
      visited.add(url);

      const json = await fetchJson(url);
      const result = json.result;

      const pageVehicles = Array.isArray(result)
        ? result
        : Array.isArray(result?.vehicles)
          ? result.vehicles
          : Array.isArray(json.vehicles)
            ? json.vehicles
            : [];

      vehicles.push(...pageVehicles);

      const paging = json.paging || result?.paging || {};
      url = paging.next_page || '';

      if (url && /^https?:\/\//i.test(url)) {
        try {
          const parsed = new URL(url);
          url = `${parsed.pathname}${parsed.search}`;
        } catch {
          url = '';
        }
      }
    }

    return vehicles;
  }

  function summariseVehicles(vehicles) {
    const summary = {
      total: vehicles.length,
      groups: {
        fire: emptyVehicleGroup(),
        ems: emptyVehicleGroup(),
        police: emptyVehicleGroup(),
        specialist: emptyVehicleGroup(),
        other: emptyVehicleGroup()
      },
      warnings: []
    };

    vehicles.forEach(vehicle => {
      const groupName = classifyVehicleService(vehicle);
      const group = summary.groups[groupName] || summary.groups.other;

      group.total += 1;

      if (isVehicleAvailable(vehicle)) group.available += 1;
      else group.committed += 1;
    });

    Object.entries(summary.groups).forEach(([name, group]) => {
      if (group.total <= 0 || name === 'other') return;

      const ratio = group.available / group.total;

      if (group.available === 0) summary.warnings.push(`${labelService(name)}: no available units`);
      else if (ratio <= 0.15 && group.total >= 5) summary.warnings.push(`${labelService(name)}: low cover (${group.available}/${group.total})`);
    });

    return summary;
  }

  function emptyVehicleGroup() {
    return { total: 0, available: 0, committed: 0 };
  }

  function classifyVehicleService(vehicle) {
    const text = normalise([
      vehicle.caption,
      vehicle.name,
      vehicle.vehicle_type_caption,
      vehicle.vehicle_type_name,
      vehicle.type_caption,
      vehicle.type,
      vehicle.vehicle_type,
      vehicle.building_caption
    ].join(' '));

    if (SERVICE_PATTERNS.ems.test(text)) return 'ems';
    if (SERVICE_PATTERNS.police.test(text)) return 'police';
    if (SERVICE_PATTERNS.specialist.test(text)) return 'specialist';
    if (SERVICE_PATTERNS.fire.test(text)) return 'fire';

    return 'other';
  }

  function isVehicleAvailable(vehicle) {
    const rawStatus = vehicle.fms_real ?? vehicle.fms ?? vehicle.status ?? vehicle.vehicle_status ?? vehicle.fms_show;
    const status = Number(rawStatus);

    if (Number.isFinite(status)) return status === 1 || status === 2;

    const text = normalise(`${vehicle.status_caption || ''} ${vehicle.fms_text || ''}`);

    if (/available|ready|station|frei|verfügbar/.test(text)) return true;
    if (/en.?route|scene|busy|transport|not available|unavailable/.test(text)) return false;

    return false;
  }

  function labelService(name) {
    return ({
      fire: 'Fire',
      ems: 'EMS',
      police: 'Police',
      specialist: 'Specialist',
      other: 'Other'
    })[name] || name;
  }

  function updateResourcePanel() {
    const box = document.getElementById('mc-ops-resource-status');
    if (!box) return;

    if (!state.config.apiEnabled) {
      box.className = 'mc-ops-muted';
      box.textContent = 'API disabled.';
      return;
    }

    if (state.apiError) {
      box.className = 'mc-ops-danger';
      box.textContent = `API issue: ${state.apiError}`;
      return;
    }

    const summary = state.vehicleSummary;

    if (!summary) {
      box.className = 'mc-ops-muted';
      box.textContent = 'No vehicle data loaded yet.';
      return;
    }

    const groupText = Object.entries(summary.groups)
      .filter(([name, group]) => name !== 'other' && group.total > 0)
      .map(([name, group]) => `${labelService(name)} ${group.available}/${group.total}`)
      .join(' · ');

    const warnings = summary.warnings.slice(0, 4).join(' | ');

    box.className = summary.warnings.length ? 'mc-ops-warn' : 'mc-ops-good';
    box.textContent = warnings ? `${groupText}. ${warnings}` : `${groupText || 'Vehicles loaded'} · cover looks stable.`;
  }

  function getLeafletMap() {
    if (state.leafletMap && state.leafletMap.addLayer && state.leafletMap.removeLayer) return state.leafletMap;

    if (window.map && typeof window.map.addLayer === 'function') {
      state.leafletMap = window.map;
      return state.leafletMap;
    }

    if (window.L?.Map) {
      for (const key in window) {
        try {
          const value = window[key];
          if (value instanceof window.L.Map) {
            state.leafletMap = value;
            return value;
          }
        } catch {
          // Ignore protected window properties.
        }
      }
    }

    return null;
  }

  function updateCoveragePanel() {
    const box = document.getElementById('mc-ops-coverage-status');
    if (!box) return;

    if (!state.config.coverageEnabled) {
      box.className = 'mc-ops-muted';
      box.textContent = 'Coverage disabled.';
      return;
    }

    if (!window.L || !getLeafletMap()) {
      box.className = 'mc-ops-warn';
      box.textContent = 'Leaflet map object not detected yet.';
      return;
    }

    const count = state.coverageLayers.length;

    box.className = count ? 'mc-ops-good' : 'mc-ops-muted';
    box.textContent = `${count} coverage circle${count === 1 ? '' : 's'} active.`;
  }

  function drawCoverage() {
    clearCoverageLayers();

    if (!state.config.enabled || !state.config.coverageEnabled) {
      updateCoveragePanel();
      return;
    }

    const map = getLeafletMap();

    if (!map || !window.L) {
      updateCoveragePanel();
      return;
    }

    const mode = state.config.coverageMode;

    const buildings = Array.from(state.buildings.values()).filter(building => {
      const lat = Number(building.latitude ?? building.lat);
      const lon = Number(building.longitude ?? building.lng ?? building.lon);

      if (!Number.isFinite(lat) || !Number.isFinite(lon)) return false;
      if (mode === 'all') return true;

      return classifyBuildingService(building) === mode;
    }).slice(0, Number(state.config.coverageCircleLimit));

    const radius = clamp(Number(state.config.coverageRadiusKm) || 12, 1, 100) * 1000;

    buildings.forEach(building => {
      const lat = Number(building.latitude ?? building.lat);
      const lon = Number(building.longitude ?? building.lng ?? building.lon);
      const service = classifyBuildingService(building);

      const circle = window.L.circle([lat, lon], {
        radius,
        color: serviceColor(service),
        weight: 1,
        opacity: 0.35,
        fillColor: serviceColor(service),
        fillOpacity: 0.035,
        interactive: false
      });

      circle.addTo(map);
      state.coverageLayers.push(circle);
    });

    updateCoveragePanel();
  }

  function clearCoverageLayers() {
    const map = getLeafletMap();

    state.coverageLayers.forEach(layer => {
      try {
        if (map) map.removeLayer(layer);
      } catch {
        // Ignore stale Leaflet layer errors.
      }
    });

    state.coverageLayers = [];
  }

  function serviceColor(service) {
    return ({
      fire: '#ff3b30',
      ems: '#0a84ff',
      police: '#bf5af2',
      specialist: '#ff9f0a',
      other: '#8e8e93',
      all: '#8e8e93'
    })[service] || '#8e8e93';
  }

  function classifyBuildingService(building) {
    const extensionText = Array.isArray(building.extensions)
      ? building.extensions.map(extension => extension.caption || extension.type || '').join(' ')
      : '';

    const specializationText = Array.isArray(building.specializations)
      ? building.specializations.map(specialization => specialization.caption || specialization.type || '').join(' ')
      : '';

    const text = normalise([
      building.caption,
      building.name,
      building.building_type_caption,
      building.type_caption,
      building.type,
      building.building_type,
      extensionText,
      specializationText
    ].join(' '));

    if (SERVICE_PATTERNS.ems.test(text)) return 'ems';
    if (SERVICE_PATTERNS.police.test(text)) return 'police';
    if (SERVICE_PATTERNS.specialist.test(text)) return 'specialist';
    if (SERVICE_PATTERNS.fire.test(text)) return 'fire';

    return 'other';
  }

  function buildingTypeLabel(building) {
    const explicit = safeText(building.building_type_caption || building.type_caption || building.type_name || building.building_type_name);

    if (explicit) return explicit;

    const numeric = building.building_type ?? building.type;

    if (numeric !== undefined && numeric !== null && numeric !== '') return `type:${numeric}`;

    return 'Unknown type';
  }

  function getBuildingMarkerEntries() {
    const mapContainer = getMapContainer();
    const entries = [];

    if (!mapContainer) return entries;

    const possibleGlobals = [
      window.building_markers,
      window.buildingMarkers,
      window.building_marker,
      window.buildingMarker
    ].filter(Boolean);

    for (const markerObject of possibleGlobals) {
      if (!markerObject || typeof markerObject !== 'object') continue;

      Object.entries(markerObject).forEach(([id, marker]) => {
        const icon = marker?._icon || marker?.icon || null;
        if (!icon || !isRealMapIcon(icon, mapContainer)) return;
        if (!/^\d+$/.test(String(id))) return;

        entries.push([String(id), icon]);
      });

      if (entries.length) return entries;
    }

    return entries;
  }

  function applyBuildingIconOverrides() {
    if (!state.config.enabled || !state.config.buildingIconOverrideEnabled) return;
    if (!state.buildings.size) return;

    const overrides = state.config.iconOverrides || {};
    if (!Object.keys(overrides).length) return;

    getBuildingMarkerEntries().forEach(([id, marker]) => {
      const building = state.buildings.get(String(id));
      if (!building) return;

      const label = buildingTypeLabel(building);
      const numericKey = `type:${building.building_type ?? building.type}`;
      const serviceKey = `service:${classifyBuildingService(building)}`;
      const iconUrl = overrides[label] || overrides[numericKey] || overrides[serviceKey];

      if (!iconUrl) return;

      if (!marker.dataset.mcOpsOriginalSrc) marker.dataset.mcOpsOriginalSrc = marker.getAttribute('src') || '';
      if (marker.getAttribute('src') !== iconUrl) marker.setAttribute('src', iconUrl);

      marker.dataset.mcOpsIconType = label;
    });

    const iconStatus = document.getElementById('mc-ops-icon-status');

    if (iconStatus) {
      iconStatus.textContent = `${Object.keys(overrides).length} icon override${Object.keys(overrides).length === 1 ? '' : 's'} configured.`;
    }
  }

  function listBuildingTypes() {
    refreshApiData(true).then(() => {
      const counts = new Map();

      state.buildings.forEach(building => {
        const label = buildingTypeLabel(building);
        counts.set(label, (counts.get(label) || 0) + 1);
      });

      const lines = Array.from(counts.entries())
        .sort((a, b) => a[0].localeCompare(b[0]))
        .map(([label, count]) => `${label} (${count})`);

      alert(lines.length ? `Detected building types:\n\n${lines.join('\n')}` : 'No building types detected. Try again on the main map after it loads.');
    });
  }

  function setIconOverride() {
    const type = prompt('Exact building type to override. Use "List types" first, or use service:fire, service:ems, service:police, service:specialist.');
    if (!type) return;

    const iconUrl = prompt(`Image URL or data:image value for:\n${type}`);
    if (!iconUrl) return;

    state.config.iconOverrides[type.trim()] = iconUrl.trim();
    saveConfig();

    applyBuildingIconOverrides();

    const iconStatus = document.getElementById('mc-ops-icon-status');
    if (iconStatus) iconStatus.textContent = `Override saved for ${type.trim()}.`;
  }

  function clearIconOverride() {
    const current = Object.keys(state.config.iconOverrides || {});

    if (!current.length) {
      alert('No icon overrides are configured.');
      return;
    }

    const type = prompt(`Type to clear, or ALL to reset everything:\n\n${current.join('\n')}`);
    if (!type) return;

    if (type.trim().toUpperCase() === 'ALL') state.config.iconOverrides = {};
    else delete state.config.iconOverrides[type.trim()];

    saveConfig();
    restoreOriginalBuildingIcons();
    queueScan(true);
  }

  function restoreOriginalBuildingIcons() {
    document.querySelectorAll('[data-mc-ops-original-src]').forEach(marker => {
      const original = marker.dataset.mcOpsOriginalSrc;
      if (original) marker.setAttribute('src', original);

      delete marker.dataset.mcOpsOriginalSrc;
      delete marker.dataset.mcOpsIconType;
    });
  }

  function setupObservers() {
    if (state.observer) state.observer.disconnect();

    state.observer = new MutationObserver(mutations => {
      if (state.isScanning) return;

      const meaningful = mutations.some(mutation => {
        if (mutation.target?.id === 'mc-opsmap-panel' || mutation.target?.closest?.('#mc-opsmap-panel')) return false;
        if (mutation.target?.id === 'mc-opsmap-badges' || mutation.target?.closest?.('#mc-opsmap-badges')) return false;
        return true;
      });

      if (meaningful) queueScan(false);
    });

    state.observer.observe(document.body, {
      childList: true,
      subtree: true,
      attributes: true,
      attributeFilter: ['class', 'style', 'src', 'href', 'title']
    });

    window.addEventListener('resize', repositionBadges, { passive: true });
    window.addEventListener('scroll', repositionBadges, { passive: true });
    document.addEventListener('mousemove', throttle(repositionBadges, 250), { passive: true });
  }

  function throttle(fn, waitMs) {
    let last = 0;
    let timer = null;

    return function throttled(...args) {
      const now = Date.now();
      const remaining = waitMs - (now - last);

      if (remaining <= 0) {
        last = now;
        fn.apply(this, args);
      } else if (!timer) {
        timer = setTimeout(() => {
          timer = null;
          last = Date.now();
          fn.apply(this, args);
        }, remaining);
      }
    };
  }

  function startTimers() {
    window.clearInterval(state.scanTimer);
    window.clearInterval(state.badgeTimer);
    window.clearInterval(state.apiTimer);

    state.scanTimer = window.setInterval(() => queueScan(false), Number(state.config.scanIntervalMs) || 3000);
    state.badgeTimer = window.setInterval(repositionBadges, 800);
    state.apiTimer = window.setInterval(() => refreshApiData(false), Number(state.config.apiRefreshMs) || 60000);
  }

  function exposeDebugApi() {
    window.MCOpsMapCommand = {
      version: VERSION,
      state,
      scan: () => scanNow(),
      refreshApi: () => refreshApiData(true),
      config: () => JSON.parse(JSON.stringify(state.config)),
      resetConfig: () => {
        localStorage.removeItem(CFG_KEY);
        location.reload();
      }
    };
  }

  function init() {
    injectStyles();
    ensureBadgeLayer();
    injectPanel();
    setupObservers();
    startTimers();
    exposeDebugApi();
    queueScan(true);
    refreshApiData(true);
    log('started', VERSION);
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init, { once: true });
  } else {
    init();
  }
})();