Skip to content

feat: GitHub-style contribution calendar heatmap#144

Merged
SantiagoDePolonia merged 2 commits intomainfrom
feature/github-style-dashboard
Mar 13, 2026
Merged

feat: GitHub-style contribution calendar heatmap#144
SantiagoDePolonia merged 2 commits intomainfrom
feature/github-style-dashboard

Conversation

@SantiagoDePolonia
Copy link
Copy Markdown
Contributor

@SantiagoDePolonia SantiagoDePolonia commented Mar 13, 2026

Summary

  • Add a year-long GitHub-style contribution calendar heatmap to the dashboard Overview page showing daily token consumption or cost data
  • Extend DailyUsage struct with InputCost, OutputCost, TotalCost fields and update all three database readers (SQLite, PostgreSQL, MongoDB) to aggregate cost data
  • New contribution-calendar.js module with independent data fetching (/admin/api/v1/usage/daily?days=365), switchable Tokens/Costs modes, quartile-based intensity levels, hover tooltips, and month labels
  • Full dark/light theme support using GitHub's green heatmap palette, responsive layout with horizontal scroll on mobile

Test plan

  • make test — all 23 packages pass, all pre-commit hooks pass
  • make run — verify calendar renders on /admin/dashboard/overview
  • Toggle between Tokens and Costs modes
  • Hover cells to verify tooltip shows date + value
  • Switch dark/light theme — verify heatmap colors adapt
  • Check mobile layout — calendar scrolls horizontally, day labels hidden

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Yearly contribution calendar added to the admin dashboard showing daily activity.
    • Toggle between Tokens and Costs views, with interactive tooltips and month/day labels.
    • Daily usage now shows input, output, and total costs alongside token metrics.
    • Mobile-responsive calendar layout and compact adjustments for small screens.

Add a year-long activity heatmap to the Overview page showing daily
token consumption or cost data. Includes switchable Tokens/Costs modes,
hover tooltips, dark/light theme support, and responsive layout.

Backend: add cost fields (InputCost, OutputCost, TotalCost) to the
DailyUsage struct and update SQLite, PostgreSQL, and MongoDB readers
to aggregate and return cost data alongside token counts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 13, 2026

📝 Walkthrough

Walkthrough

Adds a yearly contribution calendar UI and client module to the admin dashboard, extends DailyUsage and UsageLogEntry to include cost fields, and updates MongoDB/PostgreSQL/SQLite readers to aggregate/return cost totals. Also inserts the contribution-calendar script tag twice in layout.html.

Changes

Cohort / File(s) Summary
Contribution Calendar styles & template
internal/admin/dashboard/static/css/dashboard.css, internal/admin/dashboard/templates/index.html
Adds contribution calendar markup and styles: calendar section, grid, day/month labels, tooltip, legend, responsive/mobile tweaks, and theme-aware color variables (cal-level-0…cal-level-4).
Contribution Calendar module
internal/admin/dashboard/static/js/modules/contribution-calendar.js
New IIFE module exposing dashboardContributionCalendarModule: fetches calendar data, builds week-aligned grid, computes quartile levels, handles tokens/costs mode switching, tooltip display, and summary text.
Dashboard integration
internal/admin/dashboard/static/js/dashboard.js, internal/admin/dashboard/templates/layout.html
Wires calendar module factory into initialization and conditionally fetches calendar data; adds script tag for contribution-calendar.js in layout (note: inserted twice).
Charts/compat data shape
internal/admin/dashboard/static/js/modules/charts.js
fillMissingDays now returns default day objects that include input_cost, output_cost, and total_cost (initialized to null).
Backend usage types
internal/usage/reader.go
DailyUsage and UsageLogEntry structs expanded to include optional cost fields: InputCost, OutputCost, and TotalCost (*float64 with json tags).
Database readers
internal/usage/reader_mongodb.go, internal/usage/reader_postgresql.go, internal/usage/reader_sqlite.go
Readers updated to aggregate and return cost columns (input_cost, output_cost, total_cost); MongoDB decoding conditionally populates cost pointers when has_costs > 0; PostgreSQL/SQLite SELECTs and scanning include cost fields (COALESCE removed for cost sums).

Sequence Diagram

sequenceDiagram
    participant Dashboard as Dashboard Init
    participant CalendarModule as Calendar Module
    participant Backend as Backend API
    participant Database as Database

    Dashboard->>CalendarModule: init / request calendarModuleFactory
    CalendarModule->>Backend: fetchCalendarData()
    Backend->>Database: Query daily usage with cost+token aggregates
    Database-->>Backend: Return usage records (dates, tokens, costs)
    Backend-->>CalendarModule: JSON response
    CalendarModule->>CalendarModule: buildCalendarGrid() / compute levels
    CalendarModule->>Dashboard: render calendar cells + tooltips
    User->>Dashboard: toggle tokens/costs
    Dashboard->>CalendarModule: toggleCalendarMode()
    CalendarModule->>CalendarModule: rebuild grid & re-render
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through rows of weeks and days,
Tokens and costs in rosy arrays,
From DB roots to cells that glow,
I nibbled bugs so metrics grow,
A tiny hop — the dashboard sings! 🎨📈

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: GitHub-style contribution calendar heatmap' accurately summarizes the main change—adding a GitHub-style contribution calendar visualization to the dashboard.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/github-style-dashboard
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/usage/reader.go (1)

73-76: 🧹 Nitpick | 🔵 Trivial

Minor formatting inconsistency.

Line 75 has inconsistent alignment compared to lines 73-74. Consider running gofmt to normalize spacing.

🔧 Proposed formatting fix
-	InputCost              *float64  `json:"input_cost"`
-	OutputCost             *float64  `json:"output_cost"`
-	TotalCost              *float64       `json:"total_cost"`
+	InputCost              *float64       `json:"input_cost"`
+	OutputCost             *float64       `json:"output_cost"`
+	TotalCost              *float64       `json:"total_cost"`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/usage/reader.go` around lines 73 - 76, The struct field alignment is
inconsistent for TotalCost compared to InputCost and OutputCost; run gofmt or
adjust spacing so the field declarations and struct tags align consistently
(e.g., make TotalCost use the same spacing/tab alignment as
InputCost/OutputCost) in the struct that contains fields InputCost, OutputCost,
TotalCost, and RawData in reader.go to normalize formatting.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/admin/dashboard/static/js/dashboard.js`:
- Line 109: Init currently calls fetchCalendarData() unconditionally which can
throw if the optional calendar module wasn't registered; remove that direct call
from init() and instead guard all calendar calls by checking the module exists
(e.g., calendarModule or whatever identifier you use when registering the
calendar) before invoking fetchCalendarData(), and add a call to
fetchCalendarData() from the shared refresh path (fetchAll()) so auth retries
and other refreshes also update calendar data only when the module is present;
update any registration logic (the optional registerModule call) to ensure the
presence check matches the registration identifier.

In `@internal/admin/dashboard/static/js/modules/contribution-calendar.js`:
- Around line 131-145: calendarSummaryText currently calls buildCalendarGrid
(expensive) to compute a total; change it to iterate over the raw calendarData
instead to avoid rebuilding the grid: inside calendarSummaryText, sum values
directly from this.calendarData (skip empty or falsy entries) and format the
result based on this.calendarMode ('costs' => dollar with toFixed(2), else use
toLocaleString for tokens). Keep calendarSummaryText signature and return
strings unchanged and remove the buildCalendarGrid call from this method so the
expensive grid construction is not triggered on every reactive render.

In `@internal/admin/dashboard/templates/index.html`:
- Around line 60-61: Add explicit type="button" attributes to the calendar mode
toggle buttons to prevent implicit form submission; update the two elements
using the usage-mode-btn class (the buttons that call
toggleCalendarMode('tokens') and toggleCalendarMode('costs')) to include
type="button" while preserving their class bindings and click handlers.

