Skip to content

feat(dashboard): stream live audit and usage logs#334

Merged
SantiagoDePolonia merged 16 commits into
mainfrom
feat/low-latency-logs
May 17, 2026
Merged

feat(dashboard): stream live audit and usage logs#334
SantiagoDePolonia merged 16 commits into
mainfrom
feat/low-latency-logs

Conversation

@SantiagoDePolonia
Copy link
Copy Markdown
Contributor

@SantiagoDePolonia SantiagoDePolonia commented May 15, 2026

Summary

  • add an in-process live log broker and SSE endpoint for dashboard audit/usage previews
  • stream sequential audit lifecycle updates with compact rows and lazy detail loading
  • mark live workflow chart current steps in blue and only mark audit/usage persisted after flush events
  • add dashboard runtime config and docs for live logs controls

Tests

  • go test ./...
  • node --test internal/admin/dashboard/static/js/modules/*.test.cjs
  • commit hook: make test-race, make lint, dashboard JS tests, perf guard

Summary by CodeRabbit

  • New Features

    • Dashboard: real-time SSE streaming of audit & usage previews, cursored replay, heartbeat, start/stop controls, single-entry audit detail endpoint, UI live-state styling and current workflow step highlighting.
  • Configuration

    • New env/runtime flags to enable live logs and tune buffer (10000), replay limit (1000), and heartbeat (15s); templates and runtime defaults updated.
  • Documentation

    • Docs and README expanded with live-logs settings, behavior, and examples.
  • Tests

    • Extensive unit/integration coverage for broker, handler, client parsing, merge logic, and end-to-end live flows.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 15, 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
  • ✅ Review completed - (🔄 Check again to review again)
📝 Walkthrough

Walkthrough

Adds end-to-end realtime dashboard live logs: config flags, an in-process Broker with bounded replay and snapshots, audit/usage live-event emission, SSE handlers (/admin/live/logs, /admin/audit/detail), App wiring, a browser live-logs module with merge logic, UI styles, tests, and docs.

Changes

Dashboard Live Logs Streaming Feature

Layer / File(s) Summary
Configuration and Broker core
config/admin.go, config/config.go, config/config_test.go, internal/live/broker.go, internal/live/broker_test.go
New AdminConfig fields and defaults for live logs; Broker with sequence/replay window, Subscription handle, publish/replay logic, snapshot merging, and broker tests.
Audit & Usage publishers and loggers
internal/auditlog/auditlog.go, internal/auditlog/constants.go, internal/auditlog/logger.go, internal/auditlog/middleware.go, internal/usage/usage.go, internal/usage/logger.go
Live-event constants/interfaces; Logger support for SetLivePublisher/PublishLiveEvent, live queues and loops, per-entry live outcomes on write/flush, and middleware publishing updates.
App integration
internal/app/app.go, internal/app/app_test.go
Broker created from Admin config, injected into audit/usage loggers, passed to admin init/handler, runtime flag exposure, and closed on shutdown with ordering test.
Admin handlers & routes
internal/admin/handler.go, internal/admin/handler_audit.go, internal/admin/handler_live.go, internal/admin/routes.go, internal/admin/routes_test.go, internal/admin/handler_test.go
SSE LiveLogs handler with cursor/type filtering, replay/reset, heartbeat emission; AuditLogDetail handler; route registration and tests.
Dashboard frontend & visualization
internal/admin/dashboard/static/js/modules/live-logs.js, internal/admin/dashboard/static/js/modules/live-logs.test.cjs, internal/admin/dashboard/static/js/dashboard.js, internal/admin/dashboard/static/js/modules/audit-list.js, internal/admin/dashboard/static/js/modules/workflows.js, internal/admin/dashboard/static/css/dashboard.css, internal/admin/dashboard/templates/layout.html, internal/admin/dashboard/templates/page-audit-logs.html, internal/admin/dashboard/templates/audit-pane.html
Browser dashboardLiveLogsModule streams/parses/merges events into audit/usage lists, fetches audit detail, reconnects/backoff, gating/merge rules, workflow live-step rendering, CSS .workflow-node-current and live-in-progress styles; init coordinates live start/stop and template bindings updated.
Tests, docs & templates
internal/*_test.go, internal/admin/dashboard/static/js/*test.cjs, CLAUDE.md, README.md, docs/advanced/*, .env.template
Extensive tests for broker, handlers, frontend module, logger failure paths; documentation and .env.template updated for DASHBOARD_LIVE_LOGS_* variables; template adjustments for audit pane/layout.

Sequence Diagram

sequenceDiagram
  participant Browser
  participant LiveLogsHandler
  participant Broker
  participant DashboardModule
  Browser->>LiveLogsHandler: GET /admin/live/logs?cursor=...
  LiveLogsHandler->>Broker: Subscribe(cursor)
  Broker-->>LiveLogsHandler: Replay events (filtered)
  LiveLogsHandler->>Browser: SSE replay frames
  loop live stream
    Broker->>LiveLogsHandler: publish Event
    LiveLogsHandler->>Browser: SSE Event
    Browser->>DashboardModule: parse & applyLiveLogEvent
    DashboardModule->>DashboardModule: merge into audit/usage lists
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested labels

release:internal

"I hop and watch the logs align,
Broker counts each event in time,
Blue nodes light where live entries stream,
Dash hums along the realtime dream,
A rabbit cheers this streaming rhyme!"

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/low-latency-logs

Comment thread internal/admin/dashboard/static/js/dashboard.js Fixed
Comment thread internal/admin/dashboard/static/js/dashboard.js Fixed
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 15, 2026

Greptile Summary

This PR adds live audit and usage log streaming to the admin dashboard. It changes:

  • A new in-process live log broker with bounded replay and SSE streaming.
  • Dashboard JavaScript for live audit/usage row merging and lazy audit detail loading.
  • Workflow chart styling for current live steps and persisted async states.
  • Admin runtime config, environment docs, and tests for live log behavior.

Confidence Score: 4/5

This is close, but the reconnect cursor bug should be fixed before merging.

  • A stale cursor after a broker restart can skip replay and leave live dashboard rows missing.
  • The issue is contained to live log reconnect behavior.
  • The rest of the changed paths follow the existing admin auth and dashboard patterns.

internal/live/broker.go

Important Files Changed

Filename Overview
internal/live/broker.go Adds the live replay broker and active snapshot tracking for dashboard streams.
internal/admin/dashboard/static/js/modules/live-logs.js Adds client-side SSE parsing, cursor reconnects, and live audit/usage merging.
internal/admin/handler_live.go Adds the admin SSE endpoint for filtered live log streams.

Reviews (4): Last reviewed commit: "fix(live): snapshot nested preview data" | Re-trigger Greptile

Comment thread internal/admin/handler_live.go
Comment thread internal/live/broker.go
Comment thread internal/live/broker.go Outdated
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented May 15, 2026

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 79.86231% with 117 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/live/broker.go 88.48% 23 Missing and 18 partials ⚠️
internal/app/app.go 10.71% 25 Missing ⚠️
internal/admin/handler_live.go 75.00% 12 Missing and 12 partials ⚠️
internal/auditlog/middleware.go 36.36% 12 Missing and 2 partials ⚠️
internal/auditlog/logger.go 77.27% 3 Missing and 2 partials ⚠️
internal/admin/handler_audit.go 80.95% 2 Missing and 2 partials ⚠️
internal/usage/logger.go 81.81% 2 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

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: 4

Caution

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

⚠️ Outside diff range comments (1)
internal/auditlog/logger.go (1)

205-215: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Emit a terminal live event when batch persistence fails.

At Line 205, WriteBatch failure drops the whole batch, but only the success path emits LiveEventAuditFlushed. That leaves previously published audit.completed entries stuck as pending in live clients forever.

Suggested fix
 if err := l.store.WriteBatch(ctx, batch); err != nil {
     slog.Error("failed to write audit log batch",
         "error", err,
         "count", len(batch),
     )
+    for _, entry := range batch {
+        l.PublishLiveEvent(LiveEventAuditRemoved, entry)
+    }
     return
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/auditlog/logger.go` around lines 205 - 215, The WriteBatch error
path currently logs the failure but does not notify live clients, leaving prior
audit.completed events pending; after the slog.Error call in the WriteBatch
error branch (where l.store.WriteBatch is invoked), iterate the batch and call
l.PublishLiveEvent for each entry with a terminal failure event (e.g.,
LiveEventAuditFailed) so live clients receive a terminal state; use the same
entry objects from batch and the existing l.PublishLiveEvent method to mark them
failed (mirroring the success loop that uses LiveEventAuditFlushed) so pending
entries are cleared.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CLAUDE.md`:
- Line 121: The documentation lists only defaults for DASHBOARD_LIVE_LOGS_* but
lacks actionable tuning guidance; update the CLAUDE.md entry for
DASHBOARD_LIVE_LOGS_ENABLED / DASHBOARD_LIVE_LOGS_BUFFER_SIZE /
DASHBOARD_LIVE_LOGS_REPLAY_LIMIT / DASHBOARD_LIVE_LOGS_HEARTBEAT_SECONDS to
include one-line guidance for each: when to increase (e.g., high traffic, large
spikes, long client reconnect windows), when to decrease (e.g., memory/latency
constraints), and minimal example thresholds (e.g., buffer size increase for
>1000 msgs/sec, replay limit for long reconnects, heartbeat lower for frequent
client liveness) so operators know how to tune rather than only seeing defaults.

In `@config/config_test.go`:
- Around line 200-217: The test assertions for cfg.Admin.LiveLogs* require a
clean environment; extend the existing clearAllConfigEnvVars helper to also
unset the four new env vars (DASHBOARD_LIVE_LOGS_ENABLED,
DASHBOARD_LIVE_LOGS_BUFFER_SIZE, DASHBOARD_LIVE_LOGS_REPLAY_LIMIT,
DASHBOARD_LIVE_LOGS_HEARTBEAT_SECONDS) so the Admin defaults used by the config
tests (cfg.Admin.EndpointsEnabled, UIEnabled, LiveLogsEnabled,
LiveLogsBufferSize, LiveLogsReplayLimit, LiveLogsHeartbeatSeconds) are
deterministic; update clearAllConfigEnvVars to call os.Unsetenv for those four
keys (or remove them before the test) so the assertions in config_test.go always
run against default values.

In `@internal/admin/handler_audit.go`:
- Around line 176-206: Add Swagger/OpenAPI annotations for the AuditLogDetail
handler to match the pattern used by AuditLog and AuditConversation: annotate
the AuditLogDetail function (AuditLogDetail) with operation summary,
description, tags (e.g., "admin"), parameters (query param "log_id" required),
success response schema (returning an audit log entry / 200), and error
responses (400/404/500). Place these comments immediately above the
AuditLogDetail function declaration so the API generator picks them up and
ensure the parameter name and response type match the types used in
auditlog.LogEntry and the existing audit endpoints.

In `@README.md`:
- Line 277: Update the README config table to include the three missing
dashboard live-log controls: DASHBOARD_LIVE_LOGS_BUFFER_SIZE,
DASHBOARD_LIVE_LOGS_REPLAY_LIMIT, and DASHBOARD_LIVE_LOGS_HEARTBEAT_SECONDS; for
each add the default value, a one-line description (what it controls and a short
hint when to change it), and keep the existing DASHBOARD_LIVE_LOGS_ENABLED row
style so operators can tune buffering, replay bounds and heartbeat behavior from
the main docs entry point.

---

Outside diff comments:
In `@internal/auditlog/logger.go`:
- Around line 205-215: The WriteBatch error path currently logs the failure but
does not notify live clients, leaving prior audit.completed events pending;
after the slog.Error call in the WriteBatch error branch (where
l.store.WriteBatch is invoked), iterate the batch and call l.PublishLiveEvent
for each entry with a terminal failure event (e.g., LiveEventAuditFailed) so
live clients receive a terminal state; use the same entry objects from batch and
the existing l.PublishLiveEvent method to mark them failed (mirroring the
success loop that uses LiveEventAuditFlushed) so pending entries are cleared.
🪄 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: 7a1a2521-0b87-4ac6-9c72-194aa67d680e

📥 Commits

Reviewing files that changed from the base of the PR and between ddd80ae and 6bf7428.

📒 Files selected for processing (29)
  • CLAUDE.md
  • README.md
  • config/admin.go
  • config/config.go
  • config/config_test.go
  • internal/admin/dashboard/static/css/dashboard.css
  • internal/admin/dashboard/static/js/dashboard.js
  • internal/admin/dashboard/static/js/modules/audit-list.js
  • internal/admin/dashboard/static/js/modules/live-logs.js
  • internal/admin/dashboard/static/js/modules/live-logs.test.cjs
  • internal/admin/dashboard/static/js/modules/workflows.js
  • internal/admin/dashboard/static/js/modules/workflows.test.cjs
  • internal/admin/dashboard/templates/layout.html
  • internal/admin/handler.go
  • internal/admin/handler_audit.go
  • internal/admin/handler_live.go
  • internal/admin/handler_test.go
  • internal/admin/routes.go
  • internal/admin/routes_test.go
  • internal/app/app.go
  • internal/app/app_test.go
  • internal/auditlog/auditlog.go
  • internal/auditlog/constants.go
  • internal/auditlog/logger.go
  • internal/auditlog/middleware.go
  • internal/live/broker.go
  • internal/live/broker_test.go
  • internal/usage/logger.go
  • internal/usage/usage.go

Comment thread CLAUDE.md Outdated
Comment thread config/config_test.go
Comment thread internal/admin/handler_audit.go
Comment thread README.md
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: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/admin/handler_live.go`:
- Around line 17-87: Add unit tests for LiveLogs exercising cursor parsing, type
filtering, replay/reset behavior, heartbeat pings, and live event forwarding by
creating tests that call Handler.LiveLogs with controlled inputs; specifically,
add *_test.go cases that (1) pass an invalid cursor string to ensure liveCursor
error handling, (2) set query "types" to verify liveTypeFilter excludes/accepts
events, (3) supply a mocked/standalone liveBroker Subscribe result where
sub.Reset is true and sub.Replay contains events to assert writeLiveEvent sends
a reset and replayed events, and (4) provide a Subscribe with an Events channel
and Heartbeat interval to assert periodic heartbeat events and forwarding of
live events. Implement a test liveBroker stub (implementing Enabled, Subscribe,
LatestSeq, Heartbeat) and use an echo request/response recorder to capture SSE
output for assertions, ensuring to close the Events channel to end the handler
loop.
- Around line 101-125: The current liveTypeFilter(raw string) +
liveLogTypeFilter.matches(eventType string) treats an input like "types=foo" (no
valid tokens) the same as no filter and returns true for all events; fix by
distinguishing "no filter provided" from "filter provided but no valid tokens":
change liveLogTypeFilter to carry a boolean (e.g., provided) or equivalent
sentinel set by liveTypeFilter when raw is non-empty, set provided=true only if
raw was given (even if no tokens were valid), and then update matches to return
true when provided==false (no filter specified), but return false when
provided==true and the internal map is empty (filter provided but contained no
valid tokens); update liveTypeFilter(raw string) to trim raw and set the
provided flag accordingly and populate the map only with valid tokens.

In `@internal/live/broker.go`:
- Around line 245-248: When the replay buffer exceeds b.bufferSize, avoid
allocating a new slice; instead shift entries left in-place with copy(b.events,
b.events[drop:]), zero out the now-unused tail slots to avoid retaining
references (e.g. for i := b.bufferSize; i < len(b.events); i++ { b.events[i] =
Event{} }), and then reslice with b.events = b.events[:b.bufferSize]; replace
the current append([]Event(nil), ...) reallocation with this in-place trim using
the identifiers b.events, b.bufferSize, and drop.
🪄 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: 98702627-7b01-4603-81ae-effb459b69e8

📥 Commits

Reviewing files that changed from the base of the PR and between 7b499b9 and 54e97b8.

📒 Files selected for processing (5)
  • internal/admin/handler_live.go
  • internal/app/app.go
  • internal/app/app_test.go
  • internal/live/broker.go
  • internal/live/broker_test.go

Comment on lines +17 to +87
// LiveLogs handles GET /admin/live/logs.
func (h *Handler) LiveLogs(c *echo.Context) error {
if h.liveBroker == nil || !h.liveBroker.Enabled() {
return handleError(c, featureUnavailableError("live logs are unavailable"))
}

cursor, err := liveCursor(c.QueryParam("cursor"))
if err != nil {
return handleError(c, err)
}
filter := liveTypeFilter(c.QueryParam("types"))
sub := h.liveBroker.Subscribe(cursor)
if sub == nil {
return handleError(c, featureUnavailableError("live logs are unavailable"))
}
defer sub.Close()

res := c.Response()
// SSE responses are intentionally long-lived; keep disconnect detection via writes.
_ = http.NewResponseController(res).SetWriteDeadline(time.Time{})
res.Header().Set(echo.HeaderContentType, "text/event-stream")
res.Header().Set(echo.HeaderCacheControl, "no-cache, no-transform")
res.Header().Set(echo.HeaderConnection, "keep-alive")
res.Header().Set("X-Accel-Buffering", "no")
res.WriteHeader(http.StatusOK)

if sub.Reset {
if err := writeLiveEvent(res, live.Event{
Seq: h.liveBroker.LatestSeq(),
Type: live.EventReset,
}); err != nil {
return err
}
}
for _, event := range sub.Replay {
if !filter.matches(event.Type) {
continue
}
if err := writeLiveEvent(res, event); err != nil {
return err
}
}

ticker := time.NewTicker(h.liveBroker.Heartbeat())
defer ticker.Stop()

ctx := c.Request().Context()
for {
select {
case <-ctx.Done():
return nil
case event, ok := <-sub.Events:
if !ok {
return nil
}
if !filter.matches(event.Type) {
continue
}
if err := writeLiveEvent(res, event); err != nil {
return err
}
case <-ticker.C:
if err := writeLiveEvent(res, live.Event{
Seq: h.liveBroker.LatestSeq(),
Type: live.EventHeartbeat,
}); err != nil {
return err
}
}
}
}
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.

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Add targeted tests for cursor/type parsing and SSE replay/reset flow.

This endpoint introduces multiple control-path branches (invalid cursor, type filtering, reset replay, heartbeat/live forwarding), but file-level coverage is currently missing in this PR context.

As per coding guidelines: **/*_test.go: Add or update tests for behavior changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/admin/handler_live.go` around lines 17 - 87, Add unit tests for
LiveLogs exercising cursor parsing, type filtering, replay/reset behavior,
heartbeat pings, and live event forwarding by creating tests that call
Handler.LiveLogs with controlled inputs; specifically, add *_test.go cases that
(1) pass an invalid cursor string to ensure liveCursor error handling, (2) set
query "types" to verify liveTypeFilter excludes/accepts events, (3) supply a
mocked/standalone liveBroker Subscribe result where sub.Reset is true and
sub.Replay contains events to assert writeLiveEvent sends a reset and replayed
events, and (4) provide a Subscribe with an Events channel and Heartbeat
interval to assert periodic heartbeat events and forwarding of live events.
Implement a test liveBroker stub (implementing Enabled, Subscribe, LatestSeq,
Heartbeat) and use an echo request/response recorder to capture SSE output for
assertions, ensuring to close the Events channel to end the handler loop.

Comment thread internal/admin/handler_live.go Outdated
Comment thread internal/live/broker.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: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/admin/dashboard/static/js/modules/audit-list.js`:
- Around line 285-290: The function formatDurationNs can produce "NaN s" for
non-numeric inputs; modify formatDurationNs to coerce ns to a Number (e.g.,
const v = Number(ns)) and guard with Number.isFinite(v) (or isFinite(v)) before
doing threshold comparisons, returning '-' (or another sentinel) for non-finite
values; then use v in the existing <=0 / <1000000 / <1000000000 branches so only
real numeric durations are formatted.

