/*! trentpower.fr · /js/local.js · generated · loaded after first paint · signed via /integrity.json */
(function () {
'use strict';
var LOCAL_KEYS = [
{ key: 'tp-theme', storage: 'local', prefix: false },
{ key: 'tp-last-edition', storage: 'local', prefix: false },
{ key: 'tp-last-read:/en-au/', storage: 'local', prefix: false },
{ key: 'tp-last-read:/fr/', storage: 'local', prefix: false },
{ key: 'tp-fr-disclosure-seen', storage: 'local', prefix: false },
{ key: 'tp-sw-meta', storage: 'local', prefix: false },
{ key: 'tp-welcomed:', storage: 'session', prefix: true },
{ key: 'tp-typed-', storage: 'session', prefix: true },
{ key: 'tp-skip-hero-anim', storage: 'session', prefix: false },
{ key: 'tp-show-gate', storage: 'session', prefix: false }
];
function $(sel, root) { return (root || document).querySelector(sel); }
function $$(sel, root) { return Array.prototype.slice.call((root || document).querySelectorAll(sel)); }
function field(name) { return $('[data-local-field="' + name + '"]'); }
function setField(name, value) {
var el = field(name);
if (!el) return;
var code = el.querySelector('code');
var target = code || el;
target.textContent = value;
}
function safeStorage(kind) {
try { return kind === 'session' ? window.sessionStorage : window.localStorage; }
catch (_) { return null; }
}
function readJSON(storage, key) {
if (!storage) return null;
try { var raw = storage.getItem(key); return raw ? JSON.parse(raw) : null; }
catch (_) { return null; }
}
function formatDate(ts) {
if (!ts) return null;
try {
var d = new Date(ts);
if (isNaN(d.getTime())) return null;
var pad = function (n) { return n < 10 ? '0' + n : '' + n; };
return d.getUTCFullYear() + '-' + pad(d.getUTCMonth() + 1) + '-' + pad(d.getUTCDate())
+ ' ' + pad(d.getUTCHours()) + ':' + pad(d.getUTCMinutes()) + ' UTC';
} catch (_) { return null; }
}
function formatDay(ts) {
if (!ts) return null;
try {
var d = new Date(ts);
if (isNaN(d.getTime())) return null;
var pad = function (n) { return n < 10 ? '0' + n : '' + n; };
return d.getUTCFullYear() + '-' + pad(d.getUTCMonth() + 1) + '-' + pad(d.getUTCDate());
} catch (_) { return null; }
}
function formatBytes(n) {
if (typeof n !== 'number' || !isFinite(n) || n < 0) return '—';
var u = ['B', 'KB', 'MB', 'GB'];
var i = 0;
while (n >= 1024 && i < u.length - 1) { n /= 1024; i++; }
return n.toFixed(i === 0 ? 0 : 1) + ' ' + u[i];
}
function parseEditionFromCacheName(name) {
if (!name) return null;
var m = String(name).match(/-edition-(\d{4}-\d{2}-\d{2})/) || String(name).match(/(\d{4}-\d{2}-\d{2})/);
return m ? m[1] : null;
}
function parseBuildFromCacheName(name) {
if (!name) return null;
var m = String(name).match(/^tp-(\d{4}-\d{2}-\d{2}\.[0-9a-f]+)/);
return m ? m[1] : null;
}
function languageLabel(value) {
if (value === 'en-au') return 'English';
if (value === 'fr') return 'Français';
return 'Not set (gate on next visit)';
}
function appearanceLabel(value) {
if (value === 'light') return 'Light';
if (value === 'dark') return 'Dark';
return 'Auto (system)';
}
var state = {
sw: null,
caches: [],
cachedEdition: null,
cachedBuild: null,
storageEstimate: null,
prefs: [],
lastCheckedAt: null
};
function classifyPubStatus(live, cached) {
if (!cached) return 'none';
if (!live) return 'unavailable';
if (live === cached) return 'match';
return live > cached ? 'older' : 'newer';
}
function buildDiagnostics() {
var live = document.body.dataset.edition || null;
var meta = readJSON(safeStorage('local'), 'tp-sw-meta') || {};
return {
generatedAt: new Date().toISOString(),
page: {
url: location.href,
canonical: 'https://trentpower.fr/local/',
edition: live,
sha256Short: document.body.dataset.sourceSha256Short || null
},
serviceWorker: {
state: swStateString(),
scope: state.sw && state.sw.scope || null,
scriptURL: state.sw && state.sw.active ? state.sw.active.scriptURL : null,
installedAt: meta.installedAt || null,
activatedAt: meta.activatedAt || null,
lastCheckedAt: meta.lastCheckedAt || state.lastCheckedAt || null,
lastUpdateCheckAt: meta.lastUpdateCheckAt || null,
cachedEdition: state.cachedEdition,
cachedBuild: state.cachedBuild
},
caches: state.caches.slice(),
publication: {
liveEdition: live,
cachedEdition: state.cachedEdition,
integrityManifestURL: '/integrity.json',
signatureURL: '/integrity.json.sig',
publicKeyURL: '/.well-known/pgp-key.asc',
status: classifyPubStatus(live, state.cachedEdition)
},
preferences: state.prefs.slice(),
storageEstimate: state.storageEstimate
};
}
function swStateString() {
if (!('serviceWorker' in navigator)) return 'unsupported';
if (!state.sw) return 'unregistered';
if (state.sw.installing) return 'installing';
if (state.sw.waiting) return 'waiting';
if (state.sw.active) return state.sw.active.state;
return 'unknown';
}
function renderStrip() {
var live = document.body.dataset.edition || '—';
var s = swStateString();
var swDisplay = (s === 'unsupported' || s === 'unregistered') ? s
: s === 'activated' ? 'Installed'
: s.charAt(0).toUpperCase() + s.slice(1);
setField('strip.edition', 'Edition ' + live);
setField('strip.sw', swDisplay);
var prefsCount = countStoredPrefs();
setField('strip.prefs', prefsCount === 1 ? '1 preference' : prefsCount + ' preferences');
var cacheSize = state.storageEstimate && state.storageEstimate.usage
? formatBytes(state.storageEstimate.usage)
: (state.caches.length ? state.caches.length + ' caches' : '—');
setField('strip.cache', cacheSize);
}
function countStoredPrefs() {
var n = 0;
var lcl = safeStorage('local');
if (!lcl) return 0;
LOCAL_KEYS.forEach(function (spec) {
if (spec.storage !== 'local') return;
if (spec.prefix) {
for (var i = 0; i < lcl.length; i++) {
var k = lcl.key(i);
if (k && k.indexOf(spec.key) === 0) n++;
}
} else if (lcl.getItem(spec.key) !== null) {
n++;
}
});
return n;
}
function renderPubState() {
var live = document.body.dataset.edition || '—';
var meta = readJSON(safeStorage('local'), 'tp-sw-meta') || {};
var s = swStateString();
var statusWord = s === 'activated' ? 'Installed'
: s === 'unregistered' ? 'Not registered'
: s === 'unsupported' ? 'Service workers unsupported'
: s.charAt(0).toUpperCase() + s.slice(1);
var updatedDay = meta.activatedAt ? formatDay(meta.activatedAt) : (meta.installedAt ? formatDay(meta.installedAt) : '—');
var summary = statusWord + ' · Edition ' + live + (updatedDay !== '—' ? ' · Updated ' + updatedDay : '');
setField('pub.summary', summary);
setField('pub.tag', statusWord);
setField('sw.summary', statusWord);
setField('sw.status', statusWord);
setField('sw.scope', state.sw && state.sw.scope ? state.sw.scope : '—');
setField('sw.installed_at', meta.installedAt ? formatDate(meta.installedAt) : 'Not recorded for this installation');
setField('sw.activated_at', meta.activatedAt ? formatDate(meta.activatedAt) : 'Not recorded for this installation');
setField('sw.version', state.cachedBuild || '—');
setField('sw.cached_edition', state.cachedEdition || '—');
setField('sw.cache_count', state.caches.length ? String(state.caches.length) : '—');
setField('sw.storage_estimate',
state.storageEstimate
? (formatBytes(state.storageEstimate.usage) + ' / ' + formatBytes(state.storageEstimate.quota))
: '—');
var sw = $('.local-record [data-local-disclosure="sw"]');
if (sw) {
var skip = sw.querySelector('[data-local-action="skip-waiting"]');
if (skip) skip.hidden = !(state.sw && state.sw.waiting);
}
var entryTotal = state.caches.reduce(function (acc, c) { return acc + (c.count || 0); }, 0);
var sizeText = state.storageEstimate && state.storageEstimate.usage
? formatBytes(state.storageEstimate.usage) : '—';
setField('caches.summary', state.caches.length
? state.caches.length + ' cache · ' + entryTotal + ' entries · ' + sizeText
: 'No caches');
setField('caches.entries', String(entryTotal));
setField('caches.size', sizeText);
setField('caches.edition', state.cachedEdition || '—');
setField('caches.cached_pages', state.caches.length ? String(state.caches[0].count) + ' (' + (state.caches[0].edition || 'unversioned') + ')' : '—');
renderCachesInventory();
var sha = document.body.dataset.sourceSha256Short || '—';
var pubStatus = classifyPubStatus(live, state.cachedEdition);
var verifySummary =
pubStatus === 'match' ? 'Cached matches live · ' + live
: pubStatus === 'older' ? 'Cached older than live'
: pubStatus === 'none' ? 'No cached edition'
: pubStatus === 'newer' ? 'Cached newer than live'
: 'Verification data unavailable';
setField('verify.summary', verifySummary);
setField('verify.fingerprint', sha);
}
function renderCachesInventory() {
var host = field('caches.list');
if (!host) return;
host.textContent = '';
if (!('caches' in window)) {
var p = document.createElement('p');
p.className = 'local-empty';
p.textContent = 'Cache Storage API not supported in this browser.';
host.appendChild(p);
return;
}
if (!state.caches.length) {
var empty = document.createElement('p');
empty.className = 'local-empty';
empty.textContent = 'No caches found on this device.';
host.appendChild(empty);
return;
}
state.caches.forEach(function (c) {
var card = document.createElement('details');
card.className = 'local-cache';
card.setAttribute('data-cache-name', c.name);
var summary = document.createElement('summary');
summary.className = 'local-cache__summary';
var nameLine = document.createElement('code');
nameLine.className = 'local-cache__name';
nameLine.textContent = c.name;
summary.appendChild(nameLine);
var meta = document.createElement('span');
meta.className = 'local-cache__meta';
var bits = [c.count + ' entries'];
if (c.edition) bits.push('Edition ' + c.edition);
meta.textContent = bits.join(' · ');
summary.appendChild(meta);
card.appendChild(summary);
var body = document.createElement('div');
body.className = 'local-cache__body';
var loading = document.createElement('p');
loading.className = 'local-empty';
loading.textContent = 'Reading headers…';
loading.hidden = true;
body.appendChild(loading);
var ul = document.createElement('ul');
ul.className = 'local-cache__entries';
body.appendChild(ul);
card.appendChild(body);
var loaded = false;
card.addEventListener('toggle', function () {
if (!card.open || loaded) return;
loaded = true;
loading.hidden = false;
loadCacheEntries(c.name).then(function (rows) {
loading.hidden = true;
rows.forEach(function (r) { ul.appendChild(r); });
}).catch(function (err) {
loading.textContent = 'Could not read · ' + (err && err.message || 'unknown');
});
});
host.appendChild(card);
});
}
function loadCacheEntries(name) {
return caches.open(name).then(function (cache) {
return cache.keys().then(function (reqs) {
return Promise.all(reqs.map(function (req) {
return cache.match(req).then(function (res) {
var li = document.createElement('li');
li.className = 'local-cache__entry';
var u = document.createElement('code');
u.className = 'local-cache__url';
u.textContent = req.method + ' ' + req.url.replace(location.origin, '');
li.appendChild(u);
if (res) {
var fields = document.createElement('span');
fields.className = 'local-cache__fields';
var bits = [];
bits.push('type: ' + res.type);
var lm = res.headers.get('last-modified');
if (lm) bits.push('last-modified: ' + lm);
var et = res.headers.get('etag');
if (et) bits.push('etag: ' + et);
var cl = res.headers.get('content-length');
if (cl) bits.push('content-length: ' + cl);
fields.textContent = bits.join(' · ');
li.appendChild(fields);
}
return li;
});
}));
});
});
}
function collectPrefs() {
var rows = [];
var lcl = safeStorage('local');
var sess = safeStorage('session');
LOCAL_KEYS.forEach(function (spec) {
var storage = spec.storage === 'session' ? sess : lcl;
if (!storage) return;
if (spec.prefix) {
for (var i = 0; i < storage.length; i++) {
var k = storage.key(i);
if (!k || k.indexOf(spec.key) !== 0) continue;
rows.push({ key: k, value: storage.getItem(k), storage: spec.storage });
}
} else {
var v = storage.getItem(spec.key);
if (v !== null) rows.push({ key: spec.key, value: v, storage: spec.storage });
}
});
return rows;
}
function renderPrefs() {
state.prefs = collectPrefs();
var lcl = safeStorage('local');
var lang = lcl ? (lcl.getItem('tp-last-edition') || '') : '';
var theme = lcl ? (lcl.getItem('tp-theme') || '') : '';
setField('pref.language', languageLabel(lang));
setField('pref.appearance', appearanceLabel(theme));
var meta = readJSON(lcl, 'tp-sw-meta') || {};
var firstVisit = meta.installedAt ? formatDay(meta.installedAt) : 'No record yet';
var lastUpdate = meta.lastCheckedAt ? formatDay(meta.lastCheckedAt) : (meta.activatedAt ? formatDay(meta.activatedAt) : 'No record yet');
setField('pref.first_visit', firstVisit);
setField('pref.last_update', lastUpdate);
$$('[data-local-set-lang]').forEach(function (b) {
var match = b.getAttribute('data-local-set-lang') === lang;
b.classList.toggle('is-current', match);
});
$$('[data-local-set-theme]').forEach(function (b) {
var match = b.getAttribute('data-local-set-theme') === theme;
b.classList.toggle('is-current', match);
});
var raw = field('raw.json');
if (raw) {
var dump = {
localStorage: {},
sessionStorage: {},
cacheNames: state.caches.map(function (c) { return c.name; })
};
state.prefs.forEach(function (r) {
var bucket = r.storage === 'session' ? 'sessionStorage' : 'localStorage';
dump[bucket][r.key] = r.value;
});
var code = raw.querySelector('code');
(code || raw).textContent = JSON.stringify(dump, null, 2);
}
}
function wireConfirm(btn, run) {
var armed = false;
var label = btn.textContent;
var confirmLabel = btn.getAttribute('data-confirm-label') || 'Tap again to confirm';
var timer = null;
btn.addEventListener('click', function (e) {
e.preventDefault();
if (armed) {
clearTimeout(timer);
btn.textContent = label;
armed = false;
btn.classList.remove('is-armed');
return run();
}
btn.textContent = confirmLabel;
btn.classList.add('is-armed');
armed = true;
timer = setTimeout(function () {
btn.textContent = label;
btn.classList.remove('is-armed');
armed = false;
}, 4000);
});
}
function actionCheckUpdate() {
if (!state.sw || !state.sw.update) return;
var lcl = safeStorage('local');
state.sw.update().then(function () {
var meta = readJSON(lcl, 'tp-sw-meta') || {};
meta.lastUpdateCheckAt = new Date().toISOString();
try { lcl.setItem('tp-sw-meta', JSON.stringify(meta)); } catch (_) {}
refresh();
}).catch(refresh);
}
function actionSkipWaiting() {
if (!state.sw || !state.sw.waiting) return;
try { state.sw.waiting.postMessage({ type: 'SKIP_WAITING' }); } catch (_) {}
setTimeout(function () { location.reload(); }, 200);
}
function actionResetCache() {
var jobs = [];
if ('serviceWorker' in navigator) {
jobs.push(navigator.serviceWorker.getRegistrations().then(function (rs) {
return Promise.all((rs || []).map(function (r) { return r.unregister(); }));
}).catch(function () { return null; }));
}
if ('caches' in window) {
jobs.push(caches.keys().then(function (ns) {
return Promise.all((ns || []).map(function (n) { return caches.delete(n); }));
}).catch(function () { return null; }));
}
Promise.all(jobs).then(refresh);
}
function actionReload() { location.reload(); }
function actionClearPreferences() {
var lcl = safeStorage('local');
var sess = safeStorage('session');
LOCAL_KEYS.forEach(function (spec) {
var storage = spec.storage === 'session' ? sess : lcl;
if (!storage) return;
if (spec.prefix) {
var rm = [];
for (var i = 0; i < storage.length; i++) {
var k = storage.key(i);
if (k && k.indexOf(spec.key) === 0) rm.push(k);
}
rm.forEach(function (k) { try { storage.removeItem(k); } catch (_) {} });
} else {
try { storage.removeItem(spec.key); } catch (_) {}
}
});
try { delete document.documentElement.dataset.theme; } catch (_) {}
refresh();
}
function actionFullReset() {
actionClearPreferences();
actionResetCache();
setTimeout(function () { location.href = '/'; }, 400);
}
function actionExportJson() {
var data = JSON.stringify(buildDiagnostics(), null, 2);
var blob = new Blob([data], { type: 'application/json' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = 'trentpower-local-diagnostics.json';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setTimeout(function () { URL.revokeObjectURL(url); }, 1000);
}
function actionCopyJson(btn) {
var data = JSON.stringify(buildDiagnostics(), null, 2);
copyText(btn, data);
}
function actionCopyFingerprint(btn) {
var sha = document.body.dataset.sourceSha256Short || '';
if (!sha) return;
copyText(btn, sha);
}
function copyText(btn, text) {
var label = btn.textContent;
function done() {
btn.textContent = 'Copied · just now';
btn.classList.add('is-acknowledging');
setTimeout(function () {
btn.textContent = label;
btn.classList.remove('is-acknowledging');
}, 1800);
}
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(done, done);
} else {
try {
var ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed'; ta.style.left = '-9999px';
document.body.appendChild(ta); ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
done();
} catch (_) {}
}
}
function setLanguage(value) {
var lcl = safeStorage('local');
if (!lcl) return;
try {
if (value) lcl.setItem('tp-last-edition', value);
else lcl.removeItem('tp-last-edition');
} catch (_) {}
flashSaved('language');
refresh();
}
function setAppearance(value) {
var lcl = safeStorage('local');
if (!lcl) return;
try {
if (value) {
lcl.setItem('tp-theme', value);
document.documentElement.dataset.theme = value;
} else {
lcl.removeItem('tp-theme');
try { delete document.documentElement.dataset.theme; } catch (_) {}
}
} catch (_) {}
flashSaved('appearance');
refresh();
}
function flashSaved(name) {
var el = document.querySelector('[data-local-saved="' + name + '"]');
if (!el) return;
el.hidden = false;
setTimeout(function () { el.hidden = true; }, 1800);
}
function collectCaches() {
if (!('caches' in window)) return Promise.resolve([]);
return caches.keys().then(function (names) {
return Promise.all((names || []).map(function (n) {
return caches.open(n).then(function (c) {
return c.keys().then(function (rs) {
return { name: n, count: rs.length, edition: parseEditionFromCacheName(n) };
});
}).catch(function () {
return { name: n, count: 0, edition: parseEditionFromCacheName(n) };
});
}));
}).catch(function () { return []; });
}
function collectSW() {
if (!('serviceWorker' in navigator)) return Promise.resolve(null);
return navigator.serviceWorker.getRegistration().catch(function () { return null; });
}
function collectEstimate() {
if (!navigator.storage || !navigator.storage.estimate) return Promise.resolve(null);
return navigator.storage.estimate().catch(function () { return null; });
}
function refresh() {
return Promise.all([collectSW(), collectCaches(), collectEstimate()]).then(function (r) {
state.sw = r[0];
state.caches = r[1] || [];
state.cachedEdition = state.caches.length ? state.caches[0].edition : null;
state.cachedBuild = state.caches.length ? parseBuildFromCacheName(state.caches[0].name) : null;
state.storageEstimate = r[2];
state.lastCheckedAt = new Date().toISOString();
var lcl = safeStorage('local');
if (lcl) {
try {
var meta = readJSON(lcl, 'tp-sw-meta') || {};
meta.lastCheckedAt = state.lastCheckedAt;
lcl.setItem('tp-sw-meta', JSON.stringify(meta));
} catch (_) {}
}
renderStrip();
renderPubState();
renderPrefs();
window.TP_LOCAL_DIAGNOSTICS = buildDiagnostics();
});
}
function init() {
$$('[data-local-action]').forEach(function (btn) {
var act = btn.getAttribute('data-local-action');
var handler;
switch (act) {
case 'check-update': handler = actionCheckUpdate; break;
case 'skip-waiting': handler = actionSkipWaiting; break;
case 'reset-cache': handler = actionResetCache; break;
case 'reload': handler = actionReload; break;
case 'clear-preferences': handler = actionClearPreferences; break;
case 'full-reset': handler = actionFullReset; break;
case 'export-json': handler = actionExportJson; break;
case 'copy-json': handler = function () { actionCopyJson(btn); }; break;
case 'copy-fingerprint': handler = function () { actionCopyFingerprint(btn); }; break;
default: return;
}
if (btn.hasAttribute('data-confirm-required')) wireConfirm(btn, handler);
else btn.addEventListener('click', function (e) { e.preventDefault(); handler(); });
});
$$('[data-local-set-lang]').forEach(function (b) {
b.addEventListener('click', function (e) { e.preventDefault(); setLanguage(b.getAttribute('data-local-set-lang')); });
});
$$('[data-local-set-theme]').forEach(function (b) {
b.addEventListener('click', function (e) { e.preventDefault(); setAppearance(b.getAttribute('data-local-set-theme')); });
});
refresh();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
