Skip to content

Commit d661d40

Browse files
fix(admin): address audit log review findings
1 parent a599b41 commit d661d40

8 files changed

Lines changed: 68 additions & 107 deletions

File tree

cmd/gomodel/docs/docs.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ const docTemplate = `{
136136
"in": "query"
137137
},
138138
{
139-
"type": "string",
139+
"type": "boolean",
140140
"description": "Filter by stream mode (true/false)",
141141
"name": "stream",
142142
"in": "query"

cmd/gomodel/docs/swagger.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@
132132
"in": "query"
133133
},
134134
{
135-
"type": "string",
135+
"type": "boolean",
136136
"description": "Filter by stream mode (true/false)",
137137
"name": "stream",
138138
"in": "query"

internal/admin/dashboard/static/css/dashboard.css

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1127,6 +1127,11 @@ td.col-price {
11271127
color: var(--text-muted);
11281128
}
11291129

1130+
.audit-status-badge.status-unknown {
1131+
color: var(--text-muted);
1132+
border-color: color-mix(in srgb, var(--border) 75%, transparent);
1133+
}
1134+
11301135
.audit-entry-details {
11311136
border-top: 1px solid transparent;
11321137
padding: 0 12px;
@@ -1207,7 +1212,6 @@ td.col-price {
12071212
.audit-json-body {
12081213
white-space: pre-wrap;
12091214
overflow-wrap: anywhere;
1210-
word-break: break-word;
12111215
}
12121216

12131217
.conversation-body-highlight {
@@ -1375,11 +1379,11 @@ td.col-price {
13751379
}
13761380

13771381
.chat-content {
1378-
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
1382+
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
13791383
font-size: 13px;
13801384
line-height: 1.5;
13811385
white-space: pre-wrap;
1382-
word-break: break-word;
1386+
overflow-wrap: break-word;
13831387
color: var(--text);
13841388
}
13851389

internal/admin/dashboard/static/js/modules/audit-list.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,12 @@
6868
},
6969

7070
statusCodeClass(statusCode) {
71-
if (statusCode >= 500) return 'status-error';
72-
if (statusCode >= 400) return 'status-warning';
73-
if (statusCode >= 300) return 'status-neutral';
71+
if (statusCode === null || statusCode === undefined || statusCode === '') return 'status-unknown';
72+
const parsedStatus = Number(statusCode);
73+
if (!Number.isFinite(parsedStatus)) return 'status-unknown';
74+
if (parsedStatus >= 500) return 'status-error';
75+
if (parsedStatus >= 400) return 'status-warning';
76+
if (parsedStatus >= 300) return 'status-neutral';
7477
return 'status-success';
7578
},
7679

internal/admin/dashboard/templates/index.html

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -344,22 +344,22 @@ <h2>Audit Logs</h2>
344344
<div class="audit-log-section">
345345
<div class="audit-log-toolbar">
346346
<div class="audit-filter-row">
347-
<input type="text" placeholder="Search by request ID, path, provider, error..." x-model="auditSearch" @input.debounce.300ms="fetchAuditLog(true)" class="filter-input">
348-
<input type="text" placeholder="Model" x-model="auditModel" @input.debounce.300ms="fetchAuditLog(true)" class="filter-input audit-filter-input">
349-
<input type="text" placeholder="Provider" x-model="auditProvider" @input.debounce.300ms="fetchAuditLog(true)" class="filter-input audit-filter-input">
350-
<input type="text" placeholder="Path" x-model="auditPath" @input.debounce.300ms="fetchAuditLog(true)" class="filter-input audit-filter-input">
347+
<input id="audit-filter-search" type="text" placeholder="Search by request ID, path, provider, error..." aria-label="Search by request ID, path, provider, or error" x-model="auditSearch" @input.debounce.300ms="fetchAuditLog(true)" class="filter-input">
348+
<input id="audit-filter-model" type="text" placeholder="Model" aria-label="Model filter" x-model="auditModel" @input.debounce.300ms="fetchAuditLog(true)" class="filter-input audit-filter-input">
349+
<input id="audit-filter-provider" type="text" placeholder="Provider" aria-label="Provider filter" x-model="auditProvider" @input.debounce.300ms="fetchAuditLog(true)" class="filter-input audit-filter-input">
350+
<input id="audit-filter-path" type="text" placeholder="Path" aria-label="Path filter" x-model="auditPath" @input.debounce.300ms="fetchAuditLog(true)" class="filter-input audit-filter-input">
351351
<button class="pagination-btn" @click="clearAuditFilters()">Clear</button>
352352
</div>
353353
<div class="audit-filter-row audit-filter-row-selects">
354-
<select x-model="auditMethod" @change="fetchAuditLog(true)" class="usage-log-select audit-filter-select">
354+
<select id="audit-filter-method" aria-label="HTTP method filter" x-model="auditMethod" @change="fetchAuditLog(true)" class="usage-log-select audit-filter-select">
355355
<option value="">All Methods</option>
356356
<option value="GET">GET</option>
357357
<option value="POST">POST</option>
358358
<option value="PUT">PUT</option>
359359
<option value="PATCH">PATCH</option>
360360
<option value="DELETE">DELETE</option>
361361
</select>
362-
<select x-model="auditStatusCode" @change="fetchAuditLog(true)" class="usage-log-select audit-filter-select">
362+
<select id="audit-filter-status" aria-label="Status code filter" x-model="auditStatusCode" @change="fetchAuditLog(true)" class="usage-log-select audit-filter-select">
363363
<option value="">All Statuses</option>
364364
<option value="200">200</option>
365365
<option value="201">201</option>
@@ -372,7 +372,7 @@ <h2>Audit Logs</h2>
372372
<option value="502">502</option>
373373
<option value="503">503</option>
374374
</select>
375-
<select x-model="auditStream" @change="fetchAuditLog(true)" class="usage-log-select audit-filter-select">
375+
<select id="audit-filter-stream" aria-label="Streaming mode filter" x-model="auditStream" @change="fetchAuditLog(true)" class="usage-log-select audit-filter-select">
376376
<option value="">All Modes</option>
377377
<option value="true">Streaming</option>
378378
<option value="false">Non-streaming</option>
@@ -397,6 +397,7 @@ <h2>Audit Logs</h2>
397397
<button class="audit-conversation-trigger"
398398
x-show="canShowConversation(entry)"
399399
title="Open conversation"
400+
aria-label="Open conversation"
400401
@click.stop.prevent="openConversation(entry, $el.closest('details'), true)">
401402
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
402403
<path d="M9 6l6 6-6 6"/>

internal/admin/handler.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,7 @@ func (h *Handler) UsageLog(c echo.Context) error {
320320
// @Param path query string false "Filter by request path"
321321
// @Param error_type query string false "Filter by error type"
322322
// @Param status_code query int false "Filter by status code"
323-
// @Param stream query string false "Filter by stream mode (true/false)"
323+
// @Param stream query bool false "Filter by stream mode (true/false)"
324324
// @Param search query string false "Search across request_id/model/provider/method/path/error_type"
325325
// @Param limit query int false "Page size (default 25, max 100)"
326326
// @Param offset query int false "Offset for pagination"
@@ -423,6 +423,9 @@ func (h *Handler) AuditConversation(c echo.Context) error {
423423
if err != nil {
424424
return handleError(c, core.NewInvalidRequestError("invalid limit, expected integer", nil))
425425
}
426+
if parsed < 1 || parsed > 200 {
427+
return handleError(c, core.NewInvalidRequestError("invalid limit parameter: limit must be between 1 and 200", nil))
428+
}
426429
limit = parsed
427430
}
428431

internal/auditlog/reader_mongodb.go

Lines changed: 40 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,40 @@ type MongoDBReader struct {
1717
collection *mongo.Collection
1818
}
1919

20+
type mongoLogRow struct {
21+
ID string `bson:"_id"`
22+
Timestamp time.Time `bson:"timestamp"`
23+
DurationNs int64 `bson:"duration_ns"`
24+
Model string `bson:"model"`
25+
Provider string `bson:"provider"`
26+
StatusCode int `bson:"status_code"`
27+
RequestID string `bson:"request_id"`
28+
ClientIP string `bson:"client_ip"`
29+
Method string `bson:"method"`
30+
Path string `bson:"path"`
31+
Stream bool `bson:"stream"`
32+
ErrorType string `bson:"error_type"`
33+
Data *LogData `bson:"data"`
34+
}
35+
36+
func (r mongoLogRow) toLogEntry() *LogEntry {
37+
return &LogEntry{
38+
ID: r.ID,
39+
Timestamp: r.Timestamp,
40+
DurationNs: r.DurationNs,
41+
Model: r.Model,
42+
Provider: r.Provider,
43+
StatusCode: r.StatusCode,
44+
RequestID: r.RequestID,
45+
ClientIP: r.ClientIP,
46+
Method: r.Method,
47+
Path: r.Path,
48+
Stream: r.Stream,
49+
ErrorType: r.ErrorType,
50+
Data: r.Data,
51+
}
52+
}
53+
2054
// NewMongoDBReader creates a new MongoDB audit log reader.
2155
func NewMongoDBReader(database *mongo.Database) (*MongoDBReader, error) {
2256
if database == nil {
@@ -180,21 +214,7 @@ func (r *MongoDBReader) GetLogs(ctx context.Context, params LogQueryParams) (*Lo
180214

181215
// GetLogByID returns a single audit log entry by ID.
182216
func (r *MongoDBReader) GetLogByID(ctx context.Context, id string) (*LogEntry, error) {
183-
var row struct {
184-
ID string `bson:"_id"`
185-
Timestamp time.Time `bson:"timestamp"`
186-
DurationNs int64 `bson:"duration_ns"`
187-
Model string `bson:"model"`
188-
Provider string `bson:"provider"`
189-
StatusCode int `bson:"status_code"`
190-
RequestID string `bson:"request_id"`
191-
ClientIP string `bson:"client_ip"`
192-
Method string `bson:"method"`
193-
Path string `bson:"path"`
194-
Stream bool `bson:"stream"`
195-
ErrorType string `bson:"error_type"`
196-
Data *LogData `bson:"data"`
197-
}
217+
var row mongoLogRow
198218

199219
err := r.collection.FindOne(ctx, bson.D{{Key: "_id", Value: id}}).Decode(&row)
200220
if err != nil {
@@ -204,21 +224,7 @@ func (r *MongoDBReader) GetLogByID(ctx context.Context, id string) (*LogEntry, e
204224
return nil, fmt.Errorf("failed to query audit log by id: %w", err)
205225
}
206226

207-
return &LogEntry{
208-
ID: row.ID,
209-
Timestamp: row.Timestamp,
210-
DurationNs: row.DurationNs,
211-
Model: row.Model,
212-
Provider: row.Provider,
213-
StatusCode: row.StatusCode,
214-
RequestID: row.RequestID,
215-
ClientIP: row.ClientIP,
216-
Method: row.Method,
217-
Path: row.Path,
218-
Stream: row.Stream,
219-
ErrorType: row.ErrorType,
220-
Data: row.Data,
221-
}, nil
227+
return row.toLogEntry(), nil
222228
}
223229

224230
// GetConversation returns a linear conversation thread around a seed log entry.
@@ -318,21 +324,7 @@ func (r *MongoDBReader) findByResponseID(ctx context.Context, responseID string)
318324
filter := bson.D{{Key: "data.response_body.id", Value: responseID}}
319325
opts := options.FindOne().SetSort(bson.D{{Key: "timestamp", Value: 1}})
320326

321-
var row struct {
322-
ID string `bson:"_id"`
323-
Timestamp time.Time `bson:"timestamp"`
324-
DurationNs int64 `bson:"duration_ns"`
325-
Model string `bson:"model"`
326-
Provider string `bson:"provider"`
327-
StatusCode int `bson:"status_code"`
328-
RequestID string `bson:"request_id"`
329-
ClientIP string `bson:"client_ip"`
330-
Method string `bson:"method"`
331-
Path string `bson:"path"`
332-
Stream bool `bson:"stream"`
333-
ErrorType string `bson:"error_type"`
334-
Data *LogData `bson:"data"`
335-
}
327+
var row mongoLogRow
336328

337329
if err := r.collection.FindOne(ctx, filter, opts).Decode(&row); err != nil {
338330
if err == mongo.ErrNoDocuments {
@@ -341,42 +333,14 @@ func (r *MongoDBReader) findByResponseID(ctx context.Context, responseID string)
341333
return nil, fmt.Errorf("failed to query audit log by response id: %w", err)
342334
}
343335

344-
return &LogEntry{
345-
ID: row.ID,
346-
Timestamp: row.Timestamp,
347-
DurationNs: row.DurationNs,
348-
Model: row.Model,
349-
Provider: row.Provider,
350-
StatusCode: row.StatusCode,
351-
RequestID: row.RequestID,
352-
ClientIP: row.ClientIP,
353-
Method: row.Method,
354-
Path: row.Path,
355-
Stream: row.Stream,
356-
ErrorType: row.ErrorType,
357-
Data: row.Data,
358-
}, nil
336+
return row.toLogEntry(), nil
359337
}
360338

361339
func (r *MongoDBReader) findByPreviousResponseID(ctx context.Context, previousResponseID string) (*LogEntry, error) {
362340
filter := bson.D{{Key: "data.request_body.previous_response_id", Value: previousResponseID}}
363341
opts := options.FindOne().SetSort(bson.D{{Key: "timestamp", Value: 1}})
364342

365-
var row struct {
366-
ID string `bson:"_id"`
367-
Timestamp time.Time `bson:"timestamp"`
368-
DurationNs int64 `bson:"duration_ns"`
369-
Model string `bson:"model"`
370-
Provider string `bson:"provider"`
371-
StatusCode int `bson:"status_code"`
372-
RequestID string `bson:"request_id"`
373-
ClientIP string `bson:"client_ip"`
374-
Method string `bson:"method"`
375-
Path string `bson:"path"`
376-
Stream bool `bson:"stream"`
377-
ErrorType string `bson:"error_type"`
378-
Data *LogData `bson:"data"`
379-
}
343+
var row mongoLogRow
380344

381345
if err := r.collection.FindOne(ctx, filter, opts).Decode(&row); err != nil {
382346
if err == mongo.ErrNoDocuments {
@@ -385,19 +349,5 @@ func (r *MongoDBReader) findByPreviousResponseID(ctx context.Context, previousRe
385349
return nil, fmt.Errorf("failed to query audit log by previous_response_id: %w", err)
386350
}
387351

388-
return &LogEntry{
389-
ID: row.ID,
390-
Timestamp: row.Timestamp,
391-
DurationNs: row.DurationNs,
392-
Model: row.Model,
393-
Provider: row.Provider,
394-
StatusCode: row.StatusCode,
395-
RequestID: row.RequestID,
396-
ClientIP: row.ClientIP,
397-
Method: row.Method,
398-
Path: row.Path,
399-
Stream: row.Stream,
400-
ErrorType: row.ErrorType,
401-
Data: row.Data,
402-
}, nil
352+
return row.toLogEntry(), nil
403353
}

internal/auditlog/reader_sqlite.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ func (r *SQLiteReader) GetConversation(ctx context.Context, logID string, limit
184184
if _, ok := seen[parent.ID]; ok {
185185
break
186186
}
187-
thread = append([]*LogEntry{parent}, thread...)
187+
thread = append(thread, parent)
188188
seen[parent.ID] = struct{}{}
189189
current = parent
190190
}

0 commit comments

Comments
 (0)