hub v0.43.0: remote app-log diagnostics — copyable issues + context + on-demand log tails + range/dismissal fixes

- store: app_log_issues gains context/context_customer (first capture wins) + dismissed_at (resurface only on last_seen > dismissed_at); log_tail_requests (pending operator intents, consume-once) + app_log_tails (transient, keep last 2 per app)
- api: /report ingests log_tails (stores + clears the request); ACK advertises log_tail_requests (same additive omit-when-empty pattern as escrow)
- web: Known Issues rows click-to-expand (full copyable message + context with provenance + explicit affected-customers list); Dismiss replaces Delete; period selector now filters issues (F); ?customer= filtered view + customer-page drill-down links (H); per-app Request-log-tail button + pending badge + App Log Tails section + ordered tail view with line numbers + .log download; customer-visible log_tail_requested event
- tests: store (context first-capture/late-adopt, range filter, dismissal old-window vs new-occurrence, tail request/fulfill/prune/scoping), api ACK round-trip, web render (expanded row, customer page sections, tail view + download + cross-customer 404)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
2026-07-10 16:03:32 +02:00
parent 0cc70a7a5a
commit c084046af0
15 changed files with 1180 additions and 44 deletions
@@ -49,6 +49,7 @@
{{else if eq .Flash "offsite_unfrozen"}}Offsite storage unfrozen — read-write restored.
{{else if eq .Flash "blocked"}}Customer blocked — hidden from Dashboard.
{{else if eq .Flash "unblocked"}}Customer unblocked — visible on Dashboard again.
{{else if eq .Flash "log_tail_requested"}}Log tail requested — the controller delivers it on its next report cycle (a few minutes). A customer-visible event line was recorded.
{{end}}
</div>
{{end}}
@@ -616,18 +617,62 @@
<th>Catalog Limit</th>
<th>Errors</th>
<th>Warnings</th>
<th>Logs</th>
</tr>
</thead>
<tbody>
{{range .AppTelemetry}}
<tr>
<td><a href="/apps/{{.AppName}}">{{if .DisplayName}}{{.DisplayName}}{{else}}{{.AppName}}{{end}}</a></td>
<td><a href="/apps/{{.AppName}}?customer={{$.CustomerID}}" title="Known issues filtered to this customer">{{if .DisplayName}}{{.DisplayName}}{{else}}{{.AppName}}{{end}}</a></td>
<td class="{{memoryColor .MemoryCurrentMB .CatalogLimit}}">{{formatFloat .MemoryCurrentMB}} MB</td>
<td>{{formatFloat .MemoryAvgMB}} MB</td>
<td>{{formatFloat .MemoryPeakMB}} MB</td>
<td>{{if .CatalogLimit}}{{.CatalogLimit}}{{else}}—{{end}}</td>
<td>{{if gt .LogErrors 0}}<span class="badge badge-error">{{.LogErrors}}</span>{{else}}0{{end}}</td>
<td>{{if gt .LogWarnings 0}}<span class="badge badge-warn">{{.LogWarnings}}</span>{{else}}0{{end}}</td>
<td>
{{if index $.PendingTails .AppName}}
<span class="badge badge-neutral" title="The controller delivers the tail on its next report cycle">tail pending</span>
{{else}}
<form method="POST" action="/customers/{{$.CustomerID}}/request-log-tail" style="display: inline;">
<input type="hidden" name="_csrf" value="{{$.CSRFToken}}">
<input type="hidden" name="app" value="{{.AppName}}">
<button type="submit" class="btn btn-sm" title="Pull-based: the controller ships the last 200 log lines on its next report; a customer-visible event line is recorded">Request log tail</button>
</form>
{{end}}
</td>
</tr>
{{end}}
</tbody>
</table>
</section>
{{end}}
<!-- Received log tails (on-demand, transient — last 2 per app) -->
{{if .HasLogTails}}
<section class="card">
<h2>App Log Tails <span class="text-muted" style="font-size: 0.85em; font-weight: normal;">(on-demand, last 2 per app kept)</span></h2>
<table class="data-table">
<thead>
<tr>
<th>App</th>
<th>Collected</th>
<th>Received</th>
<th>Lines</th>
<th></th>
</tr>
</thead>
<tbody>
{{range .LogTails}}
<tr>
<td style="font-family: var(--font-mono);">{{.AppName}}</td>
<td>{{.CollectedAt.Format "2006-01-02 15:04:05"}} ({{timeAgo .CollectedAt}})</td>
<td>{{timeAgo .ReceivedAt}}</td>
<td>{{len .Lines}}</td>
<td style="white-space: nowrap;">
<a href="/customers/{{$.CustomerID}}/log-tail/{{.ID}}" class="btn btn-sm">View</a>
<a href="/customers/{{$.CustomerID}}/log-tail/{{.ID}}?download=1" class="btn btn-sm">Download .log</a>
</td>
</tr>
{{end}}
</tbody>