Skip to content

Feature: audit logs sidemenu#110

Merged
SantiagoDePolonia merged 10 commits intomainfrom
feature/audit-logs-sidemenu
Feb 28, 2026
Merged

Feature: audit logs sidemenu#110
SantiagoDePolonia merged 10 commits intomainfrom
feature/audit-logs-sidemenu

Conversation

@SantiagoDePolonia
Copy link
Copy Markdown
Contributor

@SantiagoDePolonia SantiagoDePolonia commented Feb 28, 2026

Summary by CodeRabbit

  • New Features

    • Audit Logs page in the admin dashboard with filters, pagination, mobile layout, and an inline Conversation drawer.
    • New admin API endpoints to query audit logs and fetch conversations.
    • New public batch-oriented API (batches endpoints) plus embedding, file, model, and usage response enhancements.
  • UI Enhancements

    • Date-picker, charts, usage modules, copyable request/response views, and comprehensive audit styling and JS modules.
  • Documentation

    • Guidelines added to keep API docs and Swagger exports synchronized when endpoints change.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 28, 2026

📝 Walkthrough

Walkthrough

Adds admin audit logging: new admin API routes and handlers, an auditlog reader abstraction with SQLite/Postgres/Mongo implementations and indexes, admin dashboard UI (templates, CSS, modular JS) for browsing/filtering/paginating logs and viewing threaded conversations, and regenerated Swagger/docs.

Changes

