diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index bafd75e..b075711 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -17,6 +17,16 @@ type ActionLogSummary = Pick< Database["public"]["Tables"]["actions_log"]["Row"], "target_id" | "status" | "action" | "created_at" | "error_message" >; +type HealthLogSummary = Pick< + Database["public"]["Tables"]["actions_log"]["Row"], + | "id" + | "target_id" + | "status" + | "action" + | "created_at" + | "error_message" + | "metadata" +>; function normalizeDomainLike(value: string) { return value.trim().toLowerCase().replace(/\.$/, ""); @@ -49,6 +59,7 @@ export default async function DashboardPage() { { data: domains }, { data: billing }, { data: autoSyncLogs }, + { data: healthLogs }, ] = (await Promise.all([ supabase .from("agencies") @@ -58,7 +69,7 @@ export default async function DashboardPage() { supabase .from("plesk_instances") .select( - "id, name, base_url, auth_type, status, error_message, last_connected_at, last_sync_at, last_sync_status, last_sync_error, last_sync_subscriptions, last_sync_domains, auto_sync_enabled, auto_sync_frequency_minutes, last_auto_sync_at, next_auto_sync_at, auto_sync_lock_until", + "id, name, base_url, auth_type, status, error_message, last_connected_at, last_sync_at, last_sync_status, last_sync_error, last_sync_subscriptions, last_sync_domains, auto_sync_enabled, auto_sync_frequency_minutes, last_auto_sync_at, next_auto_sync_at, auto_sync_lock_until, health_enabled, health_status, health_last_checked_at, health_last_ok_at, health_last_error, health_last_latency_ms, health_check_frequency_minutes, health_next_check_at, health_lock_until", ) .eq("agency_id", context.agencyId) .order("created_at", { ascending: false }), @@ -89,6 +100,21 @@ export default async function DashboardPage() { .eq("action", "plesk_sync_auto") .order("created_at", { ascending: false }) .limit(200), + supabase + .from("actions_log") + .select( + "id, target_id, status, action, created_at, error_message, metadata", + ) + .eq("agency_id", context.agencyId) + .eq("target_type", "plesk_instance") + .in("action", [ + "health_check_started", + "health_check_ok", + "health_check_degraded", + "health_check_failed", + ]) + .order("created_at", { ascending: false }) + .limit(400), ])) as [ { data: AgencySummary | null }, { @@ -110,6 +136,15 @@ export default async function DashboardPage() { last_auto_sync_at: string | null; next_auto_sync_at: string | null; auto_sync_lock_until: string | null; + health_enabled: boolean; + health_status: "ok" | "degraded" | "down" | "unknown"; + health_last_checked_at: string | null; + health_last_ok_at: string | null; + health_last_error: string | null; + health_last_latency_ms: number | null; + health_check_frequency_minutes: number; + health_next_check_at: string | null; + health_lock_until: string | null; }> | null; error: { message: string } | null; }, @@ -138,6 +173,7 @@ export default async function DashboardPage() { }, { data: BillingSummary | null }, { data: ActionLogSummary[] | null }, + { data: HealthLogSummary[] | null }, ]; const domainInstanceIds = Array.from( @@ -230,6 +266,33 @@ export default async function DashboardPage() { }); } + const healthEventsByInstance = new Map< + string, + Array<{ + id: string; + action: string; + status: string; + created_at: string; + error_message: string | null; + metadata: unknown; + }> + >(); + + for (const log of healthLogs ?? []) { + if (!log.target_id) continue; + const existing = healthEventsByInstance.get(log.target_id) ?? []; + if (existing.length >= 10) continue; + existing.push({ + id: log.id, + action: log.action, + status: log.status, + created_at: log.created_at, + error_message: log.error_message, + metadata: log.metadata, + }); + healthEventsByInstance.set(log.target_id, existing); + } + return (
@@ -312,6 +375,9 @@ export default async function DashboardPage() { }; })} autoSyncByInstance={Object.fromEntries(autoSyncByInstance.entries())} + healthEventsByInstance={Object.fromEntries( + healthEventsByInstance.entries(), + )} />
); diff --git a/components/dashboard/dashboard-controls.tsx b/components/dashboard/dashboard-controls.tsx index 6e493ec..116aad2 100644 --- a/components/dashboard/dashboard-controls.tsx +++ b/components/dashboard/dashboard-controls.tsx @@ -23,6 +23,15 @@ type Instance = { last_auto_sync_at: string | null; next_auto_sync_at: string | null; auto_sync_lock_until: string | null; + health_enabled: boolean; + health_status: "ok" | "degraded" | "down" | "unknown"; + health_last_checked_at: string | null; + health_last_ok_at: string | null; + health_last_error: string | null; + health_last_latency_ms: number | null; + health_check_frequency_minutes: number; + health_next_check_at: string | null; + health_lock_until: string | null; }; type Subscription = { @@ -54,8 +63,44 @@ type Props = { string, { status: string; created_at: string; error_message: string | null } >; + healthEventsByInstance: Record< + string, + Array<{ + id: string; + action: string; + status: string; + created_at: string; + error_message: string | null; + metadata: unknown; + }> + >; }; +function getHealthBadge(status: string | null) { + const normalized = (status ?? "unknown").toLowerCase(); + + if (normalized === "ok") { + return { label: "OK", className: "bg-emerald-100 text-emerald-700" }; + } + + if (normalized === "degraded") { + return { label: "Degraded", className: "bg-amber-100 text-amber-700" }; + } + + if (normalized === "down") { + return { label: "Down", className: "bg-rose-100 text-rose-700" }; + } + + return { label: "Unknown", className: "bg-slate-100 text-slate-600" }; +} + +function mapHealthResult(action: string) { + if (action === "health_check_ok") return "ok"; + if (action === "health_check_degraded") return "degraded"; + if (action === "health_check_failed") return "failed"; + return "started"; +} + function getDomainStatusBadge(status: string | null) { const normalized = (status ?? "unknown").toLowerCase(); @@ -120,6 +165,7 @@ export function DashboardControls({ subscriptions, domains, autoSyncByInstance, + healthEventsByInstance, }: Props) { const autoSyncFrequencyOptions = [15, 30, 60, 180, 360, 1440]; const [instanceName, setInstanceName] = useState(""); @@ -245,6 +291,22 @@ export function DashboardControls({ } } + async function onRunHealthCheck(instanceId: string) { + setLoading(true); + setMessage(null); + try { + await runRequest(`/api/plesk/instances/${instanceId}/health`); + setMessage("Health check completed."); + window.location.reload(); + } catch (error) { + setMessage( + error instanceof Error ? error.message : "Health check failed", + ); + } finally { + setLoading(false); + } + } + async function onUpdateAutoSync( instanceId: string, input: { auto_sync_enabled: boolean; auto_sync_frequency_minutes: number }, @@ -442,159 +504,308 @@ export function DashboardControls({ key={instance.id} className="rounded border border-slate-200 p-3" > -
-
- {autoSyncByInstance[instance.id] ? ( - - Auto-sync {autoSyncByInstance[instance.id].status} - - ) : null} -

- {instance.name} -

-

- {instance.base_url} -

-

- Status:{" "} - {instance.status} -

-

- Subscriptions: {instance.last_sync_subscriptions ?? 0} -

-

- Domains: {instance.last_sync_domains ?? 0} -

-

- Last sync status:{" "} - - {instance.last_sync_status ?? "unknown"} - -

-

- Last sync time: {formatDateTime(instance.last_sync_at)} -

-

- Last connected:{" "} - {instance.last_connected_at - ? formatDateTime(instance.last_connected_at) - : "never"} -

-

- Auto-sync:{" "} - {instance.auto_sync_enabled ? "Enabled" : "Disabled"} -

-

- Auto-sync frequency:{" "} - {instance.auto_sync_frequency_minutes ?? 60} min -

-

- Last auto-sync:{" "} - {formatDateTime(instance.last_auto_sync_at)} -

-

- Next auto-sync:{" "} - {formatDateTime(instance.next_auto_sync_at)} -

-

- Lock status:{" "} - {instance.auto_sync_lock_until && - new Date(instance.auto_sync_lock_until).getTime() > - Date.now() - ? "Sync in progress" - : "Idle"} -

- {autoSyncByInstance[instance.id] ? ( -

- Last auto-sync:{" "} - {formatDateTime( - autoSyncByInstance[instance.id].created_at, - )} -

- ) : null} - {instance.error_message ? ( -

- {instance.error_message} -

- ) : null} - {instance.last_sync_error ? ( -

- Last sync error: {instance.last_sync_error} -

- ) : null} - {autoSyncByInstance[instance.id]?.error_message ? ( -

- Auto-sync error:{" "} - {autoSyncByInstance[instance.id].error_message} -

- ) : null} -
-
-
- - ) => - onUpdateAutoSync(instance.id, { - auto_sync_enabled: e.target.checked, - auto_sync_frequency_minutes: - instance.auto_sync_frequency_minutes ?? 60, - }) - } - disabled={loading} - /> + {(() => { + const health = getHealthBadge(instance.health_status); + + return ( +
+
+ {autoSyncByInstance[instance.id] ? ( + + Auto-sync {autoSyncByInstance[instance.id].status} + + ) : null} +

+ {instance.name} +

+

+ {instance.base_url} +

+

+ Status:{" "} + + {instance.status} + +

+

+ Health:{" "} + + {health.label} + +

+

+ Health last checked:{" "} + {formatDateTime(instance.health_last_checked_at)} +

+

+ Health latency:{" "} + {instance.health_last_latency_ms != null + ? `${instance.health_last_latency_ms} ms` + : "—"} +

+

+ Subscriptions:{" "} + {instance.last_sync_subscriptions ?? 0} +

+

+ Domains: {instance.last_sync_domains ?? 0} +

+

+ Last sync status:{" "} + + {instance.last_sync_status ?? "unknown"} + +

+

+ Last sync time:{" "} + {formatDateTime(instance.last_sync_at)} +

+

+ Last connected:{" "} + {instance.last_connected_at + ? formatDateTime(instance.last_connected_at) + : "never"} +

+

+ Auto-sync:{" "} + {instance.auto_sync_enabled + ? "Enabled" + : "Disabled"} +

+

+ Auto-sync frequency:{" "} + {instance.auto_sync_frequency_minutes ?? 60} min +

+

+ Last auto-sync:{" "} + {formatDateTime(instance.last_auto_sync_at)} +

+

+ Next auto-sync:{" "} + {formatDateTime(instance.next_auto_sync_at)} +

+

+ Lock status:{" "} + {instance.auto_sync_lock_until && + new Date(instance.auto_sync_lock_until).getTime() > + Date.now() + ? "Sync in progress" + : "Idle"} +

+ {autoSyncByInstance[instance.id] ? ( +

+ Last auto-sync:{" "} + {formatDateTime( + autoSyncByInstance[instance.id].created_at, + )} +

+ ) : null} + {instance.error_message ? ( +

+ {instance.error_message} +

+ ) : null} + {instance.last_sync_error ? ( +

+ Last sync error: {instance.last_sync_error} +

+ ) : null} + {autoSyncByInstance[instance.id]?.error_message ? ( +

+ Auto-sync error:{" "} + {autoSyncByInstance[instance.id].error_message} +

+ ) : null} + {instance.health_last_error ? ( +

+ Health error: {instance.health_last_error} +

+ ) : null} +
+
+
+ + ) => + onUpdateAutoSync(instance.id, { + auto_sync_enabled: e.target.checked, + auto_sync_frequency_minutes: + instance.auto_sync_frequency_minutes ?? 60, + }) + } + disabled={loading} + /> +
+ + + + +
- - - + ); + })()} + +
+

+ Health Panel +

+
+

+ Current Status:{" "} + + {getHealthBadge(instance.health_status).label} + +

+

+ Last Checked:{" "} + + {formatDateTime(instance.health_last_checked_at)} + +

+

+ Last Successful Check:{" "} + + {formatDateTime(instance.health_last_ok_at)} + +

+

+ Latency:{" "} + + {instance.health_last_latency_ms != null + ? `${instance.health_last_latency_ms} ms` + : "—"} + +

+

+ Last Error:{" "} + + {instance.health_last_error ?? "—"} + +

+
+ +
+ + + + + + + + + + + {(healthEventsByInstance[instance.id] ?? []) + .length === 0 ? ( + + + + ) : ( + (healthEventsByInstance[instance.id] ?? []).map( + (event) => ( + + + + + + + ), + ) + )} + +
TimeEventResultMessage
+ No health events yet. +
+ {formatDateTime(event.created_at)} + {event.action} + {mapHealthResult(event.action)} + + {event.error_message ?? "—"} + {event.metadata ? ( +
+ + metadata + +
+                                          {JSON.stringify(
+                                            event.metadata,
+                                            null,
+                                            2,
+                                          )}
+                                        
+
+ ) : null} +