In `@internal/usage/reader_postgresql.go`:
- Around line 190-191: The cost SUM aggregations are wrapped with COALESCE(...,
0) which forces NULL -> 0 and breaks the nullable *float64 semantics; in
GetDailyUsage (see variable costCols and the surrounding query),
GetUsageByModel, and GetSummary remove the COALESCE around SUM(input_cost),
SUM(output_cost), and SUM(total_cost) so the SQL returns NULL when costs are
unknown, letting the Go *float64 fields remain nil; update the SELECT fragments
that currently use COALESCE(SUM(...), 0) to use SUM(...) directly in those three
functions.

In `@internal/usage/reader_sqlite.go`:
- Around line 198-199: The cost aggregation uses COALESCE(SUM(...),0) which
forces missing costs to 0.0 and prevents DailyUsage's *float64 cost pointers
from being nil; remove the COALESCE wrappers in the costCols variable and any
corresponding SELECT fragments so SUM(...) can return NULL, then ensure
GetDailyUsage and GetSummary continue scanning into the DailyUsage cost pointer
fields (so they stay nil when no cost rows exist). Apply the same change for the
analogous query fragments in reader_postgresql.go and update any related query
construction logic that references costCols, query, GetDailyUsage, GetSummary,
and DailyUsage so NULL aggregates map to nil pointers.

---

Outside diff comments:
In `@internal/usage/reader.go`:
- Around line 73-76: The struct field alignment is inconsistent for TotalCost
compared to InputCost and OutputCost; run gofmt or adjust spacing so the field
declarations and struct tags align consistently (e.g., make TotalCost use the
same spacing/tab alignment as InputCost/OutputCost) in the struct that contains
fields InputCost, OutputCost, TotalCost, and RawData in reader.go to normalize
formatting.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 79528bea-cf91-4f41-9524-c1483ef513e9

📥 Commits

Reviewing files that changed from the base of the PR and between f5a9e75 and 5ad86e6.