In `@internal/admin/dashboard/static/js/modules/live-logs.js`:
- Around line 233-236: The code always decrements auditLog.total by 1 when
replacing auditLog.entries with next, which miscounts when multiple entries were
removed; compute the actual removedCount as (previousEntriesLength -
next.length) before assignment (use the current this.auditLog.entries length or
store it in a temp like prevLen), then set this.auditLog.total = Math.max(0,
Number(this.auditLog.total || 0) - removedCount) and finally assign
this.auditLog.entries = next so total reflects how many entries were actually
removed.
- Around line 79-83: The loop in live-logs parsing only checks for '\n\n' so
CRLF-separated frames (\r\n\r\n) are missed; update the delimiter detection in
the while loop inside the code that slices the buffer (the block that calls
this.handleLiveLogsFrame(frame)) to detect either '\n\n' or '\r\n\r\n' (for
example by using a regex /\r?\n\r?\n/ to find the first match and use
match.index and match[0].length to compute splitAt and the slice offset) so
frames are correctly extracted regardless of LF or CRLF line endings.

In `@internal/admin/handler_audit.go`:
- Around line 190-219: Add unit/handler tests covering AuditLogDetail: test the
missing log_id query path returning 400 (call Handler.AuditLogDetail and assert
handleError/invalid request), test auditReader == nil path returning
503/feature-unavailable, test GetLogByID returning (nil, nil) yields 404, test
GetLogByID returning an error yields the propagated error, and test the
successful 200 path where auditReader.GetLogByID returns an entry and
auditLogResponse enriches usage (assert response JSON equals the single entry).
Use mocks/stubs for h.auditReader.GetLogByID and h.auditLogResponse (or inject a
test auditReader and auditLogResponse helper) to simulate each flow and verify
the correct status codes and responses for AuditLogDetail.

In `@internal/admin/handler_live.go`:
- Around line 60-61: Guard against non-positive heartbeat values returned by
h.liveBroker.Heartbeat() before calling time.NewTicker: read the value into a
variable (e.g., hb := h.liveBroker.Heartbeat()), check if hb <= 0 and replace it
with a safe default (e.g., time.Second) or return early, then call
time.NewTicker(hb). Ensure ticker variable and defer ticker.Stop() are only used
when a ticker is created.

In `@internal/auditlog/logger.go`:
- Around line 73-77: Write currently calls l.PublishLiveEvent synchronously
before the non-blocking send to l.buffer, which can make Write block if a
publisher is slow; change this by decoupling live-event emission: introduce a
bounded buffered queue (e.g., liveEventQueue on the logger struct) and have
Write push events into that queue using a non-blocking select (default/drop) so
Write stays non-blocking, and run a dedicated background goroutine that reads
from liveEventQueue and calls l.PublishLiveEvent; apply the same non-blocking
enqueue pattern to all other synchronous PublishLiveEvent sites (the other
blocks around lines 101-112 and 210-218) and add optional metric/log when an
event is dropped.

In `@internal/usage/logger.go`:
- Around line 73-77: The inline calls to l.publishLiveEvent (e.g., in Write
around LiveEventUsageCompleted/LiveEventUsageFailed and the other occurrences at
100-111 and 209-216) can block request handling; instead make publishing
fire-and-forget by introducing a single buffered, bounded channel (e.g.,
l.livePublishChan) and a dedicated background worker goroutine that consumes
that channel and calls publishLiveEvent; in the Write path (and the other
locations) replace direct calls to l.publishLiveEvent with a non-blocking send
to l.livePublishChan (use select { case l.livePublishChan <- event: default: /*
drop or metric increment to indicate backpressure */ }) so publishing has
bounded backpressure and never blocks the request write path.
🪄 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: e243389d-ca84-44ea-a25d-0ceb155a857b

