Merge pull request 'feat/cl4-subscription-status-cli' (#9) from feat/cl4-subscription-status-cli into master

Reviewed-on: http://gitea.lan:3000/robbond/pleskSaas/pulls/9
This commit was merged in pull request #9.
This commit is contained in:
2026-03-05 12:57:15 +00:00
4 changed files with 145 additions and 18 deletions

View File

@@ -57,6 +57,10 @@ function getDomainStatusBadge(status: string | null) {
return "bg-rose-100 text-rose-700"; 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")) { if (normalized.includes("active") || normalized.includes("enabled")) {
return "bg-emerald-100 text-emerald-700"; return "bg-emerald-100 text-emerald-700";
} }
@@ -576,7 +580,7 @@ export function DashboardControls({
<thead> <thead>
<tr className="border-b border-slate-200 text-xs uppercase text-slate-500"> <tr className="border-b border-slate-200 text-xs uppercase text-slate-500">
<th className="px-2 py-2">Domain</th> <th className="px-2 py-2">Domain</th>
<th className="px-2 py-2">Status</th> <th className="px-2 py-2">Subscription Status</th>
<th className="px-2 py-2">Hosting</th> <th className="px-2 py-2">Hosting</th>
<th className="px-2 py-2">Created</th> <th className="px-2 py-2">Created</th>
<th className="px-2 py-2">Aliases</th> <th className="px-2 py-2">Aliases</th>

View File

@@ -16,6 +16,8 @@ export type PleskSubscription = {
external_id?: string; external_id?: string;
name?: string; name?: string;
status?: string; status?: string;
status_raw?: string | null;
status_sync_error?: string | null;
owner_login?: string; owner_login?: string;
owner?: { login?: string }; owner?: { login?: string };
plan_name?: string; plan_name?: string;
@@ -148,16 +150,27 @@ export async function listPleskSubscriptions(connection: PleskConnection) {
const subscriptions: PleskSubscription[] = []; const subscriptions: PleskSubscription[] = [];
for (const subscriptionName of subscriptionNames.slice(0, 500)) { 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 { try {
const infoResult = await pleskCliCall(connection, "subscription", [ const infoResult = await pleskCliCall(connection, "subscription", [
"--info", "--info",
subscriptionName, subscriptionName,
]); ]);
status = parseSubscriptionStatusFromInfo(infoResult.stdout ?? ""); const parsed = parseSubscriptionStatusDetailsFromInfo(
} catch { infoResult.stdout ?? "",
);
status = parsed.status;
statusRaw = parsed.statusRaw;
} catch (error) {
status = "unknown"; status = "unknown";
statusSyncError =
error instanceof Error
? error.message.slice(0, 1000)
: "status_sync_failed";
} }
subscriptions.push({ subscriptions.push({
@@ -165,33 +178,57 @@ export async function listPleskSubscriptions(connection: PleskConnection) {
subscription_id: subscriptionName, subscription_id: subscriptionName,
name: subscriptionName, name: subscriptionName,
status, status,
status_raw: statusRaw,
status_sync_error: statusSyncError,
}); });
} }
return subscriptions; return subscriptions;
} }
export function parseSubscriptionStatusFromInfo( export function parseSubscriptionStatusDetailsFromInfo(stdout: string): {
stdout: string, status: "active" | "suspended" | "disabled" | "expired" | "unknown";
): "active" | "suspended" | "unknown" { statusRaw: string | null;
} {
const line = stdout const line = stdout
.split(/\r?\n/) .split(/\r?\n/)
.map((entry) => entry.trim()) .map((entry) => entry.trim())
.find((entry) => /^domain\s+status\s*:/i.test(entry)); .find((entry) => /^domain\s+status\s*:/i.test(entry));
if (!line) return "unknown"; if (!line) {
return { status: "unknown", statusRaw: null };
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";
} }
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( export async function pleskCliCall(

View File

@@ -32,6 +32,31 @@ export async function syncPleskInstance(
): Promise<SyncResult> { ): Promise<SyncResult> {
const startedAtIso = new Date().toISOString(); const startedAtIso = new Date().toISOString();
const logStatusSyncEvent = async (
eventAction:
| "subscription_status_sync_started"
| "subscription_status_sync_completed"
| "subscription_status_sync_failed",
metadata?: Record<string, unknown>,
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 await supabase
.from("plesk_instances") .from("plesk_instances")
.update({ .update({
@@ -67,6 +92,10 @@ export async function syncPleskInstance(
} }
try { try {
await logStatusSyncEvent("subscription_status_sync_started", {
started_at: startedAtIso,
});
const [subscriptionsRaw, domainsRaw] = await Promise.all([ const [subscriptionsRaw, domainsRaw] = await Promise.all([
listPleskSubscriptions({ listPleskSubscriptions({
baseUrl: instance.base_url, baseUrl: instance.base_url,
@@ -95,6 +124,8 @@ export async function syncPleskInstance(
plesk_subscription_id: String(sourceId), plesk_subscription_id: String(sourceId),
name: item?.name ? String(item.name) : String(sourceId), name: item?.name ? String(item.name) : String(sourceId),
status: item?.status ? String(item.status) : "unknown", 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 owner_login: item?.owner_login
? String(item.owner_login) ? String(item.owner_login)
: item?.owner?.login : item?.owner?.login
@@ -109,6 +140,10 @@ export async function syncPleskInstance(
}) })
.filter(Boolean) as Array<Record<string, unknown>>; .filter(Boolean) as Array<Record<string, unknown>>;
const subscriptionStatusSyncFailures = subscriptionsRaw.filter(
(item: any) => Boolean(item?.status_sync_error),
);
if (subscriptionsForUpsert.length > 0) { if (subscriptionsForUpsert.length > 0) {
const { error } = await supabase const { error } = await supabase
.from("plesk_subscriptions") .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 { return {
instanceId: instance.id, instanceId: instance.id,
agencyId: instance.agency_id, agencyId: instance.agency_id,
@@ -232,6 +301,15 @@ export async function syncPleskInstance(
const message = const message =
error instanceof Error ? error.message.slice(0, 1000) : "Sync failed"; 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 await supabase
.from("plesk_instances") .from("plesk_instances")
.update({ .update({

View File

@@ -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;