Skip to content

fix(admin): honor user timezones in dashboard#185

Merged
SantiagoDePolonia merged 8 commits intomainfrom
fix/timezones-dashboard
Mar 29, 2026
Merged

fix(admin): honor user timezones in dashboard#185
SantiagoDePolonia merged 8 commits intomainfrom
fix/timezones-dashboard

Conversation

@SantiagoDePolonia
Copy link
Copy Markdown
Contributor

@SantiagoDePolonia SantiagoDePolonia commented Mar 28, 2026

Summary

  • detect the browser timezone automatically, add a persisted dashboard override in Settings, and send the effective IANA timezone with admin requests
  • apply the effective timezone to admin date parsing and usage grouping across SQLite, PostgreSQL, and MongoDB, and show timezone context in the calendar footer and log timestamps
  • fix SQLite fractional-second boundary filtering and keep timezone-aware daily usage aggregation in SQL instead of decoding raw rows in Go

Test Plan

  • go test ./...
  • node --test internal/admin/dashboard/static/js/modules/*.test.js

Summary by CodeRabbit

  • New Features

    • Timezone settings page with detection/override, Settings link in nav, and calendar footer showing effective timezone.
    • Updated settings panel and responsive UI for timezone help and form controls.
  • Bug Fixes

    • Improved request cancellation to avoid stale updates.
    • More consistent timestamp formatting and timezone-aware date-range handling across dashboard and backend.
  • UI

    • Sidebar/layout tuned for sticky, flexible sizing and centered content; contribution calendar footer spacing improved.
  • Tests

    • New tests for timezone behavior, request cancellation, charts, layout, and date-range boundaries.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 28, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds timezone support and request-cancellation across the admin dashboard: new frontend timezone module and settings UI, UTC-based date logic in calendar/chart/date-picker, abortable fetches and abort-safe state updates, backend timezone-aware parsing/grouping for usage/audit queries, DB normalization/indexing, and many tests.

Changes

Cohort / File(s) Summary
Styling & Layout
internal/admin/dashboard/static/css/dashboard.css, internal/admin/dashboard/templates/layout.html, internal/admin/dashboard/templates/index.html
Reworked sidebar to sticky/flex layout and centered content; added contribution-calendar meta and settings-panel styles; added Settings nav, timezone UI, timestamp title attributes, and pinned script integrity attributes.
Dashboard bootstrap & runtime
internal/admin/dashboard/static/js/dashboard.js
Added module-factory resolver, timezone module integration (init/hooks/routing), X-GoModel-Timezone header injection, abortable-request helpers, and new timestamp helpers (formatTimestampUTC).
Timezone feature & tests
internal/admin/dashboard/static/js/modules/timezone.js, .../timezone.test.js, .../timezone-layout.test.js
New timezone module: detection, override persistence, timezoneOptions, date-key and formatting APIs, refresh hooks, and unit tests for behavior and template placement.
Charts, Calendar & Date logic
internal/admin/dashboard/static/js/modules/charts.js, .../date-picker.js, .../contribution-calendar.js, .../charts.test.js
Shifted date math to UTC/date-key helpers, added chart config helpers, ensured proper chart destroy/replace lifecycle, and updated calendar/grid generation to UTC semantics with tests.
Abortable fetches & frontend request tests
internal/admin/dashboard/static/js/modules/usage.js, .../contribution-calendar.js, internal/admin/dashboard/static/js/modules/request-cancellation.test.js
Implemented AbortController-based cancellable fetches, abort-aware error handling/cleanup, guarded state updates for stale responses, and tests asserting cancellation and stale-response ignoring.
Frontend tests & layout assertions
internal/admin/dashboard/static/js/modules/*.test.js, internal/admin/dashboard/static/js/modules/dashboard-layout.test.js, internal/admin/dashboard/static/js/modules/timestamp-format.test.js
Added VM-based tests for layout/CSS rules, timestamp formatting in effective timezone, request cancellation, chart behavior, and script integrity/version checks.
Backend handler & request parsing
internal/admin/handler.go, internal/admin/handler_test.go
Plumbed X-GoModel-Timezone header into parsing, used request-derived location for date parsing and default range computation, and added tests validating timezone-aware default ranges.
Usage API & timezone plumbing
internal/usage/reader.go, internal/usage/timezone.go
Added TimeZone to UsageQueryParams and helper functions to resolve usage time zone, location, and exclusive end-date semantics.
DB drivers & timezone-aware grouping
internal/usage/reader_mongodb.go, internal/usage/reader_postgresql.go, internal/usage/reader_sqlite.go, internal/usage/store_sqlite.go
Made grouping queries timezone-aware (Mongo $dateToString timeZone, Postgres AT TIME ZONE, SQLite epoch/grouping with offset segments), normalized timestamp handling, and added SQLite index on epoch expression.
SQLite & auditlog boundary handling
internal/auditlog/reader_sqlite.go, internal/auditlog/reader_sqlite_boundary_test.go
Introduced UTC timestamp boundary formatter for SQLite queries and added tests verifying fractional-second inclusion/exclusion semantics.
Extensive SQLite usage tests
internal/usage/reader_sqlite_*.go
Large suite of tests covering boundary handling, DST grouping, mixed timestamp formats, grouping correctness, ordering, and cleanup behavior.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as "Dashboard UI"
    participant TZ as "Timezone Module"
    participant API as "Backend Handler"
    participant DB as "Database"

    User->>UI: Open Settings / select timezone
    UI->>TZ: saveTimezoneOverride(zone)
    TZ->>TZ: persist override (localStorage) and recompute options
    TZ-->>UI: refreshTimeZoneData()
    UI->>UI: include X-GoModel-Timezone header for requests
    UI->>API: GET /usage?start=...&end=... (Header: X-GoModel-Timezone)
    API->>API: dashboardTimeZone() -> location
    API->>API: parseDateRangeParams() using location
    API->>DB: Query grouped usage with timezone-aware boundaries
    DB-->>API: Return grouped results
    API-->>UI: Respond with usage data
    UI->>TZ: formatTimestampInEffectiveTimeZone(ts)
    TZ-->>UI: Formatted timestamp
    UI-->>User: Render dashboard with timezone-adjusted dates
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

release:fix

Poem

🐰 In burrows of code I softly scurry,
I add a zone so dates don't worry,
Charts refreshed, stale fetches flee,
Time and tokens aligned for thee.
Hoppity-hop — the dashboard's merry!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.76% 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 'fix(admin): honor user timezones in dashboard' is clear, specific, and directly describes the main objective of the changeset—enabling timezone support in the admin dashboard.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/timezones-dashboard

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.

@SantiagoDePolonia SantiagoDePolonia marked this pull request as ready for review March 29, 2026 13:14
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/admin/dashboard/templates/index.html (1)

1013-1021: ⚠️ Potential issue | 🟡 Minor

Add type="button" attribute to prevent form submission behavior.

Per HTMLHint, the <button> element should have an explicit type attribute. Without it, the default is type="submit", which could cause unintended behavior in some contexts.

🔧 Proposed fix
-                                <button class="audit-conversation-trigger"
+                                <button type="button" class="audit-conversation-trigger"
🤖 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 1013 - 1021, The
button with class "audit-conversation-trigger" that uses
x-show="canShowConversation(entry)" and
`@click.stop.prevent`="openConversation(entry, $el.closest('details'), true, $el)"
lacks an explicit type and thus may act as a submit button; update that <button>
to include type="button" to prevent unintended form submissions when clicked
while keeping existing attributes and behavior.
🤖 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`:
- Around line 285-318: fetchModels can let a stale response mutate shared state;
ensure you verify the request controller is still current before calling
handleFetchResponse, changing this.models, or toggling auth/error state. After
obtaining the controller (via _startAbortableRequest) and again immediately
after awaiting fetch() and res.json(), check that controller is the current
active controller (e.g., compare controller === this._modelsFetchController or
call controller.signal.aborted) and return early if not; only then call
handleFetchResponse, assign this.models and call syncDisplayModels. Also apply
the same current-controller check in the catch block (besides _isAbortError)
before mutating state, and keep using
_clearAbortableRequest('_modelsFetchController', controller) in finally.

In `@internal/admin/dashboard/static/js/modules/contribution-calendar.js`:
- Around line 10-40: The current fetch flow allows stale requests to overwrite
state because it doesn't verify the controller is still the active one; ensure
only the active request (the controller returned by _startAbortableRequest) may
change calendarLoading, calendarData, or call _clearAbortableRequest. Update the
logic in the async function that uses
_startAbortableRequest('_calendarFetchController') so that before calling
handleFetchResponse, before assigning this.calendarData, and before clearing the
controller or toggling this.calendarLoading, you check the returned controller
is strictly equal to this._calendarFetchController (and still not aborted). Also
only call _clearAbortableRequest('_calendarFetchController', controller) when
that equality holds; keep the existing _isAbortError check for thrown aborts.

In `@internal/admin/dashboard/static/js/modules/timezone.js`:
- Around line 101-104: The guard in formatTimestampInTimeZone incorrectly treats
0 as missing; change the check from a falsy test to an explicit null/undefined
check (e.g., ts === null || ts === undefined) so epoch value 0 is accepted;
update the guard inside formatTimestampInTimeZone to only return '-' for
null/undefined inputs and leave numeric 0 to be formatted normally.
- Around line 11-13: The browserStorage() accessor and any code that calls
storage methods (e.g., saveTimeZone, loadTimeZone, clearTimeZone) must defend
against DOMException by wrapping all accesses to localStorage and its methods
(getItem, setItem, removeItem) in try-catch blocks; update browserStorage() to
return null if accessing global.localStorage throws, and wrap every call site
that uses storage (calls through browserStorage()) so failures safely fall back
to no-op behavior and do not throw (log or ignore the error as appropriate) to
preserve timezone initialization, saving, and clearing flows.

In `@internal/usage/reader_sqlite.go`:
- Around line 161-175: The query comparisons rely on lexicographic ordering of
the TEXT timestamp column but some rows use a space-separated offset format that
sorts before the RFC3339 "T" form; update sqliteDateRangeConditions and
sqliteGroupingRange to compare a normalized timestamp expression (e.g.
REPLACE(timestamp, ' ', 'T')) instead of raw timestamp, and ensure
sqliteTimestampBoundary returns a strictly RFC3339-"T" formatted boundary (use
UTC/.Format(sqliteTimestampBoundaryLayout) or otherwise force the 'T' separator)
including usageEndExclusive where used; also audit parseUsageTimestamp to
normalize parsed values to the canonical "T" form if they are ever re-written or
used for MIN/MAX computations so MIN(timestamp)/MAX(timestamp) and range filters
behave correctly on mixed-format data.

---

Outside diff comments:
In `@internal/admin/dashboard/templates/index.html`:
- Around line 1013-1021: The button with class "audit-conversation-trigger" that
uses x-show="canShowConversation(entry)" and
`@click.stop.prevent`="openConversation(entry, $el.closest('details'), true, $el)"
lacks an explicit type and thus may act as a submit button; update that <button>
to include type="button" to prevent unintended form submissions when clicked
while keeping existing attributes and behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: a369c081-cac4-4730-986f-678949c02a02

📥 Commits

Reviewing files that changed from the base of the PR and between 6b4608e and d0edf3c.

📒 Files selected for processing (25)
  • 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/charts.test.js
  • internal/admin/dashboard/static/js/modules/contribution-calendar.js
  • internal/admin/dashboard/static/js/modules/date-picker.js
  • internal/admin/dashboard/static/js/modules/request-cancellation.test.js
  • internal/admin/dashboard/static/js/modules/timestamp-format.test.js
  • internal/admin/dashboard/static/js/modules/timezone-layout.test.js
  • internal/admin/dashboard/static/js/modules/timezone.js
  • internal/admin/dashboard/static/js/modules/timezone.test.js
  • internal/admin/dashboard/static/js/modules/usage.js
  • internal/admin/dashboard/templates/index.html
  • internal/admin/dashboard/templates/layout.html
  • internal/admin/handler.go
  • internal/admin/handler_test.go
  • internal/auditlog/reader_sqlite.go
  • internal/auditlog/reader_sqlite_boundary_test.go
  • internal/usage/reader.go
  • internal/usage/reader_mongodb.go
  • internal/usage/reader_postgresql.go
  • internal/usage/reader_sqlite.go
  • internal/usage/reader_sqlite_boundary_test.go
  • internal/usage/reader_sqlite_timezone_test.go
  • internal/usage/timezone.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.

♻️ Duplicate comments (1)
internal/usage/reader_sqlite.go (1)

163-165: ⚠️ Potential issue | 🟠 Major

Fix timestamp comparison to use absolute-time ordering, not lexicographic text ordering.

Lines 170, 311, and 328 rely on text-based comparisons of RFC 3339 timestamps. The REPLACE(timestamp, ' ', 'T') call normalizes the separator, but timestamps with mixed timezone offsets (e.g., 2026-01-16T00:30:00+02:00 vs 2026-01-15T23:30:00Z) still sort incorrectly—+02:00 offsets come after Z lexicographically, inverting chronological order.

This causes incorrect results in:

  • sqliteDateRangeConditions() (lines 163–178): Range filters return wrong rows
  • sqliteGroupExpr() CASE statement (lines 308–316): Segment boundary routing breaks
  • sqliteGroupingRange() (lines 328–343): Min/max derivation errors with DST transitions

Convert to epoch-based comparison via unixepoch():

Proposed fix
-const sqliteTimestampBoundaryLayout = "2006-01-02T15:04:05"
+const sqliteTimestampBoundaryLayout = "2006-01-02T15:04:05Z07:00"

 func sqliteTimestampExpr() string {
 	return "REPLACE(timestamp, ' ', 'T')"
 }
+
+func sqliteTimestampComparableExpr() string {
+	return "unixepoch(" + sqliteTimestampExpr() + ")"
+}

 func sqliteDateRangeConditions(params UsageQueryParams) (conditions []string, args []any) {
 	if !params.StartDate.IsZero() {
-		conditions = append(conditions, sqliteTimestampExpr()+" >= ?")
+		conditions = append(conditions, sqliteTimestampComparableExpr()+" >= unixepoch(?)")
 		args = append(args, sqliteTimestampBoundary(params.StartDate))
 	}
 	if !params.EndDate.IsZero() {
-		conditions = append(conditions, sqliteTimestampExpr()+" < ?")
+		conditions = append(conditions, sqliteTimestampComparableExpr()+" < unixepoch(?)")
 		args = append(args, sqliteTimestampBoundary(usageEndExclusive(params)))
 	}
 	return conditions, args
 }

 func sqliteTimestampBoundary(t time.Time) string {
 	return t.UTC().Format(sqliteTimestampBoundaryLayout)
 }

 // inside sqliteGroupExpr CASE construction:
-		builder.WriteString(sqliteTimestampExpr())
-		builder.WriteString(" < ? THEN ")
+		builder.WriteString(sqliteTimestampComparableExpr())
+		builder.WriteString(" < unixepoch(?) THEN ")

 // inside sqliteGroupingRange:
-	var minTS, maxTS sql.NullString
-	query := `SELECT MIN(` + sqliteTimestampExpr() + `), MAX(` + sqliteTimestampExpr() + `) FROM usage`
-	if err := r.db.QueryRowContext(ctx, query).Scan(&minTS, &maxTS); err != nil {
+	var minEpoch, maxEpoch sql.NullInt64
+	query := `SELECT MIN(` + sqliteTimestampComparableExpr() + `), MAX(` + sqliteTimestampComparableExpr() + `) FROM usage`
+	if err := r.db.QueryRowContext(ctx, query).Scan(&minEpoch, &maxEpoch); err != nil {
 		return time.Time{}, time.Time{}, false, fmt.Errorf("failed to determine sqlite usage range: %w", err)
 	}
-	if !minTS.Valid || !maxTS.Valid {
+	if !minEpoch.Valid || !maxEpoch.Valid {
 		return time.Time{}, time.Time{}, false, nil
 	}
-
-	start := parseUsageTimestamp(minTS.String)
-	end := parseUsageTimestamp(maxTS.String)
-	if start.IsZero() || end.IsZero() {
-		return time.Time{}, time.Time{}, false, nil
-	}
-
-	return start.UTC(), end.UTC().Add(time.Second), true, nil
+	start := time.Unix(minEpoch.Int64, 0).UTC()
+	end := time.Unix(maxEpoch.Int64, 0).UTC().Add(time.Second)
+	return start, end, true, nil
 }

Demonstrated failure (legacy text comparison vs. epoch-based):

legacy_text_compare: [('2026-01-15T23:30:00Z',), ('2026-01-16T00:30:00+02:00',)]  ← both rows (wrong)
fixed_epoch_compare: [('2026-01-15T23:30:00Z',)]  ← correct row only
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/usage/reader_sqlite.go` around lines 163 - 165, The current
sqliteTimestampExpr() returns a text-normalized timestamp and causes
lexicographic ordering errors; change it so comparisons use epoch seconds (e.g.,
return an expression using unixepoch(REPLACE(timestamp, ' ', 'T'))), then update
all consumers—sqliteDateRangeConditions(), sqliteGroupExpr() CASE branches, and
sqliteGroupingRange() min/max expressions—to use that epoch expression for all
comparisons and ordering (keep original timestamp string only when emitting
values, but use unixepoch(...) for range checks, CASE routing, and MIN/MAX
derivation) so ordering is absolute-time based across timezones.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@internal/usage/reader_sqlite.go`:
- Around line 163-165: The current sqliteTimestampExpr() returns a
text-normalized timestamp and causes lexicographic ordering errors; change it so
comparisons use epoch seconds (e.g., return an expression using
unixepoch(REPLACE(timestamp, ' ', 'T'))), then update all
consumers—sqliteDateRangeConditions(), sqliteGroupExpr() CASE branches, and
sqliteGroupingRange() min/max expressions—to use that epoch expression for all
comparisons and ordering (keep original timestamp string only when emitting
values, but use unixepoch(...) for range checks, CASE routing, and MIN/MAX
derivation) so ordering is absolute-time based across timezones.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: e6ff5189-6dfa-4699-b3a8-20f1c029d2b3

📥 Commits

Reviewing files that changed from the base of the PR and between d0edf3c and 4d4b3a9.

📒 Files selected for processing (10)
  • internal/admin/dashboard/static/js/dashboard.js
  • internal/admin/dashboard/static/js/modules/contribution-calendar.js
  • internal/admin/dashboard/static/js/modules/request-cancellation.test.js
  • internal/admin/dashboard/static/js/modules/timezone-layout.test.js
  • internal/admin/dashboard/static/js/modules/timezone.js
  • internal/admin/dashboard/static/js/modules/timezone.test.js
  • internal/admin/dashboard/templates/index.html
  • internal/usage/reader_sqlite.go
  • internal/usage/reader_sqlite_boundary_test.go
  • internal/usage/store_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: 8

🤖 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/css/dashboard.css`:
- Around line 345-352: The CSS removed the .content.content-collapsed rule but
the template and JS still toggle the content-collapsed contract (see layout.html
and dashboard.js) and tests expect it; either restore a dedicated collapsed rule
for .content (e.g., define .content.content-collapsed with the intended
collapsed width/margins/transitions) or remove the toggling and update tests to
match the new layout contract—pick one approach, update the CSS (.content or
.content.content-collapsed), and ensure layout.html,
internal/admin/dashboard/static/js/dashboard.js, and the layout test are all
consistent with that choice.
- Around line 108-119: The .sidebar rule currently uses position: sticky with
height: 100vh which prevents internal scrolling and can clip lower controls;
update the .sidebar CSS (the .sidebar selector) to allow internal scrolling by
replacing rigid height with a max-height (e.g. max-height: 100vh or compute
using top/bottom), add overflow-y: auto, and ensure sticky positioning remains
(keep top: 0) so the sidebar stays fixed while its contents can scroll; preserve
existing transitions like transition: flex-basis 0.2s, width 0.2s.

In `@internal/admin/dashboard/static/js/modules/charts.js`:
- Around line 146-151: The code constructs `end` with new
Date(this.customEndDate) which is inconsistent with the UTC-safe helpers; change
the `end` creation to use the same date-key helpers (use
`dateToDateKey`/`dateKeyToDate`) so both custom and default paths go through
`dateKeyToDate` for UTC-consistent parsing; update the `end` assignment (and
keep existing `start` logic) to derive a date key from `this.customEndDate` or
`this.todayDate()` and convert it with `dateKeyToDate` (referencing
`customEndDate`, `todayDate`, `dateToDateKey`, `dateKeyToDate`,
`addDaysToDateKey`, and `days`).

In `@internal/admin/dashboard/static/js/modules/dashboard-layout.test.js`:
- Around line 10-15: The readCSSRule function's regex is brittle for CRLF and
indented closing braces; update the pattern used in new RegExp inside
readCSSRule (which uses escapedSelector and match) to be newline-agnostic and
allow optional whitespace before the closing brace — e.g. keep the non-greedy
body capture ([\s\S]*?) but replace the trailing `\n\}` requirement with `\s*\}`
(and use `\r?\n` if you need explicit linebreaks) so the regex matches rules
regardless of CRLF/LF or indentation.

In `@internal/admin/dashboard/templates/layout.html`:
- Line 11: The three external script tags loading Chart.js
(https://cdn.jsdelivr.net/npm/chart.js@4.5.0/dist/chart.umd.min.js), Alpine.js,
and HTMX in internal/admin/dashboard/templates/layout.html lack Subresource
Integrity and crossorigin attributes; update each <script> element to include a
correct integrity="sha384-..." SRI hash for the exact file version and add
crossorigin="anonymous" (or replace with a self-hosted copy), verifying the
hashes match the served files; ensure you update the Chart.js, Alpine.js and
HTMX script tags consistently so the template uses integrity + crossorigin for
all CDN scripts.

In `@internal/usage/reader_sqlite.go`:
- Around line 161-179: GetUsageLog() still orders/paginates by the raw
"timestamp" causing inconsistent ordering with mixed formats; change its ORDER
BY to use the same normalized epoch expression used in
sqliteDateRangeConditions() (i.e. sqliteTimestampEpochExpr() built from
sqliteTimestampTextExpr()) and add a stable tiebreaker (e.g. id or rowid) to the
ORDER BY so results are consistently ordered for LIMIT/OFFSET pagination.
- Around line 397-409: parseUsageTimestamp is unused and duplicates parsing
logic in GetUsageLog; either remove the helper or replace the duplicate parse
chain in GetUsageLog to call parseUsageTimestamp(ts). If you choose reuse,
update GetUsageLog to call parseUsageTimestamp for all timestamp parses and
ensure the existing slog.Warn still includes the request_id context (preserve
the "request_id" key/value when logging the failed parse) so log output remains
identical. Remove any now-unused imports/variables if you delete the helper.

In `@internal/usage/store_sqlite.go`:
- Around line 74-77: The retention delete uses the raw text column causing
incorrect ordering for legacy rows; change the DELETE SQL that currently uses
"timestamp < ?" to compare the normalized/epoch expression instead, e.g. use
"unixepoch(REPLACE(timestamp, ' ', 'T')) < unixepoch(?)" (or equivalent) so it
matches the index created and ensures correct retention semantics for both
space- and 'T'-separated timestamps; update the SQL string where the delete is
executed (the DELETE FROM usage WHERE timestamp < ? statement) and ensure the
bound parameter is passed in a compatible timestamp/ISO format or converted with
unixepoch(?) as well.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5b0b19af-e0f7-4941-8cec-39060d1e7f36

📥 Commits

Reviewing files that changed from the base of the PR and between 4d4b3a9 and ca1ae01.

📒 Files selected for processing (8)
  • internal/admin/dashboard/static/css/dashboard.css
  • internal/admin/dashboard/static/js/modules/charts.js
  • internal/admin/dashboard/static/js/modules/charts.test.js
  • internal/admin/dashboard/static/js/modules/dashboard-layout.test.js
  • internal/admin/dashboard/templates/layout.html
  • internal/usage/reader_sqlite.go
  • internal/usage/reader_sqlite_boundary_test.go
  • internal/usage/store_sqlite.go

Comment on lines +146 to +151
const end = this.customEndDate ? new Date(this.customEndDate) : this.todayDate();
let start = this.customStartDate ? new Date(this.customStartDate) : new Date(end);
if (!this.customStartDate) {
start.setDate(start.getDate() - (parseInt(this.days, 10) - 1));
start = this.dateKeyToDate(
this.addDaysToDateKey(this.dateToDateKey(end), -(parseInt(this.days, 10) - 1))
);
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.

🧹 Nitpick | 🔵 Trivial

Inconsistent date parsing: use dateKeyToDate() for UTC consistency.

Lines 146-147 use new Date(this.customEndDate) directly, while lines 149-151 correctly use the UTC-safe dateKeyToDate() helper. The new Date("YYYY-MM-DD") parsing behavior can vary by implementation, whereas dateKeyToDate() explicitly uses Date.UTC() for consistent results.

♻️ Proposed fix for consistent UTC date handling
-            const end = this.customEndDate ? new Date(this.customEndDate) : this.todayDate();
-            let start = this.customStartDate ? new Date(this.customStartDate) : new Date(end);
+            const end = this.customEndDate ? this.dateKeyToDate(this.customEndDate) : this.todayDate();
+            let start = this.customStartDate ? this.dateKeyToDate(this.customStartDate) : new Date(end);
📝 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
const end = this.customEndDate ? new Date(this.customEndDate) : this.todayDate();
let start = this.customStartDate ? new Date(this.customStartDate) : new Date(end);
if (!this.customStartDate) {
start.setDate(start.getDate() - (parseInt(this.days, 10) - 1));
start = this.dateKeyToDate(
this.addDaysToDateKey(this.dateToDateKey(end), -(parseInt(this.days, 10) - 1))
);
const end = this.customEndDate ? this.dateKeyToDate(this.customEndDate) : this.todayDate();
let start = this.customStartDate ? this.dateKeyToDate(this.customStartDate) : new Date(end);
if (!this.customStartDate) {
start = this.dateKeyToDate(
this.addDaysToDateKey(this.dateToDateKey(end), -(parseInt(this.days, 10) - 1))
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/admin/dashboard/static/js/modules/charts.js` around lines 146 - 151,
The code constructs `end` with new Date(this.customEndDate) which is
inconsistent with the UTC-safe helpers; change the `end` creation to use the
same date-key helpers (use `dateToDateKey`/`dateKeyToDate`) so both custom and
default paths go through `dateKeyToDate` for UTC-consistent parsing; update the
`end` assignment (and keep existing `start` logic) to derive a date key from
`this.customEndDate` or `this.todayDate()` and convert it with `dateKeyToDate`
(referencing `customEndDate`, `todayDate`, `dateToDateKey`, `dateKeyToDate`,
`addDaysToDateKey`, and `days`).

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

♻️ Duplicate comments (1)
internal/admin/dashboard/static/css/dashboard.css (1)

108-119: ⚠️ Potential issue | 🟠 Major

Allow the sticky sidebar to scroll internally.

With position: sticky and height: 100vh, the added Settings item plus the footer controls can be clipped on shorter viewports or high zoom because the sidebar itself never becomes scrollable. Please add vertical overflow on the sidebar container.

Suggested diff
 .sidebar {
     flex: 0 0 var(--sidebar-width);
     width: var(--sidebar-width);
     background: var(--bg-surface);
     border-right: 1px solid var(--border);
     display: flex;
     flex-direction: column;
     position: sticky;
     top: 0;
     height: 100vh;
+    overflow-y: auto;
     z-index: 10;
     transition: flex-basis 0.2s, width 0.2s;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/admin/dashboard/static/css/dashboard.css` around lines 108 - 119,
The .sidebar container is sticky with height: 100vh so its contents can be
clipped on small viewports; make the sidebar scrollable by adding vertical
overflow (e.g., set overflow-y: auto and optionally -webkit-overflow-scrolling:
touch) to the .sidebar rule so extra items and footer controls can scroll inside
the fixed sidebar while preserving its sticky positioning and transitions.
🤖 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/modules/timezone-layout.test.js`:
- Around line 19-33: The assertions are fragile because they depend on attribute
order and exact markup; update the tests to assert each binding/element
independently (e.g. check existence of x-ref="timezoneOverrideSelect",
x-model="timezoneOverride", and x-effect containing "timezoneOptions.length;
timezoneOverride" as separate matches) or parse the template fragment into a DOM
and assert attributes via selectors (for class="settings-panel-header" and its
x-data, button with class timezone-help-toggle and its `@click`, span with x-text
expression, p#timezone-help-copy with x-show and x-transition,
x-text="calendarTimeZoneText()", presence of usage-ts and audit-entry-meta
elements with formatTimestamp(...) and :title="timestampTitle(...)", and the
button with class audit-conversation-trigger and type="button"); remove combined
regexes like the compound select/option patterns and replace them with
per-attribute or per-element checks so tests are order-independent.

In `@internal/admin/dashboard/templates/index.html`:
- Around line 234-235: The help text inside the element with id
"timezone-help-copy" incorrectly says log rows render in the browser timezone;
update the copy to state that usage/audit rows render in the "effective
timezone" (since formatTimestamp() uses the effective timezone when an override
is set). Change the string in the template to mention "effective timezone" and
update the related fixture/test expectation to match the new wording so tests
assert the updated help copy.

---

Duplicate comments:
In `@internal/admin/dashboard/static/css/dashboard.css`:
- Around line 108-119: The .sidebar container is sticky with height: 100vh so
its contents can be clipped on small viewports; make the sidebar scrollable by
adding vertical overflow (e.g., set overflow-y: auto and optionally
-webkit-overflow-scrolling: touch) to the .sidebar rule so extra items and
footer controls can scroll inside the fixed sidebar while preserving its sticky
positioning and transitions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 19c0bf74-3127-4092-9778-07c5331a1209

📥 Commits

Reviewing files that changed from the base of the PR and between ca1ae01 and d240691.

📒 Files selected for processing (3)
  • internal/admin/dashboard/static/css/dashboard.css
  • internal/admin/dashboard/static/js/modules/timezone-layout.test.js
  • internal/admin/dashboard/templates/index.html

Comment on lines +234 to +235
<p id="timezone-help-copy" class="settings-panel-copy settings-panel-copy-collapsible" x-show="timezoneHelpOpen" x-transition.opacity.duration.200ms>
Day-based analytics, charts, and date filters use your effective timezone. Usage and audit logs keep UTC in the hover title while rendering row timestamps in your local browser timezone.
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 | 🟡 Minor

Fix the help copy to describe the effective timezone.

formatTimestamp() renders usage/audit rows in the effective timezone, so once a manual override is set these rows are no longer in the browser timezone. The current copy will tell users the override affects day grouping but not log rendering, which is inaccurate. Please switch this wording to “effective timezone” and update the fixture test with it.

Suggested diff
-                    Day-based analytics, charts, and date filters use your effective timezone. Usage and audit logs keep UTC in the hover title while rendering row timestamps in your local browser timezone.
+                    Day-based analytics, charts, and date filters use your effective timezone. Usage and audit logs keep UTC in the hover title while rendering row timestamps in your effective timezone.
🤖 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 234 - 235, The
help text inside the element with id "timezone-help-copy" incorrectly says log
rows render in the browser timezone; update the copy to state that usage/audit
rows render in the "effective timezone" (since formatTimestamp() uses the
effective timezone when an override is set). Change the string in the template
to mention "effective timezone" and update the related fixture/test expectation
to match the new wording so tests assert the updated help copy.

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.

♻️ Duplicate comments (1)
internal/usage/reader_sqlite.go (1)

110-110: ⚠️ Potential issue | 🟠 Major

Preserve sub-second ordering in usage-log queries.

sqliteTimestampEpochExpr() uses SQLite's unixepoch(), which returns an integer and truncates fractional seconds. Line 110 will order all rows within the same second by id instead of actual event time, since timestamps are stored with time.RFC3339Nano precision.

Suggested fix
-		FROM usage` + where + ` ORDER BY ` + sqliteTimestampEpochExpr() + ` DESC, id DESC LIMIT ? OFFSET ?`
+		FROM usage` + where + ` ORDER BY julianday(` + sqliteTimestampTextExpr() + `) DESC, id DESC LIMIT ? OFFSET ?`

Also add a regression test with two rows in the same second but different fractional parts to prevent future regressions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/usage/reader_sqlite.go` at line 110, The ORDER BY currently uses
sqliteTimestampEpochExpr() which truncates fractional seconds; update
sqliteTimestampEpochExpr() to return an epoch expression that preserves
fractional seconds (for example compute epoch from julianday(timestamp) so the
fractional part is kept, e.g. (julianday(<timestamp_column>) - 2440587.5) *
86400.0) so the ORDER BY ... DESC, id DESC line uses a real-valued epoch with
sub-second precision; then add a regression test that inserts two rows within
the same second but with different fractional parts and asserts the more-recent
fractional timestamp sorts first.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@internal/usage/reader_sqlite.go`:
- Line 110: The ORDER BY currently uses sqliteTimestampEpochExpr() which
truncates fractional seconds; update sqliteTimestampEpochExpr() to return an
epoch expression that preserves fractional seconds (for example compute epoch
from julianday(timestamp) so the fractional part is kept, e.g.
(julianday(<timestamp_column>) - 2440587.5) * 86400.0) so the ORDER BY ... DESC,
id DESC line uses a real-valued epoch with sub-second precision; then add a
regression test that inserts two rows within the same second but with different
fractional parts and asserts the more-recent fractional timestamp sorts first.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: f16af65c-2504-4cf5-b315-df4b0d18c7dd

📥 Commits

Reviewing files that changed from the base of the PR and between d240691 and 2a1cce2.

📒 Files selected for processing (8)
  • internal/admin/dashboard/static/css/dashboard.css
  • internal/admin/dashboard/static/js/modules/dashboard-layout.test.js
  • internal/admin/dashboard/static/js/modules/timezone-layout.test.js
  • internal/admin/dashboard/templates/index.html
  • internal/admin/dashboard/templates/layout.html
  • internal/usage/reader_sqlite.go
  • internal/usage/reader_sqlite_boundary_test.go
  • internal/usage/store_sqlite.go

@SantiagoDePolonia SantiagoDePolonia merged commit 3740a6c into main Mar 29, 2026
16 checks passed
@SantiagoDePolonia SantiagoDePolonia deleted the fix/timezones-dashboard branch April 1, 2026 21:29
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