📒 Files selected for processing (10)
  • internal/admin/dashboard/static/css/dashboard.css
  • internal/admin/dashboard/static/js/dashboard.js
  • internal/admin/dashboard/static/js/modules/charts.js
  • internal/admin/dashboard/static/js/modules/contribution-calendar.js
  • internal/admin/dashboard/templates/index.html
  • internal/admin/dashboard/templates/layout.html
  • internal/usage/reader.go
  • internal/usage/reader_mongodb.go
  • internal/usage/reader_postgresql.go
  • internal/usage/reader_sqlite.go

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/admin/dashboard/templates/index.html`:
- Around line 84-87: The calendar cells (div.contribution-calendar-cell) are
only hoverable via `@mouseenter/`@mouseleave so keyboard and screen-reader users
can't access day-level info; make each cell keyboard-accessible by adding
tabindex="0" and an appropriate ARIA role (e.g., role="button" or
role="gridcell") and an aria-label that includes the day and level (derived from
day and day.level), wire focus/blur handlers to call showCalendarTooltip and
hideCalendarTooltip (or reuse existing methods) and add a `@keydown` handler that
triggers showCalendarTooltip on Enter/Space and hideCalendarTooltip on Escape to
mirror mouse behavior; update handlers referenced (showCalendarTooltip,
hideCalendarTooltip) to accept keyboard events if needed and ensure empty days
still convey emptiness in the aria-label (e.g., "no contributions") for screen
readers.
- Around line 56-110: The calendar HTML unconditionally calls calendar helpers
(calendarMonthLabels(), buildCalendarGrid(), calendarSummaryText(),
calendarTooltip, toggleCalendarMode, showCalendarTooltip, hideCalendarTooltip)
which breaks when the optional calendar module isn't registered; wrap the entire
calendar block (the contribution-calendar-section and the
contribution-calendar-tooltip) with a guard that only renders/binds when the
module is available (e.g., add a top-level x-if/x-show like
calendarModuleAvailable or a runtime check such as typeof calendarMonthLabels
=== 'function'), and update any inline bindings to short-circuit against that
same guard so events like `@click`="toggleCalendarMode('tokens')" and tooltip
mouse handlers are only attached when those functions/objects exist.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 465f803b-f1ba-400c-87fc-68dcf8e06b6b

📥 Commits

Reviewing files that changed from the base of the PR and between 5ad86e6 and addd903.

📒 Files selected for processing (6)
  • internal/admin/dashboard/static/js/dashboard.js
  • internal/admin/dashboard/static/js/modules/contribution-calendar.js
  • internal/admin/dashboard/templates/index.html
  • internal/usage/reader.go
  • internal/usage/reader_postgresql.go
  • internal/usage/reader_sqlite.go

Comment on lines +56 to +110
<div class="contribution-calendar-section">
<div class="contribution-calendar-header">
<h3>Activity</h3>
<div class="usage-mode-toggle">
<button type="button" class="usage-mode-btn" :class="{active: calendarMode==='tokens'}" @click="toggleCalendarMode('tokens')">Tokens</button>
<button type="button" class="usage-mode-btn" :class="{active: calendarMode==='costs'}" @click="toggleCalendarMode('costs')">Costs</button>
</div>
</div>
<div class="contribution-calendar-grid-wrapper">
<div class="contribution-calendar-day-labels">
<span></span>
<span>Mon</span>
<span></span>
<span>Wed</span>
<span></span>
<span>Fri</span>
<span></span>
</div>
<div class="contribution-calendar-scroll">
<div class="contribution-calendar-months">
<template x-for="ml in calendarMonthLabels()" :key="ml.key">
<span class="contribution-calendar-month-label" :style="'grid-column: ' + (ml.col + 1)" x-text="ml.label"></span>
</template>
</div>
<div class="contribution-calendar-grid">
<template x-for="(week, wi) in buildCalendarGrid()" :key="wi">
<div class="contribution-calendar-week">
<template x-for="(day, di) in week" :key="wi + '-' + di">
<div class="contribution-calendar-cell"
:class="[day.empty ? 'empty' : 'level-' + day.level]"
@mouseenter="showCalendarTooltip($event, day)"
@mouseleave="hideCalendarTooltip()"></div>
</template>
</div>
</template>
</div>
</div>
</div>
<div class="contribution-calendar-footer">
<span class="contribution-calendar-summary" x-text="calendarSummaryText()"></span>
<div class="contribution-calendar-legend">
<span>Less</span>
<div class="contribution-calendar-cell level-0"></div>
<div class="contribution-calendar-cell level-1"></div>
<div class="contribution-calendar-cell level-2"></div>
<div class="contribution-calendar-cell level-3"></div>
<div class="contribution-calendar-cell level-4"></div>
<span>More</span>
</div>
</div>
</div>
<div class="contribution-calendar-tooltip"
x-show="calendarTooltip.show"
x-text="calendarTooltip.text"
:style="'left: ' + calendarTooltip.x + 'px; top: ' + (calendarTooltip.y - 40) + 'px'"></div>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard calendar bindings when the module is unavailable.

This block unconditionally evaluates calendar methods/properties, but dashboard.js treats the module as optional. If it is not registered, Alpine can fail at render time (e.g., calling undefined methods).

💡 Proposed fix
-    <div class="contribution-calendar-section">
+    <div class="contribution-calendar-section" x-show="hasCalendarModule">
@@
-    <div class="contribution-calendar-tooltip"
-         x-show="calendarTooltip.show"
+    <div class="contribution-calendar-tooltip"
+         x-show="hasCalendarModule && calendarTooltip.show"
          x-text="calendarTooltip.text"
          :style="'left: ' + calendarTooltip.x + 'px; top: ' + (calendarTooltip.y - 40) + 'px'"></div>
+    <p class="empty-state" x-show="!hasCalendarModule">Activity calendar unavailable.</p>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/admin/dashboard/templates/index.html` around lines 56 - 110, The
calendar HTML unconditionally calls calendar helpers (calendarMonthLabels(),
buildCalendarGrid(), calendarSummaryText(), calendarTooltip, toggleCalendarMode,
showCalendarTooltip, hideCalendarTooltip) which breaks when the optional
calendar module isn't registered; wrap the entire calendar block (the
contribution-calendar-section and the contribution-calendar-tooltip) with a
guard that only renders/binds when the module is available (e.g., add a
top-level x-if/x-show like calendarModuleAvailable or a runtime check such as
typeof calendarMonthLabels === 'function'), and update any inline bindings to
short-circuit against that same guard so events like
`@click`="toggleCalendarMode('tokens')" and tooltip mouse handlers are only
attached when those functions/objects exist.