Cohort / File(s) Summary
API Docs & Generated Artifacts
AGENTS.md, cmd/gomodel/docs/docs.go, cmd/gomodel/docs/swagger.json
Adds documentation guidance and regenerates OpenAPI artifacts; registers admin audit endpoints and many new public schema definitions (auditlog, batch, embedding, file, toolcall, usage, etc.).
Audit Reader API & Helpers
internal/auditlog/reader.go, internal/auditlog/reader_factory.go, internal/auditlog/reader_helpers.go, internal/auditlog/conversation_helpers.go
Defines Reader interface, query/result types, storage-backed reader factory, and helper utilities (clamping, where-clause, wildcard escaping, string extraction).
DB-backed Readers
internal/auditlog/reader_sqlite.go, internal/auditlog/reader_postgresql.go, internal/auditlog/reader_mongodb.go
Implements SQLite/Postgres/Mongo readers: GetLogs, GetLogByID, GetConversation with DB-specific queries/aggregation, pagination, filters, traversal and JSON data mapping.
Store Indexes
internal/auditlog/store_sqlite.go, internal/auditlog/store_postgresql.go, internal/auditlog/store_mongodb.go
Creates indexes on response_body.id and request_body.previous_response_id to accelerate conversation lookups.
Server Routing & Handler Wiring
internal/server/http.go, internal/app/app.go, internal/admin/handler.go
Adds routes /admin/api/v1/audit/log and /admin/api/v1/audit/conversation, introduces Handler Option type and WithAuditReader, updates NewHandler to accept options, and implements AuditLog/AuditConversation handlers and date-range parsing.
Admin Handler Tests & Reader Tests
internal/admin/handler_test.go, internal/auditlog/*_test.go, internal/server/http_test.go, tests/e2e/admin_test.go
Adds tests and mocks for audit handlers and readers, conversation helpers tests, sanitization tests, and expands endpoint enablement checks (E2E).
Admin Dashboard Templates & Layout
internal/admin/dashboard/templates/layout.html, internal/admin/dashboard/templates/index.html
Adds Audit Logs nav item, loads new client modules, and inserts full audit logs page template (filters, list, details, conversation drawer, pagination).
Dashboard JS Core & Modules
internal/admin/dashboard/static/js/dashboard.js, internal/admin/dashboard/static/js/modules/*
Refactors dashboard to modular composition and introduces modules: audit-list, conversation-drawer, conversation-helpers, date-picker, usage, charts; exposes module factories globally.
Static Assets (CSS/JS)
internal/admin/dashboard/static/css/dashboard.css, internal/admin/dashboard/static/js/*.js, internal/admin/dashboard/static/js/modules/*.js
Adds extensive audit UI CSS and new frontend module scripts including mobile reflow and interactive styles.
Generated Handler Doc Comments
internal/server/handlers.go
Adds/expands Swagger/OpenAPI annotations on many handlers to match regenerated docs.
Core Type Annotations
internal/core/batch.go, internal/core/types.go
Adds Swagger struct tags (swaggertype) for embedding and batch fields (docs-only).
Dashboard Static Tests
internal/admin/dashboard/dashboard_test.go
Adds assertion for audit nav presence and test serving module JS file.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Admin UI
    participant Handler as Admin Handler
    participant Reader as Audit Reader
    participant DB as Database (SQLite/PG/Mongo)

    Client->>Handler: GET /admin/api/v1/audit/log?filters
    Handler->>Reader: GetLogs(ctx, LogQueryParams)
    Reader->>DB: Query/Aggregate logs with filters & pagination
    DB-->>Reader: Rows/Documents
    Reader-->>Handler: LogListResult (entries + total/limit/offset)
    Handler-->>Client: JSON response (entries + metadata)
Loading
sequenceDiagram
    participant Client as Admin UI (Conversation)
    participant Handler as Admin Handler
    participant Reader as Audit Reader
    participant DB as Database

    Client->>Handler: GET /admin/api/v1/audit/conversation?log_id=X&limit=Y
    Handler->>Reader: GetConversation(ctx, logID, limit)
    Reader->>DB: GetLogByID(seed)
    DB-->>Reader: seed LogEntry
    Reader->>DB: findByPreviousResponseID (walk backwards)
    DB-->>Reader: previous entries
    Reader->>DB: findByResponseID (walk forwards)
    DB-->>Reader: subsequent entries
    Reader-->>Handler: ConversationResult (anchor + ordered entries)
    Handler-->>Client: JSON response (conversation thread)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested labels

enhancement

Poem

🐰 Hopping through logs with a bright-eyed cheer,

Threads unfurl, filters bring messages near.
SQLite, Postgres, Mongo—each lends a paw,
Conversations stitched, audited for law.
A rabbit applauds: the dashboard is here!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The PR title 'Feature: audit logs sidemenu' is vague and overly narrow. It only mentions the UI navigation item but omits the comprehensive backend audit log functionality (API endpoints, database readers, schema changes) that constitutes the bulk of this PR's changes. Revise the title to better reflect the main feature scope, e.g., 'Feature: audit logs API and admin dashboard integration' or 'Add audit log retrieval and conversation threading API with admin dashboard'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/audit-logs-sidemenu

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

❤️ Share

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

@SantiagoDePolonia SantiagoDePolonia marked this pull request as ready for review February 28, 2026 19:26
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: 10

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

Inline comments:
In `@cmd/gomodel/docs/swagger.json`:
- Around line 135-138: The OpenAPI parameter definition for "stream" is declared
as "type": "string" but the backend uses LogQueryParams with Stream *bool;
update the swagger parameter for name "stream" (in: query) to use "type":
"boolean" so the spec matches the LogQueryParams boolean field and generated
clients will send boolean values.

In `@internal/admin/dashboard/static/css/dashboard.css`:
- Around line 1207-1211: Replace the deprecated usage of "word-break:
break-word" in the .audit-json-body selector by removing that property and
relying on "overflow-wrap: anywhere" (already present) to achieve the same
behavior; update the .audit-json-body rule to drop "word-break: break-word" and
keep only "white-space: pre-wrap" and "overflow-wrap: anywhere" so you eliminate
the deprecated property while preserving intended wrapping.
- Around line 1377-1384: Update the .chat-content CSS: remove the unnecessary
quotes around the Inter font name in the font-family list and replace the
deprecated property word-break: break-word with overflow-wrap: break-word (keep
other properties like white-space and line-height intact); locate these changes
in the .chat-content rule to ensure font-family becomes Inter, -apple-system,
BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif and word wrapping is handled
via overflow-wrap: break-word.

In `@internal/admin/dashboard/static/js/modules/audit-list.js`:
- Around line 70-75: statusCodeClass currently treats null/undefined/non-numeric
inputs as success; update the statusCodeClass function to first detect invalid
or non-numeric statusCode (e.g., using Number.isFinite or typeof checks) and
return a distinct class like 'status-unknown' (or 'status-neutral') for those
cases before the numeric comparisons for 500/400/300, so unknown states are not
rendered as 'status-success'.

In `@internal/admin/dashboard/templates/index.html`:
- Around line 347-350: The filter inputs rely only on placeholders and lack
accessible names; add explicit labels or aria-label attributes and corresponding
ids to each control (the inputs bound to auditSearch, auditModel, auditProvider,
auditPath and any similar controls in the 354-379 range) so screen readers and
voice-control tools can identify them, ensuring you either add <label for="...">
elements pointing to new id attributes or set descriptive aria-label values
(e.g., "Search by request ID", "Model filter", "Provider filter", "Path filter")
and keep the existing x-model and `@input.debounce.300ms`="fetchAuditLog(true)"
bindings intact.
- Around line 397-404: The icon-only button with class
"audit-conversation-trigger" relies on title for accessibility but needs an
explicit accessible name; add an aria-label (e.g., aria-label="Open
conversation") to the button element alongside the existing title, ensuring it
is present on the element that calls openConversation(entry,
$el.closest('details'), true) and visible when canShowConversation(entry) is
true; keep the same descriptive text as the title (or use your localization
helper) so screen readers have a clear name for the control.

In `@internal/admin/handler.go`:
- Around line 407-444: AuditConversation currently parses limit with
strconv.Atoi and passes it to h.auditReader.GetConversation without validating
bounds; update AuditConversation to validate the parsed limit (after
strconv.Atoi) and return a core.NewInvalidRequestError for out-of-range values
(e.g., enforce limit >= 1 and <= 1000 or another agreed-upon max) before calling
h.auditReader.GetConversation, mentioning the parameter name "limit" in the
error; reference the AuditConversation function, the limit variable,
strconv.Atoi, and the GetConversation call when making the change.

In `@internal/auditlog/reader_mongodb.go`:
- Around line 317-403: The three functions GetLogByID, findByResponseID, and
findByPreviousResponseID repeat the same anonymous scan struct; extract that
into a shared named type (e.g., mongoLogRow) with the existing bson tags and add
a helper method (e.g., toLogEntry) to convert mongoLogRow -> *LogEntry, then
replace the inline struct declarations in GetLogByID, findByResponseID, and
findByPreviousResponseID with the new mongoLogRow type and call toLogEntry
before returning.

In `@internal/auditlog/reader_sqlite.go`:
- Around line 258-275: The JSON extraction in findByResponseID (and any
analogous lookup such as the method that queries json_extract(data,
'$.request_body.previous_response_id')) will cause full table scans on large
datasets; add SQLite expression indexes for these JSON paths (e.g., an index on
json_extract(data, '$.response_body.id') and one on json_extract(data,
'$.request_body.previous_response_id')) to speed lookups, and include their
CREATE INDEX statements in your migration or DB initialization so the queries in
SQLiteReader (findByResponseID and the previous-response lookup) use the
indexes.
- Around line 167-190: The loop that builds the thread currently prepends
entries using append([]*LogEntry{parent}, thread...), causing O(n²) behavior;
change it to append parents at the end (thread = append(thread, parent)) while
still marking seen[parent.ID] and advancing current = parent, and then rely on
the existing sort-by-timestamp step (or reverse once after the loop) to produce
the final chronological order; update any logic that assumes the list was built
front-to-back (e.g., usages of thread, extractPreviousResponseID,
findByResponseID, anchor, limit) to accommodate the new append-then-sort/reverse
approach.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 07900ae and 9f1bc55.

📒 Files selected for processing (31)
  • AGENTS.md
  • cmd/gomodel/docs/docs.go
  • cmd/gomodel/docs/swagger.json
  • internal/admin/dashboard/dashboard.go
  • internal/admin/dashboard/dashboard_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/charts.js
  • internal/admin/dashboard/static/js/modules/conversation-drawer.js
  • internal/admin/dashboard/static/js/modules/conversation-helpers.js
  • internal/admin/dashboard/static/js/modules/date-picker.js
  • internal/admin/dashboard/static/js/modules/usage.js
  • internal/admin/dashboard/templates/index.html
  • internal/admin/dashboard/templates/layout.html
  • internal/admin/handler.go
  • internal/admin/handler_test.go
  • internal/app/app.go
  • internal/auditlog/conversation_helpers.go
  • internal/auditlog/reader.go
  • internal/auditlog/reader_factory.go
  • internal/auditlog/reader_helpers.go
  • internal/auditlog/reader_mongodb.go
  • internal/auditlog/reader_postgresql.go
  • internal/auditlog/reader_sqlite.go
  • internal/auditlog/store_mongodb.go
  • internal/auditlog/store_postgresql.go
  • internal/auditlog/store_sqlite.go
  • internal/server/http.go
  • internal/server/http_test.go
  • tests/e2e/admin_test.go

Comment on lines +258 to +275
func (r *SQLiteReader) findByResponseID(ctx context.Context, responseID string) (*LogEntry, error) {
query := `SELECT id, timestamp, duration_ns, model, provider, status_code, request_id,
client_ip, method, path, stream, error_type, data
FROM audit_logs
WHERE json_extract(data, '$.response_body.id') = ?
ORDER BY timestamp ASC
LIMIT 1`

rows, err := r.db.QueryContext(ctx, query, responseID)
if err != nil {
return nil, fmt.Errorf("failed to query audit log by response id: %w", err)
}
defer rows.Close()
if !rows.Next() {
return nil, nil
}
return scanSQLiteLogEntry(rows)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Verify JSON path extraction performance on large datasets.

The queries use json_extract(data, '$.response_body.id') and json_extract(data, '$.request_body.previous_response_id') for conversation threading. Without a computed/virtual column or expression index, these queries will perform full table scans.

Consider adding expression indexes if conversation lookups become a performance bottleneck:

CREATE INDEX idx_audit_logs_response_id ON audit_logs(json_extract(data, '$.response_body.id'));
CREATE INDEX idx_audit_logs_prev_response_id ON audit_logs(json_extract(data, '$.request_body.previous_response_id'));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/auditlog/reader_sqlite.go` around lines 258 - 275, The JSON
extraction in findByResponseID (and any analogous lookup such as the method that
queries json_extract(data, '$.request_body.previous_response_id')) will cause
full table scans on large datasets; add SQLite expression indexes for these JSON
paths (e.g., an index on json_extract(data, '$.response_body.id') and one on
json_extract(data, '$.request_body.previous_response_id')) to speed lookups, and
include their CREATE INDEX statements in your migration or DB initialization so
the queries in SQLiteReader (findByResponseID and the previous-response lookup)
use the indexes.

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

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

258-294: 🧹 Nitpick | 🔵 Trivial

Consider adding expression indexes for JSON path queries.

The json_extract queries on $.response_body.id and $.request_body.previous_response_id will perform full table scans. While acceptable for low-volume admin queries, this could become a bottleneck with large audit logs.

-- Suggested indexes for conversation threading performance
CREATE INDEX IF NOT EXISTS idx_audit_logs_response_id 
  ON audit_logs(json_extract(data, '$.response_body.id'));
CREATE INDEX IF NOT EXISTS idx_audit_logs_prev_response_id 
  ON audit_logs(json_extract(data, '$.request_body.previous_response_id'));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/auditlog/reader_sqlite.go` around lines 258 - 294, The JSON path
lookups in findByResponseID and findByPreviousResponseID cause full table scans;
add persistent expression indexes for json_extract(data, '$.response_body.id')
and json_extract(data, '$.request_body.previous_response_id') in your DB
initialization/migration so those queries use indexes (create indexes if not
exists, e.g., idx_audit_logs_response_id and idx_audit_logs_prev_response_id)
and ensure the migration runs before these reader methods are used.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/gomodel/docs/swagger.json`:
- Around line 715-727: Update the OpenAPI 2.0 declarations: for the
/v1/chat/completions operation replace the single invalid produces entry "json
text/event-stream" with two separate produces entries "application/json" and
"text/event-stream"; for the /v1/files/{id}/content response change the response
schema type from "string" to "file" so the binary download is correctly
declared; and for the POST operation on /v1/batches/{id}/cancel add an
appropriate consumes array (e.g., "application/json" or the actual request
content type) to the operation object so it conforms to Swagger 2.0
requirements.

In `@internal/admin/dashboard/static/css/dashboard.css`:
- Around line 1135-1150: The stylesheet uses a hardcoded max-height: 2200px for
.audit-entry[open] .audit-entry-details which can make the expand animation
jumpy for taller content; replace this by removing the arbitrary large
max-height and either (a) set a reasonable bounded value on .audit-entry-details
and document the limitation, or (b) implement a JS-driven height transition: on
open, measure the element's scrollHeight and apply that value as an inline
max-height, then remove or reset it after the transition to allow natural
sizing; update the logic around the .audit-entry-details and .audit-entry[open]
.audit-entry-details selectors to use the JS-managed inline style for smooth
animations on large entries.

In `@internal/admin/dashboard/static/js/modules/audit-list.js`:
- Around line 102-110: The copyAuditJSON function currently only logs errors to
the console; update its catch block to provide brief user-visible feedback on
failure by either (a) changing the originating button text/icon temporarily
(e.g., set button.innerText = 'Copy failed' then restore after ~2s) or (b)
invoking an existing toast/notification helper (e.g., call showToast or dispatch
a custom 'notify' event) so the user sees the failure; keep the console.error
for debugging, ensure you reference copyAuditJSON and formatJSON in your change,
and restore any temporary UI change after a short timeout.
- Around line 63-68: The condition in formatDurationNs redundantly checks both
ns == null and ns === undefined; replace the two-part check with a single loose
equality check (ns == null) to cover null and undefined, keeping the rest of the
function intact so it still returns '-' for missing values and formats values
for µs/ms/s as before; update the method name formatDurationNs accordingly.

In `@internal/admin/dashboard/templates/index.html`:
- Line 479: The overlay div with class "conversation-overlay" (the element using
x-show="conversationOpen" and `@click`="closeConversation()") should be marked as
non‑interactive for assistive tech; add aria-hidden="true" to that element so
screen readers ignore the visual backdrop while preserving its click behavior to
close the conversation.
- Around line 480-505: Add ARIA attributes to the conversation drawer element
and its controls to treat it as an accessible dialog: on the aside with class
"conversation-drawer" (which uses conversationOpen and closeConversation()) add
role="dialog", aria-modal="true" and bind aria-hidden based on conversationOpen;
add aria-labelledby pointing to the <h3> header (or give the header an id) and
aria-describedby pointing to the main content container (the conversation-thread
or the appropriate empty-state elements) so screen readers announce the title
and contents; ensure the Close button has an accessible name (aria-label) and
that focus management is implemented when opening/closing the drawer (set
initial focus to the dialog or first focusable element and return focus to the
trigger on close) while keeping existing bindings like conversationMessages,
conversationError, conversationLoading and conversationAnchorID intact.

In `@internal/auditlog/reader_mongodb.go`:
- Around line 36-51: The toLogEntry mapping in mongoLogRow returns LogEntry.Data
verbatim, risking leakage of sensitive headers; update mongoLogRow.toLogEntry to
run a redaction pass over Data's "request_headers" and "response_headers" keys
before returning (e.g., call a helper like redactSensitiveHeaders or similar) to
remove/obfuscate Authorization, Cookie, X-API-Key and other sensitive header
names while leaving other fields intact; ensure the helper is deterministic and
handles nil/missing keys and non-map values gracefully so all legacy/untrusted
rows are sanitized prior to creating the LogEntry.

---

Duplicate comments:
In `@internal/auditlog/reader_sqlite.go`:
- Around line 258-294: The JSON path lookups in findByResponseID and
findByPreviousResponseID cause full table scans; add persistent expression
indexes for json_extract(data, '$.response_body.id') and json_extract(data,
'$.request_body.previous_response_id') in your DB initialization/migration so
those queries use indexes (create indexes if not exists, e.g.,
idx_audit_logs_response_id and idx_audit_logs_prev_response_id) and ensure the
migration runs before these reader methods are used.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9f1bc55 and d661d40.

📒 Files selected for processing (14)
  • AGENTS.md
  • cmd/gomodel/docs/docs.go
  • cmd/gomodel/docs/swagger.json
  • internal/admin/dashboard/static/css/dashboard.css
  • internal/admin/dashboard/static/js/modules/audit-list.js
  • internal/admin/dashboard/templates/index.html
  • internal/admin/handler.go
  • internal/app/app.go
  • internal/auditlog/reader_mongodb.go
  • internal/auditlog/reader_sqlite.go
  • internal/core/batch.go
  • internal/core/types.go
  • internal/server/handlers.go
  • internal/server/http.go

Comment on lines +1135 to +1150
.audit-entry-details {
border-top: 1px solid transparent;
padding: 0 12px;
background: var(--bg-surface);
max-height: 0;
opacity: 0;
overflow: hidden;
transition: max-height 0.12s ease-out, opacity 0.08s linear, padding 0.12s ease-out, border-color 0.12s ease-out;
}

.audit-entry[open] .audit-entry-details {
border-top-color: var(--border);
padding: 12px;
max-height: 2200px;
opacity: 1;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider reducing the hardcoded max-height transition value.

The max-height: 2200px is an arbitrary large value used for the expand/collapse animation. While functional, if actual content exceeds this, the animation may appear jumpy. Consider using a JavaScript-based height calculation for smoother transitions on very large entries, or document this as a known limitation.

🤖 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 1135 - 1150,
The stylesheet uses a hardcoded max-height: 2200px for .audit-entry[open]
.audit-entry-details which can make the expand animation jumpy for taller
content; replace this by removing the arbitrary large max-height and either (a)
set a reasonable bounded value on .audit-entry-details and document the
limitation, or (b) implement a JS-driven height transition: on open, measure the
element's scrollHeight and apply that value as an inline max-height, then remove
or reset it after the transition to allow natural sizing; update the logic
around the .audit-entry-details and .audit-entry[open] .audit-entry-details
selectors to use the JS-managed inline style for smooth animations on large
entries.

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

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/static/js/dashboard.js (1)

79-107: ⚠️ Potential issue | 🟠 Major

Fetch audit data during initial audit-route load.

audit is parsed as a valid route, but audit fetch runs only in popstate/navigate; direct first-load on audit can stay empty.

🛠️ Proposed fix
 init() {
@@
-    this.fetchAll();
+    this.fetchAll();
+    if (this.page === 'audit' && typeof this.fetchAuditLog === 'function') {
+        this.fetchAuditLog(true);
+    }
 },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/admin/dashboard/static/js/dashboard.js` around lines 79 - 107, The
init() routine parses the current route but only triggers fetchAuditLog(true) on
popstate, so a direct first-load to the "audit" route shows no data; after
parsing the route (using this._parseRoute) and setting this.page, add a call to
this.fetchAuditLog(true) when page === 'audit' (similar to how usageMode and
renderChart are handled) so initial loads of the audit route fetch data
immediately before returning from init().
🤖 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/audit-list.js`:
- Around line 12-35: fetchAuditLog can be subject to race conditions where a
slower earlier response overwrites a newer one; add a simple in-flight ordering
guard by introducing a monotonically-incremented request token (e.g.
this.auditFetchToken or this._auditRequestId) that you increment at the start of
fetchAuditLog, capture into a local variable, and only apply the response (and
error fallback) to this.auditLog if the captured token still matches the
instance token; implement the check before assigning this.auditLog in both the
success and catch paths so out-of-order responses are ignored.

In `@internal/auditlog/reader_mongodb.go`:
- Around line 198-214: GetLogs is returning row.Data directly from
facetResult.Data which bypasses redaction and can leak sensitive headers; update
the construction of LogEntry in GetLogs to pass row.Data through the existing
redaction helpers (e.g., call sanitizeLogData(row.Data) or use toLogEntry(row)
so the Data field is the sanitized result) before assigning to LogEntry.Data,
ensuring sensitive headers (Authorization, Cookie, X-API-Key, etc.) are removed.

---

Outside diff comments:
In `@internal/admin/dashboard/static/js/dashboard.js`:
- Around line 79-107: The init() routine parses the current route but only
triggers fetchAuditLog(true) on popstate, so a direct first-load to the "audit"
route shows no data; after parsing the route (using this._parseRoute) and
setting this.page, add a call to this.fetchAuditLog(true) when page === 'audit'
(similar to how usageMode and renderChart are handled) so initial loads of the
audit route fetch data immediately before returning from init().

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d661d40 and 2c79dbe.

📒 Files selected for processing (12)
  • cmd/gomodel/docs/docs.go
  • cmd/gomodel/docs/swagger.json
  • 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/conversation-drawer.js
  • internal/admin/dashboard/templates/index.html
  • internal/auditlog/conversation_helpers.go
  • internal/auditlog/conversation_helpers_test.go
  • internal/auditlog/reader_mongodb.go
  • internal/auditlog/reader_mongodb_test.go
  • internal/server/handlers.go

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

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

26-35: ⚠️ Potential issue | 🟠 Major

Move the token guard before handleFetchResponse to prevent stale auth-state flips.

On Line 27, handleFetchResponse can set authError/needsAuth before the staleness check on Line 28. A slower older 401 can still overwrite UI auth state after a newer successful request. Also normalize entries as an array defensively.

Suggested fix
-                    const res = await fetch('/admin/api/v1/audit/log?' + qs, { headers: this.headers() });
-                    if (!this.handleFetchResponse(res, 'audit log')) {
-                        if (requestToken !== this.auditFetchToken) return;
+                    const res = await fetch('/admin/api/v1/audit/log?' + qs, { headers: this.headers() });
+                    if (requestToken !== this.auditFetchToken) return;
+                    if (!this.handleFetchResponse(res, 'audit log')) {
                         this.auditLog = { entries: [], total: 0, limit: 25, offset: 0 };
                         return;
                     }
                     const payload = await res.json();
                     if (requestToken !== this.auditFetchToken) return;
-                    this.auditLog = payload;
-                    if (!this.auditLog.entries) this.auditLog.entries = [];
+                    this.auditLog = {
+                        ...payload,
+                        entries: Array.isArray(payload.entries) ? payload.entries : []
+                    };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/admin/dashboard/static/js/modules/audit-list.js` around lines 26 -
35, Move the staleness guard so you check "if (requestToken !==
this.auditFetchToken) return;" immediately after the fetch and before calling
this.handleFetchResponse, to avoid an older response changing auth state; keep
calling this.handleFetchResponse only for non-stale responses. After parsing
payload (const payload = await res.json()), set "if (requestToken !==
this.auditFetchToken) return;" again and then assign this.auditLog = payload ||
{}; and ensure this.auditLog.entries = Array.isArray(this.auditLog.entries) ?
this.auditLog.entries : []; so entries is normalized to an array and stale
responses never flip authError/needsAuth in handleFetchResponse.
🤖 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/auditlog/reader_mongodb.go`:
- Around line 215-290: GetConversation currently walks parent/child links with
repeated DB calls (via findByResponseID and findByPreviousResponseID) which is
correct but can be optimized; replace or add an alternative implementation that
uses a single MongoDB aggregation with $graphLookup to fetch the full thread
around the anchor ID (using the same link fields extracted by
extractPreviousResponseID/extractResponseID) up to
clampConversationLimit(limit), then sort and map results into the same
ConversationResult shape, preserving cycle-safety and redaction by reusing
toLogEntry() or existing mapping logic.
- Around line 204-210: The code currently compares mongo.ErrNoDocuments using
`err == mongo.ErrNoDocuments` after calling
r.collection.FindOne(...).Decode(&row); change these to use errors.Is(err,
mongo.ErrNoDocuments) for robust error matching (import the "errors" package if
not already). Update the same pattern wherever similar checks occur (the other
occurrences in the file around the code handling queries at the spots
referenced, e.g., the checks near lines 315 and 331) so all mongo.ErrNoDocuments
comparisons use errors.Is.

---

Duplicate comments:
In `@internal/admin/dashboard/static/js/modules/audit-list.js`:
- Around line 26-35: Move the staleness guard so you check "if (requestToken !==
this.auditFetchToken) return;" immediately after the fetch and before calling
this.handleFetchResponse, to avoid an older response changing auth state; keep
calling this.handleFetchResponse only for non-stale responses. After parsing
payload (const payload = await res.json()), set "if (requestToken !==
this.auditFetchToken) return;" again and then assign this.auditLog = payload ||
{}; and ensure this.auditLog.entries = Array.isArray(this.auditLog.entries) ?
this.auditLog.entries : []; so entries is normalized to an array and stale
responses never flip authError/needsAuth in handleFetchResponse.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2c79dbe and 37ed7f2.

📒 Files selected for processing (3)
  • internal/admin/dashboard/static/js/dashboard.js
  • internal/admin/dashboard/static/js/modules/audit-list.js
  • internal/auditlog/reader_mongodb.go

Comment on lines +204 to +210
err := r.collection.FindOne(ctx, bson.D{{Key: "_id", Value: id}}).Decode(&row)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, nil
}
return nil, fmt.Errorf("failed to query audit log by id: %w", 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.

🧹 Nitpick | 🔵 Trivial

Prefer errors.Is() for error comparison.

Direct comparison err == mongo.ErrNoDocuments works, but errors.Is() is more robust and idiomatic Go, especially if the driver behavior changes to wrap errors in future versions. The same pattern appears on lines 315 and 331.

♻️ Proposed refactor
+import "errors"

-	if err == mongo.ErrNoDocuments {
+	if errors.Is(err, mongo.ErrNoDocuments) {
 		return nil, nil
 	}

Apply similar changes to lines 315 and 331.

📝 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
err := r.collection.FindOne(ctx, bson.D{{Key: "_id", Value: id}}).Decode(&row)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, nil
}
return nil, fmt.Errorf("failed to query audit log by id: %w", err)
}
err := r.collection.FindOne(ctx, bson.D{{Key: "_id", Value: id}}).Decode(&row)
if err != nil {
if errors.Is(err, mongo.ErrNoDocuments) {
return nil, nil
}
return nil, fmt.Errorf("failed to query audit log by id: %w", err)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/auditlog/reader_mongodb.go` around lines 204 - 210, The code
currently compares mongo.ErrNoDocuments using `err == mongo.ErrNoDocuments`
after calling r.collection.FindOne(...).Decode(&row); change these to use
errors.Is(err, mongo.ErrNoDocuments) for robust error matching (import the
"errors" package if not already). Update the same pattern wherever similar
checks occur (the other occurrences in the file around the code handling queries
at the spots referenced, e.g., the checks near lines 315 and 331) so all
mongo.ErrNoDocuments comparisons use errors.Is.

Comment on lines +215 to +290
// GetConversation returns a linear conversation thread around a seed log entry.
func (r *MongoDBReader) GetConversation(ctx context.Context, logID string, limit int) (*ConversationResult, error) {
limit = clampConversationLimit(limit)

anchor, err := r.GetLogByID(ctx, logID)
if err != nil {
return nil, err
}
if anchor == nil {
return &ConversationResult{
AnchorID: logID,
Entries: []LogEntry{},
}, nil
}

thread := []*LogEntry{anchor}
seen := map[string]struct{}{anchor.ID: {}}

current := anchor
for len(thread) < limit {
prevID := extractPreviousResponseID(current)
if prevID == "" {
break
}
parent, err := r.findByResponseID(ctx, prevID)
if err != nil {
return nil, err
}
if parent == nil {
break
}
if _, ok := seen[parent.ID]; ok {
break
}
thread = append([]*LogEntry{parent}, thread...)
seen[parent.ID] = struct{}{}
current = parent
}

current = anchor
for len(thread) < limit {
respID := extractResponseID(current)
if respID == "" {
break
}
child, err := r.findByPreviousResponseID(ctx, respID)
if err != nil {
return nil, err
}
if child == nil {
break
}
if _, ok := seen[child.ID]; ok {
break
}
thread = append(thread, child)
seen[child.ID] = struct{}{}
current = child
}

sort.Slice(thread, func(i, j int) bool {
return thread[i].Timestamp.Before(thread[j].Timestamp)
})

entries := make([]LogEntry, 0, len(thread))
for _, entry := range thread {
if entry != nil {
entries = append(entries, *entry)
}
}

return &ConversationResult{
AnchorID: anchor.ID,
Entries: entries,
}, nil
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Implementation is correct with proper cycle detection.

The conversation threading logic correctly:

  • Uses a seen map to prevent infinite loops from cycles
  • Traverses both parent and child chains
  • Applies redaction through toLogEntry() in all helper methods

One consideration: this makes sequential database queries (one per entry in the thread), which is bounded by limit but could be optimized with a single aggregation query using $graphLookup if conversation threads become large or latency-sensitive in production.

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

In `@internal/auditlog/reader_mongodb.go` around lines 215 - 290, GetConversation
currently walks parent/child links with repeated DB calls (via findByResponseID
and findByPreviousResponseID) which is correct but can be optimized; replace or
add an alternative implementation that uses a single MongoDB aggregation with
$graphLookup to fetch the full thread around the anchor ID (using the same link
fields extracted by extractPreviousResponseID/extractResponseID) up to
clampConversationLimit(limit), then sort and map results into the same
ConversationResult shape, preserving cycle-safety and redaction by reusing
toLogEntry() or existing mapping logic.

@SantiagoDePolonia SantiagoDePolonia merged commit 2fd8791 into main Feb 28, 2026
13 checks passed
@SantiagoDePolonia SantiagoDePolonia deleted the feature/audit-logs-sidemenu branch March 22, 2026 14:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant