aboutsummaryrefslogtreecommitdiff
path: root/backend
diff options
context:
space:
mode:
Diffstat (limited to 'backend')
-rw-r--r--backend/internal/alerts/alerts.go2
-rw-r--r--backend/internal/api/handler.go91
-rw-r--r--backend/internal/api/router.go5
-rw-r--r--backend/internal/monitor/worker.go59
-rw-r--r--backend/internal/store/memory.go81
-rw-r--r--backend/internal/store/postgres.go75
-rw-r--r--backend/internal/store/store.go7
7 files changed, 294 insertions, 26 deletions
diff --git a/backend/internal/alerts/alerts.go b/backend/internal/alerts/alerts.go
index 44d4a50..ca5e807 100644
--- a/backend/internal/alerts/alerts.go
+++ b/backend/internal/alerts/alerts.go
@@ -37,7 +37,7 @@ func SendDiscord(webhookURL string, svc model.Service, hb model.Heartbeat, wasUp
fields = append(fields, map[string]any{"name": "Erro", "value": hb.ErrorMessage, "inline": false})
}
if hb.IsUp {
- desc = "o servico voltou"
+ desc = "O servico voltou"
}
payload := map[string]any{
diff --git a/backend/internal/api/handler.go b/backend/internal/api/handler.go
index f304351..5f934da 100644
--- a/backend/internal/api/handler.go
+++ b/backend/internal/api/handler.go
@@ -142,12 +142,40 @@ func (h *Handler) GetHistory(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "id invalido"})
return
}
- history, err := h.store.GetHistory(id, 50)
+
+ page, _ := strconv.Atoi(r.URL.Query().Get("page"))
+ if page < 1 {
+ page = 1
+ }
+ perPage, _ := strconv.Atoi(r.URL.Query().Get("per_page"))
+ if perPage < 1 || perPage > 100 {
+ perPage = 50
+ }
+
+ offset := (page - 1) * perPage
+ history, err := h.store.GetHistory(id, perPage, offset)
if err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
- writeJSON(w, http.StatusOK, history)
+
+ total, err := h.store.CountHeartbeats(id)
+ if err != nil {
+ total = len(history)
+ }
+
+ totalPages := (total + perPage - 1) / perPage
+ if totalPages < 1 {
+ totalPages = 1
+ }
+
+ writeJSON(w, http.StatusOK, map[string]any{
+ "data": history,
+ "page": page,
+ "per_page": perPage,
+ "total": total,
+ "total_pages": totalPages,
+ })
}
func (h *Handler) GetActiveMaintenance(w http.ResponseWriter, r *http.Request) {
@@ -163,6 +191,65 @@ func (h *Handler) GetActiveMaintenance(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]any{"maintenance": m})
}
+func (h *Handler) ListMaintenances(w http.ResponseWriter, r *http.Request) {
+ maintenances, err := h.store.ListMaintenances()
+ if err != nil {
+ writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+ return
+ }
+ writeJSON(w, http.StatusOK, maintenances)
+}
+
+func (h *Handler) CreateMaintenance(w http.ResponseWriter, r *http.Request) {
+ var m model.Maintenance
+ if err := json.NewDecoder(r.Body).Decode(&m); err != nil {
+ writeJSON(w, http.StatusBadRequest, map[string]string{"error": "json invalido: " + err.Error()})
+ return
+ }
+ if m.Title == "" {
+ writeJSON(w, http.StatusBadRequest, map[string]string{"error": "titulo obrigatorio"})
+ return
+ }
+ created, err := h.store.AddMaintenance(m)
+ if err != nil {
+ writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+ return
+ }
+ writeJSON(w, http.StatusCreated, created)
+}
+
+func (h *Handler) UpdateMaintenance(w http.ResponseWriter, r *http.Request) {
+ id, err := strconv.Atoi(r.PathValue("id"))
+ if err != nil {
+ writeJSON(w, http.StatusBadRequest, map[string]string{"error": "id invalido"})
+ return
+ }
+ var m model.Maintenance
+ if err := json.NewDecoder(r.Body).Decode(&m); err != nil {
+ writeJSON(w, http.StatusBadRequest, map[string]string{"error": "json invalido: " + err.Error()})
+ return
+ }
+ updated, err := h.store.UpdateMaintenance(id, m)
+ if err != nil {
+ writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+ return
+ }
+ writeJSON(w, http.StatusOK, updated)
+}
+
+func (h *Handler) DeleteMaintenance(w http.ResponseWriter, r *http.Request) {
+ id, err := strconv.Atoi(r.PathValue("id"))
+ if err != nil {
+ writeJSON(w, http.StatusBadRequest, map[string]string{"error": "id invalido"})
+ return
+ }
+ if err := h.store.DeleteMaintenance(id); err != nil {
+ writeJSON(w, http.StatusNotFound, map[string]string{"error": err.Error()})
+ return
+ }
+ w.WriteHeader(http.StatusNoContent)
+}
+
func (h *Handler) BadgeSVG(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
diff --git a/backend/internal/api/router.go b/backend/internal/api/router.go
index 9a18cc5..8188b25 100644
--- a/backend/internal/api/router.go
+++ b/backend/internal/api/router.go
@@ -23,6 +23,11 @@ func NewRouter(h *Handler, stream *StreamHandler, auth *AuthHandler) http.Handle
mux.HandleFunc("GET /api/active-maintenance", h.GetActiveMaintenance)
+ mux.HandleFunc("GET /api/maintenances", h.ListMaintenances)
+ mux.HandleFunc("POST /api/maintenances", h.CreateMaintenance)
+ mux.HandleFunc("PUT /api/maintenances/{id}", h.UpdateMaintenance)
+ mux.HandleFunc("DELETE /api/maintenances/{id}", h.DeleteMaintenance)
+
mux.Handle("GET /api/stream", stream)
return corsMiddleware(auth.Middleware(mux))
diff --git a/backend/internal/monitor/worker.go b/backend/internal/monitor/worker.go
index 0cf980a..70855a2 100644
--- a/backend/internal/monitor/worker.go
+++ b/backend/internal/monitor/worker.go
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"log"
+ "sync"
"time"
"yaum/internal/alerts"
@@ -25,6 +26,8 @@ type Worker struct {
broadcaster *sse.Broadcaster
smtpCfg *alerts.SMTPConfig
quit chan struct{}
+ mu sync.Mutex
+ lastChecked map[int]time.Time
}
func NewWorker(s store.Store, interval, timeout time.Duration, b *sse.Broadcaster) *Worker {
@@ -35,6 +38,7 @@ func NewWorker(s store.Store, interval, timeout time.Duration, b *sse.Broadcaste
results: make(chan checkResult, 100),
broadcaster: b,
quit: make(chan struct{}),
+ lastChecked: make(map[int]time.Time),
}
}
@@ -44,10 +48,14 @@ func (w *Worker) WithSMTP(cfg *alerts.SMTPConfig) *Worker {
}
func (w *Worker) Start(ctx context.Context) {
- ticker := time.NewTicker(w.interval)
+ granularity := w.interval
+ if granularity > 10*time.Second {
+ granularity = 10 * time.Second
+ }
+ ticker := time.NewTicker(granularity)
defer ticker.Stop()
- log.Printf("[worker] iniciado | intervalo=%v timeout=%v", w.interval, w.timeout)
+ log.Printf("[worker] iniciado | global_interval=%v granularity=%v timeout=%v", w.interval, granularity, w.timeout)
go w.resultConsumer()
@@ -64,6 +72,40 @@ func (w *Worker) Start(ctx context.Context) {
}
}
+func (w *Worker) runChecks(ctx context.Context) {
+ services, err := w.store.ListActiveServices()
+ if err != nil {
+ log.Printf("[worker] erro ao listar servicos ativos: %v", err)
+ return
+ }
+
+ now := time.Now()
+ var due []model.Service
+
+ w.mu.Lock()
+ for _, svc := range services {
+ svcInterval := time.Duration(svc.IntervalSeconds) * time.Second
+ if svcInterval <= 0 {
+ svcInterval = w.interval
+ }
+ last, ok := w.lastChecked[svc.ID]
+ if !ok || now.Sub(last) >= svcInterval {
+ w.lastChecked[svc.ID] = now
+ due = append(due, svc)
+ }
+ }
+ w.mu.Unlock()
+
+ if len(due) == 0 {
+ return
+ }
+ log.Printf("[worker] verificando %d servico(s) de %d ativo(s)", len(due), len(services))
+
+ for _, svc := range due {
+ go w.checkService(ctx, svc)
+ }
+}
+
func (w *Worker) resultConsumer() {
for {
select {
@@ -120,19 +162,6 @@ func (w *Worker) fireAlerts(svc model.Service, hb model.Heartbeat, wasUp bool) {
}
}
-func (w *Worker) runChecks(ctx context.Context) {
- services, err := w.store.ListActiveServices()
- if err != nil {
- log.Printf("[worker] erro ao listar servicos ativos: %v", err)
- return
- }
- log.Printf("[worker] verificando %d servico(s)", len(services))
-
- for _, svc := range services {
- go w.checkService(ctx, svc)
- }
-}
-
func (w *Worker) checkService(ctx context.Context, svc model.Service) {
checkCtx, cancel := context.WithTimeout(ctx, w.timeout)
defer cancel()
diff --git a/backend/internal/store/memory.go b/backend/internal/store/memory.go
index a77e877..1ee2e05 100644
--- a/backend/internal/store/memory.go
+++ b/backend/internal/store/memory.go
@@ -49,6 +49,17 @@ func (s *MemoryStore) SeedMockData() {
s.heartbeats[svc.ID] = make([]*model.Heartbeat, 0)
s.seedHistory(svc.ID)
}
+
+ s.maintenances = []model.Maintenance{
+ {
+ ID: s.nextMtnID,
+ Title: "Manutenção Programada — Servidores",
+ StartTime: now.Add(-1 * time.Hour),
+ EndTime: now.Add(1 * time.Hour),
+ IsActive: true,
+ },
+ }
+ s.nextMtnID++
}
func (s *MemoryStore) seedHistory(serviceID int) {
@@ -187,21 +198,35 @@ func (s *MemoryStore) AddHeartbeat(hb model.Heartbeat) error {
return nil
}
-func (s *MemoryStore) GetHistory(serviceID int, limit int) ([]model.Heartbeat, error) {
+func (s *MemoryStore) GetHistory(serviceID int, limit int, offset int) ([]model.Heartbeat, error) {
s.mu.RLock()
defer s.mu.RUnlock()
hbs := s.heartbeats[serviceID]
- if len(hbs) > limit {
- hbs = hbs[len(hbs)-limit:]
- }
- result := make([]model.Heartbeat, len(hbs))
+ reverse := make([]*model.Heartbeat, len(hbs))
for i, hb := range hbs {
+ reverse[len(hbs)-1-i] = hb
+ }
+ if offset >= len(reverse) {
+ return []model.Heartbeat{}, nil
+ }
+ if offset+limit > len(reverse) {
+ limit = len(reverse) - offset
+ }
+ page := reverse[offset : offset+limit]
+ result := make([]model.Heartbeat, len(page))
+ for i, hb := range page {
result[i] = *hb
}
return result, nil
}
+func (s *MemoryStore) CountHeartbeats(serviceID int) (int, error) {
+ s.mu.RLock()
+ defer s.mu.RUnlock()
+ return len(s.heartbeats[serviceID]), nil
+}
+
func (s *MemoryStore) GetLastHeartbeat(serviceID int) (*model.Heartbeat, error) {
s.mu.RLock()
defer s.mu.RUnlock()
@@ -225,6 +250,52 @@ func (s *MemoryStore) GetActiveMaintenance(now time.Time) (*model.Maintenance, e
return nil, nil
}
+func (s *MemoryStore) ListMaintenances() ([]model.Maintenance, error) {
+ s.mu.RLock()
+ defer s.mu.RUnlock()
+
+ result := make([]model.Maintenance, len(s.maintenances))
+ copy(result, s.maintenances)
+ return result, nil
+}
+
+func (s *MemoryStore) AddMaintenance(m model.Maintenance) (*model.Maintenance, error) {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+
+ m.ID = s.nextMtnID
+ s.nextMtnID++
+ s.maintenances = append(s.maintenances, m)
+ return &m, nil
+}
+
+func (s *MemoryStore) UpdateMaintenance(id int, m model.Maintenance) (*model.Maintenance, error) {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+
+ for i, existing := range s.maintenances {
+ if existing.ID == id {
+ m.ID = id
+ s.maintenances[i] = m
+ return &m, nil
+ }
+ }
+ return nil, errors.New("manutencao nao encontrada")
+}
+
+func (s *MemoryStore) DeleteMaintenance(id int) error {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+
+ for i, m := range s.maintenances {
+ if m.ID == id {
+ s.maintenances = append(s.maintenances[:i], s.maintenances[i+1:]...)
+ return nil
+ }
+ }
+ return errors.New("manutencao nao encontrada")
+}
+
func (s *MemoryStore) GetServiceStats(serviceID int) (*model.ServiceStats, error) {
s.mu.RLock()
defer s.mu.RUnlock()
diff --git a/backend/internal/store/postgres.go b/backend/internal/store/postgres.go
index a8d072a..57fa258 100644
--- a/backend/internal/store/postgres.go
+++ b/backend/internal/store/postgres.go
@@ -213,15 +213,16 @@ func (s *PostgresStore) AddHeartbeat(hb model.Heartbeat) error {
return nil
}
-func (s *PostgresStore) GetHistory(serviceID int, limit int) ([]model.Heartbeat, error) {
+func (s *PostgresStore) GetHistory(serviceID int, limit int, offset int) ([]model.Heartbeat, error) {
query := `
SELECT id, service_id, status_code, response_time_ms, is_up, error_message, tested_at
FROM heartbeats
WHERE service_id = $1
ORDER BY tested_at DESC
LIMIT $2
+ OFFSET $3
`
- rows, err := s.pool.Query(context.Background(), query, serviceID, limit)
+ rows, err := s.pool.Query(context.Background(), query, serviceID, limit, offset)
if err != nil {
return nil, fmt.Errorf("get history: %w", err)
}
@@ -234,6 +235,18 @@ func (s *PostgresStore) GetHistory(serviceID int, limit int) ([]model.Heartbeat,
})
}
+func (s *PostgresStore) CountHeartbeats(serviceID int) (int, error) {
+ var count int
+ err := s.pool.QueryRow(context.Background(),
+ `SELECT COUNT(*) FROM heartbeats WHERE service_id = $1`,
+ serviceID,
+ ).Scan(&count)
+ if err != nil {
+ return 0, fmt.Errorf("count heartbeats: %w", err)
+ }
+ return count, nil
+}
+
func (s *PostgresStore) GetLastHeartbeat(serviceID int) (*model.Heartbeat, error) {
query := `
SELECT id, service_id, status_code, response_time_ms, is_up, error_message, tested_at
@@ -322,3 +335,61 @@ func (s *PostgresStore) GetActiveMaintenance(now time.Time) (*model.Maintenance,
}
return &m, nil
}
+
+func (s *PostgresStore) ListMaintenances() ([]model.Maintenance, error) {
+ query := `SELECT id, title, start_time, end_time, is_active FROM maintenances ORDER BY start_time DESC`
+ rows, err := s.pool.Query(context.Background(), query)
+ if err != nil {
+ return nil, fmt.Errorf("list maintenances: %w", err)
+ }
+ defer rows.Close()
+ return pgx.CollectRows(rows, func(row pgx.CollectableRow) (model.Maintenance, error) {
+ var m model.Maintenance
+ err := row.Scan(&m.ID, &m.Title, &m.StartTime, &m.EndTime, &m.IsActive)
+ return m, err
+ })
+}
+
+func (s *PostgresStore) AddMaintenance(m model.Maintenance) (*model.Maintenance, error) {
+ query := `
+ INSERT INTO maintenances (title, start_time, end_time, is_active)
+ VALUES ($1, $2, $3, $4)
+ RETURNING id
+ `
+ err := s.pool.QueryRow(context.Background(), query, m.Title, m.StartTime, m.EndTime, m.IsActive).
+ Scan(&m.ID)
+ if err != nil {
+ return nil, fmt.Errorf("add maintenance: %w", err)
+ }
+ return &m, nil
+}
+
+func (s *PostgresStore) UpdateMaintenance(id int, m model.Maintenance) (*model.Maintenance, error) {
+ query := `
+ UPDATE maintenances
+ SET title = $1, start_time = $2, end_time = $3, is_active = $4
+ WHERE id = $5
+ RETURNING id, title, start_time, end_time, is_active
+ `
+ var updated model.Maintenance
+ err := s.pool.QueryRow(context.Background(), query, m.Title, m.StartTime, m.EndTime, m.IsActive, id).
+ Scan(&updated.ID, &updated.Title, &updated.StartTime, &updated.EndTime, &updated.IsActive)
+ if err != nil {
+ if err == pgx.ErrNoRows {
+ return nil, fmt.Errorf("manutencao nao encontrada")
+ }
+ return nil, fmt.Errorf("update maintenance: %w", err)
+ }
+ return &updated, nil
+}
+
+func (s *PostgresStore) DeleteMaintenance(id int) error {
+ tag, err := s.pool.Exec(context.Background(), "DELETE FROM maintenances WHERE id = $1", id)
+ if err != nil {
+ return fmt.Errorf("delete maintenance: %w", err)
+ }
+ if tag.RowsAffected() == 0 {
+ return fmt.Errorf("manutencao nao encontrada")
+ }
+ return nil
+}
diff --git a/backend/internal/store/store.go b/backend/internal/store/store.go
index e5e05f6..2c34b1c 100644
--- a/backend/internal/store/store.go
+++ b/backend/internal/store/store.go
@@ -15,9 +15,14 @@ type Store interface {
ToggleService(int) (*model.Service, error)
DeleteService(int) error
AddHeartbeat(model.Heartbeat) error
- GetHistory(int, int) ([]model.Heartbeat, error)
+ GetHistory(int, int, int) ([]model.Heartbeat, error)
+ CountHeartbeats(int) (int, error)
GetLastHeartbeat(int) (*model.Heartbeat, error)
GetServiceStats(int) (*model.ServiceStats, error)
GetActiveMaintenance(now time.Time) (*model.Maintenance, error)
+ ListMaintenances() ([]model.Maintenance, error)
+ AddMaintenance(model.Maintenance) (*model.Maintenance, error)
+ UpdateMaintenance(int, model.Maintenance) (*model.Maintenance, error)
+ DeleteMaintenance(int) error
}