my personal site
0
fork

Configure Feed

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

fix(gymtracker): safe CTR and impression formatting in admin ad cards

PostHog stats or partial rows could leave ctr_percent undefined, which
threw during renderAdCards and hid all cards after group headers.

+18 -5
+18 -5
gymtracker/src/admin-html.ts
··· 889 889 } 890 890 891 891 function formatCompact(n) { 892 - if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M'; 893 - if (n >= 1000) return (n / 1000).toFixed(1) + 'k'; 894 - return String(n); 892 + const x = Number(n); 893 + if (!Number.isFinite(x)) return '0'; 894 + if (x >= 1000000) return (x / 1000000).toFixed(1) + 'M'; 895 + if (x >= 1000) return (x / 1000).toFixed(1) + 'k'; 896 + return String(Math.trunc(x)); 897 + } 898 + 899 + /** PostHog rows may omit or stringify ctr_percent; never throw from card render. */ 900 + function formatCtrPct(stats) { 901 + if (!stats || typeof stats !== 'object') return '0.0'; 902 + const x = Number(stats.ctr_percent); 903 + return Number.isFinite(x) ? x.toFixed(1) : '0.0'; 895 904 } 896 905 897 906 function getAdStats(adId) { ··· 915 924 box.hidden = false; 916 925 grid.innerHTML = '<div class="kpi-item"><span class="kpi-label">Impressions</span><span class="kpi-value">' + formatCompact(stats.impressions) + '</span></div>' + 917 926 '<div class="kpi-item"><span class="kpi-label">Clicks</span><span class="kpi-value">' + formatCompact(stats.clicks) + '</span></div>' + 918 - '<div class="kpi-item"><span class="kpi-label">CTR</span><span class="kpi-value">' + stats.ctr_percent.toFixed(1) + '%</span></div>'; 927 + '<div class="kpi-item"><span class="kpi-label">CTR</span><span class="kpi-value">' + formatCtrPct(stats) + '%</span></div>'; 919 928 } 920 929 921 930 async function loadPostHogStats() { ··· 1046 1055 const s = adStatus(ad); 1047 1056 const stats = getAdStats(ad.id); 1048 1057 const statsLine = stats 1049 - ? '<span class="ad-card-stats">' + formatCompact(stats.impressions) + ' imp · ' + formatCompact(stats.clicks) + ' clk · ' + stats.ctr_percent.toFixed(1) + '% CTR</span>' 1058 + ? '<span class="ad-card-stats">' + formatCompact(stats.impressions) + ' imp · ' + formatCompact(stats.clicks) + ' clk · ' + formatCtrPct(stats) + '% CTR</span>' 1050 1059 : ''; 1051 1060 const card = document.createElement('div'); 1052 1061 card.className = 'ad-card-wrap'; ··· 1065 1074 '<span class="ad-card-tier">' + (ad.tier || 'banner') + '</span>' + 1066 1075 '</div>'; 1067 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 + } 1068 1081 cardEl.addEventListener('click', (e) => { 1069 1082 if (e.target.closest('.ad-card-action')) return; 1070 1083 selectAd(i);