updated dash
This commit is contained in:
@@ -3,6 +3,7 @@ package handlers
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -20,20 +21,32 @@ func NewAlertsHandler(deps Deps) *AlertsHandler {
|
||||
return &AlertsHandler{deps: deps}
|
||||
}
|
||||
|
||||
const alertsPerPage = 50
|
||||
|
||||
// AlertsData is passed to the alerts template.
|
||||
type AlertsData struct {
|
||||
PageData
|
||||
Alerts []crowdsec.Alert
|
||||
Filter crowdsec.AlertFilter
|
||||
Alerts []crowdsec.Alert
|
||||
Filter crowdsec.AlertFilter
|
||||
Page int
|
||||
HasNext bool
|
||||
ShowUpdates bool
|
||||
}
|
||||
|
||||
// List renders the alerts list.
|
||||
// List renders the paginated alerts list.
|
||||
func (h *AlertsHandler) List(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
showUpdates := r.URL.Query().Get("show_updates") == "1"
|
||||
|
||||
filter := crowdsec.AlertFilter{
|
||||
Limit: 200,
|
||||
Limit: alertsPerPage + 1,
|
||||
Offset: (page - 1) * alertsPerPage,
|
||||
Scenario: r.URL.Query().Get("scenario"),
|
||||
IP: r.URL.Query().Get("ip"),
|
||||
Since: r.URL.Query().Get("since"),
|
||||
@@ -41,19 +54,34 @@ func (h *AlertsHandler) List(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
alerts, err := h.deps.LAPI.ListAlerts(ctx, filter)
|
||||
if err != nil {
|
||||
h.deps.Renderer.RenderError(w, http.StatusBadGateway, "failed to fetch alerts")
|
||||
log.Printf("alerts: LAPI error: %v", err)
|
||||
pd := NewPageData(r, "Alerts", h.deps.CLIAvailable, h.deps.PollInterval)
|
||||
if crowdsec.IsForbidden(err) {
|
||||
pd = pd.WithFlash("error", "Access denied: re-register machine with admin rights: cscli machines delete crowdsec-dashy && cscli machines add crowdsec-dashy -a")
|
||||
} else {
|
||||
pd = pd.WithFlash("error", "Failed to fetch alerts from LAPI.")
|
||||
}
|
||||
h.deps.Renderer.Render(w, "alerts", AlertsData{PageData: pd})
|
||||
return
|
||||
}
|
||||
|
||||
hasNext := len(alerts) > alertsPerPage
|
||||
if hasNext {
|
||||
alerts = alerts[:alertsPerPage]
|
||||
}
|
||||
|
||||
pd := NewPageData(r, "Alerts", h.deps.CLIAvailable, h.deps.PollInterval)
|
||||
if f := readFlash(r); f.Message != "" {
|
||||
pd.Flash = f
|
||||
}
|
||||
|
||||
h.deps.Renderer.Render(w, "alerts", AlertsData{
|
||||
PageData: pd,
|
||||
Alerts: alerts,
|
||||
Filter: filter,
|
||||
PageData: pd,
|
||||
Alerts: alerts,
|
||||
Filter: filter,
|
||||
Page: page,
|
||||
HasNext: hasNext,
|
||||
ShowUpdates: showUpdates,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"crowdsec-dashy/internal/crowdsec"
|
||||
@@ -20,7 +21,8 @@ func NewAPIHandler(deps Deps) *APIHandler {
|
||||
|
||||
type statsResponse struct {
|
||||
Decisions int `json:"decisions"`
|
||||
Alerts int `json:"alerts"`
|
||||
Alerts24h int `json:"alerts_24h"`
|
||||
Alerts7d int `json:"alerts_7d"`
|
||||
Bouncers int `json:"bouncers"`
|
||||
Machines int `json:"machines"`
|
||||
Healthy bool `json:"healthy"`
|
||||
@@ -36,18 +38,31 @@ func (h *APIHandler) Stats(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
decisions, _ := h.deps.LAPI.ListDecisions(ctx, crowdsec.DecisionFilter{Limit: 500})
|
||||
alerts, _ := h.deps.LAPI.ListAlerts(ctx, crowdsec.AlertFilter{Limit: 500})
|
||||
// Fetch 24h and 7d alert counts in parallel to minimize latency.
|
||||
var alerts24h, alerts7d []crowdsec.Alert
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
alerts24h, _ = h.deps.LAPI.ListAlerts(ctx, crowdsec.AlertFilter{Limit: 5000, Since: "24h"})
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
alerts7d, _ = h.deps.LAPI.ListAlerts(ctx, crowdsec.AlertFilter{Limit: 5000, Since: "168h"})
|
||||
}()
|
||||
wg.Wait()
|
||||
|
||||
resp := statsResponse{
|
||||
Decisions: len(decisions),
|
||||
Alerts: len(alerts),
|
||||
Alerts24h: len(alerts24h),
|
||||
Alerts7d: len(alerts7d),
|
||||
Healthy: h.deps.LAPI.IsHealthy(ctx),
|
||||
}
|
||||
|
||||
if h.deps.CLIAvailable {
|
||||
decisions, _ := h.deps.CLI.ListDecisions(ctx, crowdsec.DecisionFilter{Limit: 500})
|
||||
bouncers, _ := h.deps.CLI.ListBouncers(ctx)
|
||||
machines, _ := h.deps.CLI.ListMachines(ctx)
|
||||
resp.Decisions = len(decisions)
|
||||
resp.Bouncers = len(bouncers)
|
||||
resp.Machines = len(machines)
|
||||
}
|
||||
|
||||
@@ -29,7 +29,10 @@ func (h *DashboardHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
decisions, _ := h.deps.LAPI.ListDecisions(ctx, crowdsec.DecisionFilter{Limit: 10})
|
||||
var decisions []crowdsec.Decision
|
||||
if h.deps.CLIAvailable {
|
||||
decisions, _ = h.deps.CLI.ListDecisions(ctx, crowdsec.DecisionFilter{Limit: 10})
|
||||
}
|
||||
alerts, _ := h.deps.LAPI.ListAlerts(ctx, crowdsec.AlertFilter{Limit: 10})
|
||||
|
||||
h.deps.Renderer.Render(w, "dashboard", DashboardData{
|
||||
|
||||
@@ -3,6 +3,7 @@ package handlers
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"regexp"
|
||||
@@ -45,6 +46,13 @@ func (h *DecisionsHandler) List(w http.ResponseWriter, r *http.Request) {
|
||||
page = 1
|
||||
}
|
||||
|
||||
if !h.deps.CLIAvailable {
|
||||
pd := NewPageData(r, "Decisions", h.deps.CLIAvailable, h.deps.PollInterval)
|
||||
pd = pd.WithFlash("error", "cscli is required for decisions management but is not available.")
|
||||
h.deps.Renderer.Render(w, "decisions", DecisionsData{PageData: pd})
|
||||
return
|
||||
}
|
||||
|
||||
filter := crowdsec.DecisionFilter{
|
||||
Limit: decisionsPerPage + 1,
|
||||
Offset: (page - 1) * decisionsPerPage,
|
||||
@@ -54,9 +62,12 @@ func (h *DecisionsHandler) List(w http.ResponseWriter, r *http.Request) {
|
||||
Origin: r.URL.Query().Get("origin"),
|
||||
}
|
||||
|
||||
decisions, err := h.deps.LAPI.ListDecisions(ctx, filter)
|
||||
decisions, err := h.deps.CLI.ListDecisions(ctx, filter)
|
||||
if err != nil {
|
||||
h.deps.Renderer.RenderError(w, http.StatusBadGateway, "failed to fetch decisions")
|
||||
log.Printf("decisions: CLI error: %v", err)
|
||||
pd := NewPageData(r, "Decisions", h.deps.CLIAvailable, h.deps.PollInterval)
|
||||
pd = pd.WithFlash("error", "Failed to fetch decisions via cscli.")
|
||||
h.deps.Renderer.Render(w, "decisions", DecisionsData{PageData: pd})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -112,9 +123,8 @@ func (h *DecisionsHandler) Add(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := h.deps.LAPI.AddDecision(ctx, crowdsec.DecisionInput{
|
||||
if err := h.deps.CLI.AddDecision(ctx, crowdsec.DecisionInput{
|
||||
Duration: duration,
|
||||
Origin: "cscli",
|
||||
Scenario: scenario,
|
||||
Scope: scope,
|
||||
Type: decType,
|
||||
@@ -157,7 +167,7 @@ func (h *DecisionsHandler) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
errs = append(errs, fmt.Sprintf("invalid id: %q", s))
|
||||
continue
|
||||
}
|
||||
if err := h.deps.LAPI.DeleteDecision(ctx, id); err != nil {
|
||||
if err := h.deps.CLI.DeleteDecision(ctx, id); err != nil {
|
||||
errs = append(errs, fmt.Sprintf("id %d: %v", id, err))
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -21,22 +21,24 @@ func NewMetricsHandler(deps Deps) *MetricsHandler {
|
||||
// MetricsData is passed to the metrics template.
|
||||
type MetricsData struct {
|
||||
PageData
|
||||
Sections []crowdsec.MetricsSection
|
||||
Sections []crowdsec.MetricsSection
|
||||
RawOutput string
|
||||
}
|
||||
|
||||
func (h *MetricsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
pd := NewPageData(r, "Metrics", h.deps.CLIAvailable, h.deps.PollInterval)
|
||||
|
||||
var sections []crowdsec.MetricsSection
|
||||
var raw string
|
||||
if h.deps.CLIAvailable {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
var err error
|
||||
sections, err = h.deps.CLI.GetMetrics(ctx)
|
||||
sections, raw, err = h.deps.CLI.GetMetrics(ctx)
|
||||
if err != nil {
|
||||
pd.Flash = FlashMessage{Type: "error", Message: fmt.Sprintf("cscli error: %v", err)}
|
||||
}
|
||||
}
|
||||
|
||||
h.deps.Renderer.Render(w, "metrics", MetricsData{PageData: pd, Sections: sections})
|
||||
h.deps.Renderer.Render(w, "metrics", MetricsData{PageData: pd, Sections: sections, RawOutput: raw})
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
@@ -112,6 +113,28 @@ func buildFuncMap() template.FuncMap {
|
||||
return template.FuncMap{
|
||||
"inc": func(i int) int { return i + 1 },
|
||||
"dec": func(i int) int { return i - 1 },
|
||||
// alertsPageURL builds the prev/next link for the alerts page.
|
||||
"alertsPageURL": func(f crowdsec.AlertFilter, page int, next bool, showUpdates bool) string {
|
||||
p := page - 1
|
||||
if next {
|
||||
p = page + 1
|
||||
}
|
||||
q := url.Values{}
|
||||
q.Set("page", fmt.Sprintf("%d", p))
|
||||
if f.Scenario != "" {
|
||||
q.Set("scenario", f.Scenario)
|
||||
}
|
||||
if f.IP != "" {
|
||||
q.Set("ip", f.IP)
|
||||
}
|
||||
if f.Since != "" {
|
||||
q.Set("since", f.Since)
|
||||
}
|
||||
if showUpdates {
|
||||
q.Set("show_updates", "1")
|
||||
}
|
||||
return "/alerts?" + q.Encode()
|
||||
},
|
||||
// dict builds a map for passing multiple values to a sub-template.
|
||||
// Usage: {{template "foo" dict "Key1" val1 "Key2" val2}}
|
||||
"dict": func(pairs ...any) (map[string]any, error) {
|
||||
|
||||
Reference in New Issue
Block a user