fix(admin): honor user timezones in dashboard#185
Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 | 🟡 MinorAdd
type="button"attribute to prevent form submission behavior.Per HTMLHint, the
<button>element should have an explicittypeattribute. Without it, the default istype="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
📒 Files selected for processing (25)
internal/admin/dashboard/static/css/dashboard.cssinternal/admin/dashboard/static/js/dashboard.jsinternal/admin/dashboard/static/js/modules/charts.jsinternal/admin/dashboard/static/js/modules/charts.test.jsinternal/admin/dashboard/static/js/modules/contribution-calendar.jsinternal/admin/dashboard/static/js/modules/date-picker.jsinternal/admin/dashboard/static/js/modules/request-cancellation.test.jsinternal/admin/dashboard/static/js/modules/timestamp-format.test.jsinternal/admin/dashboard/static/js/modules/timezone-layout.test.jsinternal/admin/dashboard/static/js/modules/timezone.jsinternal/admin/dashboard/static/js/modules/timezone.test.jsinternal/admin/dashboard/static/js/modules/usage.jsinternal/admin/dashboard/templates/index.htmlinternal/admin/dashboard/templates/layout.htmlinternal/admin/handler.gointernal/admin/handler_test.gointernal/auditlog/reader_sqlite.gointernal/auditlog/reader_sqlite_boundary_test.gointernal/usage/reader.gointernal/usage/reader_mongodb.gointernal/usage/reader_postgresql.gointernal/usage/reader_sqlite.gointernal/usage/reader_sqlite_boundary_test.gointernal/usage/reader_sqlite_timezone_test.gointernal/usage/timezone.go
internal/admin/dashboard/static/js/modules/contribution-calendar.js
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
♻️ Duplicate comments (1)
internal/usage/reader_sqlite.go (1)
163-165:⚠️ Potential issue | 🟠 MajorFix 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:00vs2026-01-15T23:30:00Z) still sort incorrectly—+02:00offsets come afterZlexicographically, inverting chronological order.This causes incorrect results in:
sqliteDateRangeConditions()(lines 163–178): Range filters return wrong rowssqliteGroupExpr()CASE statement (lines 308–316): Segment boundary routing breakssqliteGroupingRange()(lines 328–343): Min/max derivation errors with DST transitionsConvert 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
📒 Files selected for processing (10)
internal/admin/dashboard/static/js/dashboard.jsinternal/admin/dashboard/static/js/modules/contribution-calendar.jsinternal/admin/dashboard/static/js/modules/request-cancellation.test.jsinternal/admin/dashboard/static/js/modules/timezone-layout.test.jsinternal/admin/dashboard/static/js/modules/timezone.jsinternal/admin/dashboard/static/js/modules/timezone.test.jsinternal/admin/dashboard/templates/index.htmlinternal/usage/reader_sqlite.gointernal/usage/reader_sqlite_boundary_test.gointernal/usage/store_sqlite.go
There was a problem hiding this comment.
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
📒 Files selected for processing (8)
internal/admin/dashboard/static/css/dashboard.cssinternal/admin/dashboard/static/js/modules/charts.jsinternal/admin/dashboard/static/js/modules/charts.test.jsinternal/admin/dashboard/static/js/modules/dashboard-layout.test.jsinternal/admin/dashboard/templates/layout.htmlinternal/usage/reader_sqlite.gointernal/usage/reader_sqlite_boundary_test.gointernal/usage/store_sqlite.go
| 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)) | ||
| ); |
There was a problem hiding this comment.
🧹 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.
| 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`).
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
internal/admin/dashboard/static/css/dashboard.css (1)
108-119:⚠️ Potential issue | 🟠 MajorAllow the sticky sidebar to scroll internally.
With
position: stickyandheight: 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
📒 Files selected for processing (3)
internal/admin/dashboard/static/css/dashboard.cssinternal/admin/dashboard/static/js/modules/timezone-layout.test.jsinternal/admin/dashboard/templates/index.html
| <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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
♻️ Duplicate comments (1)
internal/usage/reader_sqlite.go (1)
110-110:⚠️ Potential issue | 🟠 MajorPreserve sub-second ordering in usage-log queries.
sqliteTimestampEpochExpr()uses SQLite'sunixepoch(), which returns an integer and truncates fractional seconds. Line 110 will order all rows within the same second byidinstead of actual event time, since timestamps are stored withtime.RFC3339Nanoprecision.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
📒 Files selected for processing (8)
internal/admin/dashboard/static/css/dashboard.cssinternal/admin/dashboard/static/js/modules/dashboard-layout.test.jsinternal/admin/dashboard/static/js/modules/timezone-layout.test.jsinternal/admin/dashboard/templates/index.htmlinternal/admin/dashboard/templates/layout.htmlinternal/usage/reader_sqlite.gointernal/usage/reader_sqlite_boundary_test.gointernal/usage/store_sqlite.go
Summary
Test Plan
go test ./...node --test internal/admin/dashboard/static/js/modules/*.test.jsSummary by CodeRabbit
New Features
Bug Fixes
UI
Tests