Appearance
Reporting Dashboard - Humology Integration Progress
Master document for tracking reporting dashboard improvements, with focus on Bunny Analytics integration and UX improvements for Humology's workflow.
Done
- [x] Group-level checkboxes - Creative groups have three-state checkboxes (unchecked/partial/checked) with toggle-all behavior
- [x] Selection count on groups - Group headers show "X/Y" when partially selected, plain count when none
- [x] Impression counts in picker - Lazy-loaded, shows lifetime impressions per creative and group, formatted K/M
- [x] "Active only" filter - Filters to creatives with >0 impressions, waits for data to load, auto-expands
- [x] Bunny standard metrics in MetricPicker - "Bunny Analytics" section with standard metrics
- [x] CSV parser: standard column extraction - STANDARD_COL_TO_KEY mapping for standard Bunny columns
- [x] Fix: standard Bunny metrics sent as custom_metric[] params - Separated BUNNY_STANDARD_KEYS from custom API keys
- [x] Fix: 204 No Content crash - Guard in bunnyCsvParser for undefined CSV response
- [x] Dynamic widget layout - ReportViewer builds widgets from selected metrics, not hardcoded views
- [x] Chart tooltip UX -
interaction: { mode: 'index', intersect: false }for hover anywhere - [x] Bunny metric codes in URL - BUNNY_METRICS constant, b_* codes, proper encode/decode
- [x] Chart zoom/scroll - chartjs-plugin-zoom with useChartZoom composable, ChartZoomControls component
- [x] Bunny "no data" messaging - BunnyNoDataState component, hasBunnyNoData in composables, per-card "No data"
- [x] Fix: Interaction Rate 1256.9% - Computed metrics (interaction-rate, ctr) were multiplied by 100 twice. Now return ratio, formatPercentage handles display.
- [x] Fix: Viewable Impressions and Seconds in View = 0 - Wrong key names (dashes vs underscores). Also: aggregation API requires
metric[]=basenot individual keys. useCounts and useBuckets now always request 'base'. - [x] Computed metrics infrastructure - COMPUTED_METRICS and COMPUTED_DEPENDENCIES in metricCodes.ts. useReportTotals auto-fetches dependencies and calculates derived values.
- [x] Engagement funnel improvements - Value placed outside bar with gap (no text crash). Logarithmic scale for visible sub-metrics. Percentage rate labels next to counts.
- [x] Toggle all / Clear all per metric group - "Select all" and "Clear" buttons on Standard and Bunny Analytics sections in MetricPicker
- [x] Zero-value KPI card fading - Cards with 0 values get opacity 0.4 so they don't dominate the report
- [x] Custom metric discovery - "+ Add custom metric" panel discovers per-creative custom metrics (e.g. scratch_done, scratch_start)
- [x] Trim BUNNY_METRICS to actual CSV columns - Backend CSV only has 9 metric columns (not 22). Removed 13 ghost metrics that were showing as empty cards.
- [x] Expand STANDARD_METRICS to all 18 aggregation API metrics - Added interacted, continued, creative_actions, link_clicks, header_clicks, background_clicks, reached_end, plays, watched_25/50/75/100. All available via
metric[]=base. - [x] Metric source investigation - Confirmed: aggregation API has 16 metrics, Bunny parquet has all of them + extras (clicks, seconds_total_active, custom_metrics). Backend CSV only exposes 9. During migration, use aggregation for full coverage, Bunny for extras + custom metrics.
- [x] Fix: duplicate Bunny API calls - Each WidgetCard created its own useReportTotals → N separate API calls. Refactored: WidgetGrid fetches ALL metrics + rate denominators in one useReportTotals call, passes data down via props.
- [x] Fix: Bunny KPI cards showing "0" instead of "No data" - WidgetCard now receives
bunnyNoDataprop from WidgetGrid and shows "No data" for unavailable bunny metrics. - [x] Fix: chart showing only 2 days -
findActiveRangeused absolute threshold (300) that filtered out low-volume days. Changed to> 0so all days with data are visible. - [x] Fix: chart pill buttons using wrong data source -
ALL_CHARTABLEdedup always preferred aggregation source. Now respects the report's data source: if report uses bunny metrics, chart pill buttons use bunny too. - [x] Creative ID search in ResourcePicker - Search by creative ID and name, shows
#IDnext to creative name - [x] Creative IDs on report cards - Reports list shows creative IDs (e.g.
#94281) extracted from URL - [x] Root cause: Bunny low impression counts - Logs-Parser
bunny_analyticsbranch writes flat file, backend expects hive-partitioned glob. Seebunny-parquet-mismatch.md.
TODO
Widget CRUD System (brainstorming in progress, 2026-06-29)
Design decisions made so far:
Workflow:
- MetricPicker remains the "starter" -- bulk-select metrics and creatives
- In the report view, toggle Edit mode (button, logged-in users only) to adjust
- Edit mode enables: remove individual cards (X), reorder (DnD or arrows), add new metrics (inline "+ Add metric" dropdown), change widget type per metric
- Preview button in edit mode shows the report as the customer will see it (no edit controls)
- Shared/public report links are always read-only (no edit controls)
Persistence:
- Widget layout encoded in URL query params (not localStorage)
- Compact format, e.g.
?w=imp:kpi,b_clicks:hidden,custom:scratch_done:kpi - Shared links preserve exact layout
Widget types (phased):
- Phase 1: KPI cards with CRUD (add, remove, reorder, hide)
- Phase 2: Multiple widget types per metric (KPI card, line chart, bar chart)
- Phase 3: DnD repositioning, resize
Design decisions (completed 2026-06-29):
- [x] Reordering: Full DnD with 12-column grid slots
- [x] URL format:
?layout=metricCode:colspan,...(order = position) - [x] "+ Add metric": Inline dropdown in last grid cell, grouped by source
- [x] Remove = omit from layout and metrics (no hidden state in phase 1)
- [x] Grid applies app-wide: Reports list also uses 12-column grid (4 cols per report card)
- [x] No native browser dialogs -- custom confirm/alert components
- [x] Delete buttons as icons, not text (fixes overflow on report cards)
- [x] All dropdowns/controls integrated with app design tokens
Full spec: todos/Humology/widget-crud-spec.md
Metric Source Unification
- [x] Investigate Bunny vs aggregation overlap - Confirmed: near-complete overlap. Aggregation API (Cloudflare) is prod, Bunny is migrating. Both compute same metrics from same events, different pipelines.
- [ ] Go all-in on Bunny - Kevin confirms all creatives use Bunny for 6+ months. Plan: hide CF metrics behind devTools flag (localhost only), use Bunny as sole data source. Eliminates source conflicts (e.g. 75K bunny imps vs 553K CF imps showing side-by-side, rates calculated across sources).
- [ ] Unified MetricPicker - Merge into single metric list when Bunny CSV expands. For now, keep separation since aggregation covers more metrics.
UX Improvements
- [ ] Date sorting in ResourcePicker - Sort by created_at so newer creatives appear first. DataTreeNode has created_at.
- [ ] Campaign-level impression counts - Sum impressions in campaign row without expanding
- [ ] Visual indicator for inactive creatives - Faded styling for 0 impressions
- [ ] Custom metric discovery indicator - Show which creatives have Bunny custom metrics
- [ ] Impression threshold filter - Filter picker to creatives with >1000 impressions (meaningful data only)
Bunny Analytics
- [ ] Blocked: Parquet mismatch - Logs-Parser writes flat file, backend expects hive-partitioned. Kevin investigating. See
bunny-parquet-mismatch.md - [ ] Blocked: Expand backend CSV - Once parquet fix lands, add missing columns (viewable_impressions, started, continued, reached_end, plays, etc.) to backend SELECT
- [ ] Test Bunny clicks end-to-end - Verify clicks, watched_25, etc. display correctly with a creative that has real engagement data
- [ ] Per-creative "no Bunny data" state - Show which creatives in a report lack Bunny data
- [ ] Bunny custom metric preview in picker - Show available metric names per creative before adding
Code Quality
- [ ] Extract shared types - DateRange duplicated 4-5x, DataTreeNode types duplicated in 3 files
- [ ] Convention alignment - Arrow functions,
typeinstead ofinterface - [ ] Component splitting - ResourcePicker ~700 lines, split into composables/components
- [ ] Add CLAUDE.md / ESLint / Prettier config - Align with AF conventions
- [ ] Stores reactivity - Better reactive wiring for metric changes through widgets
Backlog (from Linear)
- [ ] CAV-170: Report links - Shareable report URLs
- [ ] CAV-171: Report link management UI - Manage saved/shared reports
- [ ] CAV-168: Export - PDF/CSV export of reports
Architecture Notes
Two Data Pipelines (verified 2026-06-29)
Pipeline 1: Aggregation API (Cloudflare Worker relay)
- Backend proxies requests via
RelayService.tsto externalcavai.aggregation.url - Routes:
/api/aggregation-api/counts,/api/aggregation-api/buckets metric[]=basereturns all 16 metrics at once- Supports:
impressions, interacted, started, continued, creative_actions, link_clicks, header_clicks, background_clicks, reached_end, plays, watched_25, watched_50, watched_75, watched_100, viewable_impressions, seconds_in_view - This is the OLD pipeline -- data comes from Cloudflare Worker log processing
Pipeline 2: Bunny Analytics (DuckDB parquet files)
- Logs-Parser processes raw Bunny CDN logs -> decoded.parquet -> aggregated.parquet -> hourly_summary parquet
- Backend reads parquet via DuckDB and returns CSV:
/api/analytics/bunny/hourly-summary/:creative_id - Custom metrics discovered per-creative:
/api/analytics/bunny/metrics/:creative_id - This is the NEW pipeline -- replacing Cloudflare
Parquet Has ALL Metrics (verified 2026-06-29)
The hourly_summary parquet files (written by Logs-Parser Aggregator.js) contain ALL these columns:
impressions, renderizations, viewable_impressions, interacted, started,
continued, seconds_in_view, seconds_to_active, creative_actions,
link_clicks, reached_end, header_clicks, background_clicks, plays,
watched_25, watched_50, watched_75, watched_100,
analytics_allowed_count, analytics_disallowed_count,
seconds_total_active, clicks, custom_metrics (MAP)But the backend CSV endpoint (analytics.ts) only SELECTs 9 of them:
impressions, link_clicks, creative_actions (as "Total Interactions"),
clicks, watched_25/50/75/100, seconds_total_activeMissing from CSV but present in parquet:viewable_impressions, started, continued, reached_end, plays, header_clicks, background_clicks, seconds_in_view, seconds_to_active, renderizations, analytics_allowed_count, analytics_disallowed_count
Overlap Between Sources
| Metric | Aggregation API | Bunny Parquet | Bunny CSV |
|---|---|---|---|
| impressions | x | x | x |
| link_clicks | x | x | x |
| watched_25/50/75/100 | x | x | x |
| interacted | x | x (as creative_actions>0) | - |
| started | x | x | - |
| continued | x | x | - |
| viewable_impressions | x | x | - |
| seconds_in_view | x | x | - |
| creative_actions | x | x | - |
| reached_end | x | x | - |
| plays | x | x | - |
| header_clicks | x | x | - |
| background_clicks | x | x | - |
| seconds_to_active | - | x | - |
| seconds_total_active | - | x | x |
| clicks | - | x | x |
| renderizations | - | x | - |
| analytics_allowed/disallowed | - | x | - |
| custom_metrics | - | x | x |
Next Step: Expand Backend CSV
The fastest path to "all metrics available" is adding the missing columns to the backend's DuckDB SELECT in analytics.ts. No new infrastructure needed -- the data is already in parquet. Then the reporting frontend can show everything from Bunny alone, and we can phase out the aggregation API relay.
Other Notes
interaction-rateandctrare computed client-side from raw counts- 204 No Content from Bunny = creative has no Bunny analytics (expected for older creatives)
- Standard Bunny CSV columns always present; only custom metrics need
custom_metric[]API params
PRs That Built This Pipeline
- AB#553 - Initial Bunny DuckDB analytics route
- AB#554 - Added Seconds Total Active
- AB#557 - Added Clicks + Companion Creative Clicks
- LP#37 - DuckDB optimization, parquet pipeline
- LP#35 - Manual date processing for historic data
Debugging
Open Chrome DevTools Network tab:
GET /api/analytics/bunny/hourly-summary/{creative_id}-- CSV (or 204 if no data)GET /api/analytics/bunny/metrics/{creative_id}--{ metrics: [...] }GET /api/aggregation-api/counts?...&metric[]=base-- all standard metrics- 204 +
Content-Disposition: attachment; filename="86403.csv"= no Bunny data for this creative