Files
pleno-vue/src/components/release/releaseEntityFacts.js
T

237 lines
13 KiB
JavaScript

const UNKNOWN = "unknown";
const NOT_CONFIGURED = "not configured";
const NOT_CHECKED = "not checked";
const isBlank = (value) => value === null || value === undefined || String(value).trim() === "";
const valueOr = (value, fallback = UNKNOWN) => (isBlank(value) ? fallback : String(value));
const boolValue = (value, trueLabel = "yes", falseLabel = "no", fallback = UNKNOWN) => {
if (value === true || value === 1 || value === "1") return trueLabel;
if (value === false || value === 0 || value === "0") return falseLabel;
return fallback;
};
const shortSha = (value) => {
const normalized = String(value || "").trim();
return normalized.length > 12 ? normalized.slice(0, 12) : normalized || UNKNOWN;
};
const publicRouteSlug = (entity = {}) => {
const slug = String(entity.route_slug || entity.public_route_slug || entity.channel_slug || entity.slug || "").trim();
if (!slug) return UNKNOWN;
return slug.toLowerCase() === "stable" ? "master" : slug;
};
const readableDuration = (start, end) => {
const started = Date.parse(start || "");
const finished = Date.parse(end || "");
if (!Number.isFinite(started) || !Number.isFinite(finished) || finished < started) {
return UNKNOWN;
}
const seconds = Math.round((finished - started) / 1000);
if (seconds < 60) return `${seconds}s`;
const minutes = Math.round(seconds / 60);
if (minutes < 60) return `${minutes}m`;
return `${Math.round(minutes / 60)}h`;
};
const uptimeText = (entity = {}) => {
const direct = entity.uptime || entity.uptime_text || entity.running_for || entity.uptime_human;
if (!isBlank(direct)) return String(direct);
const seconds = Number(entity.uptime_seconds || entity.uptime_sec || entity.last_status?.uptime_seconds);
if (!Number.isFinite(seconds) || seconds <= 0) return UNKNOWN;
if (seconds < 60) return `${Math.round(seconds)}s`;
if (seconds < 3600) return `${Math.round(seconds / 60)}m`;
if (seconds < 86400) return `${Math.round(seconds / 3600)}h`;
return `${Math.round(seconds / 86400)}d`;
};
const endpointHost = (entity = {}) =>
entity.hostname ||
entity.host ||
entity.ip ||
entity.endpoint?.host ||
entity.replication?.host ||
entity.target?.endpoint?.host ||
entity.target?.replication?.host;
const endpointPort = (entity = {}) =>
entity.port || entity.endpoint?.port || entity.replication?.port || entity.target?.endpoint?.port || entity.target?.replication?.port;
const lastCheck = (entity = {}) =>
entity.last_checked_at ||
entity.checked_at ||
entity.health_checked_at ||
entity.last_health_check_at ||
entity.last_check ||
entity.replication?.last_checked_at ||
entity.target?.last_reconciled_at ||
entity.target?.replication?.last_checked_at ||
NOT_CHECKED;
const addFact = (facts, label, value, fallback = UNKNOWN) => {
facts.push({ label, value: valueOr(value, fallback) });
};
const currentDeploymentLabel = (deployment) => {
if (!deployment) return NOT_CONFIGURED;
return [`#${deployment.id || UNKNOWN}`, deployment.status || UNKNOWN, shortSha(deployment.commit_sha)].join(" / ");
};
const channelFacts = (entity = {}) => {
const facts = [];
const frontend = entity.current_deployments?.frontend;
const api = entity.current_deployments?.api;
const branch = entity.branch || entity.mapped_branch || entity.branch_status?.frontend?.branch || publicRouteSlug(entity);
addFact(facts, "Public route", `/${publicRouteSlug(entity)}/{api|frontend}`);
addFact(facts, "Mapped branch", branch);
addFact(facts, "Frontend deployment", currentDeploymentLabel(frontend));
addFact(facts, "API deployment", currentDeploymentLabel(api));
addFact(facts, "Health", entity.readiness || entity.state || entity.status || entity.availability?.status);
addFact(facts, "Last sync", entity.last_sync_at || entity.last_synced_at || entity.operation?.completed_at, NOT_CHECKED);
addFact(facts, "Active blockers", entity.active_blockers ?? entity.issues?.length ?? entity.blockers?.length ?? 0);
return facts;
};
const branchFacts = (entity = {}) => {
const facts = [];
addFact(facts, "Repository", entity.repository || entity.full_name);
addFact(facts, "Branch", entity.branch || entity.name);
addFact(facts, "Existence", entity.exists === false ? "missing" : entity.state || entity.status || "available");
addFact(facts, "Latest commit", shortSha(entity.latest_commit_sha || entity.commit_sha || entity.commit?.sha));
addFact(facts, "Commit date", entity.commit_date || entity.latest_commit_at || entity.commit?.date, UNKNOWN);
addFact(facts, "Protected", boolValue(entity.protected));
addFact(facts, "Last sync", entity.last_sync_result || entity.last_webhook_result || entity.last_checked_at, NOT_CHECKED);
return facts;
};
const deploymentFacts = (entity = {}) => {
const facts = [];
addFact(facts, "Channel", entity.channel_slug || entity.channel_name);
addFact(facts, "App", entity.app || entity.deployment_kind);
addFact(facts, "Commit", shortSha(entity.commit_sha));
addFact(facts, "Status", entity.status);
addFact(facts, "Target", entity.coolify_service_uuid || entity.target_id || entity.service_set_id, NOT_CONFIGURED);
addFact(facts, "Started", entity.started_at || entity.created_at, UNKNOWN);
addFact(facts, "Finished", entity.completed_at || entity.updated_at, entity.status === "running" ? "running" : UNKNOWN);
addFact(facts, "Duration", entity.duration || readableDuration(entity.started_at || entity.created_at, entity.completed_at || entity.updated_at));
addFact(facts, "Current state", entity.active_current || entity.current ? "current" : entity.status === "superseded" ? "superseded" : "history");
return facts;
};
const endpointFacts = (entity = {}) => {
const facts = [];
const url = entity.url || entity.public_url || entity.endpoint?.url || entity.endpoint?.display || entity.endpoint;
addFact(facts, "Public URL", url);
addFact(facts, "Route prefix", entity.route_prefix || entity.prefix || (url ? `/${String(url).split("/").slice(3, 5).join("/")}` : ""));
addFact(facts, "Target", entity.app || entity.service || entity.target || entity.target_service, UNKNOWN);
addFact(facts, "Last probe", entity.probe_status || entity.health_status || entity.status, NOT_CHECKED);
addFact(facts, "Response time", entity.response_time_ms ? `${entity.response_time_ms}ms` : entity.latency_ms ? `${entity.latency_ms}ms` : "", UNKNOWN);
addFact(facts, "TLS/route", entity.tls_warning || entity.route_warning || entity.warning || "ok");
return facts;
};
const coolifyFacts = (entity = {}) => {
const facts = [];
addFact(facts, "Instance", entity.instance_label || entity.instance || entity.instance_id);
addFact(facts, "Project/App", entity.project_uuid || entity.project_id || entity.app_id || entity.resource_uuid, NOT_CONFIGURED);
addFact(facts, "Environment", entity.environment || entity.environment_name || entity.coolify_environment_name, UNKNOWN);
addFact(facts, "Target host", endpointHost(entity), UNKNOWN);
addFact(facts, "Status", entity.status || entity.deployment_status || entity.availability_state || (entity.integrated ? "integrated" : ""));
addFact(facts, "Last deploy", entity.last_deployed_at || entity.last_reconciled_at || entity.updated_at, NOT_CHECKED);
addFact(facts, "Exposed URL", entity.public_url || entity.coolify_public_url || entity.endpoint?.url || entity.endpoint?.display, NOT_CONFIGURED);
addFact(facts, "Health check", entity.health_status || entity.health_check_result || entity.last_reconcile_status, NOT_CHECKED);
return facts;
};
const hostFacts = (entity = {}) => {
const facts = [];
addFact(facts, "Node", entity.node || entity.node_name || entity.label || entity.host_label);
addFact(facts, "Hostname/IP", endpointHost(entity));
addFact(facts, "Provider/location", entity.provider || entity.location || entity.region, UNKNOWN);
addFact(facts, "SSH", boolValue(entity.ssh_available ?? entity.ssh_enabled ?? entity.ssh));
addFact(facts, "Services", entity.service_count ?? entity.services?.length, UNKNOWN);
addFact(facts, "Last health check", lastCheck(entity), NOT_CHECKED);
return facts;
};
const dataServiceFacts = (entity = {}) => {
const target = entity.target || entity;
const replication = entity.replication || target.replication || {};
const facts = [];
addFact(facts, "Service", entity.kind || target.kind || replication.kind);
addFact(facts, "Mode", entity.mode || target.mode || entity.replication_policy?.mode);
addFact(facts, "Node", entity.node || entity.node_name || target.instance_label || replication.label, UNKNOWN);
addFact(facts, "Online state", entity.online === true ? "online" : entity.online === false ? "offline" : entity.online_state || entity.state || target.availability_state || target.deployment_status || replication.status);
addFact(facts, "Hostname", endpointHost({ ...entity, target, replication }), NOT_CONFIGURED);
addFact(facts, "Port", endpointPort({ ...entity, target, replication }), NOT_CONFIGURED);
addFact(facts, "Uptime", uptimeText({ ...entity, ...target, ...replication }));
addFact(facts, "Version", entity.version || target.version || replication.version, UNKNOWN);
addFact(facts, "Replication role", entity.replication_role || replication.role || entity.role, UNKNOWN);
addFact(facts, "Replication lag", entity.replication_lag || replication.lag || replication.lag_seconds, UNKNOWN);
addFact(facts, "Last check", lastCheck({ ...entity, target, replication }), NOT_CHECKED);
return facts;
};
const failoverFacts = (entity = {}) => {
const facts = [];
addFact(facts, "Primary node", entity.primary_node || entity.primary_host || entity.primary?.host, UNKNOWN);
addFact(facts, "Replica node", entity.replica_node || entity.replica_host || entity.replica?.host, UNKNOWN);
addFact(facts, "Readiness", entity.readiness || entity.status || (entity.integrated ? "ready" : "not configured"));
addFact(facts, "Blockers", entity.blockers?.length ?? entity.issues?.length ?? 0);
addFact(facts, "Last failover", entity.last_failover_at || entity.last_checked_at, NOT_CHECKED);
addFact(facts, "Recovery action", entity.recovery_action || entity.next_action || entity.change_control, UNKNOWN);
return facts;
};
const operationFacts = (entity = {}) => {
const currentStep =
entity.current_step ||
(Array.isArray(entity.steps) ? entity.steps.find((step) => ["running", "failed"].includes(step.status)) : null);
const failedStep =
entity.failed_step ||
(Array.isArray(entity.steps) ? entity.steps.find((step) => String(step.status || "") === "failed") : null);
const facts = [];
addFact(facts, "Status", entity.status);
addFact(facts, "Current step", currentStep?.label || currentStep?.step_key, UNKNOWN);
addFact(facts, "Failed step", failedStep?.label || failedStep?.step_key, entity.status === "failed" ? UNKNOWN : "none");
addFact(facts, "Diagnostic", failedStep?.diagnostic || entity.diagnostic, UNKNOWN);
addFact(facts, "Solution", failedStep?.solution_hint || entity.solution_hint, UNKNOWN);
addFact(facts, "Started", entity.started_at || entity.created_at, UNKNOWN);
addFact(facts, "Duration", entity.duration || readableDuration(entity.started_at || entity.created_at, entity.completed_at || entity.updated_at));
return facts;
};
const genericFacts = (entity = {}, detail = "") => {
const facts = [];
addFact(facts, "Type", entity.type || entity.kind || UNKNOWN);
addFact(facts, "Status", entity.status || entity.state || entity.availability_state, UNKNOWN);
addFact(facts, "Host", endpointHost(entity), UNKNOWN);
addFact(facts, "Port", endpointPort(entity), UNKNOWN);
addFact(facts, "Last check", lastCheck(entity), NOT_CHECKED);
if (!isBlank(detail)) addFact(facts, "Detail", detail);
return facts;
};
export const releaseEntityFacts = ({ type = "entity", entity = {}, detail = "" } = {}) => {
const normalized = String(type || "").toLowerCase();
const enriched = { ...(entity || {}), ...(entity?.entity_facts || {}) };
if (normalized.includes("channel")) return channelFacts(enriched);
if (normalized.includes("branch") || normalized.includes("repo") || normalized.includes("source")) return branchFacts(enriched);
if (normalized.includes("deployment")) return deploymentFacts(enriched);
if (normalized.includes("endpoint")) return endpointFacts(enriched);
if (normalized.includes("coolify")) return coolifyFacts(enriched);
if (normalized.includes("host") || normalized.includes("location")) return hostFacts(enriched);
if (normalized.includes("data") || ["database", "redis", "minio"].some((kind) => normalized.includes(kind))) return dataServiceFacts(enriched);
if (normalized.includes("failover")) return failoverFacts(enriched);
if (normalized.includes("operation") || normalized.includes("test")) return operationFacts(enriched);
return genericFacts(enriched, detail);
};
export const releaseEntityTooltipText = (facts = []) =>
facts.map((fact) => `${fact.label}: ${valueOr(fact.value)}`).join("\n");
export const releaseEntityDisplayLabel = (label, entity = {}) =>
label || entity.label || entity.name || entity.slug || entity.resource_name || entity.resource_uuid || "--";