📥 Commits

Reviewing files that changed from the base of the PR and between 54e97b8 and 00dc0b8.

📒 Files selected for processing (21)
  • CLAUDE.md
  • README.md
  • config/config_test.go
  • internal/admin/dashboard/static/css/dashboard.css
  • internal/admin/dashboard/static/js/dashboard.js
  • internal/admin/dashboard/static/js/modules/audit-list.js
  • internal/admin/dashboard/static/js/modules/audit-list.test.cjs
  • internal/admin/dashboard/static/js/modules/live-logs.js
  • internal/admin/dashboard/static/js/modules/live-logs.test.cjs
  • internal/admin/dashboard/templates/page-audit-logs.html
  • internal/admin/handler_audit.go
  • internal/admin/handler_live.go
  • internal/admin/handler_live_test.go
  • internal/auditlog/auditlog.go
  • internal/auditlog/auditlog_test.go
  • internal/auditlog/logger.go
  • internal/live/broker.go
  • internal/live/broker_test.go
  • internal/usage/logger.go
  • internal/usage/usage.go
  • internal/usage/usage_test.go

Comment thread internal/admin/dashboard/static/js/modules/audit-list.js Outdated
Comment thread internal/admin/dashboard/static/js/modules/live-logs.js Outdated
Comment thread internal/admin/dashboard/static/js/modules/live-logs.js Outdated
Comment on lines +190 to +219
func (h *Handler) AuditLogDetail(c *echo.Context) error {
logID := strings.TrimSpace(c.QueryParam("log_id"))
if logID == "" {
return handleError(c, core.NewInvalidRequestError("log_id is required", nil))
}
if h.auditReader == nil {
return handleError(c, featureUnavailableError("audit log detail is unavailable"))
}

entry, err := h.auditReader.GetLogByID(c.Request().Context(), logID)
if err != nil {
return handleError(c, err)
}
if entry == nil {
return handleError(c, core.NewNotFoundError("audit log not found: "+logID))
}

response, err := h.auditLogResponse(c.Request().Context(), &auditlog.LogListResult{
Entries: []auditlog.LogEntry{*entry},
Total: 1,
Limit: 1,
})
if err != nil {
return handleError(c, err)
}
if len(response.Entries) == 0 {
return handleError(c, core.NewNotFoundError("audit log not found: "+logID))
}
return c.JSON(http.StatusOK, response.Entries[0])
}
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 | ⚡ Quick win

Add handler tests for the new AuditLogDetail behavior before merge.

This introduces a new public endpoint path (400/404/200 flows plus usage enrichment reuse), but there are no corresponding test additions in the provided changes, and this file is reported at 0% patch coverage.