Comment on lines +84 to +87
<div class="contribution-calendar-cell"
:class="[day.empty ? 'empty' : 'level-' + day.level]"
@mouseenter="showCalendarTooltip($event, day)"
@mouseleave="hideCalendarTooltip()"></div>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add keyboard-accessible semantics for calendar cells.

The cells are hover-only (mouseenter/mouseleave) and not keyboard focusable, so day-level values are inaccessible for keyboard/screen-reader users.

💡 Proposed fix
-                                <div class="contribution-calendar-cell"
+                                <div class="contribution-calendar-cell"
                                      :class="[day.empty ? 'empty' : 'level-' + day.level]"
+                                     :tabindex="day.empty ? -1 : 0"
+                                     role="img"
+                                     :aria-label="day.empty ? '' : (calendarMode === 'costs'
+                                        ? ('$' + (day.value || 0).toFixed(4) + ' on ' + day.dateStr)
+                                        : ((day.value || 0).toLocaleString() + ' tokens on ' + day.dateStr))"
                                      `@mouseenter`="showCalendarTooltip($event, day)"
                                      `@mouseleave`="hideCalendarTooltip()"></div>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div class="contribution-calendar-cell"
:class="[day.empty ? 'empty' : 'level-' + day.level]"
@mouseenter="showCalendarTooltip($event, day)"
@mouseleave="hideCalendarTooltip()"></div>
<div class="contribution-calendar-cell"
:class="[day.empty ? 'empty' : 'level-' + day.level]"
:tabindex="day.empty ? -1 : 0"
role="img"
:aria-label="day.empty ? '' : (calendarMode === 'costs'
? ('$' + (day.value || 0).toFixed(4) + ' on ' + day.dateStr)
: ((day.value || 0).toLocaleString() + ' tokens on ' + day.dateStr))"
`@mouseenter`="showCalendarTooltip($event, day)"
`@mouseleave`="hideCalendarTooltip()"></div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/admin/dashboard/templates/index.html` around lines 84 - 87, The
calendar cells (div.contribution-calendar-cell) are only hoverable via
`@mouseenter/`@mouseleave so keyboard and screen-reader users can't access
day-level info; make each cell keyboard-accessible by adding tabindex="0" and an
appropriate ARIA role (e.g., role="button" or role="gridcell") and an aria-label
that includes the day and level (derived from day and day.level), wire
focus/blur handlers to call showCalendarTooltip and hideCalendarTooltip (or
reuse existing methods) and add a `@keydown` handler that triggers
showCalendarTooltip on Enter/Space and hideCalendarTooltip on Escape to mirror
mouse behavior; update handlers referenced (showCalendarTooltip,
hideCalendarTooltip) to accept keyboard events if needed and ensure empty days
still convey emptiness in the aria-label (e.g., "no contributions") for screen
readers.

@SantiagoDePolonia SantiagoDePolonia merged commit eb4cb1b into main Mar 13, 2026
12 of 13 checks passed
@SantiagoDePolonia SantiagoDePolonia deleted the feature/github-style-dashboard branch April 4, 2026 11:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant