my personal site
0
fork

Configure Feed

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

fix(gymtracker): restore known-good ad card render flow

Revert renderAdCards to pre-regression imperative append flow that previously
rendered cards reliably in production browsers. Keep safe stats formatting and
null guard so malformed PostHog rows cannot crash card rendering.

+77 -87
+77 -87
gymtracker/src/admin-html.ts
··· 6 6 <meta http-equiv="Cache-Control" content="no-store, no-cache, must-revalidate, max-age=0"> 7 7 <meta http-equiv="Pragma" content="no-cache"> 8 8 <meta name="viewport" content="width=device-width, initial-scale=1"> 9 - <!-- admin-build: 20260324e-vendor-flatpickr --> 9 + <!-- admin-build: 20260324f-revert-render-flow --> 10 10 <title>Gym Tracker Ads Admin</title> 11 11 <link rel="icon" href="/favicon/favicon.ico" sizes="any"> 12 12 <link rel="icon" href="/favicon/favicon-32x32.png" type="image/png" sizes="32x32"> ··· 303 303 .status-filter.active { border-color: var(--accent); color: var(--accent); } 304 304 .ad-cards-group:last-child { margin-bottom: 0; } 305 305 .ad-cards-group-label { font-size: var(--text-xs); color: var(--muted); letter-spacing: 0.08em; text-transform: uppercase; margin-bottom: 2px; } 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%; } 306 + .ad-card-wrap { position: relative; min-width: 0; height: 100%; } 307 + .ad-card-wrap .ad-card { position: relative; min-width: 0; min-height: 80px; width: 100%; height: 100%; } 308 308 .ad-card-action { 309 309 position: absolute; top: 8px; right: 8px; 310 310 padding: 8px; display: flex; align-items: center; justify-content: center; ··· 1016 1016 } 1017 1017 1018 1018 function renderAdCards() { 1019 - if (!adCards) { 1020 - console.error('Admin: #adCards missing from DOM'); 1021 - return; 1022 - } 1023 1019 const adsHeader = document.getElementById('adsHeader'); 1024 1020 if (adsHeader) adsHeader.hidden = scheduledAds.length === 0; 1025 - const root = document.createDocumentFragment(); 1026 - try { 1027 - const newAdGroup = document.createElement('div'); 1028 - newAdGroup.className = 'ad-cards-group ad-cards-group-new'; 1029 - const newAdWrap = document.createElement('div'); 1030 - newAdWrap.className = 'ad-cards'; 1031 - const newAdCardWrap = document.createElement('div'); 1032 - newAdCardWrap.className = 'ad-card-wrap'; 1033 - const newAdBtn = document.createElement('button'); 1034 - newAdBtn.type = 'button'; 1035 - newAdBtn.id = 'newAdBtn'; 1036 - newAdBtn.className = 'ad-card new-ad-card'; 1037 - newAdBtn.innerHTML = '<span class="new-ad-plus">+</span><span class="new-ad-label">New ad</span>'; 1038 - newAdBtn.addEventListener('click', goToNewAd); 1039 - newAdCardWrap.appendChild(newAdBtn); 1040 - newAdWrap.appendChild(newAdCardWrap); 1041 - newAdGroup.appendChild(newAdWrap); 1042 - root.appendChild(newAdGroup); 1021 + adCards.innerHTML = ''; 1043 1022 1044 - const filtered = filterAdsForDisplay(); 1045 - const groups = { live: [], scheduled: [], paused: [], ended: [] }; 1046 - filtered.forEach(({ ad, i }) => { 1047 - const status = adStatus(ad); 1048 - if (groups[status]) groups[status].push({ ad, i }); 1049 - }); 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 + adCards.appendChild(newAdGroup); 1050 1039 1051 - const order = ['live', 'scheduled', 'paused', 'ended']; 1052 - order.forEach((status) => { 1053 - const list = groups[status]; 1054 - if (list.length === 0) return; 1055 - list.sort((a, b) => sortKeyForGroup(a.ad, status) - sortKeyForGroup(b.ad, status)); 1056 - const section = document.createElement('div'); 1057 - section.className = 'ad-cards-group'; 1058 - section.innerHTML = '<div class="ad-cards-group-label">' + status + '</div>'; 1059 - const cardWrap = document.createElement('div'); 1060 - cardWrap.className = 'ad-cards'; 1061 - list.forEach(({ ad, i }) => { 1062 - const s = adStatus(ad); 1063 - const stats = getAdStats(ad.id); 1064 - const statsLine = stats 1065 - ? '<span class="ad-card-stats">' + formatCompact(stats.impressions) + ' imp · ' + formatCompact(stats.clicks) + ' clk · ' + formatCtrPct(stats) + '% CTR</span>' 1066 - : ''; 1067 - const card = document.createElement('div'); 1068 - card.className = 'ad-card-wrap'; 1069 - const canToggle = s === 'live' || s === 'paused'; 1070 - const stateLabel = s === 'paused' ? 'Paused' : 'Live'; 1071 - const stateIcon = s === 'paused' ? '⏸' : '▶'; 1072 - const actionIcon = s === 'paused' ? '▶' : '⏸'; 1073 - 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>' : ''; 1074 - const chipHtml = (s === 'live' || s === 'paused') ? '' : '<span class="chip ' + statusClass(s) + '">' + s + '</span>'; 1075 - card.innerHTML = '<div role="button" tabindex="0" class="ad-card' + (selectedIndex === i ? ' selected' : '') + '" data-ad-idx="' + i + '">' + 1076 - actionHtml + 1077 - '<span class="ad-card-head">' + escapeHtml(ad.sponsor) + ' — ' + escapeHtml(ad.id) + '</span>' + 1078 - chipHtml + 1079 - '<span class="ad-card-dates">' + formatDateRange(ad) + '</span>' + 1080 - (statsLine ? statsLine : '') + 1081 - '<span class="ad-card-tier">' + (ad.tier || 'banner') + '</span>' + 1082 - '</div>'; 1083 - const cardEl = card.querySelector('.ad-card'); 1084 - if (!cardEl) { 1085 - console.warn('Admin: could not build card DOM for ad', ad.id); 1086 - return; 1087 - } 1088 - cardEl.addEventListener('click', (e) => { 1089 - if (e.target.closest('.ad-card-action')) return; 1090 - selectAd(i); 1091 - openFormOverlay('Edit: ' + (ad.sponsor || ad.id)); 1092 - }); 1093 - cardEl.addEventListener('keydown', (e) => { 1094 - if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); cardEl.click(); } 1095 - }); 1096 - const actionBtn = card.querySelector('.ad-card-action'); 1097 - if (actionBtn) actionBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); toggleAdActive(parseInt(actionBtn.dataset.adIdx, 10)); }); 1098 - cardWrap.appendChild(card); 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 + }); 1046 + 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(); } 1099 1091 }); 1100 - section.appendChild(cardWrap); 1101 - root.appendChild(section); 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); 1102 1095 }); 1103 - adCards.innerHTML = ''; 1104 - adCards.appendChild(root); 1105 - } catch (err) { 1106 - console.error('Admin: renderAdCards failed', err); 1107 - showErrorBanner('Could not render ad list', (err && err.message ? String(err.message) : 'Unknown error') + ' — try Refresh or hard-reload the page.'); 1108 - } 1096 + section.appendChild(cardWrap); 1097 + adCards.appendChild(section); 1098 + }); 1109 1099 } 1110 1100 1111 1101 function escapeHtml(s) {