diff --git a/components/dashboard/dashboard-controls.tsx b/components/dashboard/dashboard-controls.tsx index 2c1d472..a868e1b 100644 --- a/components/dashboard/dashboard-controls.tsx +++ b/components/dashboard/dashboard-controls.tsx @@ -57,6 +57,10 @@ function getDomainStatusBadge(status: string | null) { return "bg-rose-100 text-rose-700"; } + if (normalized.includes("expired")) { + return "bg-amber-100 text-amber-700"; + } + if (normalized.includes("active") || normalized.includes("enabled")) { return "bg-emerald-100 text-emerald-700"; } @@ -576,7 +580,7 @@ export function DashboardControls({ Domain - Status + Subscription Status Hosting Created Aliases diff --git a/lib/plesk/client.ts b/lib/plesk/client.ts index 6b3ebfc..3eb3d33 100644 --- a/lib/plesk/client.ts +++ b/lib/plesk/client.ts @@ -16,6 +16,8 @@ export type PleskSubscription = { external_id?: string; name?: string; status?: string; + status_raw?: string | null; + status_sync_error?: string | null; owner_login?: string; owner?: { login?: string }; plan_name?: string; @@ -148,16 +150,27 @@ export async function listPleskSubscriptions(connection: PleskConnection) { const subscriptions: PleskSubscription[] = []; for (const subscriptionName of subscriptionNames.slice(0, 500)) { - let status: "active" | "suspended" | "unknown" = "unknown"; + let status: "active" | "suspended" | "disabled" | "expired" | "unknown" = + "unknown"; + let statusRaw: string | null = null; + let statusSyncError: string | null = null; try { const infoResult = await pleskCliCall(connection, "subscription", [ "--info", subscriptionName, ]); - status = parseSubscriptionStatusFromInfo(infoResult.stdout ?? ""); - } catch { + const parsed = parseSubscriptionStatusDetailsFromInfo( + infoResult.stdout ?? "", + ); + status = parsed.status; + statusRaw = parsed.statusRaw; + } catch (error) { status = "unknown"; + statusSyncError = + error instanceof Error + ? error.message.slice(0, 1000) + : "status_sync_failed"; } subscriptions.push({ @@ -165,33 +178,57 @@ export async function listPleskSubscriptions(connection: PleskConnection) { subscription_id: subscriptionName, name: subscriptionName, status, + status_raw: statusRaw, + status_sync_error: statusSyncError, }); } return subscriptions; } -export function parseSubscriptionStatusFromInfo( - stdout: string, -): "active" | "suspended" | "unknown" { +export function parseSubscriptionStatusDetailsFromInfo(stdout: string): { + status: "active" | "suspended" | "disabled" | "expired" | "unknown"; + statusRaw: string | null; +} { const line = stdout .split(/\r?\n/) .map((entry) => entry.trim()) .find((entry) => /^domain\s+status\s*:/i.test(entry)); - if (!line) return "unknown"; - - const value = line - .replace(/^domain\s+status\s*:/i, "") - .trim() - .toLowerCase(); - - if (value.includes("ok")) return "active"; - if (value.includes("suspend") || value.includes("disabled")) { - return "suspended"; + if (!line) { + return { status: "unknown", statusRaw: null }; } - return "unknown"; + const value = line.replace(/^domain\s+status\s*:/i, "").trim(); + const normalized = value.toLowerCase(); + + if (normalized.includes("expired")) { + return { status: "expired", statusRaw: value }; + } + + if (normalized.includes("disabled")) { + return { status: "disabled", statusRaw: value }; + } + + if (normalized.includes("suspend")) { + return { status: "suspended", statusRaw: value }; + } + + if ( + normalized.includes("ok") || + normalized.includes("active") || + normalized.includes("enabled") + ) { + return { status: "active", statusRaw: value }; + } + + return { status: "unknown", statusRaw: value }; +} + +export function parseSubscriptionStatusFromInfo( + stdout: string, +): "active" | "suspended" | "disabled" | "expired" | "unknown" { + return parseSubscriptionStatusDetailsFromInfo(stdout).status; } export async function pleskCliCall( diff --git a/lib/plesk/sync.ts b/lib/plesk/sync.ts index 6976e2c..a3fe605 100644 --- a/lib/plesk/sync.ts +++ b/lib/plesk/sync.ts @@ -32,6 +32,31 @@ export async function syncPleskInstance( ): Promise { const startedAtIso = new Date().toISOString(); + const logStatusSyncEvent = async ( + eventAction: + | "subscription_status_sync_started" + | "subscription_status_sync_completed" + | "subscription_status_sync_failed", + metadata?: Record, + status: "success" | "failed" = "success", + errorMessage?: string | null, + ) => { + try { + await supabase.from("actions_log").insert({ + agency_id: instance.agency_id, + actor_user_id: actorUserId ?? null, + target_type: "plesk_instance", + target_id: instance.id, + action: eventAction, + status, + error_message: errorMessage ?? null, + metadata: metadata ?? {}, + }); + } catch { + // Logging should never block sync flow. + } + }; + await supabase .from("plesk_instances") .update({ @@ -67,6 +92,10 @@ export async function syncPleskInstance( } try { + await logStatusSyncEvent("subscription_status_sync_started", { + started_at: startedAtIso, + }); + const [subscriptionsRaw, domainsRaw] = await Promise.all([ listPleskSubscriptions({ baseUrl: instance.base_url, @@ -95,6 +124,8 @@ export async function syncPleskInstance( plesk_subscription_id: String(sourceId), name: item?.name ? String(item.name) : String(sourceId), status: item?.status ? String(item.status) : "unknown", + status_raw: item?.status_raw ? String(item.status_raw) : null, + last_status_sync_at: startedAtIso, owner_login: item?.owner_login ? String(item.owner_login) : item?.owner?.login @@ -109,6 +140,10 @@ export async function syncPleskInstance( }) .filter(Boolean) as Array>; + const subscriptionStatusSyncFailures = subscriptionsRaw.filter( + (item: any) => Boolean(item?.status_sync_error), + ); + if (subscriptionsForUpsert.length > 0) { const { error } = await supabase .from("plesk_subscriptions") @@ -221,6 +256,40 @@ export async function syncPleskInstance( }, }); + await logStatusSyncEvent("subscription_status_sync_completed", { + synced_subscriptions: subscriptionsRaw.length, + failed_subscriptions: subscriptionStatusSyncFailures.length, + failed_subscription_ids: subscriptionStatusSyncFailures + .slice(0, 50) + .map((item: any) => + String( + item?.plesk_subscription_id ?? item?.subscription_id ?? "unknown", + ), + ), + completed_at: new Date().toISOString(), + }); + + if (subscriptionStatusSyncFailures.length > 0) { + await logStatusSyncEvent( + "subscription_status_sync_failed", + { + failed_subscriptions: subscriptionStatusSyncFailures.length, + sample_errors: subscriptionStatusSyncFailures + .slice(0, 20) + .map((item: any) => ({ + subscription: String( + item?.plesk_subscription_id ?? + item?.subscription_id ?? + "unknown", + ), + error: String(item?.status_sync_error ?? "status_sync_failed"), + })), + }, + "failed", + `${subscriptionStatusSyncFailures.length} subscription status sync failures`, + ); + } + return { instanceId: instance.id, agencyId: instance.agency_id, @@ -232,6 +301,15 @@ export async function syncPleskInstance( const message = error instanceof Error ? error.message.slice(0, 1000) : "Sync failed"; + await logStatusSyncEvent( + "subscription_status_sync_failed", + { + failed_at: new Date().toISOString(), + }, + "failed", + message, + ); + await supabase .from("plesk_instances") .update({ diff --git a/supabase/migrations/CL4_subscription_status_fields.sql b/supabase/migrations/CL4_subscription_status_fields.sql new file mode 100644 index 0000000..7b1abee --- /dev/null +++ b/supabase/migrations/CL4_subscription_status_fields.sql @@ -0,0 +1,8 @@ +alter table public.plesk_subscriptions +add column if not exists status text default 'unknown', +add column if not exists status_raw text, +add column if not exists last_status_sync_at timestamptz; + +update public.plesk_subscriptions +set status = 'unknown' +where status is null; \ No newline at end of file