{
  "version": 3,
  "sources": ["pwa_reset.js"],
  "sourcesContent": ["/*\n * static/js/pwa_reset.js \u2014 \"Reset local data\" escape hatch (SNOW-378).\n *\n * Spec \u00A73.10 / \u00A710.6 / \u00A712.7 (non-negotiable). Single-tap recovery for\n * a stuck installation: any element with ``data-pwa-reset-trigger``\n * gets a click handler that runs the wipe from the spec and reloads the\n * page. Also the wipe ``db.js``'s Reset Required overlay CTA goes\n * through, via ``window.pwaResetLocalData``.\n *\n * SNOW-615 routed ``pwa_version_check.js``'s Update Required modal here\n * too; SNOW-609 took it back out. A blocked build is a code problem, and\n * clearing the shell caches fixes code \u2014 taking IndexedDB and the pinned\n * basemap buckets with it would destroy user data for a reason unrelated\n * to it. That path now calls ``window.pwaClearShellCachesAndReload``\n * (``sw_register.js``) instead. This file stays the everything-goes\n * path, and it asks before it runs.\n *\n * The six steps, in order:\n *   (1) Unregister every service worker via ``getRegistrations()``.\n *   (2) Delete every Cache Storage entry via ``caches.keys()``.\n *   (3) Delete every IndexedDB database. ``indexedDB.databases()`` is\n *       the preferred entry point; browsers without it (older Safari)\n *       are handled by iterating ``KNOWN_DB_NAMES`` below, which since\n *       SNOW-375 holds the one database this app owns. Extend that list\n *       rather than this comment when a second one arrives.\n *   (4) Clear ``localStorage`` and ``sessionStorage``.\n *   (5) Reload the page \u2014 but only on a clean run.\n *\n * Every step records its own outcome. A step that fails (most often a\n * ``blocked`` IndexedDB delete, meaning another tab still holds the\n * connection) is reported on the ``#pwa-reset-required`` overlay and the\n * reload is skipped: reloading would land the user back in the state the\n * reset was meant to clear, with nothing said about why.\n *\n * Emits ``pwa.reset.user_initiated`` (manage-page button, this file's own\n * ``[data-pwa-reset-trigger]`` binding) or ``pwa.reset.forced`` (the\n * ``db.js`` Reset Required overlay CTA, called with ``forced=true`` \u2014\n * SNOW-384) via ``window.pwaTelemetry?.emit``, carrying the outcome as\n * ``{ ok, failed_steps }`` properties.\n *\n * The reset is idempotent \u2014 calling it twice does the same work twice\n * and lands on the same page. A confirmation dialog (``window.confirm``)\n * gates the trigger by default; markup can opt out by setting\n * ``data-pwa-reset-skip-confirm``, for a surface that already carries\n * its own dialogue.\n *\n * SNOW-860: on /offline/ (SNOW-930; /account/settings/ before it) that dialog also quotes the total from\n * ``reset_data_summary.js``'s breakdown \u2014 the same figures the user has\n * just read on the page, from the same summary, so the two can never\n * disagree. Every other page carries the standing copy alone. The wipe\n * itself is untouched by that ticket: still all-or-nothing, still these\n * six steps.\n */\n\n(function () {\n  'use strict';\n\n  const TRIGGER_ATTR = 'data-pwa-reset-trigger';\n  const SKIP_CONFIRM_ATTR = 'data-pwa-reset-skip-confirm';\n\n  // Known DB names \u2014 deleted explicitly in the fallback path for\n  // browsers without ``indexedDB.databases()``. Kept in sync with\n  // ``static/js/db.js`` DB_NAME (SNOW-375). When db.js bumps the\n  // namespace (rare \u2014 see docs/indexeddb-scaffolding.md), add the new\n  // name here so old databases are also wiped.\n  const KNOWN_DB_NAMES = Object.freeze(['snowdesk-pwa-v1']);\n\n  // Human labels for the wipe steps, keyed by the step ids recorded in\n  // the outcome and sent to telemetry. Used to name the failed step in\n  // the overlay copy, so the user is told what survived the reset.\n  const STEP_LABELS = Object.freeze({\n    service_workers: 'the offline service worker',\n    caches: 'cached pages and map tiles',\n    indexeddb: 'the offline database',\n    web_storage: 'saved preferences',\n  });\n\n  /**\n   * Build the copy shown on the Reset Required overlay when a step did\n   * not complete. Names the data that survived, then the action that\n   * clears the common cause \u2014 a second tab holding the database open.\n   *\n   * @param {string[]} failed Step ids that did not complete.\n   * @returns {string}\n   */\n  function failureMessage(failed) {\n    const names = failed.map((step) => STEP_LABELS[step] || step).join(', ');\n    return (\n      `Snowdesk could not finish resetting local data: ${names} could not ` +\n      'be cleared. This usually means Snowdesk is open in another tab or ' +\n      'window \u2014 close the others, then tap Reset now to try again.'\n    );\n  }\n\n  /**\n   * Bind the Reset Required CTA to a retry. Only called when this module\n   * revealed the overlay itself; on the forced path ``db.js`` has already\n   * bound the same button, and a second binding would run two wipes per\n   * tap.\n   *\n   * @param {boolean} forced Carried into the retry so it keeps reporting\n   *   under the same telemetry event as the run that failed.\n   */\n  function bindRetryCta(forced) {\n    const cta = document.getElementById('pwa-reset-required-cta');\n    if (!cta || cta.dataset.pwaResetRetryBound === '1') return;\n    cta.dataset.pwaResetRetryBound = '1';\n    cta.addEventListener('click', (evt) => {\n      evt.preventDefault();\n      resetLocalData(forced);\n    });\n  }\n\n  /**\n   * Surface a failed wipe on the ``#pwa-reset-required`` overlay.\n   *\n   * @param {string[]} failed Step ids that did not complete.\n   * @param {boolean} forced Whether the run came from the forced path.\n   * @returns {boolean} True when the failure reached a visible surface;\n   *   false when the page carries no overlay markup (admin pages), which\n   *   leaves the caller to fall back to the reload.\n   */\n  function reportFailure(failed, forced) {\n    const overlay = document.getElementById('pwa-reset-required');\n    if (!overlay) return false;\n    const body = document.getElementById('pwa-reset-required-body');\n    // textContent, never innerHTML \u2014 the copy is built from a fixed\n    // label table, but the escaping guarantee shouldn't rest on that.\n    if (body) body.textContent = failureMessage(failed);\n    const alreadyOpen = !overlay.classList.contains('hidden');\n    overlay.classList.remove('hidden');\n    if (!alreadyOpen) bindRetryCta(forced === true);\n    return true;\n  }\n\n  /**\n   * Run the full six-step wipe. Every step swallows its own errors and\n   * records whether it completed; the reload happens only when all of\n   * them did. Returns a promise resolving to the outcome.\n   *\n   * @param {boolean} [forced] SNOW-384 \u2014 true when the reset was not an\n   *   elective user action but was forced by the app entering an\n   *   unrecoverable state (today: ``db.js``'s Reset Required overlay,\n   *   ``[data-pwa-reset-required-cta]``, after an IndexedDB migration\n   *   failure). Selects which telemetry event fires:\n   *   ``pwa.reset.forced`` vs ``pwa.reset.user_initiated``. Defaults to\n   *   ``false`` so every existing caller (the manage-page button, via\n   *   ``bindTrigger`` below) keeps reporting user-initiated.\n   * @returns {Promise<{ok: boolean, failed: string[]}>} The per-step\n   *   outcome. ``ok`` is false when at least one step did not complete,\n   *   in which case the page is not reloaded.\n   */\n  async function resetLocalData(forced) {\n    /** @type {string[]} Step ids that did not complete. */\n    const failed = [];\n\n    // (1) Service workers.\n    try {\n      if ('serviceWorker' in navigator) {\n        const regs = await navigator.serviceWorker.getRegistrations();\n        const results = await Promise.all(\n          regs.map((r) => r.unregister().then(() => true, () => false)),\n        );\n        if (results.some((ok) => !ok)) failed.push('service_workers');\n      }\n    } catch (_err) {\n      failed.push('service_workers');\n    }\n\n    // (2) Cache Storage.\n    try {\n      if ('caches' in window) {\n        const keys = await caches.keys();\n        const results = await Promise.all(\n          keys.map((k) => caches.delete(k).then(() => true, () => false)),\n        );\n        if (results.some((ok) => !ok)) failed.push('caches');\n      }\n    } catch (_err) {\n      failed.push('caches');\n    }\n\n    // (3) IndexedDB.\n    try {\n      if ('indexedDB' in window) {\n        const dbs =\n          typeof indexedDB.databases === 'function'\n            ? await indexedDB.databases().catch(() => [])\n            : KNOWN_DB_NAMES.map((name) => ({ name }));\n        const results = await Promise.all(\n          dbs\n            .filter((db) => db && db.name)\n            .map(\n              (db) =>\n                new Promise((resolve) => {\n                  try {\n                    const req = indexedDB.deleteDatabase(db.name);\n                    req.onsuccess = () => resolve(true);\n                    req.onerror = () => resolve(false);\n                    // A blocked delete is not a success: another tab\n                    // holds the connection open, so the database\n                    // survives into the page this reset lands on.\n                    req.onblocked = () => resolve(false);\n                  } catch (_err) {\n                    resolve(false);\n                  }\n                }),\n            ),\n        );\n        if (results.some((ok) => !ok)) failed.push('indexeddb');\n      }\n    } catch (_err) {\n      failed.push('indexeddb');\n    }\n\n    // (4) Web-storage. Both stores share one step id \u2014 a browser that\n    // refuses one (Safari private mode) refuses both, and the step is\n    // reported as failed rather than tolerated: data the reset promised\n    // to clear is still on the device either way.\n    let webStorageCleared = true;\n    try {\n      localStorage.clear();\n    } catch (_err) {\n      webStorageCleared = false;\n    }\n    try {\n      sessionStorage.clear();\n    } catch (_err) {\n      webStorageCleared = false;\n    }\n    if (!webStorageCleared) failed.push('web_storage');\n\n    const ok = failed.length === 0;\n\n    // Telemetry \u2014 SNOW-385 / SNOW-384. Both pwa.reset.forced and\n    // pwa.reset.user_initiated are critical events, so telemetry.js\n    // fires ``sendBeacon`` immediately (even opt-out clients still send\n    // the signal, spec \u00A716.6). Optional chaining because this file is\n    // loaded on admin pages too, where telemetry.js is not. The outcome\n    // rides as properties: the event names stay as they are, so\n    // apps/analytics/schema.py's allowlist is untouched.\n    try {\n      window.pwaTelemetry?.emit(\n        forced ? 'pwa.reset.forced' : 'pwa.reset.user_initiated',\n        { ok: ok, failed_steps: failed },\n      );\n    } catch (_err) {\n      // Ignore \u2014 analytics must never break the reset flow.\n    }\n\n    // (5) Reload, on a clean run only. Using ``location.reload()`` (no\n    // argument) picks up the new SW / cache state on the next\n    // navigation. In Safari private mode where storage APIs are stubs,\n    // this is still safe. A failed run reports instead \u2014 reloading\n    // would drop the user back into the state the reset was meant to\n    // clear. Pages without the overlay markup (admin) have nowhere to\n    // report, so they keep the old behaviour.\n    if (ok || !reportFailure(failed, forced === true)) {\n      window.location.reload();\n    }\n    return { ok: ok, failed: failed };\n  }\n\n  /**\n   * Copy for the default confirmation dialog. Enumerates what will and\n   * will not be lost so the user can judge before confirming.\n   */\n  const CONFIRM_MESSAGE =\n    'Reset local data?\\n\\n' +\n    'This will clear cached bulletins, offline data, and any saved ' +\n    'preferences on this device.\\n\\n' +\n    'Your subscription and account details are stored on the server ' +\n    'and are not affected.';\n\n  /**\n   * The dialog copy, with the /offline/ page's own figures folded in\n   * (SNOW-860).\n   *\n   * `/offline/` paints a breakdown of what this wipe deletes;\n   * the dialog quotes the total \u2014 and the unsent-change count, which is\n   * the one line item nothing else can get back \u2014 from THAT summary\n   * rather than computing a second one. Two numbers for the same\n   * deletion, differing, at the moment the user is deciding, would be\n   * worse than one.\n   *\n   * Absent on every other page (the module is loaded site-wide, the panel\n   * is not) and after a failed read, in which case the standing copy\n   * above stands alone. A figure that could not be read is not guessed at\n   * here.\n   *\n   * @returns {string}\n   */\n  function confirmMessage() {\n    let lines = [];\n    try {\n      lines = window.pwaResetDataSummary?.confirmLines() || [];\n    } catch (_err) {\n      // The panel's own failure must never cost the user the dialog.\n    }\n    return lines.length ? CONFIRM_MESSAGE + '\\n\\n' + lines.join('\\n') : CONFIRM_MESSAGE;\n  }\n\n  /**\n   * Wire a single trigger element. Idempotent \u2014 safe to call twice on\n   * the same node.\n   *\n   * @param {Element} el\n   */\n  function bindTrigger(el) {\n    if (el.dataset.pwaResetBound === '1') return;\n    el.dataset.pwaResetBound = '1';\n    el.addEventListener('click', (evt) => {\n      evt.preventDefault();\n      const skip = el.hasAttribute(SKIP_CONFIRM_ATTR);\n      const confirmed =\n        skip ||\n        (typeof window.confirm === 'function'\n          ? window.confirm(confirmMessage())\n          : true);\n      if (!confirmed) return;\n      resetLocalData().catch(() => window.location.reload());\n    });\n  }\n\n  /**\n   * Bind every trigger currently in the DOM.\n   */\n  function bindAll() {\n    document.querySelectorAll(`[${TRIGGER_ATTR}]`).forEach(bindTrigger);\n  }\n\n  // Expose the reset routine for programmatic callers (db.js's Reset\n  // Required overlay CTA, tests). The named export is deliberate so\n  // third-party scripts can't accidentally re-bind ``window.pwaReset``.\n  Object.defineProperty(window, 'pwaResetLocalData', {\n    value: resetLocalData,\n    writable: false,\n    configurable: false,\n  });\n\n  if (document.readyState === 'loading') {\n    document.addEventListener('DOMContentLoaded', bindAll);\n  } else {\n    bindAll();\n  }\n})();\n"],
  "mappings": "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",
  "names": ["TRIGGER_ATTR", "SKIP_CONFIRM_ATTR", "KNOWN_DB_NAMES", "STEP_LABELS", "failureMessage", "failed", "step", "bindRetryCta", "forced", "cta", "evt", "resetLocalData", "reportFailure", "overlay", "body", "alreadyOpen", "regs", "r", "ok", "keys", "k", "dbs", "name", "db", "resolve", "req", "webStorageCleared", "CONFIRM_MESSAGE", "confirmMessage", "lines", "bindTrigger", "el", "bindAll"]
}
