594 lines
26 KiB
JavaScript
594 lines
26 KiB
JavaScript
"use strict";
|
|
|
|
function createUnraidPreflightMethods({
|
|
safeRemoteFolder, path, bash, parseInspection, dockerIgnoreHasPath, checksSummary,
|
|
inventoryRemoteIdentity, deriveDetectedProfile, decodeBase64Json, shellQuote,
|
|
assertFullCommitSha, nativePath, safeRelativeRemoteFile, fs,
|
|
}) {
|
|
class UnraidPreflightMethods {
|
|
async saveOperation(operation) {
|
|
const saved = await this.store.addOperation(operation);
|
|
this.onOperationChange?.({ operations: [saved] });
|
|
return saved;
|
|
}
|
|
|
|
resolve(repository, profileId) {
|
|
const profile = this.store.getDeploymentProfile(
|
|
repository.fullName,
|
|
profileId,
|
|
);
|
|
if (!profile || profile.provider !== "ssh-unraid")
|
|
throw new Error("The SSH / Unraid deployment profile no longer exists.");
|
|
const server = this.store.getServer(profile.serverId);
|
|
if (!server) throw new Error("The deployment server no longer exists.");
|
|
const remoteFolder = safeRemoteFolder(
|
|
profile.remoteFolder || repository.name,
|
|
);
|
|
const remotePath = path.join(server.basePath, remoteFolder);
|
|
if (!remotePath.startsWith(`${server.basePath}/`))
|
|
throw new Error(
|
|
"Remote project path escapes the configured server base path.",
|
|
);
|
|
const effectiveProfile = {
|
|
...profile,
|
|
deploymentMode: ["push-bundle", "server-git", "monitor-only"].includes(profile.deploymentMode)
|
|
? profile.deploymentMode
|
|
: "push-bundle",
|
|
};
|
|
return { profile: effectiveProfile, server, remoteFolder, remotePath };
|
|
}
|
|
|
|
async discoverExisting({ repository, serverId, remoteFolder = "" }) {
|
|
const server = this.store.getServer(serverId);
|
|
if (!server) throw new Error("The deployment server no longer exists.");
|
|
const folder = safeRemoteFolder(remoteFolder || repository.name);
|
|
const remotePath = path.join(server.basePath, folder);
|
|
const inventory = await this.scanServerInventory(serverId, [repository], { autoLink: false });
|
|
const workload = inventory.workloads.find((item) =>
|
|
item.remoteFolderCandidate === folder ||
|
|
item.compose?.workingDir === remotePath ||
|
|
item.containers.some((container) => (container.mounts || []).some((mount) => {
|
|
const source = String(mount.source || "").replace(/\/+$/, "");
|
|
return source === remotePath || source.startsWith(`${remotePath}/`);
|
|
}))
|
|
);
|
|
if (!workload) {
|
|
const error = new Error(`No Docker or Compose workload could be matched to ${remotePath}. Use Server Inventory to select the running container directly.`);
|
|
error.code = "SERVER_WORKLOAD_NOT_FOUND";
|
|
throw error;
|
|
}
|
|
const profile = this.profileFromWorkload(repository, server, workload, {
|
|
linkSource: "manual",
|
|
deploymentMode: "server-git",
|
|
remoteFolder: folder,
|
|
});
|
|
const source = (value, origin, confidence = "confirmed") => ({
|
|
value,
|
|
origin,
|
|
confidence,
|
|
detectedAt: new Date().toISOString(),
|
|
overridden: false,
|
|
});
|
|
const provenance = {
|
|
remoteFolder: source(folder, "server-inventory"),
|
|
cloneUrl: source(profile.cloneUrl, workload.metadata?.sourceRepository ? "container-provenance" : "repository"),
|
|
branch: source(profile.branch, workload.metadata?.branch ? "container-provenance" : "repository"),
|
|
composeFile: source(profile.composeFile, "docker-compose-labels"),
|
|
composeService: source(profile.composeService, "docker-compose-labels"),
|
|
containerName: source(profile.containerName, "docker-inspect"),
|
|
hostPort: source(profile.hostPort, "docker-inspect"),
|
|
containerPort: source(profile.containerPort, "docker-inspect"),
|
|
webUiUrl: source(profile.webUiUrl, workload.dockerMan?.webUiUrl ? "unraid-dockerman" : "docker-labels"),
|
|
iconUrl: source(profile.serverIconReference, workload.dockerMan?.iconUrl ? "unraid-dockerman" : "docker-labels"),
|
|
dockerShell: source(profile.dockerShell, workload.dockerMan?.shell ? "unraid-dockerman" : "docker-labels"),
|
|
};
|
|
return {
|
|
repository: repository.fullName,
|
|
profile: { ...profile, id: undefined, provenance },
|
|
provenance,
|
|
workload,
|
|
runtime: {
|
|
remotePath,
|
|
containerRunning: workload.runtime.running,
|
|
containers: workload.containers.length,
|
|
services: workload.compose?.services?.length || workload.containers.length,
|
|
ports: workload.runtime.ports,
|
|
mounts: workload.containers.flatMap((container) => container.mounts || []),
|
|
networks: [...new Set(workload.containers.flatMap((container) => container.networks || []))],
|
|
envNames: [],
|
|
},
|
|
};
|
|
}
|
|
|
|
async inspect({ repository, profileId }) {
|
|
const { profile, server, remotePath } = this.resolve(repository, profileId);
|
|
const preserveProbe = (profile.preservePaths || [])
|
|
.map(
|
|
(relativePath) =>
|
|
`if [ -e "$root"/${shellQuote(relativePath)} ]; then printf '%s\\n' ${shellQuote(relativePath)}; fi`,
|
|
)
|
|
.join("\n");
|
|
const script = `
|
|
root=${shellQuote(remotePath)}
|
|
exists=false; root_git=false; head=""; branch=""; remote=""; tracked_changes=""; compose_files=""; nested_git=""; dockerfile=false; dockerignore_content=""; existing_preserve_paths=""
|
|
if [ -d "$root" ]; then
|
|
exists=true
|
|
if [ -d "$root/.git" ]; then
|
|
root_git=true
|
|
head=$(git -C "$root" rev-parse HEAD 2>/dev/null || true)
|
|
branch=$(git -C "$root" branch --show-current 2>/dev/null || true)
|
|
remote=$(git -C "$root" remote get-url origin 2>/dev/null || true)
|
|
tracked_changes=$(git -C "$root" status --porcelain --untracked-files=no 2>/dev/null | head -n 25 | base64 | tr -d '\\r\\n' || true)
|
|
fi
|
|
compose_files=$(find "$root" -maxdepth 2 -type f \\( -name 'docker-compose.yml' -o -name 'docker-compose.yaml' -o -name 'compose.yml' -o -name 'compose.yaml' -o -name 'compose.forgeflow.yml' \\) -printf '%P\\n' 2>/dev/null | sort | base64 | tr -d '\\r\\n' || true)
|
|
nested_git=$(find "$root" -mindepth 2 -maxdepth 4 -type d -name .git -printf '%h\\n' 2>/dev/null | sed "s#^$root/##" | sort | base64 | tr -d '\\r\\n' || true)
|
|
[ -f "$root/Dockerfile" ] && dockerfile=true
|
|
[ -f "$root/.dockerignore" ] && dockerignore_content=$(base64 < "$root/.dockerignore" | tr -d '\\r\\n' || true)
|
|
existing_preserve_paths=$({ ${preserveProbe || ":"}; } | sort -u | base64 | tr -d '\\r\\n' || true)
|
|
fi
|
|
printf '__FORGEFLOW_KV__\\n'
|
|
printf 'exists=%s\\n' "$exists"
|
|
printf 'rootGit=%s\\n' "$root_git"
|
|
printf 'head=%s\\n' "$head"
|
|
printf 'branch=%s\\n' "$branch"
|
|
printf 'remote=%s\\n' "$(printf '%s' "$remote" | base64 | tr -d '\\r\\n')"
|
|
printf 'trackedChanges=%s\\n' "$tracked_changes"
|
|
printf 'composeFiles=%s\\n' "$compose_files"
|
|
printf 'nestedGit=%s\\n' "$nested_git"
|
|
printf 'dockerfile=%s\\n' "$dockerfile"
|
|
printf 'dockerignoreContent=%s\\n' "$dockerignore_content"
|
|
printf 'existingPreservePaths=%s\\n' "$existing_preserve_paths"
|
|
`;
|
|
const wrapped = bash(script);
|
|
const result = await this.ssh.exec(server.id, wrapped, { timeout: 60_000 });
|
|
const parsed = parseInspection(result.stdout);
|
|
const contextCandidates = [
|
|
...new Set([
|
|
...(parsed.existingPreservePaths || []),
|
|
...(parsed.nestedGit || []),
|
|
]),
|
|
];
|
|
const inspection = {
|
|
...parsed,
|
|
dockerignore: Boolean(parsed.dockerignoreContent),
|
|
dockerignoreGitExcluded: dockerIgnoreHasPath(
|
|
parsed.dockerignoreContent,
|
|
".git",
|
|
),
|
|
dockerContextExclusionsMissing: parsed.dockerfile
|
|
? contextCandidates.filter(
|
|
(item) => !dockerIgnoreHasPath(parsed.dockerignoreContent, item),
|
|
)
|
|
: [],
|
|
serverId: server.id,
|
|
serverName: server.name,
|
|
remotePath,
|
|
profileId: profile.id,
|
|
};
|
|
await this.diagnostics?.info("unraid.inspected", {
|
|
repository: repository.fullName,
|
|
serverId: server.id,
|
|
remotePath,
|
|
exists: inspection.exists,
|
|
rootGit: inspection.rootGit,
|
|
head: inspection.head,
|
|
composeFiles: inspection.composeFiles,
|
|
nestedGitCount: inspection.nestedGit.length,
|
|
trackedChangeCount: inspection.trackedChanges.length,
|
|
dockerContextExclusionsMissing: inspection.dockerContextExclusionsMissing,
|
|
});
|
|
return inspection;
|
|
}
|
|
|
|
async preflight({ repository, profileId, sha = null }) {
|
|
const { profile, server, remotePath } = this.resolve(repository, profileId);
|
|
const deploymentMode = ["push-bundle", "server-git", "monitor-only"].includes(profile.deploymentMode)
|
|
? profile.deploymentMode
|
|
: "push-bundle";
|
|
let requestedSha = sha || repository.localStatus?.head;
|
|
if (deploymentMode === "server-git" && !sha) {
|
|
const [owner, repo] = String(repository.fullName || "").split("/");
|
|
const branch = await this.gitea.getBranch(owner, repo, profile.branch);
|
|
requestedSha = branch?.commit?.id || branch?.commit?.sha || null;
|
|
}
|
|
const targetSha = assertFullCommitSha(requestedSha);
|
|
const checks = [];
|
|
let inspection = null;
|
|
let connectionCapabilities = null;
|
|
let permissions = null;
|
|
|
|
if (deploymentMode === "monitor-only") checks.push({
|
|
id: "deployment-mode",
|
|
label: "Deployment mode",
|
|
status: "fail",
|
|
detail: "This workload is linked for monitoring only. Select Server pull or Direct copy before deploying.",
|
|
});
|
|
else checks.push({
|
|
id: "deployment-mode",
|
|
label: "Deployment mode",
|
|
status: "pass",
|
|
detail: deploymentMode === "server-git"
|
|
? "Unraid fetches the exact Gitea commit with a repository-scoped read-only deploy key."
|
|
: "ForgeFlow copies the exact committed local project directly to Unraid and runs Docker Compose there.",
|
|
});
|
|
|
|
if (!repository.localPath) {
|
|
checks.push({
|
|
id: "local-repository",
|
|
label: "Local repository",
|
|
status: deploymentMode === "server-git" ? "pass" : "fail",
|
|
detail: deploymentMode === "server-git"
|
|
? "Not required: the exact commit is fetched from Gitea by the server."
|
|
: "Link or clone the repository locally before using Direct copy.",
|
|
});
|
|
} else {
|
|
try {
|
|
const localStatus = await this.git.status(repository.localPath);
|
|
checks.push({
|
|
id: "local-repository",
|
|
label: "Local repository",
|
|
status: "pass",
|
|
detail: localStatus.root,
|
|
});
|
|
checks.push({
|
|
id: "local-branch",
|
|
label: "Allowed branch",
|
|
status: localStatus.branch.head === profile.branch ? "pass" : deploymentMode === "server-git" ? "warning" : "fail",
|
|
detail: `Current: ${localStatus.branch.head || "detached"}; required: ${profile.branch}.`,
|
|
});
|
|
checks.push({
|
|
id: "local-clean",
|
|
label: "Clean local working tree",
|
|
status: localStatus.clean ? "pass" : deploymentMode === "server-git" ? "warning" : "fail",
|
|
detail: localStatus.clean
|
|
? "No uncommitted changes."
|
|
: `${localStatus.counts.changed} changed file(s) remain.`,
|
|
});
|
|
checks.push({
|
|
id: "deployment-source",
|
|
label: deploymentMode === "server-git" ? "Gitea deployment source" : "Direct deployment source",
|
|
status: "pass",
|
|
detail: deploymentMode === "server-git"
|
|
? "Local files are not uploaded; the exact requested commit is fetched from Gitea."
|
|
: "The exact committed local HEAD is archived and copied directly to Unraid. No server-side repository access is involved.",
|
|
});
|
|
|
|
|
|
const localDeploymentFiles = deploymentMode === "push-bundle" && profile.generatedCompose
|
|
? [nativePath.join(repository.localPath, "Dockerfile")]
|
|
: deploymentMode === "push-bundle" ? this.deploymentComposeFiles(profile).map((file) =>
|
|
nativePath.join(repository.localPath, safeRelativeRemoteFile(file)),
|
|
) : [];
|
|
const missingDeploymentFiles = [];
|
|
for (const file of localDeploymentFiles) {
|
|
if (!(await fs.stat(file).catch(() => null))?.isFile()) missingDeploymentFiles.push(file);
|
|
}
|
|
if (deploymentMode === "push-bundle") checks.push({
|
|
id: "local-deployment-file",
|
|
label: profile.generatedCompose
|
|
? "Dockerfile in repository"
|
|
: localDeploymentFiles.length > 1 ? "Compose files in repository" : "Compose file in repository",
|
|
status: missingDeploymentFiles.length ? "fail" : "pass",
|
|
detail: missingDeploymentFiles.length
|
|
? `Missing from the exact local checkout: ${missingDeploymentFiles.join(", ")}`
|
|
: localDeploymentFiles.join(", "),
|
|
});
|
|
} catch (error) {
|
|
checks.push({
|
|
id: "local-repository",
|
|
label: "Local repository",
|
|
status: "fail",
|
|
detail: error.message,
|
|
});
|
|
}
|
|
}
|
|
|
|
try {
|
|
const connection = await this.ssh.test(server.id, { trustOnFirstUse: false });
|
|
connectionCapabilities = connection.capabilities || {};
|
|
checks.push({
|
|
id: "ssh",
|
|
label: "Desktop → Unraid SSH",
|
|
status: "pass",
|
|
detail: `${server.username}@${server.host}:${server.port}`,
|
|
});
|
|
checks.push({
|
|
id: "docker-runtime",
|
|
label: "Docker runtime",
|
|
status: connectionCapabilities.docker && connectionCapabilities.dockerReady ? "pass" : "fail",
|
|
detail: connectionCapabilities.dockerReady
|
|
? "Docker is reachable by the configured SSH user."
|
|
: connectionCapabilities.docker
|
|
? "Docker is installed, but the configured SSH user cannot query the daemon."
|
|
: "Docker was not detected on the server.",
|
|
});
|
|
checks.push({
|
|
id: "compose-command",
|
|
label: "Docker Compose",
|
|
status: connectionCapabilities.compose ? "pass" : "fail",
|
|
detail: connectionCapabilities.composeVersion || "Docker Compose was not detected on the server.",
|
|
});
|
|
checks.push({
|
|
id: "bundle-tools",
|
|
label: deploymentMode === "server-git" ? "Server pull tools" : "Direct copy tools",
|
|
status: connectionCapabilities.tar && connectionCapabilities.checksum && (deploymentMode !== "server-git" || connectionCapabilities.git) ? "pass" : "fail",
|
|
detail: deploymentMode === "server-git"
|
|
? `Git ${connectionCapabilities.git ? "available" : "missing"}; tar ${connectionCapabilities.tar ? "available" : "missing"}; checksum ${connectionCapabilities.checksum ? "available" : "missing"}.`
|
|
: connectionCapabilities.tar && connectionCapabilities.checksum
|
|
? "tar and a SHA-256 checksum tool are available."
|
|
: `tar ${connectionCapabilities.tar ? "available" : "missing"}; checksum tool ${connectionCapabilities.checksum ? "available" : "missing"}.`,
|
|
});
|
|
checks.push({
|
|
id: "server-base-writable",
|
|
label: "Deployment storage writable",
|
|
status: connectionCapabilities.baseWritable ? "pass" : "fail",
|
|
detail: connectionCapabilities.baseWritable ? `${server.basePath} is writable.` : `${server.basePath} cannot be created or written by this SSH user.`,
|
|
});
|
|
} catch (error) {
|
|
checks.push({
|
|
id: "ssh",
|
|
label: "Desktop → Unraid SSH",
|
|
status: "fail",
|
|
detail: error.message,
|
|
});
|
|
}
|
|
if (!server.hostFingerprint) checks.push({
|
|
id: "host-key",
|
|
label: "Server identity",
|
|
status: "fail",
|
|
detail: "Test and trust the SSH host key first.",
|
|
});
|
|
else checks.push({
|
|
id: "host-key",
|
|
label: "Server identity",
|
|
status: "pass",
|
|
detail: server.hostFingerprint,
|
|
});
|
|
|
|
if (deploymentMode === "server-git") {
|
|
const access = await this.probeServerGitAccess({ repository, profile, server });
|
|
checks.push({
|
|
id: "server-git-access",
|
|
label: "Unraid → Gitea read access",
|
|
status: access.ready ? "pass" : "fail",
|
|
detail: access.ready
|
|
? `Read-only deploy key verified${access.remoteSha ? ` at ${access.remoteSha.slice(0, 7)}` : ""}.`
|
|
: access.error,
|
|
repairAction: access.ready ? null : "configure-server-git-access",
|
|
repairLabel: "Configure read-only deploy key",
|
|
});
|
|
} else checks.push({
|
|
id: "transfer-path",
|
|
label: "Desktop → Unraid transfer",
|
|
status: "pass",
|
|
detail: "Files are copied over the configured server connection. No Gitea credential is required on Unraid.",
|
|
});
|
|
|
|
try {
|
|
permissions = await this.inspectWriteAccess({ repository, profileId });
|
|
const blockingPaths = permissions.blocking.map((target) => target.path);
|
|
checks.push({
|
|
id: "project-write-access",
|
|
label: "Project write access",
|
|
status: permissions.ready ? "pass" : "fail",
|
|
detail: permissions.ready
|
|
? `${permissions.identity.user} can create and atomically replace deployment files in ${remotePath}.`
|
|
: `No safe write access for ${permissions.identity.user}: ${blockingPaths.join(", ")}`,
|
|
help: permissions.ready
|
|
? "ForgeFlow rechecks these paths immediately before every upload and Compose activation."
|
|
: "Use Fix write access to repair only the linked project source and ForgeFlow state folders. Preserved runtime data is excluded.",
|
|
repairAction: permissions.ready ? null : "repair-deployment-write-access",
|
|
repairLabel: "Fix write access",
|
|
});
|
|
for (const target of permissions.targets.filter(
|
|
(item) => item.required && !item.effectiveWritable,
|
|
)) {
|
|
checks.push({
|
|
id: `write-path:${target.id}`,
|
|
label: target.label,
|
|
status: "fail",
|
|
detail: `${target.path} · owner ${target.owner || "unknown"}:${target.group || "unknown"} · mode ${target.mode || "unknown"}. ${target.detail}`,
|
|
repairAction: "repair-deployment-write-access",
|
|
repairLabel: "Fix write access",
|
|
});
|
|
}
|
|
} catch (error) {
|
|
checks.push({
|
|
id: "project-write-access",
|
|
label: "Project write access",
|
|
status: "fail",
|
|
detail: error.message,
|
|
});
|
|
}
|
|
|
|
|
|
try {
|
|
inspection = await this.inspect({ repository, profileId });
|
|
if (!inspection.exists) {
|
|
checks.push({
|
|
id: "remote-folder",
|
|
label: "Remote project folder",
|
|
status: "pass",
|
|
detail: `${remotePath} will be created.`,
|
|
});
|
|
} else {
|
|
checks.push({
|
|
id: "remote-folder",
|
|
label: inspection.rootGit ? "Remote project folder" : "Existing server installation",
|
|
status: "pass",
|
|
detail: inspection.rootGit
|
|
? `${remotePath} currently contains Git commit ${String(inspection.head || "").slice(0, 7) || "unknown"}.`
|
|
: `${remotePath} will receive managed release files while preserved and unknown runtime data remains untouched.`,
|
|
});
|
|
}
|
|
if (inspection.rootGit) {
|
|
checks.push({
|
|
id: "tracked-changes",
|
|
label: "Server-side tracked changes",
|
|
status: inspection.trackedChanges.length ? "warning" : "pass",
|
|
detail: inspection.trackedChanges.length
|
|
? `${inspection.trackedChanges.length} tracked server edit(s) exist. Direct copy preserves unknown runtime data and does not depend on the server Git checkout.`
|
|
: "No tracked server-only edits detected.",
|
|
});
|
|
}
|
|
|
|
if (inspection.nestedGit.length) {
|
|
checks.push({
|
|
id: "nested-git",
|
|
label: "Nested Git repositories",
|
|
status: "warning",
|
|
detail: `Detected: ${inspection.nestedGit.join(", ")}. ForgeFlow will not delete them automatically.`,
|
|
});
|
|
}
|
|
if (inspection.dockerfile && !inspection.dockerignore) {
|
|
checks.push({
|
|
id: "dockerignore",
|
|
label: "Docker build context",
|
|
status: "warning",
|
|
detail:
|
|
"A Dockerfile exists but .dockerignore is missing. Add one in the repository before large builds.",
|
|
});
|
|
} else if (inspection.dockerfile && !inspection.dockerignoreGitExcluded) {
|
|
checks.push({
|
|
id: "dockerignore-git",
|
|
label: "Git metadata excluded from Docker",
|
|
status: "warning",
|
|
detail: ".dockerignore does not explicitly exclude .git.",
|
|
});
|
|
} else if (inspection.dockerfile) {
|
|
checks.push({
|
|
id: "dockerignore-git",
|
|
label: "Git metadata excluded from Docker",
|
|
status: "pass",
|
|
detail: ".git is excluded from the Docker build context.",
|
|
});
|
|
}
|
|
if (inspection.dockerContextExclusionsMissing.length) {
|
|
checks.push({
|
|
id: "dockerignore-runtime",
|
|
label: "Runtime data excluded from Docker",
|
|
status: "warning",
|
|
detail: `Add these existing runtime or legacy paths to .dockerignore: ${inspection.dockerContextExclusionsMissing.join(", ")}.`,
|
|
});
|
|
} else if (
|
|
inspection.dockerfile &&
|
|
inspection.existingPreservePaths.length
|
|
) {
|
|
checks.push({
|
|
id: "dockerignore-runtime",
|
|
label: "Runtime data excluded from Docker",
|
|
status: "pass",
|
|
detail:
|
|
"Detected preserved runtime paths are excluded from the Docker build context.",
|
|
});
|
|
}
|
|
const composeFiles = profile.generatedCompose
|
|
? [".forgeflow/compose.forgeflow.yml"]
|
|
: (profile.composeFiles?.length ? profile.composeFiles : [profile.composeFile || "docker-compose.yml"])
|
|
.map((value) => safeRelativeRemoteFile(value));
|
|
const missingRemoteCompose = composeFiles.filter((composeFile) => !inspection.composeFiles.includes(composeFile));
|
|
checks.push({
|
|
id: "compose-file",
|
|
label: "Compose configuration",
|
|
status: "pass",
|
|
detail: profile.generatedCompose
|
|
? "ForgeFlow will generate an isolated Compose file."
|
|
: missingRemoteCompose.length
|
|
? `${composeFiles.join(", ")} will be uploaded from the exact local commit.`
|
|
: composeFiles.join(", "),
|
|
});
|
|
} catch (error) {
|
|
checks.push({
|
|
id: "inspection",
|
|
label: "Server project inspection",
|
|
status: "fail",
|
|
detail: error.message,
|
|
});
|
|
}
|
|
const iconMode =
|
|
profile.iconMode ||
|
|
(profile.iconFilePath ? "upload" : profile.iconUrl ? "url" : "builtin");
|
|
if (iconMode === "upload") {
|
|
const iconStat = await fs.stat(profile.iconFilePath).catch(() => null);
|
|
checks.push({
|
|
id: "dockerman-icon-file",
|
|
label: "DockerMan icon upload",
|
|
status:
|
|
iconStat?.isFile() &&
|
|
nativePath.extname(profile.iconFilePath).toLowerCase() === ".png"
|
|
? "pass"
|
|
: "fail",
|
|
detail: iconStat?.isFile()
|
|
? profile.iconFilePath
|
|
: "The selected local PNG icon file was not found.",
|
|
});
|
|
} else if (iconMode === "builtin") {
|
|
const builtinIcon = nativePath.join(
|
|
this.sourcePath,
|
|
"src",
|
|
"renderer",
|
|
"assets",
|
|
"itworx-mark.png",
|
|
);
|
|
const iconStat = await fs.stat(builtinIcon).catch(() => null);
|
|
checks.push({
|
|
id: "dockerman-icon-builtin",
|
|
label: "DockerMan icon",
|
|
status: iconStat?.isFile() ? "pass" : "fail",
|
|
detail: iconStat?.isFile()
|
|
? "Built-in high-contrast ITWorx mark."
|
|
: "The built-in ITWorx icon asset is missing.",
|
|
});
|
|
} else if (iconMode === "url")
|
|
checks.push({
|
|
id: "dockerman-icon",
|
|
label: "DockerMan icon",
|
|
status: profile.iconUrl ? "pass" : "fail",
|
|
detail:
|
|
profile.iconUrl || "Icon URL mode requires an HTTPS or HTTP PNG URL.",
|
|
});
|
|
else
|
|
checks.push({
|
|
id: "dockerman-icon",
|
|
label: "DockerMan icon",
|
|
status: "warning",
|
|
detail: "Custom DockerMan icon disabled.",
|
|
});
|
|
const webUiLabel = this.dockerManWebUi(profile);
|
|
checks.push({
|
|
id: "dockerman-webui",
|
|
label: "DockerMan Web UI action",
|
|
status: webUiLabel ? "pass" : "warning",
|
|
detail: webUiLabel || "No Web UI URL or host port is configured.",
|
|
});
|
|
checks.push({
|
|
id: "compose-identity",
|
|
label: "Safe Docker Compose identity",
|
|
status: "pass",
|
|
detail: `Internal project/image: ${this.internalSlug(profile, repository)}; visible container: ${profile.containerName || profile.remoteFolder || repository.name}.`,
|
|
});
|
|
checks.push({
|
|
id: "exact-sha",
|
|
label: "Exact deployment commit",
|
|
status: "pass",
|
|
detail: targetSha,
|
|
});
|
|
return {
|
|
provider: "ssh-unraid",
|
|
repository: repository.fullName,
|
|
environment: profile.environment,
|
|
sha: targetSha,
|
|
server: { id: server.id, name: server.name, host: server.host },
|
|
remotePath,
|
|
inspection,
|
|
permissions,
|
|
checks,
|
|
summary: checksSummary(checks),
|
|
};
|
|
}
|
|
}
|
|
return UnraidPreflightMethods.prototype;
|
|
}
|
|
|
|
module.exports = { createUnraidPreflightMethods };
|