As per coding guidelines **/*_test.go: “Add or update tests for behavior changes”.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/admin/handler_audit.go` around lines 190 - 219, Add unit/handler
tests covering AuditLogDetail: test the missing log_id query path returning 400
(call Handler.AuditLogDetail and assert handleError/invalid request), test
auditReader == nil path returning 503/feature-unavailable, test GetLogByID
returning (nil, nil) yields 404, test GetLogByID returning an error yields the
propagated error, and test the successful 200 path where auditReader.GetLogByID
returns an entry and auditLogResponse enriches usage (assert response JSON
equals the single entry). Use mocks/stubs for h.auditReader.GetLogByID and
h.auditLogResponse (or inject a test auditReader and auditLogResponse helper) to
simulate each flow and verify the correct status codes and responses for
AuditLogDetail.

Comment on lines +60 to +61
ticker := time.NewTicker(h.liveBroker.Heartbeat())
defer ticker.Stop()
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 | ⚡ Quick win

Guard ticker creation against non-positive heartbeat values.

time.NewTicker panics on <= 0 durations, and the handler currently uses broker heartbeat directly.

💡 Suggested fix
-	ticker := time.NewTicker(h.liveBroker.Heartbeat())
+	heartbeat := h.liveBroker.Heartbeat()
+	if heartbeat <= 0 {
+		heartbeat = 15 * time.Second
+	}
+	ticker := time.NewTicker(heartbeat)
 	defer ticker.Stop()
📝 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
ticker := time.NewTicker(h.liveBroker.Heartbeat())
defer ticker.Stop()
heartbeat := h.liveBroker.Heartbeat()
if heartbeat <= 0 {
heartbeat = 15 * time.Second
}
ticker := time.NewTicker(heartbeat)
defer ticker.Stop()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/admin/handler_live.go` around lines 60 - 61, Guard against
non-positive heartbeat values returned by h.liveBroker.Heartbeat() before
calling time.NewTicker: read the value into a variable (e.g., hb :=
h.liveBroker.Heartbeat()), check if hb <= 0 and replace it with a safe default
(e.g., time.Second) or return early, then call time.NewTicker(hb). Ensure ticker
variable and defer ticker.Stop() are only used when a ticker is created.

Comment thread internal/auditlog/logger.go
Comment thread internal/usage/logger.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: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/admin/dashboard/static/js/modules/audit-list.js`:
- Around line 203-206: The live-prepend gate (auditLogAllowsLiveEntries)
currently only checks offset and field filters; update it to also prevent live
prepending when a custom date range is active or when the custom range does not
include now. In the auditLogAllowsLiveEntries(payload) function add a check for
this.customStartDate || this.customEndDate (or validate that the provided
customStartDate/customEndDate encompass the current time) and return false if a
custom range is set that excludes now, so live audit rows are only prepended
when no custom date range is in effect (or when the range explicitly includes
the current timestamp).

In `@internal/admin/dashboard/static/js/modules/live-logs.js`:
- Around line 392-397: The code calls this.mergeLiveAuditEntry(payload,
'audit.detail') which replaces the audit row object but then clears
_detail_loading on the old `entry` reference (leaving the live row stuck);
change the flow so the merged/replacement object is the one whose
`_detail_loading` gets cleared — e.g. have mergeLiveAuditEntry return the merged
entry (or lookup the replacement it created) and then set
mergedEntry._detail_loading = false (instead of setting entry._detail_loading),
or update mergeLiveAuditEntry itself to ensure it preserves/clears
`_detail_loading` on the final stored object.

In `@internal/live/broker_test.go`:
- Around line 234-237: The test currently ranges over sub.Events with an
unbounded loop (received := 0; for range sub.Events { received++ }) which can
hang if the channel is never closed; change the loop to be bounded or
timeout-protected by either: a) iterating until an expected count (e.g., for
received < expected { select { case <-sub.Events: received++ case
<-time.After(timeout): t.Fatalf("timed out waiting for events") } }) or b) using
a context with deadline and select on ctx.Done() vs sub.Events; update the loop
around sub.Events accordingly to ensure the test always exits.

In `@internal/live/broker.go`:
- Around line 287-324: The updateActiveSnapshotsLocked logic uses a single
derived key (auditActiveKey/usageActiveKey) so events keyed by id vs request_id
end up in separate slots; change updateActiveSnapshotsLocked to normalize and
operate on both alias keys: compute both primaryKey and aliasKey (derived from
id and request_id), when reading previous entries check both maps
(activeAudit/activeUsage) and merge into the chosen canonical entry via
mergeEventData, when setting store the event under the canonical key and delete
any alternate alias entry, and when deleting (EventAuditFailed/EventUsageFailed
etc.) remove both alias keys; apply the same normalization fix to the other
similar block later in the file that handles the same active snapshot lifecycle.
🪄 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: 935bc44c-48f0-44b3-bd5c-7b276ea0a2cb

📥 Commits

Reviewing files that changed from the base of the PR and between 00dc0b8 and 1f68674.

📒 Files selected for processing (11)
  • internal/admin/dashboard/static/js/modules/audit-list.js
  • internal/admin/dashboard/static/js/modules/audit-list.test.cjs
  • internal/admin/dashboard/static/js/modules/live-logs.js
  • internal/admin/dashboard/static/js/modules/live-logs.test.cjs
  • internal/admin/handler_test.go
  • internal/auditlog/auditlog_test.go
  • internal/auditlog/logger.go
  • internal/live/broker.go
  • internal/live/broker_test.go
  • internal/usage/logger.go
  • internal/usage/usage_test.go

Comment thread internal/admin/dashboard/static/js/modules/audit-list.js
Comment thread internal/admin/dashboard/static/js/modules/live-logs.js Outdated
Comment thread internal/live/broker_test.go Outdated
Comment thread internal/live/broker.go
Comment thread internal/admin/dashboard/static/js/modules/live-logs.js
Comment thread internal/admin/dashboard/static/js/modules/live-logs.js Outdated
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/admin/dashboard/static/js/modules/audit-list.js (1)

203-206: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Disable live-row prepend when a custom date range is active.

Line 203 currently allows live preview merging even for custom historical ranges, which can show out-of-range rows and skew filtered results.

Proposed fix
 auditLogAllowsLiveEntries(payload) {
     return payload && Number(payload.offset || 0) === 0 &&
-        !this.auditSearch && !this.auditMethod && !this.auditStatusCode && !this.auditStream;
+        !this.auditSearch && !this.auditMethod && !this.auditStatusCode && !this.auditStream &&
+        !this.customStartDate && !this.customEndDate;
 },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/admin/dashboard/static/js/modules/audit-list.js` around lines 203 -
206, The current auditLogAllowsLiveEntries function lets live rows prepend even
when a custom date range is set; update the function to also require that no
custom date range is active by returning false if any date-range filters are
present (e.g., this.auditDateFrom or this.auditDateTo); specifically, in
auditLogAllowsLiveEntries(payload) keep the existing checks (payload &&
Number(payload.offset||0)===0 && !this.auditSearch && !this.auditMethod &&
!this.auditStatusCode && !this.auditStream) and add an extra clause that both
date-from and date-to range fields are falsy (for example: &&
!this.auditDateFrom && !this.auditDateTo) so live-row prepend is disabled when a
custom historical range is active.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@internal/admin/dashboard/static/js/modules/audit-list.js`:
- Around line 203-206: The current auditLogAllowsLiveEntries function lets live
rows prepend even when a custom date range is set; update the function to also
require that no custom date range is active by returning false if any date-range
filters are present (e.g., this.auditDateFrom or this.auditDateTo);
specifically, in auditLogAllowsLiveEntries(payload) keep the existing checks
(payload && Number(payload.offset||0)===0 && !this.auditSearch &&
!this.auditMethod && !this.auditStatusCode && !this.auditStream) and add an
extra clause that both date-from and date-to range fields are falsy (for
example: && !this.auditDateFrom && !this.auditDateTo) so live-row prepend is
disabled when a custom historical range is active.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9494333e-c5cd-49d5-a2eb-685287d1a406

📥 Commits

Reviewing files that changed from the base of the PR and between 00dc0b8 and 1f68674.

📒 Files selected for processing (11)
  • internal/admin/dashboard/static/js/modules/audit-list.js
  • internal/admin/dashboard/static/js/modules/audit-list.test.cjs
  • internal/admin/dashboard/static/js/modules/live-logs.js
  • internal/admin/dashboard/static/js/modules/live-logs.test.cjs
  • internal/admin/handler_test.go
  • internal/auditlog/auditlog_test.go
  • internal/auditlog/logger.go
  • internal/live/broker.go
  • internal/live/broker_test.go
  • internal/usage/logger.go
  • internal/usage/usage_test.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: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/admin/dashboard/static/js/modules/live-logs.js`:
- Around line 176-186: The merge path handling 'audit.detail' is incorrectly
driving the lifecycle: when eventType === 'audit.detail' do not compute or
overwrite lifecycle fields—avoid calling
liveAuditStateAfter/liveAuditEventFlushed for this event and preserve
previous._live_state, previous._audit_flushed and previous._live_pending; only
set patch._detail_loaded = true. Update the merge code (the block that computes
liveState, auditFlushed and builds patch in mergeLiveAuditEntry/wherever used)
to short-circuit for 'audit.detail' so it does not flip _live_pending or mark
_audit_flushed, and apply the same change to the other occurrences mentioned
(the blocks at the other ranges).
- Around line 187-190: When merging incoming live patches into currentEntries we
overwrite previously loaded detail payloads because the code replaces
previous.data when patch.data is falsy; update the merge logic in the block that
builds merged (where currentEntries.splice is called) so that if patch.data is
missing but previous.data exists we keep previous.data (i.e., prefer patch.data
when present, otherwise retain previous.data), and ensure the merged object is
created from previous and patch in that order (const merged = { ...previous,
...patch }) with an explicit conditional to preserve previous.data when
patch.data is undefined to avoid dropping audit.detail-loaded request/response
bodies.
- Around line 156-159: The auditLiveInsertAllowed() gate currently ignores the
custom date window and should reject live inserts when a customStartDate or
customEndDate is active; update auditLiveInsertAllowed() to also require that
this.customStartDate and this.customEndDate are not set (e.g., include &&
!this.customStartDate && !this.customEndDate) so live SSE rows are not prepended
while a historical audit range is selected.
🪄 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: 6cf927b7-9127-47b5-b476-f49c94d44cd7

📥 Commits

Reviewing files that changed from the base of the PR and between 1f68674 and 5d1f7b0.

📒 Files selected for processing (6)
  • internal/admin/dashboard/static/js/modules/audit-list.js
  • internal/admin/dashboard/static/js/modules/audit-list.test.cjs
  • internal/admin/dashboard/static/js/modules/live-logs.js
  • internal/admin/dashboard/static/js/modules/live-logs.test.cjs
  • internal/live/broker.go
  • internal/live/broker_test.go

Comment thread internal/admin/dashboard/static/js/modules/live-logs.js
Comment thread internal/admin/dashboard/static/js/modules/live-logs.js
Comment thread internal/admin/dashboard/static/js/modules/live-logs.js
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/admin/dashboard/static/js/modules/live-logs.js (1)

199-204: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Merge data fields when both exist to preserve loaded detail payloads.

When previous.data contains loaded detail (request_headers, response_body, etc.) and patch.data arrives with partial data, the spread { ...previous, ...patch } replaces previous.data entirely. The condition on line 201 only covers when patch.data is undefined, but not when both exist.

Proposed fix
                 if (index >= 0) {
                     const merged = { ...previous, ...patch };
-                    if (patch.data === undefined && previous.data !== undefined) merged.data = previous.data;
+                    if (previous.data && patch.data) {
+                        merged.data = { ...previous.data, ...patch.data };
+                    } else if (patch.data === undefined && previous.data !== undefined) {
+                        merged.data = previous.data;
+                    }
                     currentEntries.splice(index, 1, merged);
                     this.auditLog.entries = [...currentEntries];
                     return merged;
                 }

The same fix should be applied to line 181 in the audit.detail path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/admin/dashboard/static/js/modules/live-logs.js` around lines 199 -
204, The merge logic inside the live-logs update should combine nested data
objects instead of replacing them: when merging previous and patch (the
variables named previous, patch, merged and the array currentEntries), detect if
both previous.data and patch.data are plain objects and set merged.data to a
shallow merge of those two (e.g., {...previous.data, ...patch.data}) so existing
loaded detail fields (request_headers, response_body, etc.) are preserved when
partial patches arrive; keep the existing fallback that uses previous.data if
patch.data is undefined and update this.auditLog.entries accordingly. Apply the
same change to the analogous audit.detail merge path referenced in the comment.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@internal/admin/dashboard/static/js/modules/live-logs.js`:
- Around line 199-204: The merge logic inside the live-logs update should
combine nested data objects instead of replacing them: when merging previous and
patch (the variables named previous, patch, merged and the array
currentEntries), detect if both previous.data and patch.data are plain objects
and set merged.data to a shallow merge of those two (e.g., {...previous.data,
...patch.data}) so existing loaded detail fields (request_headers,
response_body, etc.) are preserved when partial patches arrive; keep the
existing fallback that uses previous.data if patch.data is undefined and update
this.auditLog.entries accordingly. Apply the same change to the analogous
audit.detail merge path referenced in the comment.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 778305fa-79c7-4494-a9e5-d0abf7d98e3a

📥 Commits

Reviewing files that changed from the base of the PR and between 5d1f7b0 and bedbbc2.

📒 Files selected for processing (2)
  • internal/admin/dashboard/static/js/modules/live-logs.js
  • internal/admin/dashboard/static/js/modules/live-logs.test.cjs

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: 1

♻️ Duplicate comments (1)
internal/admin/dashboard/static/js/modules/live-logs.js (1)

156-160: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reuse the same “range includes now” gate for SSE inserts.

auditLogWithLiveEntries() now allows live rows when a custom range still includes the current time, but this path still disables SSE inserts for any custom range. A filter like yesterday → tomorrow will therefore show the initial live rows from fetchAuditLog() and then stop receiving subsequent updates.

Proposed fix
             auditLiveInsertAllowed() {
-                return this.auditLog && this.auditLog.offset === 0 &&
-                    !this.auditSearch && !this.auditMethod && !this.auditStatusCode && !this.auditStream &&
-                    !this.customStartDate && !this.customEndDate;
+                const dateRangeAllowsNow = typeof this.auditLiveDateRangeAllowsNow === 'function'
+                    ? this.auditLiveDateRangeAllowsNow()
+                    : (!this.customStartDate && !this.customEndDate);
+                return this.auditLog && this.auditLog.offset === 0 &&
+                    !this.auditSearch && !this.auditMethod && !this.auditStatusCode && !this.auditStream &&
+                    dateRangeAllowsNow;
             },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/admin/dashboard/static/js/modules/live-logs.js` around lines 156 -
160, auditLiveInsertAllowed() currently disallows SSE inserts whenever a
customStartDate/customEndDate are set, causing live updates to stop even if the
custom range includes now; change auditLiveInsertAllowed to reuse the same
"range includes now" gate used by auditLogWithLiveEntries() (or call that
helper/condition) so it returns true when the effective range includes the
current time even if customStartDate/customEndDate exist, keeping the original
checks for auditSearch/auditMethod/auditStatusCode/auditStream and
auditLog.offset === 0.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/auditlog/logger.go`:
- Around line 118-135: The code currently calls snapshotLiveLogEntry
unconditionally which does a deep clone even when no live publisher exists;
change enqueueLiveEvent to first check whether a publisher is attached (inspect
l.liveEvents and the publisher callback publishLiveEventNow) and return
immediately if neither exists, and only call snapshotLiveLogEntry when you will
actually publish or enqueue (i.e., when l.liveEvents != nil or
publishLiveEventNow != nil); update enqueueLiveEvent to perform this
early-return check and move the snapshotLiveLogEntry call below that guard so
cloning is avoided when no publisher is attached.

---

Duplicate comments:
In `@internal/admin/dashboard/static/js/modules/live-logs.js`:
- Around line 156-160: auditLiveInsertAllowed() currently disallows SSE inserts
whenever a customStartDate/customEndDate are set, causing live updates to stop
even if the custom range includes now; change auditLiveInsertAllowed to reuse
the same "range includes now" gate used by auditLogWithLiveEntries() (or call
that helper/condition) so it returns true when the effective range includes the
current time even if customStartDate/customEndDate exist, keeping the original
checks for auditSearch/auditMethod/auditStatusCode/auditStream and
auditLog.offset === 0.
🪄 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: 44cb7426-88fc-4895-b482-a41a22c375df

📥 Commits

Reviewing files that changed from the base of the PR and between bedbbc2 and 9320be5.

📒 Files selected for processing (10)
  • internal/admin/dashboard/static/js/modules/audit-list.js
  • internal/admin/dashboard/static/js/modules/audit-list.test.cjs
  • internal/admin/dashboard/static/js/modules/dashboard-layout.test.cjs
  • internal/admin/dashboard/static/js/modules/live-logs.js
  • internal/admin/dashboard/static/js/modules/live-logs.test.cjs
  • internal/admin/dashboard/templates/audit-pane.html
  • internal/auditlog/auditlog_test.go
  • internal/auditlog/logger.go
  • internal/live/broker.go
  • internal/live/broker_test.go

Comment thread internal/auditlog/logger.go Outdated
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 16, 2026

Want your agent to iterate on Greptile's feedback? Try greploops.

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: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/auditlog/auditlog_test.go`:
- Around line 443-454: The test double-closes the channel publisher.release when
the timeout branch triggers; remove the redundant close or guard it with a
sync.Once so publisher.release is closed exactly once. Specifically, update the
timeout branch that currently does close(publisher.release) and _ =
logger.Close() to either omit close(publisher.release) (leave the single close
just after the select) or introduce a sync.Once (e.g., var releaseOnce
sync.Once) and replace both close(publisher.release) calls with
releaseOnce.Do(func(){ close(publisher.release) }), keeping the subsequent
logger.Close() handling as-is.

In `@internal/auditlog/logger.go`:
- Around line 204-217: The current cloneLiveJSONValue function does an expensive
json.Marshal/json.Unmarshal round-trip for every value; replace or augment it to
perform a structural deep-copy for common runtime types (at least map[string]any
and []any) to avoid serialization overhead: detect if value is a map[string]any
or []any and recursively copy keys/elements (preserving nested maps/slices and
primitive values) inside cloneLiveJSONValue, falling back to the JSON round-trip
only for unknown types; keep the function signature and behavior but implement
the fast-path deep-copy to reduce allocations and CPU for large audit bodies.

In `@internal/live/broker.go`:
- Around line 607-616: copyRawData currently performs a shallow copy which
preserves references to nested maps/slices and can lead to published events
changing if entry.RawData is mutated later; update copyRawData (used by
usagePreviewFromEntry/entry.RawData) to perform a deep copy of nested structures
(recursively copy map[string]any, []any, and primitive values) so nested maps
and slices are duplicated rather than referenced — you can implement a small
helper (e.g., deepCopyAny) that switches on types and recurses, or use a safe
marshal/unmarshal approach, and replace the current loop to call that helper for
each value before inserting into dst.
🪄 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: d6be8ff3-f0da-4742-8046-afbc62ae9d9a

📥 Commits

Reviewing files that changed from the base of the PR and between 353556c and b099e58.

📒 Files selected for processing (7)
  • internal/admin/dashboard/static/js/modules/live-logs.js
  • internal/app/app.go
  • internal/auditlog/auditlog_test.go
  • internal/auditlog/logger.go
  • internal/live/broker.go
  • internal/usage/logger.go
  • internal/usage/usage_test.go

Comment thread internal/auditlog/auditlog_test.go Outdated
Comment thread internal/auditlog/logger.go Outdated
Comment thread internal/live/broker.go
Comment thread internal/live/broker.go Outdated
Comment on lines +178 to +188
latest := b.events[len(b.events)-1].Seq
if cursor < latest && latest-cursor > uint64(b.replayLimit) {
return b.activeSnapshotsLocked(), true
}
replay := make([]Event, 0, min(len(b.events), b.replayLimit))
for _, event := range b.events {
if event.Seq > cursor {
replay = append(replay, event)
}
}
return replay, false
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Future cursors stall replay

When a dashboard tab reconnects with a cursor from before a server restart, the new broker can have a much lower latest sequence. This branch treats cursor > latest as a normal up-to-date stream, returns no replay, and does not send a reset. The tab keeps asking with the stale high cursor, so in-flight audit/usage snapshots from the new process are never replayed after reconnect.

Context Used: CLAUDE.md (source)

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: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/admin/dashboard/static/js/modules/live-logs.js`:
- Around line 35-42: The code in readLiveLogsStream is building a root-relative
URL ('/admin/live/logs') which ignores SERVER_BASE_PATH; change it to construct
the URL via the dashboard's base-path-aware helper used elsewhere (the same
helper the rest of the dashboard uses) instead of a hardcoded string, preserving
the query params built from this.liveLogsLastSeq and requestOptions (and attach
controller.signal as before); do the same fix for the audit/detail fetch
referenced around lines 379-380 so both the SSE/live logs endpoint and the audit
detail fetch use the base-path-aware helper rather than hardcoded '/admin/…'
paths.

In `@internal/app/app.go`:
- Around line 106-113: The live broker is being enabled unconditionally from
appCfg via live.NewBroker and app.live.Enabled(), causing snapshot/marshal costs
even when admin endpoints are disabled or initAdmin(...) fails; instead, compute
an effective "live enabled" flag only after admin initialization succeeds and
then call SetLivePublisher(...) (and any paths that read app.live.Enabled())
conditionally; change the wiring around live.NewBroker/app.live and calls to
SetLivePublisher, moving or gating those calls so they run only when
initAdmin(...) completed successfully (and apply the same conditional gating to
the other occurrences mentioned: the blocks around the uses at the sites
corresponding to lines 130-136, 167-173, and 472-493).

In `@internal/live/broker.go`:
- Around line 170-188: In replayAfterLocked, handle the case where an incoming
cursor is ahead of the broker's latest Seq by treating it as a reset: if cursor
> latest (where latest := b.events[len(b.events)-1].Seq) return
b.activeSnapshotsLocked(), true instead of producing an empty replay; update the
conditional logic around latest/cursor (and consider empty b.events) to detect
this out-of-range cursor and mark Reset=true, and add a regression test that
simulates a restart reconnect with cursor > latest to assert the broker returns
activeSnapshotsLocked() and Reset==true.
🪄 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: b1449505-24f4-4c2c-9fe0-55f36d8a9542

📥 Commits

Reviewing files that changed from the base of the PR and between 353556c and 22b8d8e.

📒 Files selected for processing (8)
  • internal/admin/dashboard/static/js/modules/live-logs.js
  • internal/app/app.go
  • internal/auditlog/auditlog_test.go
  • internal/auditlog/logger.go
  • internal/live/broker.go
  • internal/live/broker_test.go
  • internal/usage/logger.go
  • internal/usage/usage_test.go

Comment thread internal/admin/dashboard/static/js/modules/live-logs.js
Comment thread internal/app/app.go
Comment thread internal/live/broker.go
Comment thread internal/live/broker.go
Comment on lines +531 to +560
if entry.Data != nil {
preview.ErrorMessage = entry.Data.ErrorMessage
data := auditPreviewData{
WorkflowFeatures: entry.Data.WorkflowFeatures,
Failover: entry.Data.Failover,
}
if auditPreviewIncludesCapturedData(eventType) {
data.UserAgent = entry.Data.UserAgent
data.APIKeyHash = entry.Data.APIKeyHash
data.Temperature = entry.Data.Temperature
data.MaxTokens = entry.Data.MaxTokens
data.ErrorMessage = entry.Data.ErrorMessage
data.ErrorCode = entry.Data.ErrorCode
data.RequestHeaders = entry.Data.RequestHeaders
data.ResponseHeaders = entry.Data.ResponseHeaders
data.RequestBody = entry.Data.RequestBody
data.ResponseBody = entry.Data.ResponseBody
data.RequestBodyTooBigToHandle = entry.Data.RequestBodyTooBigToHandle
data.ResponseBodyTooBigToHandle = entry.Data.ResponseBodyTooBigToHandle
}
if data.hasValues() {
preview.Data = &data
}
}
return preview
}

func auditPreviewIncludesCapturedData(eventType string) bool {
return eventType == EventAuditCompleted || eventType == EventAuditFlushed || eventType == EventAuditFailed
}
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 | ⚡ Quick win

Keep lifecycle frames compact; don’t embed full audit detail payloads here.

audit.completed and audit.flushed now serialize captured headers/bodies into the live stream. That bypasses the new details-on-demand path in live-logs.js and can send the same large payload twice per request (completed and flushed). Keep these lifecycle events compact and reserve full request/response detail for the explicit detail fetch flow.

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/live/broker.go (1)

543-555: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep lifecycle SSE frames compact.

audit.completed / audit.failed / audit.flushed still embed captured headers and request/response bodies in the live stream. That bypasses the details-on-demand flow described in this PR and can send the same large payload multiple times on the hot SSE path. Keep the live row compact here and fetch full captured detail through the explicit audit-detail path instead.

Also applies to: 564-566

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/live/broker.go` around lines 543 - 555, The SSE lifecycle handlers
currently embed full captured headers and bodies when
auditPreviewIncludesCapturedData(eventType) is true; remove assignment of heavy
fields so lifecycle frames remain compact. In the block guarded by
auditPreviewIncludesCapturedData(eventType) (and the similar block at the later
lines called out), stop copying RequestHeaders, ResponseHeaders, RequestBody,
ResponseBody and the large-body flags (RequestBodyTooBigToHandle,
ResponseBodyTooBigToHandle) from entry.Data into data; keep only small preview
fields like UserAgent, APIKeyHash, Temperature, MaxTokens, ErrorMessage,
ErrorCode and fetch full captured details via the audit-detail endpoint instead.
Ensure eventType/auditPreviewIncludesCapturedData and the data variable remain
for small metadata only.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/app/app.go`:
- Around line 103-112: The App constructor creates app.live via live.NewBroker
(using live.Config) before performing many fallible initializations, and on
early returns those resources are not closed; modify New so that either app.live
is created only after all subsequent initializations succeed or ensure it is
closed on any error path: e.g., if you keep creating app.live upfront, add a
defer-like cleanup that calls app.live.Close() (or the broker's shutdown method)
on any early return, or move the live.NewBroker call to after the fallible steps
so app.live is only allocated on successful startup; update the constructor that
returns (*App, error) and reference app.live, New, and
live.NewBroker/live.Config when applying the change.

In `@internal/live/broker.go`:
- Around line 298-303: The code currently treats EventAuditFailed and
EventUsageFailed as terminal by calling deleteActiveSnapshot (via
deleteActiveSnapshot(b.activeAudit, auditActiveKeys(*event)) and
deleteActiveSnapshot(b.activeUsage, usageActiveKeys(*event))), which prematurely
removes rows from activeSnapshotsLocked(); change the handlers so that *.failed
events do NOT call deleteActiveSnapshot and only EventAuditFlushed,
EventAuditRemoved, and EventUsageFlushed trigger deletion—i.e., remove the
deleteActiveSnapshot call from the EventAuditFailed and EventUsageFailed cases
(and the corresponding duplicate occurrences noted) so rows remain live until
the flush/remove events.

---

Duplicate comments:
In `@internal/live/broker.go`:
- Around line 543-555: The SSE lifecycle handlers currently embed full captured
headers and bodies when auditPreviewIncludesCapturedData(eventType) is true;
remove assignment of heavy fields so lifecycle frames remain compact. In the
block guarded by auditPreviewIncludesCapturedData(eventType) (and the similar
block at the later lines called out), stop copying RequestHeaders,
ResponseHeaders, RequestBody, ResponseBody and the large-body flags
(RequestBodyTooBigToHandle, ResponseBodyTooBigToHandle) from entry.Data into
data; keep only small preview fields like UserAgent, APIKeyHash, Temperature,
MaxTokens, ErrorMessage, ErrorCode and fetch full captured details via the
audit-detail endpoint instead. Ensure eventType/auditPreviewIncludesCapturedData
and the data variable remain for small metadata only.
🪄 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: 4f898e14-3c74-48f4-947f-0b32eb3e93d6

📥 Commits

Reviewing files that changed from the base of the PR and between 353556c and 9621721.

📒 Files selected for processing (9)
  • internal/admin/dashboard/static/js/modules/live-logs.js
  • internal/admin/dashboard/static/js/modules/live-logs.test.cjs
  • internal/app/app.go
  • internal/auditlog/auditlog_test.go
  • internal/auditlog/logger.go
  • internal/live/broker.go
  • internal/live/broker_test.go
  • internal/usage/logger.go
  • internal/usage/usage_test.go

Comment thread internal/app/app.go
Comment on lines 103 to 112
app := &App{
config: appCfg,
}
app.live = live.NewBroker(live.Config{
Enabled: appCfg.Admin.LiveLogsEnabled,
BufferSize: appCfg.Admin.LiveLogsBufferSize,
ReplayLimit: appCfg.Admin.LiveLogsReplayLimit,
Heartbeat: time.Duration(appCfg.Admin.LiveLogsHeartbeatSeconds) * time.Second,
})

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 | ⚡ Quick win

Close the live broker on constructor error paths.

At Line 106, app.live is created before a long chain of fallible initializations, but early return nil, ... branches in New do not close it. If live logs are enabled, failed startup attempts can leak broker resources (e.g., goroutines/timers).

♻️ Suggested minimal fix
 	app := &App{
 		config: appCfg,
 	}
 	app.live = live.NewBroker(live.Config{
 		Enabled:     appCfg.Admin.LiveLogsEnabled,
 		BufferSize:  appCfg.Admin.LiveLogsBufferSize,
 		ReplayLimit: appCfg.Admin.LiveLogsReplayLimit,
 		Heartbeat:   time.Duration(appCfg.Admin.LiveLogsHeartbeatSeconds) * time.Second,
 	})
+	liveNeedsClose := true
+	defer func() {
+		if liveNeedsClose && app.live != nil {
+			app.live.Close()
+		}
+	}()
...
 	if livePublishersEnabled {
 		app.attachLivePublishers()
 	}
 	app.server = server.New(provider, serverCfg)
+	liveNeedsClose = false

 	return app, nil
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/app/app.go` around lines 103 - 112, The App constructor creates
app.live via live.NewBroker (using live.Config) before performing many fallible
initializations, and on early returns those resources are not closed; modify New
so that either app.live is created only after all subsequent initializations
succeed or ensure it is closed on any error path: e.g., if you keep creating
app.live upfront, add a defer-like cleanup that calls app.live.Close() (or the
broker's shutdown method) on any early return, or move the live.NewBroker call
to after the fallible steps so app.live is only allocated on successful startup;
update the constructor that returns (*App, error) and reference app.live, New,
and live.NewBroker/live.Config when applying the change.

Comment thread internal/live/broker.go
Comment on lines +298 to +303
case EventAuditFailed, EventAuditFlushed, EventAuditRemoved:
deleteActiveSnapshot(b.activeAudit, auditActiveKeys(*event))
return
case EventUsageFailed, EventUsageFlushed:
deleteActiveSnapshot(b.activeUsage, usageActiveKeys(*event))
return
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 | ⚡ Quick win

Keep *.failed rows live until *.flushed.

audit.failed / usage.failed are treated as terminal here, which contradicts the PR behavior to mark rows as persisted only after flush events. A reconnect between *.failed and *.flushed will drop the row from activeSnapshotsLocked(), and audit rows flip _live_pending to false too early.

💡 Minimal fix
-	case EventAuditFailed, EventAuditFlushed, EventAuditRemoved:
+	case EventAuditFlushed, EventAuditRemoved:
 		deleteActiveSnapshot(b.activeAudit, auditActiveKeys(*event))
 		return
-	case EventUsageFailed, EventUsageFlushed:
+	case EventUsageFlushed:
 		deleteActiveSnapshot(b.activeUsage, usageActiveKeys(*event))
 		return
@@
-func auditEventTerminal(eventType string) bool {
-	return eventType == EventAuditFailed || eventType == EventAuditFlushed || eventType == EventAuditRemoved
+func auditEventTerminal(eventType string) bool {
+	return eventType == EventAuditFlushed || eventType == EventAuditRemoved
 }

Also applies to: 526-527, 585-587

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/live/broker.go` around lines 298 - 303, The code currently treats
EventAuditFailed and EventUsageFailed as terminal by calling
deleteActiveSnapshot (via deleteActiveSnapshot(b.activeAudit,
auditActiveKeys(*event)) and deleteActiveSnapshot(b.activeUsage,
usageActiveKeys(*event))), which prematurely removes rows from
activeSnapshotsLocked(); change the handlers so that *.failed events do NOT call
deleteActiveSnapshot and only EventAuditFlushed, EventAuditRemoved, and
EventUsageFlushed trigger deletion—i.e., remove the deleteActiveSnapshot call
from the EventAuditFailed and EventUsageFailed cases (and the corresponding
duplicate occurrences noted) so rows remain live until the flush/remove events.

@SantiagoDePolonia SantiagoDePolonia merged commit 80cf7af into main May 17, 2026
19 checks passed
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.

2 participants