{
  "version": 3,
  "sources": ["sw_register.js"],
  "sourcesContent": ["/*\n * static/js/sw_register.js \u2014 Site-wide service-worker registration.\n *\n * Loaded deferred from ``public/templates/public/base.html`` so the SW\n * registration runs on every public page \u2014 required for browser\n * install prompts, which only appear after the page is served by an\n * SW with a manifest carrying valid icons.\n *\n * Kill-switch config gate (SNOW-373, spec \u00A76.2 Mechanism A)\n * ---------------------------------------------------------\n * Before registering any SW we fetch ``/api/sw-config`` (SNOW-372) \u2014\n * ``cache: 'no-store'`` so ops sees the live value. Two possible\n * outcomes drive the branch:\n *\n *   - ``{kill: true}``  \u2192 unregister every SW on this origin and stop.\n *     The next navigation goes straight to the network. Nothing on the\n *     origin is served through a SW until ops flips ``kill`` back.\n *\n *   - ``{kill: false, sw_url: <path>}`` \u2192 register the SW URL the\n *     config returned. Defaults to ``/sw.js``; ops can flip it to\n *     ``/sw-kill.js`` (Mechanism B, ``static/js/sw-kill.js``) to swap\n *     every installed client onto the wipe-and-unregister worker\n *     without a code deploy.\n *\n * If ``/api/sw-config`` is unreachable (server down, network dropped),\n * fall back to ``{sw_url: '/sw.js', kill: false}`` \u2014 the failure mode\n * of the config endpoint must never block SW registration on a working\n * server. Non-reachability is the state the endpoint would report\n * before the SNOW-372 deploy, so this also handles the transition\n * gracefully.\n *\n * Update flow (SNOW-1025)\n * -----------------------\n * The contract, end-user-facing: *a deploy asks nothing of you.* Updates\n * apply on their own, out of sight. The one message anyone sees, \"Snowdesk\n * needs a refresh\", appears only when the service worker is stuck and\n * cannot update without help.\n *\n * **Routine update: silent.** The PWA shell SW (``static/js/sw.js``) does\n * NOT ``skipWaiting()`` on install, so a new worker parks in \"waiting\".\n * This script records it (``queueSilentUpdate``) and posts\n * ``{ type: 'SKIP_WAITING' }`` to it. When depends on the page:\n *\n *   * If the waiting worker holds the same shell as this page's\n *     ``<meta name=\"pwa-shell\">`` (SNOW-1027), post at once. The page came\n *     off the network as that worker's build; only the old worker is out of\n *     date. This is the fresh-tab case, where opening the tab is what\n *     installed the new worker.\n *   * Otherwise, post the next time the page is hidden (tab switch, app\n *     switch, screen lock). The page was served by the old worker and may\n *     still read the old shell cache that activation sweeps.\n *\n * Either way, an in-flight ``warmCache`` run holds it back: the worker\n * that owns a basemap download must not be retired halfway through it. The new worker claims the page;\n * the ``controllerchange`` that follows does NOT reload. The open page\n * keeps running and its next navigation lands on the new shell.\n *\n * **Stuck worker: the banner.** ``pwa_version_check.js`` notices when the\n * server has moved on (an ``X-App-Version`` drift, confirmed by\n * ``/api/version``) and offers the banner through\n * ``window.pwaUpdateBanner.reveal``. Two gates in series decide it:\n *\n *   1. ``shellIsStale`` (SNOW-952): does the worker in control hold a\n *      different shell from the one the server would serve today? If not,\n *      there is nothing to update.\n *   2. ``workerIsStuck`` (SNOW-1025): can the browser fix that on its own?\n *      It asks the registration for an update and waits for any installing\n *      worker to settle. A worker that reaches \"waiting\" will be applied\n *      silently, so it is not stuck. A worker that goes \"redundant\" (one\n *      bad precache entry rejects ``install``'s atomic ``cache.addAll``),\n *      or no new worker at all, is stuck. That is the one case where a\n *      person has to act.\n *\n * SNOW-952's gate alone was not enough. It is true after every deploy that\n * touches a shell source, which is most of them, since the shell hash\n * covers every ``static/js`` file. So the banner kept interrupting people\n * during ordinary use.\n *\n * The copy is plain (SNOW-1025): no build SHAs, no release labels. The\n * versioned copy (SNOW-869) and the controller-identity labelling\n * (SNOW-933) are gone, and with them the build identity ``serve_sw`` used\n * to bake into the worker. That identity made the worker's bytes change on\n * every deploy, Python-only ones included.\n *\n * Clicking \"Reload\" runs ``handleReloadClick`` below:\n *\n *   * If a fresh SW is waiting \u2192 post ``{ type: 'SKIP_WAITING' }``. The\n *     worker activates, calls ``clients.claim()``, and the browser fires\n *     ``controllerchange``. Because the user asked for it, we reload the\n *     page exactly once (guarded by ``refreshing``) onto the new shell.\n *\n *   * If no worker is waiting (the stuck case) \u2192 clear the SW's shell\n *     caches and reload. Without clearing, the SW's ``_networkFirst``\n *     navigate handler can hand back cached HTML carrying the stale\n *     ``pwa-app-version`` meta tag, so the version check would re-show the\n *     banner and the user would be stuck in a reload loop.\n *\n *   Either way the click always ends in a reload: the waiting worker is\n *   re-resolved from the live registration (a captured reference can go\n *   redundant, and messaging a redundant worker is a silent no-op), and a\n *   fallback timer downgrades to the cache-clearing reload if activation\n *   never fires ``controllerchange``.\n *\n *   Either way it also takes a moment, so the click is acknowledged on\n *   screen before any of it starts. See ``showBannerBusy``.\n *\n * ``register`` passes ``updateViaCache: 'none'`` so the SW script is\n * never served from the HTTP cache during an update check \u2014 a changed\n * ``sw.js`` is always detected. We also call ``registration.update()``\n * when the tab regains focus, so a long-open tab learns about a new\n * version without needing a navigation.\n *\n * Errors from ``register()`` are logged but never surfaced to the\n * user \u2014 the site is fully usable without a service worker.\n *\n * i18n: on public pages every user-visible string lives in the banner\n * template under ``{% trans %}`` and this script only toggles visibility.\n * The self-injected admin fallback banner carries English strings inline\n * (the admin is staff-only and English-only), so it is the one exception.\n */\n\n(function () {\n  'use strict';\n\n  // SNOW-585: only present when settings.SW_DEV_SHELL_BYPASS is on (base.html;\n  // always false in production). The bypass already serves fresh shell assets\n  // on the very next reload \u2014 even from a worker that hasn't picked up the\n  // new one yet \u2014 so the update banner would be actively misleading; showing\n  // it, and its pwa.sw.update_available telemetry, is suppressed at the one\n  // call site below. See docs/decisions/dev-bypasses-the-shell-cache.md.\n  const DEV_SHELL_BYPASS_ACTIVE =\n    document.querySelector('meta[name=\"pwa-dev-shell-bypass\"]')?.getAttribute('content') === '1';\n\n  // SNOW-1027: the shell cache name this page was built against, the same\n  // CACHE_VERSION serve_sw injects into sw.js (base.html renders it). A\n  // waiting worker reporting this same name holds exactly this page's\n  // build, so it can take over at once. Empty on pages that do not extend\n  // public/base.html (the admin), which then keep the hide-to-apply rule.\n  const PAGE_SHELL = (\n    document.querySelector('meta[name=\"pwa-shell\"]')?.getAttribute('content') || ''\n  ).trim();\n\n  // SNOW-620: copy for the self-injected fallback banner (ensureBanner\n  // below), which is what admin pages get \u2014 they don't include the public\n  // chrome, so includes/_sw_update_banner.html and its already-translated\n  // strings aren't there. Rendered into a strings template by\n  // admin/base_site.html; the literals are the English fallback, and are\n  // deliberately the same three strings that partial already translates.\n  // See static/js/i18n_strings.js.\n  const STRINGS = self.pwaStrings.read('sw-update-strings-template', {\n    'update-title': 'Snowdesk needs a refresh',\n    'update-body': \"It couldn't update itself. Reload to finish.\",\n    reload: 'Reload',\n    dismiss: 'Dismiss',\n    updating: 'Updating\u2026',\n    'updating-title': 'Updating Snowdesk',\n    'updating-body':\n      'Applying the new version \u2014 the page will reload in a moment.',\n  });\n\n  // Guard on the truthy value, not ``'serviceWorker' in navigator``: the\n  // e2e SW-stripping helpers define the property with ``value: undefined``\n  // (key present, value nullish), and the ``navigator.serviceWorker``\n  // dereferences below (``.addEventListener`` etc.) would otherwise throw\n  // (SNOW-484). Equivalent to the ``in`` check wherever the SW is genuinely\n  // supported.\n  if (!navigator.serviceWorker) return;\n\n  // The installed-but-waiting worker (the pending update), captured by\n  // ``queueSilentUpdate`` so ``applyWaitingWorker`` and the Reload handler\n  // can message it.\n  let waitingWorker = null;\n  // Set true only when the user clicks \"Reload\". A ``controllerchange`` is\n  // turned into a reload only then: a first-install ``clients.claim()`` and\n  // a silent activation (SNOW-1025) both fire the same event, and neither\n  // may move the page under the user.\n  let userTriggeredUpdate = false;\n  // SNOW-1025: set when ``applyWaitingWorker`` has posted SKIP_WAITING, so\n  // the ``controllerchange`` that follows is recorded as an applied update\n  // even though it does not reload.\n  let silentUpdatePosted = false;\n  // SNOW-1027: a waiting worker whose shell matches this page's, remembered\n  // when a basemap download held it back, so the end of the download can\n  // apply it without waiting for the page to be hidden.\n  let matchedWorker = null;\n  // Guards against a double reload if controllerchange fires more than\n  // once.\n  let refreshing = false;\n  // SNOW-384 / SNOW-1025: ``pwa.sw.update_available`` counts stuck-worker\n  // banners actually shown, once per page.\n  let announcedStuck = false;\n  // SNOW-952 / SNOW-1025: the banner decision, remembered against the\n  // server shell it answered. ``pwa_version_check.js`` re-offers the banner\n  // for every response replaying a drifting version header, and each\n  // answer costs a worker round trip and a ``registration.update()``.\n  // Keyed rather than latched, because a second deploy in one session names\n  // a different shell and needs its own answer. Holds the PROMISE, so\n  // overlapping offers share one evaluation. Cleared on ``controllerchange``:\n  // since SNOW-1025 the controller can change under a live page (a silent\n  // activation) and the old answer described the old worker.\n  /** @type {{shell: string, show: Promise<boolean>} | null} */\n  let bannerAnswer = null;\n\n  // SNOW-492: slot for the in-flight \"Cache this area\" call, if any. Only\n  // one warm-cache run can be in flight at a time \u2014 see ``_warmCacheChain``\n  // below, which is what now makes that true \u2014 so a single slot is enough.\n  //\n  // SNOW-493 finding 9: the slot now carries a ``requestId`` alongside the\n  // ``resolve`` function. Without it, a worker reply that arrives AFTER\n  // ``warmCache()`` has already given up (timed out and resolved ``null``)\n  // would still find ``_warmCacheSlot.resolve`` set \u2014 a later call's own\n  // ``resolve`` \u2014 and wrongly settle it with the stale reply's numbers.\n  // Correlating on ``requestId`` (minted per call, echoed back by the\n  // worker) means a reply only ever resolves the call it actually answers.\n  let _warmCacheSlot = null;\n\n  // SNOW-951 review: the queue that keeps the single slot above honest.\n  //\n  // \"Only one run at a time\" was a claim about the CALLERS, and it held\n  // only while one click-guarded button was the sole entry point. It is\n  // not true any more: a per-area \"Sync now\" runs two warm calls of its\n  // own (tiles, then content), several areas can be pressed before the\n  // first settles, and the offline report warms the shell on its own\n  // schedule. A second call landing mid-run overwrote ``requestId``, so\n  // the worker's reply to the FIRST no longer matched the slot, and that\n  // caller sat out the full silence timeout before resolving\n  // ``'timeout'`` \u2014 a sync reported as failed while its documents were\n  // landing on disk.\n  //\n  // Serialised rather than multi-slotted, because the constraint is not\n  // only the slot: ``sw.js``'s ``_warmCache`` is itself one run with one\n  // progress stream, and overlapping runs would interleave their\n  // ``settled`` batches. Queueing costs a caller the wait for whatever is\n  // ahead of it, which is the truthful price \u2014 the work was never going\n  // to happen in parallel.\n  //\n  // The chain only ever holds a SETTLED-or-pending promise that cannot\n  // reject: ``_warmCacheRun`` resolves on every path (including its own\n  // timeout), and the ``catch`` below is belt and braces so one throw can\n  // never wedge every later call.\n  let _warmCacheChain = Promise.resolve();\n\n  // SNOW-1025: how many ``warmCache`` calls are queued or running. A silent\n  // activation waits for this to reach zero: activating retires the worker\n  // that is doing the download, and the new worker knows nothing about the\n  // run. Counted at the call rather than read from ``_warmCacheSlot``,\n  // because a queued call holds no slot while it waits its turn.\n  let _warmCacheRunsPending = 0;\n\n  /**\n   * Mint a request id for a ``warmCache()`` call. ``crypto.randomUUID`` is\n   * available on every browser this feature targets; the hex fallback\n   * mirrors ``mutation_queue.js``'s ``_mintIdempotencyKey`` /\n   * ``db.js``'s ``_randomHex`` for the rare runtime without it.\n   * @returns {string}\n   */\n  function _mintRequestId() {\n    try {\n      if (typeof crypto !== 'undefined' && crypto.randomUUID) {\n        return crypto.randomUUID();\n      }\n      const bytes = new Uint8Array(16);\n      crypto.getRandomValues(bytes);\n      let out = '';\n      for (let i = 0; i < bytes.length; i++) {\n        out += bytes[i].toString(16).padStart(2, '0');\n      }\n      return out;\n    } catch (_e) {\n      return 'warm-' + Date.now().toString(16) + '-' + Math.floor(Math.random() * 1e9).toString(16);\n    }\n  }\n\n  // SNOW-384: SW \u2192 page telemetry bridge. sw.js (and sw-kill.js) have no\n  // ``window`` and so cannot call ``window.pwaTelemetry`` directly; they\n  // instead post ``{type: 'pwa-telemetry', event, properties}`` to every\n  // client they control. This single listener forwards any such message,\n  // regardless of which SW script sent it, to ``window.pwaTelemetry.emit``.\n  //\n  // SNOW-376: the same channel also carries ``{type: 'drain-mutations'}``\n  // when a Background Sync fires for the mutation-queue tag while this\n  // tab is open \u2014 see static/js/sw.js's ``_handleMutationSync``.\n  //\n  // SNOW-492: and ``{type: 'warm-cache-done', ok, failed}`` \u2014 the\n  // completion summary for a ``warmCache()`` call below.\n  navigator.serviceWorker.addEventListener('message', (event) => {\n    const data = event.data;\n    if (!data) return;\n    if (data.type === 'pwa-telemetry' && data.event) {\n      try {\n        window.pwaTelemetry?.emit(data.event, data.properties || {});\n      } catch (_err) {\n        // Ignore \u2014 telemetry must never break the SW message channel.\n      }\n      return;\n    }\n    // SNOW-376: a Background Sync fired for the mutation-queue tag while\n    // this tab was open \u2014 sw.js delegates to the real\n    // window.pwaMutationQueue.drain() (which already has window.pwaDb /\n    // window.pwaTelemetry wired up) rather than duplicating that logic\n    // in the worker.\n    if (data.type === 'drain-mutations') {\n      try {\n        window.pwaMutationQueue?.drain();\n      } catch (_err) {\n        // Ignore \u2014 a broken drain must never break the SW message channel.\n      }\n      return;\n    }\n    // SNOW-521: a throttled progress step (~every 5%, plus the final one)\n    // for the in-flight warm-cache call. Only forwarded to the caller's\n    // onProgress if it matches the in-flight slot's requestId (same stale-\n    // reply guard as 'warm-cache-done') \u2014 and, critically, resets the\n    // silence timeout below so a long multi-tile download's own progress\n    // keeps it alive rather than resolving null mid-transfer.\n    if (data.type === 'warm-cache-progress') {\n      if (_warmCacheSlot && data.requestId === _warmCacheSlot.requestId) {\n        _warmCacheSlot.onTimeoutReset?.();\n        try {\n          // Tile-grid rework: ``data.settled`` (indices into the posted URL list\n          // that succeeded since the last message) rides along as a third\n          // argument. An older worker still serving a cached shell won't\n          // send it \u2014 callers treat ``undefined`` as \"counts only\" and\n          // fall back to a proportional fill.\n          //\n          // SNOW-632: ``data.bytes`` rides along as a fourth argument \u2014 the\n          // run's on-disk bytes so far, for a live MB readout. An older\n          // worker won't send it either \u2014 ``undefined`` reads the same way\n          // ``settled`` does above.\n          _warmCacheSlot.onProgress?.(data.done, data.total, data.settled, data.bytes);\n        } catch (_err) {\n          // A broken onProgress callback must never break the SW message\n          // channel or abort the in-flight call.\n        }\n      }\n      return;\n    }\n    if (data.type === 'warm-cache-done') {\n      // SNOW-493 finding 9: only resolve if this reply matches the\n      // in-flight slot's requestId \u2014 a stale reply for a call that already\n      // timed out (and whose slot was replaced or cleared) must not\n      // resolve a different, later call.\n      if (_warmCacheSlot && data.requestId === _warmCacheSlot.requestId) {\n        // SNOW-568: ``reason`` rides along so the caller can tell a full\n        // disk from a flaky network. An older worker still serving a\n        // cached shell won't send it \u2014 ``null`` then reads as \"failed,\n        // cause unknown\", which is what callers already assumed.\n        _warmCacheSlot.resolve({\n          ok: data.ok,\n          failed: data.failed,\n          reason: data.reason || null,\n          // SNOW-586: the run's total on-disk bytes, so the caller can\n          // record it against the area's standing budget entry. An older\n          // worker still serving a cached shell won't send it \u2014 0 then\n          // reads as \"unknown size\", which callers already treat safely\n          // (nothing is ever evicted on the strength of a zero).\n          bytes: data.bytes || 0,\n          // SNOW-632: whether the run stopped early on a cancelWarmCache()\n          // request rather than running every URL to completion. An older\n          // worker won't send it \u2014 ``false`` is the correct read, since\n          // that worker has no cancellation protocol to have honoured.\n          cancelled: !!data.cancelled,\n          // SNOW-1331: up to five failed URLs, each ``{url, status, why,\n          // detail}``, so a failed download can name what failed. An older\n          // worker won't send it \u2014 ``[]`` reads as \"nothing named\".\n          failures: Array.isArray(data.failures) ? data.failures : [],\n        });\n        _warmCacheSlot = null;\n      }\n    }\n  });\n\n  // SNOW-492: how long warmCache() waits for the worker's warm-cache-done\n  // reply before giving up. A worker terminated (idle-killed, crashed)\n  // between receiving the message and posting the reply would otherwise\n  // leave the caller's promise pending forever \u2014 cacheNowInit's button\n  // would stay aria-disabled for the rest of the page's life.\n  //\n  // SNOW-521: this is now \"30s of no progress\", not a flat 30s from the\n  // call's start \u2014 every 'warm-cache-progress' message rearms the timer\n  // (see the message listener above), so a large \"Download basemap\" run\n  // that's still making progress past the 30s mark is never cut short.\n  const WARM_CACHE_TIMEOUT_MS = 30000;\n\n  // SNOW-622: floor on how often a `visibilitychange` can trigger a\n  // `registration.update()`. That call is a network round trip for the\n  // worker script every time \u2014 `/sw.js` is served `Cache-Control: no-cache`\n  // \u2014 and the event fires on every tab switch, app switch and screen\n  // unlock. One minute is well under how often the site is deployed, so\n  // nothing is surfaced meaningfully later than before.\n  const SW_UPDATE_CHECK_INTERVAL_MS = 60000;\n\n  // SNOW-605: how long ``_activeWorker()`` waits for a controller before\n  // declaring the page uncontrolled. Sized for the activation window of an\n  // SW update \u2014 the gap between the new worker activating and its\n  // ``clients.claim()`` reaching this page \u2014 which is milliseconds, not\n  // seconds. It is deliberately NOT sized for a shift-reloaded page, where\n  // no controller is ever coming and the only cure is a reload: waiting\n  // longer there would just stall the download control with a spinner\n  // before showing the same message.\n  const CONTROLLER_WAIT_MS = 3000;\n\n  /**\n   * SNOW-605: the worker that should receive this page's messages, waiting\n   * briefly for one if the page is momentarily uncontrolled.\n   *\n   * ``navigator.serviceWorker.controller`` is null in three situations that\n   * look identical from here but are not: the activation window of an\n   * update (a controller is about to arrive), a shift-reloaded document\n   * (none ever will \u2014 Chrome loads it uncontrolled on purpose), and a first\n   * load whose worker has not yet claimed. Waiting for ``controllerchange``\n   * resolves the first and third; the second falls through to the timeout,\n   * which is the honest answer for it.\n   *\n   * ``navigator.serviceWorker.ready`` is awaited first so a page whose\n   * worker is still installing doesn't time out on a technicality \u2014 it\n   * resolves once a registration is active, which is the earliest a\n   * controller could exist.\n   *\n   * @returns {Promise<ServiceWorker | null>}\n   */\n  function _activeWorker() {\n    if (navigator.serviceWorker.controller) {\n      return Promise.resolve(navigator.serviceWorker.controller);\n    }\n    return new Promise((resolve) => {\n      let done = false;\n      const finish = (worker) => {\n        if (done) return;\n        done = true;\n        clearTimeout(timer);\n        navigator.serviceWorker.removeEventListener('controllerchange', onChange);\n        resolve(worker);\n      };\n      const onChange = () => finish(navigator.serviceWorker.controller);\n      const timer = setTimeout(() => finish(navigator.serviceWorker.controller), CONTROLLER_WAIT_MS);\n      navigator.serviceWorker.addEventListener('controllerchange', onChange);\n      // A registration that is already active may still not have claimed\n      // this page; re-check once ``ready`` settles rather than waiting out\n      // the full timeout for a controller that is already there.\n      navigator.serviceWorker.ready\n        .then(() => {\n          if (navigator.serviceWorker.controller) finish(navigator.serviceWorker.controller);\n        })\n        .catch(() => {});\n    });\n  }\n\n  /**\n   * SNOW-492: bridge for map.js's \"Download basemap\" control. Posts the\n   * given URL list to the active worker's ``warm-cache`` message handler\n   * (``static/js/sw.js``) and resolves with its ``{ok, failed}`` summary\n   * once the worker posts ``warm-cache-done`` back.\n   *\n   * SNOW-605: an uncontrolled page no longer fails on the spot. A page can\n   * have no ``controller`` while a perfectly healthy worker exists \u2014 during\n   * an update's activation window, and (for the whole life of the document)\n   * after a shift-reload, which Chrome deliberately loads uncontrolled. This\n   * used to resolve ``null`` on the first line, which the download controls\n   * report as a flat \"Download failed. Check your connection and try again.\"\n   * having dispatched nothing \u2014 no request, nothing to see in the network\n   * panel, and a message pointing at the one thing that isn't wrong. So:\n   * wait ``CONTROLLER_WAIT_MS`` for a controller to arrive, and only if none\n   * does resolve ``{ok: 0, failed: 0, reason: 'no-worker', bytes: 0}`` \u2014 a\n   * distinct reason the caller can word accurately (reload the page), rather\n   * than ``null``, which is indistinguishable from a run that fetched\n   * nothing.\n   *\n   * SNOW-568: if the worker goes ``WARM_CACHE_TIMEOUT_MS`` without a reply\n   * OR a progress message (see that constant's comment), this resolves\n   * ``{ok: 0, failed: 0, reason: 'timeout'}`` rather than ``null``. The two\n   * used to be conflated, which meant a worker that died mid-download was\n   * indistinguishable from a browser that never had one \u2014 and both were\n   * silently swallowed by the caller's \"nothing to report\" branch.\n   *\n   * SNOW-493 finding 9: mints a ``requestId`` for this call and posts it\n   * alongside the URL list; ``sw.js`` echoes it back in both\n   * ``warm-cache-progress`` and ``warm-cache-done``. On timeout, this\n   * call's own promise still resolves ``null``, but the slot is only\n   * cleared if it still belongs to THIS request \u2014 otherwise a request that\n   * arrived and repopulated the slot in the interim (vanishingly unlikely\n   * given map.js's click-guard, but not impossible if a caller bypasses\n   * it) would have its slot wiped out from under it by this stale timeout.\n   *\n   * SNOW-521: ``opts.pinned`` is forwarded verbatim in the posted message\n   * (``sw.js`` reads ``event.data.pinned``); ``opts.onProgress(done,\n   * total, settled)``, if supplied, is invoked from the message listener\n   * above on every ``warm-cache-progress`` reply matching this call's\n   * requestId. A caller passing no ``opts`` at all (the pre-SNOW-521 call\n   * shape) still works \u2014 ``pinned`` defaults false and progress is simply\n   * not observed.\n   *\n   * SNOW-586: ``opts.areaId`` is likewise forwarded verbatim\n   * (``event.data.areaId``) \u2014 REQUIRED whenever ``opts.pinned`` is true,\n   * since it selects which per-area pinned bucket the run writes into;\n   * ``sw.js`` refuses a pinned run with no ``areaId`` rather than falling\n   * back to a shared bucket (see its own docstring). The resolved\n   * summary's ``bytes`` field is the run's total on-disk size\n   * (``sw.js``'s ``_warmCache`` sums it as it writes), which the caller\n   * records against the area's standing budget entry.\n   *\n   * Tile-grid rework: ``settled`` is the batch of ``urls`` indices that succeeded\n   * since the previous report \u2014 see ``_warmCache`` in ``sw.js``.\n   *\n   * SNOW-632: ``opts.onProgress``'s fourth argument, ``bytes``, is the\n   * run's on-disk total so far \u2014 see ``_warmCache`` in ``sw.js``. The\n   * resolved summary's ``cancelled`` field is true when the run stopped\n   * early on a ``cancelWarmCache()`` request rather than running every URL\n   * to completion; a cancelled run always has ``failed: 0`` (nothing\n   * skipped by the cancel was ever attempted), so a caller MUST check\n   * ``cancelled`` before treating a short ``ok`` count as evidence of\n   * trouble.\n   *\n   * SNOW-951 review: calls QUEUE. The worker runs one warm at a time and\n   * this module tracks it in one slot, so a second caller waits for the\n   * first to settle rather than overwriting it \u2014 see ``_warmCacheChain``.\n   * Nothing about a single call's own result changes; what changes is that\n   * a caller who arrives during another's run gets its own answer instead\n   * of that run's silence timeout.\n   *\n   * @param {string[]} urls\n   * @param {{pinned?: boolean, areaId?: string, onProgress?: (done: number,\n   *   total: number, settled?: number[], bytes?: number) => void}} [opts]\n   * @returns {Promise<{ok: number, failed: number, reason: string|null,\n   *   bytes: number, cancelled: boolean,\n   *   failures: Array<{url: string, status: number|null, why: string,\n   *   detail: string}>} | null>}\n   */\n  async function warmCache(urls, opts) {\n    _warmCacheRunsPending += 1;\n    const mine = _warmCacheChain.then(() => _warmCacheRun(urls, opts));\n    _warmCacheChain = mine.then(\n      () => undefined,\n      () => undefined,\n    );\n    // SNOW-1025: the last run to finish releases any update that was held\n    // back for it, if the page went hidden in the meantime.\n    const release = () => {\n      _warmCacheRunsPending -= 1;\n      if (document.visibilityState === 'hidden') applyWaitingWorker();\n      else if (matchedWorker) applyWaitingWorker(matchedWorker);\n    };\n    mine.then(release, release);\n    return mine;\n  }\n\n  /**\n   * One warm-cache run, dispatched with the slot to itself.\n   *\n   * Split out of ``warmCache`` by the SNOW-951 review so the queueing\n   * above wraps the WHOLE of a run \u2014 the controller wait included, since\n   * a call that takes the slot before waiting for a controller would hold\n   * it while doing nothing.\n   *\n   * @param {string[]} urls\n   * @param {{pinned?: boolean, areaId?: string, glyphPrefix?: string,\n   *   onProgress?: (done: number, total: number, settled?: number[],\n   *   bytes?: number) => void}} [opts]\n   * @returns {Promise<{ok: number, failed: number, reason: string|null,\n   *   bytes: number, cancelled: boolean,\n   *   failures: Array<{url: string, status: number|null, why: string,\n   *   detail: string}>} | null>}\n   */\n  async function _warmCacheRun(urls, opts) {\n    const active = await _activeWorker();\n    // SNOW-605: no controller after the wait \u2014 the page is uncontrolled and\n    // will stay that way until it reloads. Report it as its own reason so\n    // the user is told to reload rather than to check their connection.\n    if (!active) return { ok: 0, failed: 0, reason: 'no-worker', bytes: 0, cancelled: false };\n    const options = opts || {};\n    const requestId = _mintRequestId();\n    return new Promise((resolve) => {\n      let settled = false;\n      let timeoutHandle = null;\n\n      const settle = (value) => {\n        if (settled) return;\n        settled = true;\n        if (timeoutHandle) clearTimeout(timeoutHandle);\n        resolve(value);\n      };\n\n      // SNOW-521: (re)arms the silence timeout \u2014 called once up front and\n      // again from the message listener on every progress reply, so the\n      // effective wait is \"30s since the last sign of life\", not a flat\n      // 30s from the start of a possibly-long download.\n      const armTimeout = () => {\n        if (timeoutHandle) clearTimeout(timeoutHandle);\n        timeoutHandle = setTimeout(() => {\n          if (_warmCacheSlot && _warmCacheSlot.requestId === requestId) {\n            _warmCacheSlot = null;\n          }\n          // SNOW-568: a summary with reason 'timeout', not ``null``.\n          // ``null`` keeps its single pre-existing meaning \u2014 \"there is no\n          // active worker, so there was nothing to warm\" \u2014 which callers\n          // treat as a non-event; a worker that went silent mid-run IS an\n          // event, and the user needs telling.\n          settle({ ok: 0, failed: 0, reason: 'timeout', bytes: 0, cancelled: false });\n        }, WARM_CACHE_TIMEOUT_MS);\n      };\n\n      _warmCacheSlot = {\n        requestId,\n        resolve: settle,\n        onProgress: options.onProgress,\n        onTimeoutReset: armTimeout,\n      };\n      armTimeout();\n      active.postMessage({\n        type: 'warm-cache',\n        urls: urls || [],\n        requestId,\n        pinned: !!options.pinned,\n        // SNOW-586: which pinned bucket a pinned run writes into \u2014\n        // undefined for a non-pinned call, same as before this ticket.\n        areaId: options.areaId,\n        // SNOW-742: the active basemap style's glyph URL prefix. The worker\n        // uses it to promote already-cached glyphs into the pinned bucket, so\n        // a downloaded area keeps its labels once the passive cache trims its\n        // copies. Undefined for a non-pinned call.\n        glyphPrefix: options.glyphPrefix,\n      });\n    });\n  }\n\n  Object.defineProperty(window, 'pwaWarmCache', {\n    value: warmCache,\n    writable: false,\n    configurable: false,\n  });\n\n  /**\n   * SNOW-632: ask the worker to stop DISPATCHING further URLs for the\n   * in-flight ``warmCache()`` call, if there is one \u2014 the page-side end of\n   * the overlay's Cancel button. Posts ``{type: 'warm-cache-cancel',\n   * requestId}`` for ``_warmCacheSlot``'s own requestId, the same\n   * correlation ``warmCache()`` itself relies on, so a cancel can never be\n   * misdirected at a later, unrelated call.\n   *\n   * Not an abort: ``sw.js``'s pool can already have up to\n   * ``WARM_CACHE_CONCURRENCY`` fetches in flight, and those are left to\n   * finish and write \u2014 see ``_warmCache``'s docstring in ``sw.js`` for why.\n   * The eventual ``warm-cache-done`` reply carries ``cancelled: true``,\n   * which resolves this call's own ``warmCache()`` promise exactly as any\n   * other completion does; ``cancelWarmCache()`` itself resolves once the\n   * request has been posted, not once the run has actually stopped.\n   *\n   * A safe no-op with no run in flight (``_warmCacheSlot`` is ``null``) or\n   * on an uncontrolled page (mirrors ``warmCache()``'s own wait via\n   * ``_activeWorker()``, re-checking the slot afterwards in case the run\n   * settled while we were waiting for a controller).\n   *\n   * @returns {Promise<void>}\n   */\n  async function cancelWarmCache() {\n    if (!_warmCacheSlot) return;\n    const active = await _activeWorker();\n    if (!active || !_warmCacheSlot) return;\n    active.postMessage({ type: 'warm-cache-cancel', requestId: _warmCacheSlot.requestId });\n  }\n\n  Object.defineProperty(window, 'pwaWarmCacheCancel', {\n    value: cancelWarmCache,\n    writable: false,\n    configurable: false,\n  });\n\n  /**\n   * Resolve the banner element, creating one if the page didn't render\n   * the public partial. Public pages ship ``#sw-update-banner``\n   * (Tailwind-styled, ``templates/includes/_sw_update_banner.html``) in\n   * base.html; the Django admin \u2014 which the SW also controls (scope\n   * ``/``) \u2014 does not load the public chrome, so without this a waiting\n   * worker would go unannounced there. We self-inject an inline-styled\n   * card (no Tailwind dependency) that mirrors the public banner's shape\n   * so the update contract holds on EVERY page the SW controls, with no\n   * exceptions.\n   *\n   * The reveal contract for both variants is the same: toggling the\n   * ``hidden`` class (public banner) or the equivalent ``display``\n   * property (admin fallback) via ``revealUpdateBanner`` /\n   * ``hideUpdateBanner`` below.\n   *\n   * @returns {HTMLElement | null}\n   */\n  function ensureBanner() {\n    let el = document.getElementById('sw-update-banner');\n    if (el || !document.body) return el;\n    el = document.createElement('div');\n    el.id = 'sw-update-banner';\n    el.setAttribute('role', 'status');\n    el.setAttribute('aria-live', 'polite');\n    el.dataset.fallback = '1';\n    el.style.cssText =\n      'position:fixed;bottom:1rem;left:50%;transform:translateX(-50%);' +\n      'z-index:2147483647;display:none;align-items:center;gap:.75rem;' +\n      'width:calc(100vw - 2rem);max-width:28rem;padding:.75rem 1rem;' +\n      'border-radius:12px;background:#ffffff;color:#0f172a;' +\n      'border:1px solid rgba(15,23,42,.12);font:500 14px system-ui,sans-serif;' +\n      'box-shadow:0 10px 30px rgba(0,0,0,.15);';\n    const copy = document.createElement('div');\n    copy.style.cssText = 'flex:1;min-width:0;';\n    const title = document.createElement('div');\n    title.id = 'sw-update-banner-title';\n    title.textContent = STRINGS['update-title'];\n    title.style.cssText = 'font-weight:600;font-size:14px;';\n    const sub = document.createElement('div');\n    sub.id = 'sw-update-banner-body';\n    sub.textContent = STRINGS['update-body'];\n    sub.style.cssText = 'color:#475569;font-size:12px;margin-top:2px;';\n    copy.append(title, sub);\n    const reload = document.createElement('button');\n    reload.type = 'button';\n    reload.id = 'sw-update-banner-reload';\n    reload.textContent = STRINGS.reload;\n    reload.style.cssText =\n      'cursor:pointer;border:0;border-radius:9999px;background:#0f172a;' +\n      'color:#fff;padding:.4rem .9rem;font:600 13px system-ui,sans-serif;';\n    const dismiss = document.createElement('button');\n    dismiss.type = 'button';\n    dismiss.dataset.action = 'dismiss';\n    dismiss.setAttribute('aria-label', STRINGS.dismiss);\n    // The house close glyph (templates/includes/_icon_close.html), drawn\n    // here because this banner is built in script: the same two strokes,\n    // not a typed \"\u00D7\", so every dismiss on the site is one mark.\n    const SVG_NS = 'http://www.w3.org/2000/svg';\n    const glyph = document.createElementNS(SVG_NS, 'svg');\n    [['width', '16'], ['height', '16'], ['viewBox', '0 0 24 24'], ['fill', 'none'],\n      ['stroke', 'currentColor'], ['stroke-width', '2'], ['stroke-linecap', 'round'],\n      ['stroke-linejoin', 'round'], ['aria-hidden', 'true']].forEach(([k, v]) => glyph.setAttribute(k, v));\n    ['M18 6 6 18', 'm6 6 12 12'].forEach((d) => {\n      const path = document.createElementNS(SVG_NS, 'path');\n      path.setAttribute('d', d);\n      glyph.appendChild(path);\n    });\n    dismiss.appendChild(glyph);\n    dismiss.style.cssText =\n      'cursor:pointer;border:0;background:transparent;color:#64748b;' +\n      'display:inline-flex;align-items:center;padding:0 .25rem;';\n    el.append(copy, reload, dismiss);\n    document.body.appendChild(el);\n    return el;\n  }\n\n  const banner = ensureBanner();\n\n  /**\n   * Remember a waiting worker and apply it silently at the next chance\n   * (SNOW-1025, SNOW-1027).\n   *\n   * A waiting worker is a routine update, not a problem, and nobody needs to\n   * be told about it. When it is applied depends on whether this page\n   * already is that worker's build:\n   *\n   *   * **It is** (SNOW-1027): the worker reports the shell this page's\n   *     ``<meta name=\"pwa-shell\">`` names. The page came off the network\n   *     (navigations are network-first) and the old worker is the only\n   *     thing out of date, which is exactly what happens on a fresh tab:\n   *     opening it is what installed the new worker. Apply it now, visible\n   *     or not. Nothing on screen changes, and the page and its worker stop\n   *     disagreeing from the first second rather than for a whole session.\n   *   * **It is not, or the worker does not answer**: the page was served\n   *     by the old worker, usually offline, and may still read the old shell\n   *     cache that activation sweeps. Apply the next time the page is hidden\n   *     (SNOW-1025). If it is already hidden (an update check that finished\n   *     in a background tab), that is now.\n   *\n   * @param {ServiceWorker} worker The installed, waiting worker.\n   * @returns {void}\n   */\n  function queueSilentUpdate(worker) {\n    if (worker) waitingWorker = worker;\n    if (document.visibilityState === 'hidden') {\n      applyWaitingWorker();\n      return;\n    }\n    if (!worker || !PAGE_SHELL) return;\n    workerShell(worker).then((shell) => {\n      if (shell !== PAGE_SHELL) return;\n      matchedWorker = worker;\n      applyWaitingWorker(worker);\n    });\n  }\n\n  /**\n   * Post SKIP_WAITING to the waiting worker, if it is safe to (SNOW-1025).\n   *\n   * Two callers, two rules:\n   *\n   *   * With no argument, the hide-to-apply path: only while the page is\n   *     hidden. The waiting worker is re-read from the live registration\n   *     rather than trusted from ``waitingWorker``, for the reason\n   *     ``handleReloadClick`` gives: a captured reference can have gone\n   *     redundant, and posting to a redundant worker is a silent no-op.\n   *   * With ``matched``, a worker ``queueSilentUpdate`` has just confirmed\n   *     holds this page's shell (SNOW-1027): applied whether or not the page\n   *     is visible, but only while it is still the worker waiting. A newer\n   *     install that has since replaced it was not checked, so it falls back\n   *     to the hide-to-apply rule.\n   *\n   * Both are held back while a ``warmCache`` run is queued or in flight:\n   * activating retires the worker doing the download. ``warmCache`` calls\n   * back in here when its last run settles.\n   *\n   * Both are also held back while ANY other window is on screen or running\n   * a download (``activationIsSafe``). Activation is origin-wide, so this\n   * page's own state is not enough. That window gets its own turn: it runs\n   * this same function when it is hidden or its download ends, and the last\n   * window to go quiet applies the update.\n   *\n   * Posting SKIP_WAITING to a worker that is already activating is\n   * harmless. ``skipWaiting()`` resolves at once on a worker that is not\n   * waiting.\n   *\n   * @param {ServiceWorker} [matched] A waiting worker known to hold this\n   *   page's shell.\n   * @returns {Promise<void>}\n   */\n  async function applyWaitingWorker(matched) {\n    if (_warmCacheRunsPending > 0) return;\n    let waiting =\n      waitingWorker && waitingWorker.state === 'installed' ? waitingWorker : null;\n    try {\n      const registration = await navigator.serviceWorker.getRegistration();\n      if (registration && registration.waiting) waiting = registration.waiting;\n    } catch (_err) {\n      // getRegistration failure: proceed with what we have.\n    }\n    if (!waiting) return;\n    // Re-checked after the await: a download can have started, or the page\n    // come back into view, while the registration was being read.\n    if (_warmCacheRunsPending > 0) return;\n    const immediate = matched !== undefined && waiting === matched;\n    if (!immediate && document.visibilityState !== 'hidden') return;\n    // Every window, not just this one: another may be on screen or mid-download.\n    if (!(await activationIsSafe())) return;\n    // Re-checked after that await too.\n    if (_warmCacheRunsPending > 0) return;\n    if (!immediate && document.visibilityState !== 'hidden') return;\n    matchedWorker = null;\n    silentUpdatePosted = true;\n    try {\n      waiting.postMessage({ type: 'SKIP_WAITING' });\n    } catch (_err) {\n      // Called from event handlers with nobody awaiting it, so a throw here\n      // would be an unhandled rejection. The next hide tries again.\n      silentUpdatePosted = false;\n    }\n  }\n\n  function hideUpdateBanner() {\n    if (!banner) return;\n    if (banner.dataset.fallback === '1') {\n      banner.style.display = 'none';\n    } else {\n      banner.classList.add('hidden');\n    }\n  }\n\n  /**\n   * Reveal the update banner, honouring the fallback markup's own idiom.\n   *\n   * Public pages ship the `_toast.html` partial, hidden with the `hidden`\n   * CLASS; the admin fallback this file synthesises is inline-styled and\n   * uses `display`. One fork, one place \u2014 see `window.pwaUpdateBanner`.\n   *\n   * @returns {void}\n   */\n  function revealUpdateBanner() {\n    if (DEV_SHELL_BYPASS_ACTIVE) return;\n    if (!banner) return;\n    if (banner.dataset.fallback === '1') {\n      banner.style.display = 'flex';\n    } else {\n      banner.classList.remove('hidden');\n    }\n  }\n\n  /**\n   * How long to wait for a worker to name its shell.\n   *\n   * The reply is a synchronous read of a constant in the worker's own\n   * scope, so a live worker answers in single-digit milliseconds. The\n   * budget is for the worker that will never answer: one that predates the\n   * ``shell-identity`` handler, or one wedged in a long ``waitUntil``.\n   * Bounded rather than open-ended for the reason\n   * docs/decisions/bounded-offline-read-paths.md gives: an unanswered read\n   * must resolve, not hang.\n   */\n  const WORKER_SHELL_TIMEOUT_MS = 2000;\n\n  /**\n   * Send one question to a service worker down its own MessageChannel, and\n   * resolve with the reply (SNOW-952, SNOW-1027).\n   *\n   * @param {ServiceWorker | null | undefined} worker\n   * @param {string} type The message type; the reply must carry the same.\n   * @returns {Promise<Object | null>} The reply's data, or ``null`` when there\n   *   is no worker, no ``MessageChannel``, a reply of another type, or no\n   *   reply inside ``WORKER_SHELL_TIMEOUT_MS``.\n   */\n  function askWorker(worker, type) {\n    return new Promise((resolve) => {\n      if (!worker || typeof MessageChannel !== 'function') {\n        resolve(null);\n        return;\n      }\n      /** @type {MessageChannel} */\n      let channel;\n      try {\n        channel = new MessageChannel();\n      } catch (_err) {\n        resolve(null);\n        return;\n      }\n      let settled = false;\n      /**\n       * Resolve once, and close the port however we got here, so a page\n       * re-offered the banner does not pile up live ports. ``canOpenOffline``\n       * in pwa_network_mode.js closes its port the same way.\n       *\n       * @param {Object | null} value\n       */\n      const settle = (value) => {\n        if (settled) return;\n        settled = true;\n        clearTimeout(timer);\n        try {\n          channel.port1.close();\n        } catch (_err) {\n          // Non-fatal.\n        }\n        resolve(value);\n      };\n      const timer = setTimeout(() => settle(null), WORKER_SHELL_TIMEOUT_MS);\n      channel.port1.onmessage = (event) => {\n        const data = event.data;\n        settle(data && data.type === type ? data : null);\n      };\n      try {\n        worker.postMessage({ type: type }, [channel.port2]);\n      } catch (_err) {\n        settle(null);\n      }\n    });\n  }\n\n  /**\n   * Ask a service worker which shell cache it holds (SNOW-952, SNOW-1027).\n   *\n   * The worker answers ``shell-identity`` with its ``CACHE_VERSION``,\n   * derived from the shell content hash. Asked of the controlling worker by\n   * ``shellIsStale``, and of a waiting worker by ``queueSilentUpdate``: a\n   * waiting worker's ``message`` handler runs like any other's, which is\n   * what SKIP_WAITING already relies on.\n   *\n   * @param {ServiceWorker | null | undefined} worker\n   * @returns {Promise<string>} The cache name, or ``''`` when the worker\n   *   cannot be asked or does not answer. Both callers read ``''`` as\n   *   \"cannot tell\", each in its own safe direction.\n   */\n  function workerShell(worker) {\n    return askWorker(worker, 'shell-identity').then((data) =>\n      data ? String(data.cache || '').trim() : '',\n    );\n  }\n\n  /**\n   * Is it safe, for every open window and not just this one, to replace the\n   * controlling worker now? (SNOW-1027)\n   *\n   * Activation is origin-wide. It claims every window and sweeps the old\n   * shell cache, so a decision this page makes alone can pull the worker out\n   * from under another window that is still on screen, or retire the worker\n   * that is running another window's basemap download (this page's own\n   * ``_warmCacheRunsPending`` sees only its own). The controlling worker is\n   * the one that can see all of it: it serves every window's downloads and\n   * can list every window.\n   *\n   * @returns {Promise<boolean>} ``false`` when another window is visible or\n   *   any download is running. ``true`` when neither, and also when the\n   *   worker does not answer: a worker from before this check existed\n   *   cannot be asked, and holding the update back until it could be would\n   *   hold it back forever. That is the pre-SNOW-1027 behaviour, and it\n   *   lasts one deploy.\n   */\n  function activationIsSafe() {\n    return askWorker(navigator.serviceWorker.controller, 'activation-check').then(\n      (data) => !data || (!data.othersVisible && !data.warming),\n    );\n  }\n\n  /**\n   * Is this device holding an out-of-date offline shell? (SNOW-952)\n   *\n   * The first of the banner's two gates. The server names the shell it\n   * would serve today (``shell`` on ``/api/version``), the controlling\n   * worker names the one it holds, and a difference between them is the\n   * question. A deploy that changed no shell source is never stale.\n   *\n   * Three answers rather than two, and the failure directions are\n   * deliberate and opposite:\n   *\n   *   * **No controller** \u2192 ``false``. Nothing is cached, so nothing can\n   *     be stale, and the page in front of the user came off the network.\n   *   * **A controller that cannot be read**: no ``shell`` from the\n   *     server, or no reply from the worker \u2192 ``true``. An unanswering\n   *     worker is itself a symptom of the state the banner exists for.\n   *     ``workerIsStuck`` still has to agree before anything is shown.\n   *   * **Both readable** \u2192 the comparison.\n   *\n   * @param {string} server The server's shell cache name, ``''`` if absent.\n   * @returns {Promise<boolean>}\n   */\n  function shellIsStale(server) {\n    if (!navigator.serviceWorker || !navigator.serviceWorker.controller) {\n      return Promise.resolve(false);\n    }\n    return workerShell(navigator.serviceWorker.controller).then(\n      (held) => !server || !held || server !== held,\n    );\n  }\n\n  /**\n   * How long ``workerIsStuck`` waits for an installing worker to settle.\n   *\n   * An install precaches a handful of shell URLs, which takes seconds on a\n   * poor connection. An install still running after this long is treated\n   * as stuck, which fails toward offering the banner, as ``shellIsStale``\n   * does.\n   */\n  const STUCK_INSTALL_TIMEOUT_MS = 30000;\n\n  /**\n   * Wait for an installing worker to finish, one way or the other.\n   *\n   * @param {ServiceWorker} worker\n   * @returns {Promise<boolean>} ``true`` when it failed (``redundant``) or\n   *   did not finish inside ``STUCK_INSTALL_TIMEOUT_MS``; ``false`` once it\n   *   reaches ``installed`` or later.\n   */\n  function installFailed(worker) {\n    return new Promise((resolve) => {\n      const verdict = () => {\n        if (worker.state === 'redundant') return true;\n        if (worker.state === 'installing') return null;\n        return false;\n      };\n      const now = verdict();\n      if (now !== null) {\n        resolve(now);\n        return;\n      }\n      const onChange = () => {\n        const next = verdict();\n        if (next === null) return;\n        clearTimeout(timer);\n        worker.removeEventListener('statechange', onChange);\n        resolve(next);\n      };\n      const timer = setTimeout(() => {\n        worker.removeEventListener('statechange', onChange);\n        resolve(true);\n      }, STUCK_INSTALL_TIMEOUT_MS);\n      worker.addEventListener('statechange', onChange);\n    });\n  }\n\n  /**\n   * Can the browser NOT bring this device's shell up to date on its own?\n   * (SNOW-1025)\n   *\n   * The banner's second gate, asked only once ``shellIsStale`` has said\n   * yes. A stale shell is normal for the minutes after any deploy that\n   * touched a shell source: the new worker is still downloading, or it is\n   * waiting for the page to be hidden so it can be applied silently. None\n   * of that needs a person. What needs a person is a worker that cannot\n   * install. ``install`` precaches with one atomic ``cache.addAll``, so a\n   * single bad entry rejects it and no worker ever reaches \"waiting\".\n   *\n   *   * A worker already waiting \u2192 not stuck. It will be applied.\n   *   * Otherwise, ask for an update. A worker that installs \u2192 not stuck.\n   *     One that goes ``redundant``, or does not finish in time \u2192 stuck.\n   *   * No worker appears at all, though the server's shell differs from\n   *     the one held \u2192 stuck. The browser does not see the new worker, so\n   *     nothing will change without a reload.\n   *   * ``update()`` rejects, or there is no registration \u2192 stuck. The\n   *     server has just answered ``/api/version``, so this is not a device\n   *     that is simply offline.\n   *\n   * @returns {Promise<boolean>}\n   */\n  async function workerIsStuck() {\n    let registration;\n    try {\n      registration = await navigator.serviceWorker.getRegistration();\n    } catch (_err) {\n      return true;\n    }\n    if (!registration) return true;\n    if (registration.waiting) return false;\n    try {\n      await registration.update();\n    } catch (_err) {\n      return true;\n    }\n    if (registration.waiting) return false;\n    if (registration.installing) return installFailed(registration.installing);\n    return true;\n  }\n\n  /**\n   * Reveal the banner if, and only if, the worker is stuck on a stale shell.\n   *\n   * The one entry point for the banner, published as\n   * ``window.pwaUpdateBanner.reveal`` and called by ``pwa_version_check.js``\n   * once ``/api/version`` confirms the server has moved on. Both gates live\n   * here, so no caller can reveal the banner past them. ``revealNow`` is the\n   * ungated DOM primitive underneath, which only this function and the\n   * tests that exercise the banner's copy have any business calling.\n   *\n   * @returns {Promise<boolean>} Whether the banner was revealed.\n   */\n  function revealUpdateBannerIfStuck() {\n    const info = window.pwaVersionInfo;\n    const verified =\n      info && typeof info.verified === 'function'\n        ? Promise.resolve(info.verified()).catch(() => null)\n        : Promise.resolve(null);\n    return verified\n      .then((verdict) => {\n        const server = verdict && verdict.shell ? verdict.shell : '';\n        if (bannerAnswer && bannerAnswer.shell === server) return bannerAnswer.show;\n        const show = shellIsStale(server)\n          .then((stale) => (stale ? workerIsStuck() : false))\n          .catch(() => true);\n        bannerAnswer = { shell: server, show: show };\n        return show;\n      })\n      .then((show) => {\n        if (!show) return false;\n        revealUpdateBanner();\n        if (!announcedStuck && !DEV_SHELL_BYPASS_ACTIVE) {\n          announcedStuck = true;\n          try {\n            window.pwaTelemetry?.emit('pwa.sw.update_available', {});\n          } catch (_err) {\n            // Ignore \u2014 telemetry must never break the update banner.\n          }\n        }\n        return true;\n      });\n  }\n\n  // SNOW-623: the banner has one owner. `pwa_version_check.js` offers the\n  // same element when the server declares a version drift, and used to\n  // carry its own copy of the reveal. It now delegates here.\n  //\n  // `reveal` is the GATED entry point (SNOW-952, SNOW-1025): it reveals\n  // only for a worker stuck on a stale shell. It keeps the name on purpose,\n  // so the obvious thing to reach for is the safe one. `revealNow` is the\n  // ungated DOM primitive. It exists for the tests that exercise the\n  // banner's copy; calling it from a reveal path would put the interruption\n  // straight back.\n  window.pwaUpdateBanner = Object.freeze({\n    reveal: revealUpdateBannerIfStuck,\n    revealNow: revealUpdateBanner,\n    hide: hideUpdateBanner,\n  });\n\n  if (banner) {\n    // Reload button is queried by ID on the public partial and on the\n    // self-injected admin fallback \u2014 both variants use the same ID so this\n    // single lookup covers them.\n    document\n      .getElementById('sw-update-banner-reload')\n      ?.addEventListener('click', handleReloadClick);\n    banner\n      .querySelector('[data-action=\"dismiss\"]')\n      ?.addEventListener('click', hideUpdateBanner);\n  }\n\n  /**\n   * Drop the SW shell caches, then reload. Clearing first means the\n   * navigation goes to the network for fresh HTML with the current\n   * APP_VERSION baked into ``<meta>`` \u2014 without it, ``_networkFirst``'s\n   * runtime cache could hand back HTML carrying the stale\n   * ``pwa-app-version`` meta tag (the original reload-loop bug). Bulletin\n   * JSON and other network-only paths are unaffected; the shell caches\n   * are the only thing that would keep the stale meta tag alive.\n   *\n   * Scoped to ``snowdesk-shell-*`` / ``map-shell-*`` on purpose: the\n   * user's pinned basemap buckets, and everything in IndexedDB, are not\n   * code and survive a code update. SNOW-609 made this the forced-update\n   * path's wipe too (see the ``window.pwaClearShellCachesAndReload``\n   * export below).\n   *\n   * @returns {Promise<void>}\n   */\n  async function clearShellCachesAndReload() {\n    try {\n      if ('caches' in window) {\n        const keys = await caches.keys();\n        await Promise.all(\n          keys\n            .filter(\n              (k) =>\n                k.startsWith('snowdesk-shell-') || k.startsWith('map-shell-'),\n            )\n            .map((k) => caches.delete(k)),\n        );\n      }\n    } catch (_err) {\n      // Cache API unavailable / eviction race \u2014 reload anyway.\n    }\n\n    window.location.reload();\n  }\n\n  // SNOW-609: exported so ``pwa_version_check.js``'s forced-update path\n  // reuses this wipe rather than carrying a third copy of one (the others\n  // being ``pwa_reset.js``'s everything-goes reset and ``sw-kill.js``'s\n  // activate-time sweep). Non-writable / non-configurable, matching\n  // ``window.pwaResetLocalData`` (pwa_reset.js) \u2014 the named export is\n  // deliberate so a third-party script can't swap the implementation.\n  //\n  // This script is loaded before ``pwa_version_check.js`` (load order is\n  // documented in ``pwa_client_version.js``), so the global is present by\n  // the time that module binds its modal handler. The one exception is a\n  // browser with no ``navigator.serviceWorker``, where this IIFE returns\n  // before reaching here \u2014 hence the plain-reload fallback on the calling\n  // side.\n  Object.defineProperty(window, 'pwaClearShellCachesAndReload', {\n    value: clearShellCachesAndReload,\n    writable: false,\n    configurable: false,\n  });\n\n  /**\n   * Put the banner into its working state, the moment the click lands.\n   *\n   * The click used to change nothing on screen. Both reload paths are\n   * asynchronous \u2014 the SW path posts SKIP_WAITING and waits for the new\n   * worker to activate, backstopped by a three-second timer; the\n   * version-header path enumerates and deletes Cache Storage entries\n   * first \u2014 so between the press and the reload there was a stretch of\n   * seconds in which the only feedback was a button that had stopped\n   * responding. That reads as a dead control, and the natural response to\n   * a dead control is to press it again.\n   *\n   * Three things change, in the order the eye finds them: the CTA says\n   * what it is doing and is visibly disabled (`disabled:opacity-60`,\n   * `disabled:cursor-not-allowed` \u2014 see `_button_chrome_classes` in\n   * apps/public/templatetags/components.py), the banner's refresh icon\n   * spins, and the copy switches from an offer to a progress report.\n   *\n   * No state is restored afterwards, deliberately: every path out of here\n   * ends in `location.reload()`, so the busy banner's successor is a\n   * fresh document. A restore would only ever run if the reload itself\n   * failed, and a banner that quietly went back to \"Update available\"\n   * would be claiming the update had not started when it had.\n   *\n   * The icon spin is public-page only \u2014 the admin fallback banner has no\n   * icon to spin and no Tailwind to spin it with. The label and copy\n   * changes, which are the load-bearing half, land on both.\n   *\n   * @returns {void}\n   */\n  function showBannerBusy() {\n    const btn = document.getElementById('sw-update-banner-reload');\n    if (btn) {\n      btn.setAttribute('aria-busy', 'true');\n      btn.textContent = STRINGS.updating;\n    }\n    const title = document.getElementById('sw-update-banner-title');\n    if (title) title.textContent = STRINGS['updating-title'];\n    const body = document.getElementById('sw-update-banner-body');\n    if (body) body.textContent = STRINGS['updating-body'];\n    const icon = banner && banner.querySelector('[data-overlay-icon]');\n    // `motion-safe:` so the spin honours prefers-reduced-motion; the copy\n    // and the disabled CTA carry the message on their own without it.\n    if (icon) icon.classList.add('motion-safe:animate-spin');\n  }\n\n  /**\n   * Reload click handler. Handles both the SW-driven path (a fresh worker\n   * is waiting) and the version-header-driven path (the server's\n   * ``X-App-Version`` drifted from the shell's ``<meta>`` but ``sw.js`` did\n   * not change so no worker is waiting).\n   *\n   * The waiting worker is re-resolved from the live registration rather\n   * than trusted from the reference captured when the banner was shown:\n   * that worker can have gone redundant since (superseded by a newer\n   * install, a failed update, or browser eviction), and ``postMessage`` to\n   * a redundant worker is a silent no-op \u2014 the click would do nothing and\n   * the busy-latch below would leave the button dead. For the same\n   * reason, the SW path arms a fallback timer: if ``controllerchange``\n   * hasn't fired shortly after ``SKIP_WAITING`` was posted, fall back to\n   * the cache-clearing reload so the click ALWAYS lands on a fresh shell.\n   *\n   * SNOW-609: this used to also clear ``pwa.update.first_shown_at``, the\n   * stamp behind the 24h escalation to the blocking modal. That\n   * escalation is gone \u2014 it blocked the app on elapsed time rather than\n   * on any server statement that the build was unacceptable \u2014 and the\n   * stamp is no longer written, so there is nothing to clear.\n   */\n  async function handleReloadClick() {\n    const btn = document.getElementById('sw-update-banner-reload');\n    if (btn) {\n      if (btn.dataset.busy === '1') return;\n      btn.dataset.busy = '1';\n      btn.setAttribute('disabled', 'disabled');\n    }\n    showBannerBusy();\n\n    userTriggeredUpdate = true;\n\n    // Prefer the registration's live waiting worker; the captured\n    // reference is only a fallback and only while still actually waiting\n    // (state 'installed' \u2014 anything else can no longer be activated).\n    let waiting =\n      waitingWorker && waitingWorker.state === 'installed'\n        ? waitingWorker\n        : null;\n    try {\n      const registration = await navigator.serviceWorker.getRegistration();\n      if (registration && registration.waiting) {\n        waiting = registration.waiting;\n      }\n    } catch (_err) {\n      // getRegistration failure \u2014 proceed with what we have.\n    }\n\n    if (waiting) {\n      // SW-driven path: activation fires ``controllerchange`` which\n      // triggers the guarded reload below.\n      waiting.postMessage({ type: 'SKIP_WAITING' });\n      // Safety net: a worker that never activates (stuck install, killed\n      // SW process) must not strand the user on a disabled button. The\n      // cache-clearing reload is safe even if activation later succeeds \u2014\n      // ``refreshing`` ensures whichever path runs first wins and the\n      // other becomes a no-op.\n      setTimeout(() => {\n        if (refreshing) return;\n        refreshing = true;\n        clearShellCachesAndReload();\n      }, 3000);\n      return;\n    }\n\n    // Version-header-driven path (no worker waiting).\n    await clearShellCachesAndReload();\n  }\n\n  // A new worker called ``clients.claim()`` and now controls the page.\n  //\n  // The answer ``revealUpdateBannerIfStuck`` remembered described the old\n  // worker, so it is dropped whatever happens next.\n  //\n  // Reload once onto the new shell, but only if the user pressed Reload. A\n  // first-install claim must not bounce someone's very first visit, and a\n  // silent activation (SNOW-1025) must not move a page the user just came\n  // back to. That page keeps running, and its next navigation lands on the\n  // new shell. A banner still showing from before is taken down: the worker\n  // it was about has just been replaced.\n  navigator.serviceWorker.addEventListener('controllerchange', () => {\n    bannerAnswer = null;\n    if (!userTriggeredUpdate) {\n      if (!silentUpdatePosted) return;\n      silentUpdatePosted = false;\n      hideUpdateBanner();\n      // SNOW-585: the dev bypass already serves fresh shell assets, so an\n      // applied update describes nothing a user would have seen.\n      if (DEV_SHELL_BYPASS_ACTIVE) return;\n      try {\n        window.pwaTelemetry?.emit('pwa.sw.update_applied', {});\n      } catch (_err) {\n        // Ignore \u2014 telemetry must never break the update flow.\n      }\n      return;\n    }\n    if (refreshing) return;\n    refreshing = true;\n    // SNOW-384: best-effort \u2014 the reload immediately below can tear the\n    // page down before the async emit() (IndexedDB write) settles. This\n    // mirrors the same race telemetry.js accepts for other reload-adjacent\n    // events; there is no synchronous alternative here.\n    try {\n      window.pwaTelemetry?.emit('pwa.sw.update_applied', {});\n    } catch (_err) {\n      // Ignore \u2014 telemetry must never block the reload.\n    }\n    window.location.reload();\n  });\n\n  // SNOW-1025: the moment a waiting worker is applied. Hidden means the\n  // user has switched away (tab, app, screen lock), so nobody is watching\n  // the worker change. Registered here, not inside ``register().then``, so\n  // a worker recorded before registration settles is still picked up.\n  document.addEventListener('visibilitychange', () => {\n    if (document.visibilityState === 'hidden') applyWaitingWorker();\n  });\n\n  /**\n   * Watch a service worker for the install\u2192installed transition. When\n   * it lands on ``installed`` and there is still an existing controller\n   * on the page, that means an update is ready (the existing controller\n   * is the OLD SW; this newly-installed one is the new shell). It is\n   * queued to be applied silently (SNOW-1025).\n   *\n   * When it lands on ``redundant`` instead, the install failed, and the\n   * two banner gates are asked directly. This is the stuck-worker path\n   * that needs no version drift. After a deploy, the first navigation\n   * fetches fresh HTML (navigations are network-first), so the page's\n   * ``pwa-app-version`` already matches every response header and\n   * ``pwa_version_check.js`` never offers the banner. A failed install\n   * seen here would otherwise go unreported. The remembered answer is\n   * dropped first, because it may predate the failure. A worker that\n   * reached ``installed`` and was later superseded by a newer install\n   * also ends ``redundant``, but that is not a failure, so it is excluded.\n   *\n   * @param {ServiceWorker} sw\n   */\n  function watchForInstall(sw) {\n    let installed = false;\n    sw.addEventListener('statechange', () => {\n      if (!navigator.serviceWorker.controller) return;\n      if (sw.state === 'installed') {\n        installed = true;\n        queueSilentUpdate(sw);\n      } else if (sw.state === 'redundant' && !installed) {\n        bannerAnswer = null;\n        revealUpdateBannerIfStuck();\n      }\n    });\n  }\n\n  /**\n   * Ask the server whether the SW should be registered and, if so, from\n   * which URL. See the header comment for the two branches. Failure to\n   * reach the endpoint falls back to registering the default ``/sw.js``.\n   *\n   * @returns {Promise<{sw_url: string, kill: boolean}>}\n   */\n  async function fetchSwConfig() {\n    try {\n      const res = await fetch('/api/sw-config', { cache: 'no-store' });\n      if (!res.ok) throw new Error('sw-config non-2xx: ' + res.status);\n      const json = await res.json();\n      return {\n        sw_url: typeof json.sw_url === 'string' ? json.sw_url : '/sw.js',\n        kill: json.kill === true,\n      };\n    } catch (_err) {\n      return { sw_url: '/sw.js', kill: false };\n    }\n  }\n\n  fetchSwConfig().then((config) => {\n    if (config.kill) {\n      // SNOW-384: Mechanism A's kill decision \u2014 this IS the \"pre-register\n      // kill fetch\" point: the client learns to kill before it has ever\n      // registered a SW this session. Critical event (CRITICAL_EVENTS in\n      // telemetry.js) so it fires via sendBeacon immediately.\n      try {\n        window.pwaTelemetry?.emit('pwa.kill_switch.activated', {\n          mechanism: 'a',\n        });\n      } catch (_err) {\n        // Ignore \u2014 telemetry must never block the kill switch.\n      }\n      // Mechanism A activated. Unregister every SW on this origin so the\n      // next navigation runs without a controller. Caches are deliberately\n      // left alone: that is the kill-switch SW's job when the flip goes\n      // through Mechanism B (``/sw-kill.js``), and not wiping a user's\n      // deliberate 500 MB of downloaded basemaps because ops flipped a\n      // temporary switch is the right trade.\n      //\n      // SNOW-615: this used to say \"the user can hard-refresh to clear\n      // anything else\". A hard refresh does not clear Cache Storage, and\n      // unregistering the worker also removes the only worker-side reaper\n      // (the activate sweep) \u2014 so that sentence described a recovery that\n      // does not exist. Nothing is permanently stranded: the page-side\n      // paths still work (``pwa_reset.js``, ``clearShellCachesAndReload``\n      // below, and ``evictBasemapAreas`` in map.js).\n      navigator.serviceWorker\n        .getRegistrations()\n        .then((regs) => Promise.all(regs.map((r) => r.unregister())))\n        .catch((err) => console.error('[sw] kill unregister failed:', err));\n      return;\n    }\n\n    navigator.serviceWorker\n      .register(config.sw_url, { scope: '/', updateViaCache: 'none' })\n      .then((registration) => {\n        // Three entry points to \"an update is ready\" (each queues it to be\n        // applied silently; SNOW-1025):\n        //   1. ``waiting`` is non-null at register-time \u2014 a new worker has\n        //      already installed and is parked waiting (the common case\n        //      now that we don't auto-skipWaiting).\n        //   2. ``installing`` is non-null at register-time \u2014 a SW update\n        //      check started before our register() resolved.\n        //   3. ``updatefound`` fires later \u2014 the common case during a\n        //      normal session where the SW changes on the next deploy.\n        if (registration.waiting && navigator.serviceWorker.controller) {\n          queueSilentUpdate(registration.waiting);\n        }\n        if (registration.installing) {\n          watchForInstall(registration.installing);\n        }\n        registration.addEventListener('updatefound', () => {\n          if (registration.installing) {\n            watchForInstall(registration.installing);\n          }\n        });\n\n        // Pick an update up promptly for a tab left open across a deploy:\n        // re-check when the tab regains focus. ``update()`` is a no-op when\n        // the SW script is unchanged. A worker it installs is applied the\n        // next time the page is hidden.\n        //\n        // SNOW-622: throttled. ``update()`` is a network round trip for the\n        // worker script (``/sw.js`` is ``Cache-Control: no-cache``, so it\n        // revalidates every time), and visibilitychange fires on every\n        // tab switch, every app switch on mobile, and every screen unlock.\n        // A user flicking between apps paid one request each way. A deploy\n        // the user has not noticed in the last minute is not more urgent\n        // than one they have.\n        let lastUpdateCheck = 0;\n        document.addEventListener('visibilitychange', () => {\n          if (document.visibilityState !== 'visible') return;\n          const now = Date.now();\n          if (now - lastUpdateCheck < SW_UPDATE_CHECK_INTERVAL_MS) return;\n          lastUpdateCheck = now;\n          registration.update().catch(() => {});\n        });\n      })\n      .catch((err) => {\n        console.error('[sw] registration failed:', err);\n      });\n  });\n})();\n"],
  "mappings": "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",
  "names": ["DEV_SHELL_BYPASS_ACTIVE", "PAGE_SHELL", "STRINGS", "waitingWorker", "userTriggeredUpdate", "silentUpdatePosted", "matchedWorker", "refreshing", "announcedStuck", "bannerAnswer", "_warmCacheSlot", "_warmCacheChain", "_warmCacheRunsPending", "_mintRequestId", "bytes", "out", "i", "event", "data", "WARM_CACHE_TIMEOUT_MS", "SW_UPDATE_CHECK_INTERVAL_MS", "CONTROLLER_WAIT_MS", "_activeWorker", "resolve", "done", "finish", "worker", "timer", "onChange", "warmCache", "urls", "opts", "mine", "_warmCacheRun", "release", "applyWaitingWorker", "active", "options", "requestId", "settled", "timeoutHandle", "settle", "value", "armTimeout", "cancelWarmCache", "ensureBanner", "el", "copy", "title", "sub", "reload", "dismiss", "SVG_NS", "glyph", "k", "v", "d", "path", "banner", "queueSilentUpdate", "workerShell", "shell", "matched", "waiting", "registration", "immediate", "activationIsSafe", "hideUpdateBanner", "revealUpdateBanner", "WORKER_SHELL_TIMEOUT_MS", "askWorker", "type", "channel", "shellIsStale", "server", "held", "STUCK_INSTALL_TIMEOUT_MS", "installFailed", "verdict", "now", "next", "workerIsStuck", "revealUpdateBannerIfStuck", "info", "show", "stale", "handleReloadClick", "clearShellCachesAndReload", "keys", "showBannerBusy", "btn", "body", "icon", "watchForInstall", "sw", "installed", "fetchSwConfig", "res", "json", "config", "regs", "r", "err", "lastUpdateCheck"]
}
