310 lines
11 KiB
TypeScript
310 lines
11 KiB
TypeScript
import { SignOutButton } from "@/components/auth/sign-out-button";
|
|
import { DashboardControls } from "@/components/dashboard/dashboard-controls";
|
|
import { getServerAgencyContextOrRedirect } from "@/lib/agency";
|
|
import type { Database } from "@/lib/types";
|
|
import { createSupabaseServerClient } from "@/lib/supabase/server";
|
|
|
|
type AgencySummary = Pick<
|
|
Database["public"]["Tables"]["agencies"]["Row"],
|
|
"id" | "name"
|
|
>;
|
|
type BillingSummary = Pick<
|
|
Database["public"]["Tables"]["billing_accounts"]["Row"],
|
|
"subscription_status" | "plan_id" | "current_period_end"
|
|
>;
|
|
type ActionLogSummary = Pick<
|
|
Database["public"]["Tables"]["actions_log"]["Row"],
|
|
"target_id" | "status" | "action" | "created_at" | "error_message"
|
|
>;
|
|
|
|
function normalizeDomainLike(value: string) {
|
|
return value.trim().toLowerCase().replace(/\.$/, "");
|
|
}
|
|
|
|
function findBestSuffixStatusMatch(
|
|
normalizedDomain: string,
|
|
candidates: Array<{ normalizedName: string; status: string | null }>,
|
|
) {
|
|
let best: { normalizedName: string; status: string | null } | null = null;
|
|
|
|
for (const candidate of candidates) {
|
|
if (!normalizedDomain.endsWith(`.${candidate.normalizedName}`)) continue;
|
|
if (!best || candidate.normalizedName.length > best.normalizedName.length) {
|
|
best = candidate;
|
|
}
|
|
}
|
|
|
|
return best?.status ?? null;
|
|
}
|
|
|
|
export default async function DashboardPage() {
|
|
const context = await getServerAgencyContextOrRedirect();
|
|
const supabase = createSupabaseServerClient() as any;
|
|
|
|
const [
|
|
{ data: agency },
|
|
{ data: instances, error: instancesError },
|
|
{ data: subscriptions },
|
|
{ data: domains },
|
|
{ data: billing },
|
|
{ data: autoSyncLogs },
|
|
] = (await Promise.all([
|
|
supabase
|
|
.from("agencies")
|
|
.select("id, name")
|
|
.eq("id", context.agencyId)
|
|
.maybeSingle(),
|
|
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",
|
|
)
|
|
.eq("agency_id", context.agencyId)
|
|
.order("created_at", { ascending: false }),
|
|
supabase
|
|
.from("plesk_subscriptions")
|
|
.select("id, name, status, plesk_subscription_id, plesk_instance_id")
|
|
.eq("agency_id", context.agencyId)
|
|
.order("updated_at", { ascending: false })
|
|
.limit(10),
|
|
supabase
|
|
.from("plesk_domains")
|
|
.select(
|
|
"id, plesk_instance_id, subscription_id, external_subscription_id, status, domain_name, hosting_type, source_created_at, aliases_count, updated_at",
|
|
)
|
|
.eq("agency_id", context.agencyId)
|
|
.order("updated_at", { ascending: false })
|
|
.limit(500),
|
|
supabase
|
|
.from("billing_accounts")
|
|
.select("subscription_status, plan_id, current_period_end")
|
|
.eq("agency_id", context.agencyId)
|
|
.maybeSingle(),
|
|
supabase
|
|
.from("actions_log")
|
|
.select("target_id, status, action, created_at, error_message")
|
|
.eq("agency_id", context.agencyId)
|
|
.eq("target_type", "plesk_instance")
|
|
.eq("action", "plesk_sync_auto")
|
|
.order("created_at", { ascending: false })
|
|
.limit(200),
|
|
])) as [
|
|
{ data: AgencySummary | null },
|
|
{
|
|
data: Array<{
|
|
id: string;
|
|
name: string;
|
|
base_url: string;
|
|
auth_type: "api_key" | "basic";
|
|
status: string;
|
|
error_message: string | null;
|
|
last_connected_at: string | null;
|
|
last_sync_at: string | null;
|
|
last_sync_status: string | null;
|
|
last_sync_error: string | null;
|
|
last_sync_subscriptions: number;
|
|
last_sync_domains: number;
|
|
auto_sync_enabled: boolean;
|
|
auto_sync_frequency_minutes: number;
|
|
last_auto_sync_at: string | null;
|
|
next_auto_sync_at: string | null;
|
|
auto_sync_lock_until: string | null;
|
|
}> | null;
|
|
error: { message: string } | null;
|
|
},
|
|
{
|
|
data: Array<{
|
|
id: string;
|
|
name: string | null;
|
|
status: string | null;
|
|
plesk_subscription_id: string;
|
|
plesk_instance_id: string;
|
|
}> | null;
|
|
},
|
|
{
|
|
data: Array<{
|
|
id: string;
|
|
plesk_instance_id: string;
|
|
subscription_id: string | null;
|
|
external_subscription_id: string | null;
|
|
domain_name: string;
|
|
status: string | null;
|
|
hosting_type: string | null;
|
|
source_created_at: string | null;
|
|
aliases_count: number | null;
|
|
updated_at: string;
|
|
}> | null;
|
|
},
|
|
{ data: BillingSummary | null },
|
|
{ data: ActionLogSummary[] | null },
|
|
];
|
|
|
|
const domainInstanceIds = Array.from(
|
|
new Set((domains ?? []).map((domain) => domain.plesk_instance_id)),
|
|
);
|
|
|
|
const { data: domainSubscriptions } =
|
|
domainInstanceIds.length > 0
|
|
? await supabase
|
|
.from("plesk_subscriptions")
|
|
.select("id, plesk_instance_id, plesk_subscription_id, name, status")
|
|
.eq("agency_id", context.agencyId)
|
|
.in("plesk_instance_id", domainInstanceIds)
|
|
: { data: [] };
|
|
|
|
const subscriptionStatusById = new Map<string, string | null>(
|
|
(domainSubscriptions ?? []).map((subscription: any) => [
|
|
String(subscription.id),
|
|
subscription?.status ? String(subscription.status) : null,
|
|
]),
|
|
);
|
|
|
|
const subscriptionDisplayNameById = new Map<string, string>(
|
|
(domainSubscriptions ?? [])
|
|
.filter((subscription: any) => subscription?.id)
|
|
.map((subscription: any) => [
|
|
String(subscription.id),
|
|
String(
|
|
subscription?.name ||
|
|
subscription?.plesk_subscription_id ||
|
|
subscription?.id,
|
|
),
|
|
]),
|
|
);
|
|
|
|
const subscriptionStatusByExternalId = new Map<string, string | null>(
|
|
(domainSubscriptions ?? [])
|
|
.filter((subscription: any) =>
|
|
Boolean(
|
|
subscription?.plesk_instance_id &&
|
|
subscription?.plesk_subscription_id,
|
|
),
|
|
)
|
|
.map((subscription: any) => [
|
|
`${String(subscription.plesk_instance_id)}:${String(subscription.plesk_subscription_id)}`,
|
|
subscription?.status ? String(subscription.status) : null,
|
|
]),
|
|
);
|
|
|
|
const subscriptionStatusByInstanceAndName = new Map<string, string | null>(
|
|
(domainSubscriptions ?? [])
|
|
.filter((subscription: any) =>
|
|
Boolean(subscription?.plesk_instance_id && subscription?.name),
|
|
)
|
|
.map((subscription: any) => [
|
|
`${String(subscription.plesk_instance_id)}:${normalizeDomainLike(String(subscription.name))}`,
|
|
subscription?.status ? String(subscription.status) : null,
|
|
]),
|
|
);
|
|
|
|
const subscriptionStatusNameCandidatesByInstance = new Map<
|
|
string,
|
|
Array<{ normalizedName: string; status: string | null }>
|
|
>();
|
|
|
|
for (const subscription of domainSubscriptions ?? []) {
|
|
if (!subscription?.plesk_instance_id || !subscription?.name) continue;
|
|
|
|
const instanceId = String(subscription.plesk_instance_id);
|
|
const existing =
|
|
subscriptionStatusNameCandidatesByInstance.get(instanceId) ?? [];
|
|
existing.push({
|
|
normalizedName: normalizeDomainLike(String(subscription.name)),
|
|
status: subscription?.status ? String(subscription.status) : null,
|
|
});
|
|
subscriptionStatusNameCandidatesByInstance.set(instanceId, existing);
|
|
}
|
|
|
|
const autoSyncByInstance = new Map<
|
|
string,
|
|
{ status: string; created_at: string; error_message: string | null }
|
|
>();
|
|
|
|
for (const log of autoSyncLogs ?? []) {
|
|
if (!log.target_id || autoSyncByInstance.has(log.target_id)) continue;
|
|
autoSyncByInstance.set(log.target_id, {
|
|
status: log.status,
|
|
created_at: log.created_at,
|
|
error_message: log.error_message,
|
|
});
|
|
}
|
|
|
|
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">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-slate-900">
|
|
{agency?.name ?? "Agency"} Dashboard
|
|
</h1>
|
|
<p className="text-sm text-slate-600">Role: {context.role}</p>
|
|
</div>
|
|
<SignOutButton />
|
|
</header>
|
|
|
|
<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>
|
|
<p className="text-2xl font-semibold text-slate-900">
|
|
{instances?.length ?? 0}
|
|
</p>
|
|
</div>
|
|
<div className="rounded-xl border border-slate-200 bg-white p-4">
|
|
<p className="text-sm text-slate-500">Tracked Subscriptions</p>
|
|
<p className="text-2xl font-semibold text-slate-900">
|
|
{subscriptions?.length ?? 0}
|
|
</p>
|
|
</div>
|
|
<div className="rounded-xl border border-slate-200 bg-white p-4">
|
|
<p className="text-sm text-slate-500">Billing Status</p>
|
|
<p className="text-2xl font-semibold text-slate-900">
|
|
{billing?.subscription_status ?? "none"}
|
|
</p>
|
|
</div>
|
|
</section>
|
|
{instancesError ? (
|
|
<div className="rounded-xl border border-rose-200 bg-rose-50 p-4 text-sm text-rose-700">
|
|
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;
|
|
|
|
return {
|
|
...domain,
|
|
status: joinedStatus ?? "unknown",
|
|
subscription_name: domain.subscription_id
|
|
? (subscriptionDisplayNameById.get(domain.subscription_id) ??
|
|
null)
|
|
: null,
|
|
};
|
|
})}
|
|
autoSyncByInstance={Object.fromEntries(autoSyncByInstance.entries())}
|
|
/>
|
|
</main>
|
|
);
|
|
}
|