# Bug Brief: JSON Rendering in Dashboard Cards **Status:** 🔍 Identified — Ready for Daedalus fix **Severity:** Medium — Cards show raw JSON instead of rendered HTML **Assignee:** Daedalus 🎨 --- ## The Problem Dashboard cards intermittently display **raw JSON** instead of rendered HTML content. This happens when: 1. **HTMX receives JSON** but the target element's `innerHTML` gets the raw string instead of parsed/rendered content 2. **JavaScript renderer fails** silently and falls back to showing the response text 3. **API response structure mismatches** what the frontend expects --- ## Architecture Analysis ### Current Data Flow ``` ┌─────────────┐ hx-get ┌─────────────┐ │ Dashboard │ ───────────────→│ /api/today │ │ (cards) │ (JSON) │ (FastAPI) │ └─────────────┘ └─────────────┘ │ │ │ JSON.parse() │ │ + renderCalendar() │ │ + renderWeather() │ │ + renderHealth() │ │ + renderEvents() │ ↓ │ ┌─────────────┐ │ │ card-body │ ←─── innerHTML ─────┘ │ (content) │ └─────────────┘ ``` ### The Issue: Mixed Patterns The dashboard has **two competing approaches**: | Card | Pattern | API | Status | |------|---------|-----|--------| | Calendar | HTMX + JS renderer | `/api/today` JSON | ✅ Working | | Weather | HTMX + JS renderer | `/api/today` JSON | ✅ Working | | Health | HTMX + JS renderer | `/api/today` JSON | ✅ Working | | **Event Graph** | HTMX + JS renderer | `/api/today` JSON | ⚠️ **Buggy** | | **IMAP Proxy** | HTMX direct HTML | `/api/public/imap/dashboard` HTML | ✅ Working | --- ## Root Cause: Event Graph Card ### Problem 1: Double Targeting The `events-card` and `health-card` **both target `/api/today`**: ```html
``` Both fire on the same endpoint, but the **JavaScript only handles `health-content`** target for events: ```javascript // In htmx:afterRequest handler: if (targetId === 'health-content') { document.getElementById('health-content').innerHTML = renderHealth(data.health); // Also render events from the same /api/today response if (data.events) { document.getElementById('events-content').innerHTML = renderEvents(data); } } ``` **Bug:** When the `events-card` fires its own HTMX request, the `targetId` is `events-content`, but the handler only renders events when `targetId === 'health-content'`. The `events-content` request falls through unhandled — potentially showing raw JSON. ### Problem 2: Missing `events` Key in API Response The `/api/today` endpoint returns: ```json { "calendar": {...}, "weather": {...}, "health": {...}, "generated_at": "..." } ``` **No `events` key!** The frontend checks `if (data.events)` but the backend doesn't include it. The Event Graph card tries to render from `/api/today` but the data isn't there. ### Problem 3: Events API is Separate The Event Graph data comes from a **different endpoint**: - `/api/today` — calendar, weather, health - `/api/events-dashboard` — Event Graph data (separate!) But the Event Graph card is wired to `/api/today`: ```html
``` It should be wired to `/api/events-dashboard`. --- ## Fix Required ### Option A: Unified API (Recommended) **Backend change** — Add `events` to `/api/today`: ```python @router.get("/api/today") async def api_today(request: Request): # ... existing code ... events_data = await _get_event_graph() return { "timestamp": datetime.now().isoformat(), "calendar": calendar_data, "weather": weather_data, "health": health_data, "events": events_data, # ← ADD THIS "meta": {...} } ``` **Frontend change** — Fix the HTMX handler: ```javascript document.body.addEventListener('htmx:afterRequest', function(event) { if (event.detail.failed) return; try { const data = JSON.parse(event.detail.xhr.responseText); const targetId = event.detail.target.id; // Handle each card independently if (targetId === 'calendar-content' || targetId === 'events-content' || targetId === 'weather-content' || targetId === 'health-content') { // All cards use /api/today, parse once and render all if (data.calendar) { document.getElementById('calendar-content').innerHTML = renderCalendar(data.calendar); } if (data.weather) { document.getElementById('weather-content').innerHTML = renderWeather(data.weather); } if (data.health) { document.getElementById('health-content').innerHTML = renderHealth(data.health); } if (data.events) { document.getElementById('events-content').innerHTML = renderEvents(data); } } // Update footer const refreshEl = document.getElementById('footer-refresh'); if (refreshEl) refreshEl.textContent = 'Updated ' + new Date().toLocaleTimeString(...); } catch(e) { console.error('Parse error:', e); } }); ``` ### Option B: Separate Endpoints (Current Design, Fix Wiring) Change Event Graph card to use its correct endpoint: ```html
``` Add a separate handler for events: ```javascript document.body.addEventListener('htmx:afterRequest', function(event) { // ... existing handlers ... if (targetId === 'events-content') { document.getElementById('events-content').innerHTML = renderEvents(data); } }); ``` --- ## Files to Modify | File | Path | Change | |------|------|--------| | Dashboard template | `shared/project-docs/dashboard/templates/index.html` | Fix HTMX endpoints and JS handlers | | Dashboard router | `workspace-socrates/hoffdesk-api/dashboard/router.py` | Add `events` to `/api/today` response | --- ## Verification Steps 1. Load dashboard at `http://localhost:8001/` 2. Check each card renders HTML (not JSON) 3. Verify Event Graph shows events or "No upcoming events" 4. Verify Calendar shows "Nothing scheduled today 🎉" or events 5. Verify Weather shows temperature and forecast 6. Verify Health shows system status 7. Verify IMAP shows connection status --- ## Related Issues - **CSS polish** — `shared/project-docs/dashboard/CSS-POLISH-DELIVERY.md` - **System State handoff** — `shared/project-docs/dashboard/SYSTEM-STATE-HANDOFF.md` - **Jinja2 handoff** — `shared/project-docs/dashboard/JINJA2-HANDOFF.md` --- *Prepared by Wadsworth 📋 | 2026-04-29 01:53 UTC*