Files
felhom.eu/hub/internal/web/logtail.go
T
admin 60244727ad feat(hub): Direction-2 immediate-sync wait channel (v0.58.0)
GET /api/v1/wait long-poll: the box holds an authed hanging GET; the hub
completes it the instant any operator intent bumps that customer's in-memory
generation, then the box fires its ordinary report and the ACK delivers
everything through the unchanged machinery. 240s hold with a 25s heartbeat
newline defeats the nginx 60s proxy_read_timeout with no ingress annotation;
WriteTimeout lifted per-connection via ResponseController.

- internal/intent: per-customer generation counter + waiter registry
  (Bump/Wait/Close), coalescing to latest, race-closer, in-memory by design.
  Red-proofs: counter-vs-queue + race-closer (run-fail-reverted).
- api/wait.go: the endpoint (per-customer only; global key 400; A cannot see B).
- web bumps after every intent write (fire-after-commit): config CRUD, claim
  resend, offsite re-issue/freeze, password regen, block/unblock, floors
  (global bumps all config-managed), controller log-tail + log-bundle.
- main.go: one intent hub shared by web+api; Close() before server.Shutdown.

Pairs with controller v0.140.0 (the long-poll client). Grounding:
documentation/audits/SPIKE-immediate-sync-transport-2026-07-16.md.
2026-07-16 20:44:22 +02:00

78 lines
3.0 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)
s.bumpIntent(customerID) // Direction-2: pull the tail in seconds, not on the next cycle
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)
}
}