fix: health chart labels and get logs for 12h instead of 24h

This commit is contained in:
charles-gauthereau
2026-03-25 20:45:20 +01:00
parent fb97963cc3
commit a9d6481193
2 changed files with 66 additions and 68 deletions
@@ -1,8 +1,8 @@
"use client" "use client"
import { useMemo } from "react" import {useMemo} from "react"
import { Card } from "@/components/ui/card" import {Card} from "@/components/ui/card"
import { HealthcheckLog } from "@/db/schema/15_healthcheck-log" import {HealthcheckLog} from "@/db/schema/15_healthcheck-log"
type HealthStatus = "healthy" | "degraded" | "down" | "unknown" type HealthStatus = "healthy" | "degraded" | "down" | "unknown"
@@ -13,44 +13,24 @@ interface HealthCheckData {
interface Props { interface Props {
logs: HealthcheckLog[] logs: HealthcheckLog[]
intervalMinutes?: 10 | 20 | 30
}
function normalizeInterval(interval?: number): 10 | 20 | 30 {
if (interval === 20) return 20
if (interval === 30) return 30
return 10
} }
const INTERVAL_MINUTES = 10
const WINDOW_HOURS = 12
function roundDateToInterval(date: Date, intervalMinutes: number): Date { function roundDateToInterval(date: Date, intervalMinutes: number): Date {
const ms = intervalMinutes * 60 * 1000 const ms = intervalMinutes * 60 * 1000
return new Date(Math.floor(date.getTime() / ms) * ms) return new Date(Math.floor(date.getTime() / ms) * ms)
} }
function buildTimeSeries(logs: HealthcheckLog[]): HealthCheckData[] {
function getOldestLog(logs: HealthcheckLog[]): HealthcheckLog { const intervalMs = INTERVAL_MINUTES * 60 * 1000
console.log(logs)
const validLogs = logs.filter(l => l.date)
return validLogs.reduce((oldest, current) =>
new Date(current.date!) < new Date(oldest.date!) ? current : oldest
)
}
function buildTimeSeries(
logs: HealthcheckLog[],
intervalMinutes: 10 | 20 | 30
): HealthCheckData[] {
const intervalMs = intervalMinutes * 60 * 1000
const now = new Date() const now = new Date()
const roundedNow = roundDateToInterval(now, intervalMinutes) const roundedNow = roundDateToInterval(now, INTERVAL_MINUTES)
const buckets = (24 * 60) / intervalMinutes const buckets = (WINDOW_HOURS * 60) / INTERVAL_MINUTES
const data: HealthCheckData[] = [] const data: HealthCheckData[] = []
const oldestLog = logs.length > 0 ? getOldestLog(logs) : null const oldestLog = logs.length > 0 ? getOldestLog(logs) : null
for (let i = buckets - 1; i >= 0; i--) { for (let i = buckets - 1; i >= 0; i--) {
const start = new Date(roundedNow.getTime() - i * intervalMs) const start = new Date(roundedNow.getTime() - i * intervalMs)
const end = new Date(start.getTime() + intervalMs) const end = new Date(start.getTime() + intervalMs)
@@ -83,13 +63,12 @@ function buildTimeSeries(
} }
} }
data.push({ timestamp: start, status }) data.push({timestamp: start, status})
} }
return data return data
} }
function getStatusColor(status: HealthStatus): string { function getStatusColor(status: HealthStatus): string {
switch (status) { switch (status) {
case "healthy": case "healthy":
@@ -103,6 +82,15 @@ function getStatusColor(status: HealthStatus): string {
} }
} }
function getOldestLog(logs: HealthcheckLog[]): HealthcheckLog {
console.log(logs)
const validLogs = logs.filter(l => l.date)
return validLogs.reduce((oldest, current) =>
new Date(current.date!) < new Date(oldest.date!) ? current : oldest
)
}
function formatTime(date: Date): string { function formatTime(date: Date): string {
return date.toLocaleTimeString("en-US", { return date.toLocaleTimeString("en-US", {
hour: "2-digit", hour: "2-digit",
@@ -111,31 +99,34 @@ function formatTime(date: Date): string {
}) })
} }
export const HealthCheckGraph = ({
logs,
intervalMinutes = 10,
}: Props) => {
const normalizedInterval = normalizeInterval(intervalMinutes)
export const HealthCheckGraph = ({logs}: Props) => {
const data = useMemo(() => { const data = useMemo(() => {
return buildTimeSeries(logs, normalizedInterval) return buildTimeSeries(logs)
}, [logs, normalizedInterval]) }, [logs])
const hourLabels = useMemo(() => { const hourLabels = useMemo(() => {
if (data.length === 0) return [] if (data.length === 0) return []
const step = Math.floor(120 / normalizedInterval) // label toutes les 2h const isMobile = window.innerWidth <= 768
const labels: { hour: string; index: number }[] = []
for (let i = 0; i < data.length; i += step) { return data
labels.push({ .map((item, index) => ({ item, index }))
hour: formatTime(data[i].timestamp), .filter(({ item }) => {
index: i, const hours = item.timestamp.getHours()
const minutes = item.timestamp.getMinutes()
if (isMobile) {
return minutes === 0 && hours % 3 === 0
} else {
return minutes === 0
}
}) })
} .map(({ item, index }) => ({
hour: formatTime(item.timestamp),
return labels index,
}, [data, normalizedInterval]) }))
}, [data])
const healthyCount = data.filter((d) => d.status === "healthy").length const healthyCount = data.filter((d) => d.status === "healthy").length
@@ -152,7 +143,7 @@ export const HealthCheckGraph = ({
<div> <div>
<h2 className="text-lg font-semibold">Health</h2> <h2 className="text-lg font-semibold">Health</h2>
<p className="text-zinc-500 text-sm"> <p className="text-zinc-500 text-sm">
Last 24 hours {normalizedInterval} minute intervals Last 12 hours {INTERVAL_MINUTES} minute intervals
</p> </p>
</div> </div>
<div className="text-right"> <div className="text-right">
@@ -163,18 +154,25 @@ export const HealthCheckGraph = ({
</div> </div>
</div> </div>
<div className="flex mb-1 text-xs text-zinc-500">
{hourLabels.map((label, i) => ( <div className="relative mb-1 h-4 text-xs text-zinc-500">
<div {hourLabels.map((label, i) => {
key={i} const left = (label.index / (data.length - 1)) * 100
className="flex-1 text-center first:text-left last:text-right"
> return (
{label.hour} <div
</div> key={i}
))} className="absolute -translate-x-1/2 whitespace-nowrap"
style={{ left: `${left}%` }}
>
{label.hour}
</div>
)
})}
</div> </div>
<div className="flex gap-[2px]">
<div className="flex gap-0.5">
{data.map((item, index) => ( {data.map((item, index) => (
<div <div
key={index} key={index}
@@ -185,10 +183,10 @@ export const HealthCheckGraph = ({
</div> </div>
<div className="flex items-center justify-end gap-4 mt-4 text-xs text-zinc-500"> <div className="flex items-center justify-end gap-4 mt-4 text-xs text-zinc-500">
<Legend color="bg-zinc-700" label="Unknown" /> <Legend color="bg-zinc-700" label="Unknown"/>
<Legend color="bg-red-500" label="Down" /> <Legend color="bg-red-500" label="Down"/>
<Legend color="bg-emerald-700" label="Degraded" /> <Legend color="bg-emerald-700" label="Degraded"/>
<Legend color="bg-emerald-500" label="Healthy" /> <Legend color="bg-emerald-500" label="Healthy"/>
</div> </div>
</Card> </Card>
</div> </div>
@@ -196,9 +194,9 @@ export const HealthCheckGraph = ({
) )
} }
const Legend = ({ color, label }: { color: string; label: string }) => ( const Legend = ({color, label}: { color: string; label: string }) => (
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<div className={`w-3 h-3 rounded-sm ${color}`} /> <div className={`w-3 h-3 rounded-sm ${color}`}/>
<span>{label}</span> <span>{label}</span>
</div> </div>
) )
+3 -3
View File
@@ -57,14 +57,14 @@ export const getAgentAction = userAction.schema(z.string()).action(async ({parse
return { return {
data: agent, data: agent,
health: agent ? await getLast24hLogs({ id: agent.id }) : [] health: agent ? await getLast12hLogs({ id: agent.id }) : []
}; };
}); });
export async function getLast24hLogs({id}: { id: string }) { export async function getLast12hLogs({id}: { id: string }) {
const now = new Date() const now = new Date()
const since = new Date(now.getTime() - 24 * 60 * 60 * 1000) const since = new Date(now.getTime() - 12 * 60 * 60 * 1000)
return db return db
.select() .select()