fix: display log fields in Mini App log viewer

renderLogs() was ignoring the fields property sent via WebSocket,
showing only the message. Now renders fields as {key=value, ...}
after the message, matching terminal output.

Also update CLAUDE.md: mark Log Fields masking TODO as done,
add Known Gaps section noting lack of frontend test coverage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
dj-oyu 2026-02-24 06:37:07 +09:00
parent 381ca09c41
commit 1ebdd51d7e
2 changed files with 15 additions and 2 deletions

View file

@ -21,4 +21,8 @@ Lint: `golangci-lint run`
## Security TODOs ## Security TODOs
- **Log Fields masking**: `LogEntry.Fields` (`map[string]any`) is exposed via WebSocket (`/miniapp/api/logs/ws`) and snapshots (`/miniapp/api/logs/snapshot`). If any code logs sensitive values (tokens, API keys, passwords) in Fields, they will be visible to Mini App users. Add a sanitizer in `RecentLogs()` and `wsLogs()` that masks values for keys matching patterns like `token`, `key`, `secret`, `password`, `authorization`. Track in: `pkg/logger/logger.go` (RecentLogs), `pkg/miniapp/miniapp.go` (wsLogs stream). - ~~**Log Fields masking**~~: Done. `SanitizeFields()` in `pkg/logger/logger.go` masks keys matching `token`, `key`, `secret`, `password`, `authorization`, `credential`. Applied in `RecentLogs()` and `wsLogs()` stream.
## Known Gaps
- **Mini App log viewer has no frontend tests**: `renderLogs()` in `pkg/miniapp/static/index.html` is inline vanilla JS with no unit/E2E test coverage. Backend (Go) tests cover `RecentLogs`, `SanitizeFields`, and JSON serialization, but nothing verifies the JS rendering. This allowed the Fields display bug (fields sent but not rendered) to ship undetected.

View file

@ -755,6 +755,7 @@
.log-badge.debug { background: rgba(142,142,147,0.12); color: #8e8e93; } .log-badge.debug { background: rgba(142,142,147,0.12); color: #8e8e93; }
.log-comp { color: var(--link); flex-shrink: 0; font-size: 10px; } .log-comp { color: var(--link); flex-shrink: 0; font-size: 10px; }
.log-msg { flex: 1; word-break: break-all; color: var(--text); } .log-msg { flex: 1; word-break: break-all; color: var(--text); }
.log-fields { color: var(--hint); font-size: 10px; }
.log-actions { display: flex; gap: 8px; align-items: center; margin-top: 8px; } .log-actions { display: flex; gap: 8px; align-items: center; margin-top: 8px; }
.log-snap-btn { .log-snap-btn {
padding: 6px 12px; padding: 6px 12px;
@ -1607,11 +1608,19 @@ function renderLogs() {
var lvl = (e.level || 'info').toLowerCase(); var lvl = (e.level || 'info').toLowerCase();
var ts = e.timestamp ? e.timestamp.substring(11, 19) : ''; var ts = e.timestamp ? e.timestamp.substring(11, 19) : '';
var comp = e.component ? '<span class="log-comp">' + escapeHtml(e.component) + '</span>' : ''; var comp = e.component ? '<span class="log-comp">' + escapeHtml(e.component) + '</span>' : '';
var fields = '';
if (e.fields && typeof e.fields === 'object') {
var parts = [];
for (var k in e.fields) {
if (e.fields.hasOwnProperty(k)) parts.push(k + '=' + e.fields[k]);
}
if (parts.length) fields = ' <span class="log-fields">{' + escapeHtml(parts.join(', ')) + '}</span>';
}
html += '<div class="log-entry">' + html += '<div class="log-entry">' +
'<span class="log-ts">' + ts + '</span>' + '<span class="log-ts">' + ts + '</span>' +
'<span class="log-badge ' + lvl + '">' + lvl + '</span>' + '<span class="log-badge ' + lvl + '">' + lvl + '</span>' +
comp + comp +
'<span class="log-msg">' + escapeHtml(e.message || '') + '</span>' + '<span class="log-msg">' + escapeHtml(e.message || '') + fields + '</span>' +
'</div>'; '</div>';
} }
container.innerHTML = html; container.innerHTML = html;