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 --- 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 +- 8 files changed, 613 insertions(+), 93 deletions(-) (limited to 'frontend') 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}