From d186a989b6808ff39fcd43a9f0b478aebc4aa346 Mon Sep 17 00:00:00 2001 From: zwlucas Date: Fri, 29 May 2026 17:21:48 -0300 Subject: feat: enhance maintenance management and heartbeat history features - Updated Store interface to include methods for managing maintenances. - Modified API functions to support pagination for heartbeat history and added CRUD operations for maintenances. - Enhanced types to include paginated responses and initial log entries. - Implemented maintenance management UI in the admin panel with create, edit, and delete functionalities. - Updated service detail page to display paginated heartbeat history and improved chart rendering. - Refactored status page to accommodate new data structures and ensure consistent data handling. Signed-off-by: Lucas Faria Mendes --- backend/internal/alerts/alerts.go | 2 +- backend/internal/api/handler.go | 91 +++++++- backend/internal/api/router.go | 5 + backend/internal/monitor/worker.go | 59 +++-- backend/internal/store/memory.go | 81 ++++++- backend/internal/store/postgres.go | 75 +++++- backend/internal/store/store.go | 7 +- frontend/src/lib/api.ts | 43 +++- frontend/src/lib/types.ts | 8 + frontend/src/routes/(admin)/admin/+page.svelte | 309 +++++++++++++++++++++++-- frontend/src/routes/+page.svelte | 10 +- frontend/src/routes/+page.ts | 44 +++- frontend/src/routes/services/[id]/+page.svelte | 287 ++++++++++++++++++----- frontend/src/routes/status/[id]/+page.svelte | 2 +- frontend/src/routes/status/[id]/+page.ts | 3 +- 15 files changed, 907 insertions(+), 119 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 } diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 0be9148..239c598 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -1,4 +1,4 @@ -import type { Service, Heartbeat, ServiceStats } from './types'; +import type { Service, Heartbeat, ServiceStats, PaginatedResponse } from './types'; const BASE = '/api'; @@ -26,8 +26,8 @@ export async function deleteService(id: number): Promise { if (!res.ok) throw new Error('falha ao remover serviço'); } -export async function getHistory(id: number): Promise { - const res = await fetch(`${BASE}/services/${id}/history`); +export async function getHistory(id: number, page = 1, perPage = 50): Promise> { + const res = await fetch(`${BASE}/services/${id}/history?page=${page}&per_page=${perPage}`); if (!res.ok) throw new Error('falha ao obter histórico'); return res.json(); } @@ -37,3 +37,40 @@ export async function getServiceStats(id: number): Promise { if (!res.ok) throw new Error('falha ao obter estatísticas'); return res.json(); } + +export async function listMaintenances(): Promise { + const res = await fetch(`${BASE}/maintenances`); + if (!res.ok) throw new Error('falha ao listar manutenções'); + return res.json(); +} + +export async function createMaintenance(data: { title: string; start_time: string; end_time: string }): Promise { + const res = await fetch(`${BASE}/maintenances`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }); + if (!res.ok) { + const err = await res.json().catch(() => ({})); + throw new Error(err.error || 'falha ao criar manutenção'); + } + return res.json(); +} + +export async function updateMaintenance(id: number, data: { title: string; start_time: string; end_time: string; is_active: boolean }): Promise { + const res = await fetch(`${BASE}/maintenances/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }); + if (!res.ok) { + const err = await res.json().catch(() => ({})); + throw new Error(err.error || 'falha ao atualizar manutenção'); + } + return res.json(); +} + +export async function deleteMaintenance(id: number): Promise { + const res = await fetch(`${BASE}/maintenances/${id}`, { method: 'DELETE' }); + if (!res.ok) throw new Error('falha ao remover manutenção'); +} diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index f80c933..594ab05 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -30,6 +30,14 @@ export interface Heartbeat { tested_at: string; } +export interface PaginatedResponse { + data: T[]; + page: number; + per_page: number; + total: number; + total_pages: number; +} + export interface ServiceStats { service_id: number; uptime_24h: number; diff --git a/frontend/src/routes/(admin)/admin/+page.svelte b/frontend/src/routes/(admin)/admin/+page.svelte index 24b56db..5be9154 100644 --- a/frontend/src/routes/(admin)/admin/+page.svelte +++ b/frontend/src/routes/(admin)/admin/+page.svelte @@ -13,6 +13,7 @@ token = data.token; services = data.services ?? []; initialized = true; + fetchMaintenances(); } }); let editingSvc: any = $state(null); @@ -21,10 +22,25 @@ let testing = $state>({}); let toggling = $state>({}); + // maintenance state + let maintenances = $state([]); + let showMtnForm = $state(false); + let editingMtn: any = $state(null); + let mtnSaving = $state(false); + let mtnFormError = $state(''); + function apiHeaders() { return { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }; } + async function fetchMaintenances() { + try { + const res = await fetch(`${API}/maintenances`, { headers: { Authorization: `Bearer ${token}` } }); + if (await handleUnauthorized(res)) return; + if (res.ok) maintenances = await res.json(); + } catch {} + } + async function handleUnauthorized(res: Response) { if (res.status === 401) { window.location.href = '/login'; @@ -144,6 +160,75 @@ toggling = { ...toggling, [id]: false }; } + function openMtnCreate() { + editingMtn = null; + mtnFormError = ''; + showMtnForm = true; + } + + function openMtnEdit(m: any) { + editingMtn = { ...m }; + mtnFormError = ''; + showMtnForm = true; + } + + function closeMtnForm() { + showMtnForm = false; + editingMtn = null; + mtnFormError = ''; + } + + async function handleMtnSubmit(e: Event) { + e.preventDefault(); + mtnSaving = true; + mtnFormError = ''; + const form = e.target as HTMLFormElement; + const fd = new FormData(form); + const body: Record = { + title: fd.get('title'), + start_time: fd.get('start_time'), + end_time: fd.get('end_time'), + is_active: fd.get('is_active') === 'on', + }; + try { + const id = fd.get('id'); + if (id) { + const res = await fetch(`${API}/maintenances/${id}`, { + method: 'PUT', + headers: apiHeaders(), + body: JSON.stringify(body), + }); + if (await handleUnauthorized(res)) return; + if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao salvar'); + } else { + const res = await fetch(`${API}/maintenances`, { + method: 'POST', + headers: apiHeaders(), + body: JSON.stringify({ title: body.title, start_time: body.start_time, end_time: body.end_time }), + }); + if (await handleUnauthorized(res)) return; + if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Erro ao salvar'); + } + await fetchMaintenances(); + closeMtnForm(); + } catch (e: any) { + mtnFormError = e.message; + } finally { + mtnSaving = false; + } + } + + async function handleMtnDelete(id: number) { + try { + const res = await fetch(`${API}/maintenances/${id}`, { + method: 'DELETE', + headers: apiHeaders() + }); + if (await handleUnauthorized(res)) return; + if (res.ok) await fetchMaintenances(); + } catch {} + } + async function handleTest(id: number) { testing = { ...testing, [id]: true }; try { @@ -274,7 +359,78 @@ {/each} {/if} - +
+
+
+

Manutenções Programadas

+

Janelas de manutenção exibem um aviso no topo do site

+
+ +
+ + {#if maintenances.length === 0} +
+

Nenhuma manutenção agendada

+
+ {:else} +
+ + + + + + + + + + + + {#each maintenances as mtn (mtn.id)} + + + + + + + + {/each} + +
TítuloInícioFimAtivaAções
{mtn.title}{new Date(mtn.start_time).toLocaleString('pt-BR')}{new Date(mtn.end_time).toLocaleString('pt-BR')} + + {mtn.is_active ? 'Sim' : 'Não'} + + +
+ + +
+
+
+ {/if} +
+ + {#if showForm}
- -
+ + -
- - -
+
+ + +
-
+
{/if} + +{#if showMtnForm} + +
+
e.stopPropagation()} + > +
+

+ {editingMtn ? 'Editar Manutenção' : 'Nova Manutenção'} +

+ +
+ +
+ {#if editingMtn} + + {/if} + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + {#if mtnFormError} +

{mtnFormError}

+ {/if} + +
+ + +
+
+
+
+{/if}