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) } }