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:
@@ -65,10 +65,29 @@ func (s *Server) handleApps(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *Server) handleAppDetail(w http.ResponseWriter, r *http.Request, appName string) {
|
||||
period := r.URL.Query().Get("period")
|
||||
since := parsePeriod(period, 7*24*time.Hour)
|
||||
// Part H: ?customer=<id> narrows Known Issues to rows affecting that customer
|
||||
// (the customer page's per-app drill-down carries it). The unfiltered page stays
|
||||
// the fleet view.
|
||||
customerFilter := r.URL.Query().Get("customer")
|
||||
showDismissed := r.URL.Query().Get("show_dismissed") == "1"
|
||||
|
||||
customers, _ := s.store.GetAppCustomerBreakdown(appName, since)
|
||||
history, _ := s.store.GetAppTelemetryHistory(appName, since)
|
||||
issues, _ := s.store.GetAppIssues(appName, 20)
|
||||
// Part F fix: the issues query gets the SAME period cutoff the chart uses (it used
|
||||
// to ignore the selector — a 24h view showed 25-day-old rows).
|
||||
issues, _ := s.store.GetAppIssues(appName, since, showDismissed, 20)
|
||||
if customerFilter != "" {
|
||||
filtered := issues[:0]
|
||||
for _, is := range issues {
|
||||
for _, c := range is.AffectedCustomers {
|
||||
if c == customerFilter {
|
||||
filtered = append(filtered, is)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
issues = filtered
|
||||
}
|
||||
|
||||
// Get fleet summary to find this app's summary
|
||||
fleetAll, _ := s.store.GetFleetAppSummary(since)
|
||||
@@ -98,6 +117,8 @@ func (s *Server) handleAppDetail(w http.ResponseWriter, r *http.Request, appName
|
||||
"ChartData": chartData,
|
||||
"SuggestedLimit": suggestedLimit,
|
||||
"Period": period,
|
||||
"CustomerFilter": customerFilter,
|
||||
"ShowDismissed": showDismissed,
|
||||
"CSRFToken": csrfToken,
|
||||
"Flash": r.URL.Query().Get("flash"),
|
||||
}
|
||||
@@ -130,16 +151,19 @@ func (s *Server) handleResetAppTelemetry(w http.ResponseWriter, r *http.Request,
|
||||
http.Redirect(w, r, target, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// handleDeleteAppIssues handles POST requests to delete selected or all issues for an app.
|
||||
func (s *Server) handleDeleteAppIssues(w http.ResponseWriter, r *http.Request, appName string) {
|
||||
// handleDismissAppIssues handles POST requests to dismiss selected or all issues for an app.
|
||||
// Dismissal, not deletion (Part G): the controller re-sends recurring issues every report,
|
||||
// so a hard delete reappears minutes later. A dismissed row stays hidden until a genuinely
|
||||
// NEW occurrence (last_seen > dismissed_at) resurfaces it in upsertAppIssue.
|
||||
func (s *Server) handleDismissAppIssues(w http.ResponseWriter, r *http.Request, appName string) {
|
||||
action := r.FormValue("action")
|
||||
|
||||
var deletedCount int64
|
||||
var dismissedCount int64
|
||||
var err error
|
||||
|
||||
switch action {
|
||||
case "all":
|
||||
deletedCount, err = s.store.DeleteAppIssues(appName)
|
||||
dismissedCount, err = s.store.DismissAppIssues(appName)
|
||||
case "selected":
|
||||
r.ParseForm()
|
||||
idStrs := r.Form["issue_ids"]
|
||||
@@ -159,22 +183,22 @@ func (s *Server) handleDeleteAppIssues(w http.ResponseWriter, r *http.Request, a
|
||||
ids = append(ids, id)
|
||||
}
|
||||
}
|
||||
deletedCount, err = s.store.DeleteAppIssuesByIDs(ids)
|
||||
dismissedCount, err = s.store.DismissAppIssuesByIDs(ids)
|
||||
default:
|
||||
http.Error(w, "Invalid action", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
s.logger.Printf("[ERROR] Failed to delete issues for %s: %v", appName, err)
|
||||
s.logger.Printf("[ERROR] Failed to dismiss issues for %s: %v", appName, err)
|
||||
http.Error(w, "Internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
s.logger.Printf("[INFO] Deleted %d issues for %s (action=%s)", deletedCount, appName, action)
|
||||
s.logger.Printf("[INFO] Dismissed %d issues for %s (action=%s)", dismissedCount, appName, action)
|
||||
|
||||
period := r.URL.Query().Get("period")
|
||||
target := "/apps/" + appName + "?flash=issues_deleted"
|
||||
target := "/apps/" + appName + "?flash=issues_dismissed"
|
||||
if period != "" {
|
||||
target += "&period=" + period
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user