From e55703ea6e4084b4461af57a54efbee5b6c95b0a Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Fri, 17 Apr 2026 12:52:41 -0400 Subject: [PATCH] fix: grant heatmap API key ['all'] permissions + add Reset button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A narrow (asset.read, asset.view) key was insufficient for the heatmap's /api/search/metadata call — the server returned an error. Request ['all'] so the key covers every endpoint the heatmap touches. Also add a Reset button next to the Collapse toggle so a stale or under-permissioned cached key can be regenerated without site-data surgery. Co-Authored-By: Claude Opus 4.7 (1M context) --- search-app/live-search.js | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/search-app/live-search.js b/search-app/live-search.js index f685f9d..5089382 100644 --- a/search-app/live-search.js +++ b/search-app/live-search.js @@ -961,9 +961,13 @@ // Get (or create) an Immich API key for the heatmap iframe so the user // isn't prompted to paste one. Cached in this origin's localStorage. - async function getHeatmapApiKey() { - const cached = localStorage.getItem('ls-heatmap-api-key'); - if (cached) return cached; + // Uses the ['all'] permission so the heatmap's /api/search/metadata and + // thumbnail fetches never 403. + async function getHeatmapApiKey(force) { + if (!force) { + const cached = localStorage.getItem('ls-heatmap-api-key'); + if (cached) return cached; + } try { const r = await fetch('/api/api-keys', { method: 'POST', @@ -971,11 +975,11 @@ credentials: 'include', body: JSON.stringify({ name: 'Heatmap iframe (auto)', - permissions: ['asset.read', 'asset.view'] + permissions: ['all'] }) }); if (!r.ok) { - console.warn('[live-search] api-key create failed', r.status); + console.warn('[live-search] api-key create failed', r.status, await r.text().catch(() => '')); return null; } const j = await r.json(); @@ -997,17 +1001,28 @@ banner.innerHTML = `
📍 Photo Locations Heatmap - +
+ + +
`; const iframe = banner.querySelector('iframe'); - const btn = banner.querySelector('button'); - btn.addEventListener('click', () => { + const toggleBtn = banner.querySelector('button[data-act="toggle"]'); + const resetBtn = banner.querySelector('button[data-act="reset"]'); + toggleBtn.addEventListener('click', () => { banner.classList.toggle('collapsed'); - btn.textContent = banner.classList.contains('collapsed') ? 'Expand' : 'Collapse'; + toggleBtn.textContent = banner.classList.contains('collapsed') ? 'Expand' : 'Collapse'; + }); + resetBtn.addEventListener('click', async () => { + localStorage.removeItem('ls-heatmap-api-key'); + resetBtn.textContent = 'Resetting…'; + const k = await getHeatmapApiKey(true); + iframe.src = HEATMAP_URL + (k ? '?apiKey=' + encodeURIComponent(k) + '&t=' + Date.now() : ''); + resetBtn.textContent = 'Reset'; }); // Append at the END so it lands below People + Places sections. container.appendChild(banner);