my personal site
0
fork

Configure Feed

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

fix(gymtracker): admin routing + DOM mount compat; KV single-ad shape

- Normalize pathnames (trailing slashes) so /api/admin/ads/ hits the Worker
- adsArrayFromKvJson: accept one ad object stored under KV key ads
- renderAdCards: clear #adCards then appendChild(fragment); avoid replaceChildren
quirks on some WebKit builds; guard missing #adCards

+30 -9
+7 -2
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: 20260324c-atomic-render --> 9 + <!-- admin-build: 20260324d-append-fragment --> 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"> ··· 1016 1016 } 1017 1017 1018 1018 function renderAdCards() { 1019 + if (!adCards) { 1020 + console.error('Admin: #adCards missing from DOM'); 1021 + return; 1022 + } 1019 1023 const adsHeader = document.getElementById('adsHeader'); 1020 1024 if (adsHeader) adsHeader.hidden = scheduledAds.length === 0; 1021 1025 const root = document.createDocumentFragment(); ··· 1096 1100 section.appendChild(cardWrap); 1097 1101 root.appendChild(section); 1098 1102 }); 1099 - adCards.replaceChildren(root); 1103 + adCards.innerHTML = ''; 1104 + adCards.appendChild(root); 1100 1105 } catch (err) { 1101 1106 console.error('Admin: renderAdCards failed', err); 1102 1107 showErrorBanner('Could not render ad list', (err && err.message ? String(err.message) : 'Unknown error') + ' — try Refresh or hard-reload the page.');
+23 -7
gymtracker/src/index.ts
··· 233 233 return true; 234 234 } 235 235 236 - /** Normalize KV payload: stored as `AdConfig[]` or `{ ads: AdConfig[] }` (legacy / mistaken shape). */ 236 + /** Normalize KV payload: array, `{ ads: [...] }`, or a single ad object under the `ads` key. */ 237 237 function adsArrayFromKvJson(parsed: unknown): AdConfig[] { 238 238 if (Array.isArray(parsed)) { 239 239 return parsed as AdConfig[]; ··· 246 246 ) { 247 247 return (parsed as { ads: AdConfig[] }).ads; 248 248 } 249 + if ( 250 + parsed && 251 + typeof parsed === "object" && 252 + !Array.isArray(parsed) && 253 + typeof (parsed as { id?: unknown }).id === "string" && 254 + (parsed as { id: string }).id.trim() !== "" 255 + ) { 256 + return [parsed as AdConfig]; 257 + } 249 258 return []; 259 + } 260 + 261 + /** `/admin`, `/admin/`, `/api/admin/ads/` → stable paths for routing. */ 262 + function normalizedPathname(pathname: string): string { 263 + const trimmed = pathname.replace(/\/+$/, ""); 264 + return trimmed === "" ? "/" : trimmed; 250 265 } 251 266 252 267 async function getAdsArray(kv: KVNamespace): Promise<AdConfig[]> { ··· 438 453 _ctx: ExecutionContext 439 454 ): Promise<Response> { 440 455 const url = new URL(request.url); 456 + const path = normalizedPathname(url.pathname); 441 457 442 - if (url.pathname === "/admin" || url.pathname === "/admin/") { 458 + if (path === "/admin") { 443 459 if (!hasAccessAuth(request)) { 444 460 const helpHtml = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Sign in required</title></head><body style="font-family:system-ui;max-width:32rem;margin:4rem auto;padding:2rem;"> 445 461 <h1>Sign in required</h1> ··· 459 475 }); 460 476 } 461 477 462 - if (url.pathname === "/api/admin/stats") { 478 + if (path === "/api/admin/stats") { 463 479 if (!hasAccessAuth(request)) { 464 480 return jsonResponse({ error: "Unauthorized" }, 401, request); 465 481 } ··· 470 486 return jsonResponse(stats, 200, request); 471 487 } 472 488 473 - if (url.pathname === "/api/admin/ads") { 489 + if (path === "/api/admin/ads") { 474 490 if (request.method === "OPTIONS") { 475 491 const origin = request.headers.get("Origin"); 476 492 return new Response(null, { ··· 522 538 return jsonResponse({ error: "Method not allowed" }, 405, request); 523 539 } 524 540 525 - if (url.pathname === "/" || url.pathname === "/index.html") { 541 + if (path === "/" || path === "/index.html") { 526 542 return new Response(getMainLandingHtml(), { 527 543 headers: { 528 544 "Content-Type": "text/html; charset=utf-8", ··· 533 549 }); 534 550 } 535 551 536 - if (url.pathname === "/ads" || url.pathname === "/ads/") { 552 + if (path === "/ads") { 537 553 return new Response(getAdsLandingHtml(), { 538 554 headers: { 539 555 "Content-Type": "text/html; charset=utf-8", ··· 544 560 }); 545 561 } 546 562 547 - if (url.pathname !== "/api/ads") { 563 + if (path !== "/api/ads") { 548 564 const assetResponse = await env.ASSETS.fetch(request); 549 565 if (assetResponse.status !== 404) { 550 566 return assetResponse;