c084046af0
- 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
77 lines
2.9 KiB
Go
77 lines
2.9 KiB
Go
package web
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// validAppName bounds the operator-typed/POSTed app name (it flows into the ACK and
|
|
// back into store keys — never into a shell, but keep it tight anyway).
|
|
var validAppName = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,63}$`)
|
|
|
|
// handleRequestLogTail — POST /customers/{id}/request-log-tail (form: app).
|
|
// Stores the pending pull-request the report ACK advertises; the controller ships the
|
|
// tail on its next report cycle (the hub never connects into the box). Transparency by
|
|
// default: a customer-visible event line records that the operator requested logs.
|
|
func (s *Server) handleRequestLogTail(w http.ResponseWriter, r *http.Request, customerID string) {
|
|
app := strings.TrimSpace(r.FormValue("app"))
|
|
if !validAppName.MatchString(app) {
|
|
http.Error(w, "Invalid app name", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if err := s.store.RequestLogTail(customerID, app); err != nil {
|
|
s.logger.Printf("[ERROR] RequestLogTail %s/%s: %v", customerID, app, err)
|
|
http.Error(w, "Internal error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if _, err := s.store.SaveEvent(customerID, "log_tail_requested", "info",
|
|
"Az üzemeltető lekérte a(z) "+app+" alkalmazás naplórészletét (távoli diagnosztika).", "", "hub"); err != nil {
|
|
s.logger.Printf("[WARN] SaveEvent log_tail_requested %s/%s: %v", customerID, app, err)
|
|
}
|
|
s.logger.Printf("[INFO] Log tail requested for %s/%s — controller delivers on its next report cycle", customerID, app)
|
|
http.Redirect(w, r, "/customers/"+customerID+"?flash=log_tail_requested", http.StatusSeeOther)
|
|
}
|
|
|
|
// handleLogTailView — GET /customers/{id}/log-tail/{tailID} renders the ordered log
|
|
// view (monospace, line numbers); ?download=1 serves it as a plain-text .log file.
|
|
func (s *Server) handleLogTailView(w http.ResponseWriter, r *http.Request, customerID, tailIDStr string) {
|
|
id, err := strconv.Atoi(tailIDStr)
|
|
if err != nil || id <= 0 {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
tail, err := s.store.GetLogTail(id, customerID)
|
|
if err != nil {
|
|
s.logger.Printf("[ERROR] GetLogTail %d/%s: %v", id, customerID, err)
|
|
http.Error(w, "Internal error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if tail == nil {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
if r.URL.Query().Get("download") == "1" {
|
|
filename := fmt.Sprintf("%s-%s-%s.log", customerID, tail.AppName, tail.CollectedAt.UTC().Format("20060102-150405"))
|
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
|
w.Header().Set("Content-Disposition", `attachment; filename="`+filename+`"`)
|
|
for _, line := range tail.Lines {
|
|
w.Write([]byte(line))
|
|
w.Write([]byte("\n"))
|
|
}
|
|
return
|
|
}
|
|
|
|
data := map[string]interface{}{
|
|
"CustomerID": customerID,
|
|
"Tail": tail,
|
|
}
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
if err := s.templates.ExecuteTemplate(w, "log_tail.html", data); err != nil {
|
|
s.logger.Printf("[ERROR] log_tail.html template: %v", err)
|
|
}
|
|
}
|