feat: auto-provision heatmap API key + move banner below sections

The heatmap iframe now requests an Immich API key via /api/api-keys
(asset.read, asset.view) on first load, caches it in localStorage,
and passes it via ?apiKey= so the user doesn't have to paste one.

Banner moved to the bottom of the /explore scroll container, below
People and Places.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-04-17 12:47:18 -04:00
parent 7be768e2db
commit b4e801e17f
1 changed files with 40 additions and 3 deletions

View File

@ -910,7 +910,38 @@
}[c]));
}
function injectHeatmapBanner(container) {
// 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;
try {
const r = await fetch('/api/api-keys', {
method: 'POST',
headers: getAuthHeaders(),
credentials: 'include',
body: JSON.stringify({
name: 'Heatmap iframe (auto)',
permissions: ['asset.read', 'asset.view']
})
});
if (!r.ok) {
console.warn('[live-search] api-key create failed', r.status);
return null;
}
const j = await r.json();
const secret = j.secret || (j.apiKey && j.apiKey.secret);
if (secret) {
localStorage.setItem('ls-heatmap-api-key', secret);
return secret;
}
} catch (e) {
console.warn('[live-search] api-key create error', e);
}
return null;
}
async function injectHeatmapBanner(container) {
if (container.querySelector('.ls-heatmap-banner')) return;
const banner = document.createElement('div');
banner.className = 'ls-heatmap-banner';
@ -920,16 +951,22 @@
<button type="button">Collapse</button>
</div>
<iframe class="ls-heatmap-frame"
src="${HEATMAP_URL}"
referrerpolicy="no-referrer"
allow="geolocation"></iframe>
`;
const iframe = banner.querySelector('iframe');
const btn = banner.querySelector('button');
btn.addEventListener('click', () => {
banner.classList.toggle('collapsed');
btn.textContent = banner.classList.contains('collapsed') ? 'Expand' : 'Collapse';
});
container.insertBefore(banner, container.firstChild);
// Append at the END so it lands below People + Places sections.
container.appendChild(banner);
// Resolve API key then set iframe src. The heatmap app reads ?apiKey
// and seeds its own localStorage, skipping the login prompt.
const key = await getHeatmapApiKey();
iframe.src = HEATMAP_URL + (key ? '?apiKey=' + encodeURIComponent(key) : '');
}
async function enhanceSection(section, kind) {