my personal site
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(gymtracker): bust admin cache and make ad card render atomic

- Strong HTML response Cache-Control plus CDN-Cache-Control: no-store
- Meta no-cache tags and build comment for source verification
- Build ad list in a DocumentFragment; replaceChildren only on success
so a failed PostHog re-render cannot wipe cards (stale JS / errors)
- Remove height:100% on card wrappers that could collapse grid sizing

+93 -77
+84 -76
gymtracker/src/admin-html.ts
··· 3 3 <html lang="en"> 4 4 <head> 5 5 <meta charset="utf-8"> 6 + <meta http-equiv="Cache-Control" content="no-store, no-cache, must-revalidate, max-age=0"> 7 + <meta http-equiv="Pragma" content="no-cache"> 6 8 <meta name="viewport" content="width=device-width, initial-scale=1"> 9 + <!-- admin-build: 20260324c-atomic-render --> 7 10 <title>Gym Tracker Ads Admin</title> 8 11 <link rel="icon" href="/favicon/favicon.ico" sizes="any"> 9 12 <link rel="icon" href="/favicon/favicon-32x32.png" type="image/png" sizes="32x32"> ··· 300 303 .status-filter.active { border-color: var(--accent); color: var(--accent); } 301 304 .ad-cards-group:last-child { margin-bottom: 0; } 302 305 .ad-cards-group-label { font-size: var(--text-xs); color: var(--muted); letter-spacing: 0.08em; text-transform: uppercase; margin-bottom: 2px; } 303 - .ad-card-wrap { position: relative; min-width: 0; height: 100%; } 304 - .ad-card-wrap .ad-card { position: relative; min-width: 0; min-height: 80px; width: 100%; height: 100%; } 306 + .ad-card-wrap { position: relative; min-width: 0; } 307 + .ad-card-wrap .ad-card { position: relative; min-width: 0; min-height: 80px; width: 100%; } 305 308 .ad-card-action { 306 309 position: absolute; top: 8px; right: 8px; 307 310 padding: 8px; display: flex; align-items: center; justify-content: center; ··· 1015 1018 function renderAdCards() { 1016 1019 const adsHeader = document.getElementById('adsHeader'); 1017 1020 if (adsHeader) adsHeader.hidden = scheduledAds.length === 0; 1018 - adCards.innerHTML = ''; 1021 + const root = document.createDocumentFragment(); 1022 + try { 1023 + const newAdGroup = document.createElement('div'); 1024 + newAdGroup.className = 'ad-cards-group ad-cards-group-new'; 1025 + const newAdWrap = document.createElement('div'); 1026 + newAdWrap.className = 'ad-cards'; 1027 + const newAdCardWrap = document.createElement('div'); 1028 + newAdCardWrap.className = 'ad-card-wrap'; 1029 + const newAdBtn = document.createElement('button'); 1030 + newAdBtn.type = 'button'; 1031 + newAdBtn.id = 'newAdBtn'; 1032 + newAdBtn.className = 'ad-card new-ad-card'; 1033 + newAdBtn.innerHTML = '<span class="new-ad-plus">+</span><span class="new-ad-label">New ad</span>'; 1034 + newAdBtn.addEventListener('click', goToNewAd); 1035 + newAdCardWrap.appendChild(newAdBtn); 1036 + newAdWrap.appendChild(newAdCardWrap); 1037 + newAdGroup.appendChild(newAdWrap); 1038 + root.appendChild(newAdGroup); 1019 1039 1020 - const newAdGroup = document.createElement('div'); 1021 - newAdGroup.className = 'ad-cards-group ad-cards-group-new'; 1022 - const newAdWrap = document.createElement('div'); 1023 - newAdWrap.className = 'ad-cards'; 1024 - const newAdCardWrap = document.createElement('div'); 1025 - newAdCardWrap.className = 'ad-card-wrap'; 1026 - const newAdBtn = document.createElement('button'); 1027 - newAdBtn.type = 'button'; 1028 - newAdBtn.id = 'newAdBtn'; 1029 - newAdBtn.className = 'ad-card new-ad-card'; 1030 - newAdBtn.innerHTML = '<span class="new-ad-plus">+</span><span class="new-ad-label">New ad</span>'; 1031 - newAdBtn.addEventListener('click', goToNewAd); 1032 - newAdCardWrap.appendChild(newAdBtn); 1033 - newAdWrap.appendChild(newAdCardWrap); 1034 - newAdGroup.appendChild(newAdWrap); 1035 - adCards.appendChild(newAdGroup); 1040 + const filtered = filterAdsForDisplay(); 1041 + const groups = { live: [], scheduled: [], paused: [], ended: [] }; 1042 + filtered.forEach(({ ad, i }) => { 1043 + const status = adStatus(ad); 1044 + if (groups[status]) groups[status].push({ ad, i }); 1045 + }); 1036 1046 1037 - const filtered = filterAdsForDisplay(); 1038 - const groups = { live: [], scheduled: [], paused: [], ended: [] }; 1039 - filtered.forEach(({ ad, i }) => { 1040 - const status = adStatus(ad); 1041 - if (groups[status]) groups[status].push({ ad, i }); 1042 - }); 1043 - 1044 - const order = ['live', 'scheduled', 'paused', 'ended']; 1045 - order.forEach((status) => { 1046 - const list = groups[status]; 1047 - if (list.length === 0) return; 1048 - list.sort((a, b) => sortKeyForGroup(a.ad, status) - sortKeyForGroup(b.ad, status)); 1049 - const section = document.createElement('div'); 1050 - section.className = 'ad-cards-group'; 1051 - section.innerHTML = '<div class="ad-cards-group-label">' + status + '</div>'; 1052 - const cardWrap = document.createElement('div'); 1053 - cardWrap.className = 'ad-cards'; 1054 - list.forEach(({ ad, i }) => { 1055 - const s = adStatus(ad); 1056 - const stats = getAdStats(ad.id); 1057 - const statsLine = stats 1058 - ? '<span class="ad-card-stats">' + formatCompact(stats.impressions) + ' imp · ' + formatCompact(stats.clicks) + ' clk · ' + formatCtrPct(stats) + '% CTR</span>' 1059 - : ''; 1060 - const card = document.createElement('div'); 1061 - card.className = 'ad-card-wrap'; 1062 - const canToggle = s === 'live' || s === 'paused'; 1063 - const stateLabel = s === 'paused' ? 'Paused' : 'Live'; 1064 - const stateIcon = s === 'paused' ? '⏸' : '▶'; 1065 - const actionIcon = s === 'paused' ? '▶' : '⏸'; 1066 - const actionHtml = canToggle ? '<button type="button" class="ad-card-action ad-card-action-' + s + '" data-ad-idx="' + i + '" data-action="toggle" title="' + stateLabel + ' — click to ' + (s === 'paused' ? 'resume' : 'pause') + '" aria-label="' + stateLabel + '"><span class="ad-card-action-icon-wrap"><span class="ad-card-action-icon ad-card-action-icon-state">' + stateIcon + '</span><span class="ad-card-action-icon ad-card-action-icon-action">' + actionIcon + '</span></span></button>' : ''; 1067 - const chipHtml = (s === 'live' || s === 'paused') ? '' : '<span class="chip ' + statusClass(s) + '">' + s + '</span>'; 1068 - card.innerHTML = '<div role="button" tabindex="0" class="ad-card' + (selectedIndex === i ? ' selected' : '') + '" data-ad-idx="' + i + '">' + 1069 - actionHtml + 1070 - '<span class="ad-card-head">' + escapeHtml(ad.sponsor) + ' — ' + escapeHtml(ad.id) + '</span>' + 1071 - chipHtml + 1072 - '<span class="ad-card-dates">' + formatDateRange(ad) + '</span>' + 1073 - (statsLine ? statsLine : '') + 1074 - '<span class="ad-card-tier">' + (ad.tier || 'banner') + '</span>' + 1075 - '</div>'; 1076 - const cardEl = card.querySelector('.ad-card'); 1077 - if (!cardEl) { 1078 - console.warn('Admin: could not build card DOM for ad', ad.id); 1079 - return; 1080 - } 1081 - cardEl.addEventListener('click', (e) => { 1082 - if (e.target.closest('.ad-card-action')) return; 1083 - selectAd(i); 1084 - openFormOverlay('Edit: ' + (ad.sponsor || ad.id)); 1085 - }); 1086 - cardEl.addEventListener('keydown', (e) => { 1087 - if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); cardEl.click(); } 1047 + const order = ['live', 'scheduled', 'paused', 'ended']; 1048 + order.forEach((status) => { 1049 + const list = groups[status]; 1050 + if (list.length === 0) return; 1051 + list.sort((a, b) => sortKeyForGroup(a.ad, status) - sortKeyForGroup(b.ad, status)); 1052 + const section = document.createElement('div'); 1053 + section.className = 'ad-cards-group'; 1054 + section.innerHTML = '<div class="ad-cards-group-label">' + status + '</div>'; 1055 + const cardWrap = document.createElement('div'); 1056 + cardWrap.className = 'ad-cards'; 1057 + list.forEach(({ ad, i }) => { 1058 + const s = adStatus(ad); 1059 + const stats = getAdStats(ad.id); 1060 + const statsLine = stats 1061 + ? '<span class="ad-card-stats">' + formatCompact(stats.impressions) + ' imp · ' + formatCompact(stats.clicks) + ' clk · ' + formatCtrPct(stats) + '% CTR</span>' 1062 + : ''; 1063 + const card = document.createElement('div'); 1064 + card.className = 'ad-card-wrap'; 1065 + const canToggle = s === 'live' || s === 'paused'; 1066 + const stateLabel = s === 'paused' ? 'Paused' : 'Live'; 1067 + const stateIcon = s === 'paused' ? '⏸' : '▶'; 1068 + const actionIcon = s === 'paused' ? '▶' : '⏸'; 1069 + const actionHtml = canToggle ? '<button type="button" class="ad-card-action ad-card-action-' + s + '" data-ad-idx="' + i + '" data-action="toggle" title="' + stateLabel + ' — click to ' + (s === 'paused' ? 'resume' : 'pause') + '" aria-label="' + stateLabel + '"><span class="ad-card-action-icon-wrap"><span class="ad-card-action-icon ad-card-action-icon-state">' + stateIcon + '</span><span class="ad-card-action-icon ad-card-action-icon-action">' + actionIcon + '</span></span></button>' : ''; 1070 + const chipHtml = (s === 'live' || s === 'paused') ? '' : '<span class="chip ' + statusClass(s) + '">' + s + '</span>'; 1071 + card.innerHTML = '<div role="button" tabindex="0" class="ad-card' + (selectedIndex === i ? ' selected' : '') + '" data-ad-idx="' + i + '">' + 1072 + actionHtml + 1073 + '<span class="ad-card-head">' + escapeHtml(ad.sponsor) + ' — ' + escapeHtml(ad.id) + '</span>' + 1074 + chipHtml + 1075 + '<span class="ad-card-dates">' + formatDateRange(ad) + '</span>' + 1076 + (statsLine ? statsLine : '') + 1077 + '<span class="ad-card-tier">' + (ad.tier || 'banner') + '</span>' + 1078 + '</div>'; 1079 + const cardEl = card.querySelector('.ad-card'); 1080 + if (!cardEl) { 1081 + console.warn('Admin: could not build card DOM for ad', ad.id); 1082 + return; 1083 + } 1084 + cardEl.addEventListener('click', (e) => { 1085 + if (e.target.closest('.ad-card-action')) return; 1086 + selectAd(i); 1087 + openFormOverlay('Edit: ' + (ad.sponsor || ad.id)); 1088 + }); 1089 + cardEl.addEventListener('keydown', (e) => { 1090 + if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); cardEl.click(); } 1091 + }); 1092 + const actionBtn = card.querySelector('.ad-card-action'); 1093 + if (actionBtn) actionBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); toggleAdActive(parseInt(actionBtn.dataset.adIdx, 10)); }); 1094 + cardWrap.appendChild(card); 1088 1095 }); 1089 - const actionBtn = card.querySelector('.ad-card-action'); 1090 - if (actionBtn) actionBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); toggleAdActive(parseInt(actionBtn.dataset.adIdx, 10)); }); 1091 - cardWrap.appendChild(card); 1096 + section.appendChild(cardWrap); 1097 + root.appendChild(section); 1092 1098 }); 1093 - section.appendChild(cardWrap); 1094 - adCards.appendChild(section); 1095 - }); 1099 + adCards.replaceChildren(root); 1100 + } catch (err) { 1101 + console.error('Admin: renderAdCards failed', err); 1102 + showErrorBanner('Could not render ad list', (err && err.message ? String(err.message) : 'Unknown error') + ' — try Refresh or hard-reload the page.'); 1103 + } 1096 1104 } 1097 1105 1098 1106 function escapeHtml(s) {
+9 -1
gymtracker/src/index.ts
··· 52 52 Pragma: "no-cache", 53 53 }; 54 54 55 + /** Admin HTML must not be cached at browser or Cloudflare edge (stale inline JS breaks the dashboard). */ 56 + const ADMIN_HTML_CACHE_HEADERS: Record<string, string> = { 57 + "Cache-Control": "private, no-store, no-cache, max-age=0, must-revalidate", 58 + Pragma: "no-cache", 59 + Expires: "0", 60 + "CDN-Cache-Control": "no-store", 61 + }; 62 + 55 63 function corsHeaders(origin: string | null): Record<string, string> { 56 64 const allow = 57 65 origin && ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0]; ··· 446 454 return new Response(getAdminHtml(), { 447 455 headers: { 448 456 "Content-Type": "text/html; charset=utf-8", 449 - "Cache-Control": "no-store", 457 + ...ADMIN_HTML_CACHE_HEADERS, 450 458 }, 451 459 }); 452 460 }