Compare commits
2 Commits
feature/cl
...
feature/cl
| Author | SHA1 | Date | |
|---|---|---|---|
| 627a34728e | |||
| 826e7da895 |
@@ -1,6 +1,7 @@
|
||||
import { SignOutButton } from "@/components/auth/sign-out-button";
|
||||
import { DashboardControls } from "@/components/dashboard/dashboard-controls";
|
||||
import { getServerAgencyContextOrRedirect } from "@/lib/agency";
|
||||
import { formatDateTime } from "@/lib/dates";
|
||||
import type { Database } from "@/lib/types";
|
||||
import { createSupabaseServerClient } from "@/lib/supabase/server";
|
||||
import Link from "next/link";
|
||||
@@ -17,6 +18,10 @@ type ActionLogSummary = Pick<
|
||||
Database["public"]["Tables"]["actions_log"]["Row"],
|
||||
"target_id" | "status" | "action" | "created_at" | "error_message"
|
||||
>;
|
||||
type AutosyncLatestLogSummary = Pick<
|
||||
Database["public"]["Tables"]["actions_log"]["Row"],
|
||||
"created_at"
|
||||
>;
|
||||
type HealthLogSummary = Pick<
|
||||
Database["public"]["Tables"]["actions_log"]["Row"],
|
||||
| "id"
|
||||
@@ -28,6 +33,64 @@ type HealthLogSummary = Pick<
|
||||
| "metadata"
|
||||
>;
|
||||
|
||||
const RELATIVE_TIME_FORMATTER = new Intl.RelativeTimeFormat("en", {
|
||||
numeric: "auto",
|
||||
});
|
||||
|
||||
function getLatestTimestamp(values: Array<string | null | undefined>) {
|
||||
let latestIso: string | null = null;
|
||||
let latestMs = Number.NEGATIVE_INFINITY;
|
||||
|
||||
for (const value of values) {
|
||||
if (!value) continue;
|
||||
const parsed = new Date(value).getTime();
|
||||
if (Number.isNaN(parsed)) continue;
|
||||
if (parsed > latestMs) {
|
||||
latestMs = parsed;
|
||||
latestIso = value;
|
||||
}
|
||||
}
|
||||
|
||||
return latestIso;
|
||||
}
|
||||
|
||||
function formatRelativeTime(value?: string | null) {
|
||||
if (!value) return null;
|
||||
|
||||
const targetMs = new Date(value).getTime();
|
||||
if (Number.isNaN(targetMs)) return null;
|
||||
|
||||
const deltaSeconds = Math.round((targetMs - Date.now()) / 1000);
|
||||
const absSeconds = Math.abs(deltaSeconds);
|
||||
|
||||
if (absSeconds < 60) {
|
||||
return RELATIVE_TIME_FORMATTER.format(deltaSeconds, "second");
|
||||
}
|
||||
|
||||
const deltaMinutes = Math.round(deltaSeconds / 60);
|
||||
if (Math.abs(deltaMinutes) < 60) {
|
||||
return RELATIVE_TIME_FORMATTER.format(deltaMinutes, "minute");
|
||||
}
|
||||
|
||||
const deltaHours = Math.round(deltaMinutes / 60);
|
||||
if (Math.abs(deltaHours) < 24) {
|
||||
return RELATIVE_TIME_FORMATTER.format(deltaHours, "hour");
|
||||
}
|
||||
|
||||
const deltaDays = Math.round(deltaHours / 24);
|
||||
if (Math.abs(deltaDays) < 30) {
|
||||
return RELATIVE_TIME_FORMATTER.format(deltaDays, "day");
|
||||
}
|
||||
|
||||
const deltaMonths = Math.round(deltaDays / 30);
|
||||
if (Math.abs(deltaMonths) < 12) {
|
||||
return RELATIVE_TIME_FORMATTER.format(deltaMonths, "month");
|
||||
}
|
||||
|
||||
const deltaYears = Math.round(deltaMonths / 12);
|
||||
return RELATIVE_TIME_FORMATTER.format(deltaYears, "year");
|
||||
}
|
||||
|
||||
function normalizeDomainLike(value: string) {
|
||||
return value.trim().toLowerCase().replace(/\.$/, "");
|
||||
}
|
||||
@@ -59,6 +122,7 @@ export default async function DashboardPage() {
|
||||
{ data: domains },
|
||||
{ data: billing },
|
||||
{ data: autoSyncLogs },
|
||||
{ data: latestAutoSyncLog },
|
||||
{ data: healthLogs },
|
||||
] = (await Promise.all([
|
||||
supabase
|
||||
@@ -100,6 +164,16 @@ export default async function DashboardPage() {
|
||||
.eq("action", "plesk_sync_auto")
|
||||
.order("created_at", { ascending: false })
|
||||
.limit(200),
|
||||
supabase
|
||||
.from("actions_log")
|
||||
.select("created_at")
|
||||
.eq("agency_id", context.agencyId)
|
||||
.eq("target_type", "plesk_instance")
|
||||
.eq("action", "autosync_completed")
|
||||
.eq("status", "success")
|
||||
.order("created_at", { ascending: false })
|
||||
.limit(1)
|
||||
.maybeSingle(),
|
||||
supabase
|
||||
.from("actions_log")
|
||||
.select(
|
||||
@@ -173,6 +247,7 @@ export default async function DashboardPage() {
|
||||
},
|
||||
{ data: BillingSummary | null },
|
||||
{ data: ActionLogSummary[] | null },
|
||||
{ data: AutosyncLatestLogSummary | null },
|
||||
{ data: HealthLogSummary[] | null },
|
||||
];
|
||||
|
||||
@@ -293,6 +368,40 @@ export default async function DashboardPage() {
|
||||
healthEventsByInstance.set(log.target_id, existing);
|
||||
}
|
||||
|
||||
const healthCounts = { ok: 0, degraded: 0, down: 0, unknown: 0 };
|
||||
|
||||
for (const instance of instances ?? []) {
|
||||
const status = (instance.health_status ?? "unknown") as
|
||||
| "ok"
|
||||
| "degraded"
|
||||
| "down"
|
||||
| "unknown";
|
||||
if (status === "ok") healthCounts.ok += 1;
|
||||
else if (status === "degraded") healthCounts.degraded += 1;
|
||||
else if (status === "down") healthCounts.down += 1;
|
||||
else healthCounts.unknown += 1;
|
||||
}
|
||||
|
||||
const latestHealthCheckAt = getLatestTimestamp(
|
||||
(instances ?? []).map((instance) => instance.health_last_checked_at),
|
||||
);
|
||||
|
||||
const autoSyncEnabledCount = (instances ?? []).filter(
|
||||
(instance) => instance.auto_sync_enabled,
|
||||
).length;
|
||||
|
||||
const latestAutoSyncFromInstances = getLatestTimestamp(
|
||||
(instances ?? []).map((instance) => instance.last_auto_sync_at),
|
||||
);
|
||||
|
||||
const latestAutoSyncAt =
|
||||
latestAutoSyncFromInstances ?? latestAutoSyncLog?.created_at ?? null;
|
||||
|
||||
const downInstanceNames = (instances ?? [])
|
||||
.filter((instance) => instance.health_status === "down")
|
||||
.slice(0, 3)
|
||||
.map((instance) => instance.name || instance.base_url);
|
||||
|
||||
return (
|
||||
<main className="mx-auto flex w-full max-w-6xl flex-col gap-6 px-6 py-8">
|
||||
<header className="flex items-center justify-between">
|
||||
@@ -313,6 +422,89 @@ export default async function DashboardPage() {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section className="rounded-xl border border-slate-200 bg-white p-4">
|
||||
<div className="space-y-3 text-sm text-slate-700">
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-wide text-slate-500">
|
||||
Instance Health
|
||||
</p>
|
||||
<div className="mt-1 flex flex-wrap gap-2">
|
||||
<span className="rounded bg-emerald-100 px-2 py-0.5 text-xs font-medium text-emerald-700">
|
||||
Ok: {healthCounts.ok}
|
||||
</span>
|
||||
<span className="rounded bg-amber-100 px-2 py-0.5 text-xs font-medium text-amber-700">
|
||||
Degraded: {healthCounts.degraded}
|
||||
</span>
|
||||
<span className="rounded bg-rose-100 px-2 py-0.5 text-xs font-medium text-rose-700">
|
||||
Down: {healthCounts.down}
|
||||
</span>
|
||||
<span className="rounded bg-slate-100 px-2 py-0.5 text-xs font-medium text-slate-700">
|
||||
Unknown: {healthCounts.unknown}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{healthCounts.down > 0 ? (
|
||||
<div className="rounded border border-rose-200 bg-rose-50 px-3 py-2 text-xs text-rose-700">
|
||||
{healthCounts.down} instance{healthCounts.down === 1 ? "" : "s"}{" "}
|
||||
down
|
||||
{downInstanceNames.length > 0
|
||||
? `: ${downInstanceNames.join(", ")}${healthCounts.down > downInstanceNames.length ? ", …" : ""}`
|
||||
: ""}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<p>
|
||||
<span className="font-medium text-slate-900">
|
||||
Last Health Check:
|
||||
</span>{" "}
|
||||
<span
|
||||
title={
|
||||
latestHealthCheckAt
|
||||
? formatDateTime(latestHealthCheckAt)
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{formatRelativeTime(latestHealthCheckAt) ?? "Never"}
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-medium text-slate-900">Auto Sync:</span>{" "}
|
||||
Enabled on {autoSyncEnabledCount} instance
|
||||
{autoSyncEnabledCount === 1 ? "" : "s"}
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-medium text-slate-900">Last run:</span>{" "}
|
||||
<span
|
||||
title={
|
||||
latestAutoSyncAt
|
||||
? formatDateTime(latestAutoSyncAt)
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{formatRelativeTime(latestAutoSyncAt) ?? "Not yet run"}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2 pt-1">
|
||||
<Link
|
||||
href="/dashboard#instances-panel"
|
||||
className="rounded border border-slate-300 px-3 py-1.5 text-xs font-medium text-slate-700"
|
||||
>
|
||||
Instances
|
||||
</Link>
|
||||
<Link
|
||||
href="/dashboard/activity"
|
||||
className="rounded border border-slate-300 px-3 py-1.5 text-xs font-medium text-slate-700"
|
||||
>
|
||||
Activity
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="grid gap-4 md:grid-cols-3">
|
||||
<div className="rounded-xl border border-slate-200 bg-white p-4">
|
||||
<p className="text-sm text-slate-500">Plesk Instances</p>
|
||||
@@ -338,47 +530,49 @@ export default async function DashboardPage() {
|
||||
Instances query failed: {instancesError.message}
|
||||
</div>
|
||||
) : null}
|
||||
<DashboardControls
|
||||
instances={instances ?? []}
|
||||
subscriptions={subscriptions ?? []}
|
||||
domains={(domains ?? []).map((domain) => {
|
||||
const joinedStatusByLocalId = domain.subscription_id
|
||||
? subscriptionStatusById.get(domain.subscription_id)
|
||||
: null;
|
||||
const joinedStatusByExternalId = domain.external_subscription_id
|
||||
? subscriptionStatusByExternalId.get(
|
||||
`${domain.plesk_instance_id}:${domain.external_subscription_id}`,
|
||||
)
|
||||
: null;
|
||||
const joinedStatusByName = subscriptionStatusByInstanceAndName.get(
|
||||
`${domain.plesk_instance_id}:${normalizeDomainLike(domain.domain_name)}`,
|
||||
);
|
||||
const joinedStatusBySuffix = findBestSuffixStatusMatch(
|
||||
normalizeDomainLike(domain.domain_name),
|
||||
subscriptionStatusNameCandidatesByInstance.get(
|
||||
domain.plesk_instance_id,
|
||||
) ?? [],
|
||||
);
|
||||
const joinedStatus =
|
||||
joinedStatusByLocalId ??
|
||||
joinedStatusByExternalId ??
|
||||
joinedStatusByName ??
|
||||
joinedStatusBySuffix;
|
||||
<section id="instances-panel">
|
||||
<DashboardControls
|
||||
instances={instances ?? []}
|
||||
subscriptions={subscriptions ?? []}
|
||||
domains={(domains ?? []).map((domain) => {
|
||||
const joinedStatusByLocalId = domain.subscription_id
|
||||
? subscriptionStatusById.get(domain.subscription_id)
|
||||
: null;
|
||||
const joinedStatusByExternalId = domain.external_subscription_id
|
||||
? subscriptionStatusByExternalId.get(
|
||||
`${domain.plesk_instance_id}:${domain.external_subscription_id}`,
|
||||
)
|
||||
: null;
|
||||
const joinedStatusByName = subscriptionStatusByInstanceAndName.get(
|
||||
`${domain.plesk_instance_id}:${normalizeDomainLike(domain.domain_name)}`,
|
||||
);
|
||||
const joinedStatusBySuffix = findBestSuffixStatusMatch(
|
||||
normalizeDomainLike(domain.domain_name),
|
||||
subscriptionStatusNameCandidatesByInstance.get(
|
||||
domain.plesk_instance_id,
|
||||
) ?? [],
|
||||
);
|
||||
const joinedStatus =
|
||||
joinedStatusByLocalId ??
|
||||
joinedStatusByExternalId ??
|
||||
joinedStatusByName ??
|
||||
joinedStatusBySuffix;
|
||||
|
||||
return {
|
||||
...domain,
|
||||
status: joinedStatus ?? "unknown",
|
||||
subscription_name: domain.subscription_id
|
||||
? (subscriptionDisplayNameById.get(domain.subscription_id) ??
|
||||
null)
|
||||
: null,
|
||||
};
|
||||
})}
|
||||
autoSyncByInstance={Object.fromEntries(autoSyncByInstance.entries())}
|
||||
healthEventsByInstance={Object.fromEntries(
|
||||
healthEventsByInstance.entries(),
|
||||
)}
|
||||
/>
|
||||
return {
|
||||
...domain,
|
||||
status: joinedStatus ?? "unknown",
|
||||
subscription_name: domain.subscription_id
|
||||
? (subscriptionDisplayNameById.get(domain.subscription_id) ??
|
||||
null)
|
||||
: null,
|
||||
};
|
||||
})}
|
||||
autoSyncByInstance={Object.fromEntries(autoSyncByInstance.entries())}
|
||||
healthEventsByInstance={Object.fromEntries(
|
||||
healthEventsByInstance.entries(),
|
||||
)}
|
||||
/>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user