mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-01 10:22:08 +00:00
Merge develop and preserve Slovak Monitor and CLI messages
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
// Legacy HostBackup API messages are an English wire contract shared with CLI callers.
|
||||
// Match complete, source-verified messages only; never derive a translation key from input.
|
||||
// This presentation-only adapter keeps unknown tool diagnostics and parameter text intact.
|
||||
type Translate = (key: string, params?: Record<string, string | number>) => string
|
||||
|
||||
const fixed = new Map<string, string>([
|
||||
["path must be absolute", "backup.apiErrors.absolutePath"],
|
||||
["this path is already configured", "backup.apiErrors.pathConfigured"],
|
||||
["path is required", "backup.apiErrors.pathRequired"],
|
||||
["path query parameter is required", "backup.apiErrors.pathQuery"],
|
||||
["path not found", "backup.apiErrors.pathNotFound"],
|
||||
["invalid job id", "backup.apiErrors.invalidJobId"],
|
||||
["job not found", "backup.apiErrors.jobNotFound"],
|
||||
["runner script not installed", "backup.apiErrors.runnerMissing"],
|
||||
["no restore state to dismiss", "backup.apiErrors.noRestore"],
|
||||
["invalid file name", "backup.apiErrors.invalidFile"],
|
||||
["not found", "backup.apiErrors.notFound"],
|
||||
["log path outside allowed directory", "backup.apiErrors.logOutside"],
|
||||
["targets must be a list", "backup.apiErrors.targetsList"],
|
||||
["empty expression", "backup.apiErrors.emptyExpression"],
|
||||
["invalid id (use letters, digits, _ or -)", "backup.apiErrors.invalidId"],
|
||||
["backend must be pbs, local or borg", "backup.apiErrors.backend"],
|
||||
["backend must be pbs or borg", "backup.apiErrors.remoteBackend"],
|
||||
["backend must be 'pbs' or 'borg'", "backup.apiErrors.remoteBackendQuoted"],
|
||||
["profile_mode must be default or custom", "backup.apiErrors.profileMode"],
|
||||
["borg cannot be attached (PVE vzdump only writes to pbs/local storages)", "backup.apiErrors.borgAttached"],
|
||||
["borg is not a valid backend for attached jobs (PVE vzdump only writes to pbs/local storages)", "backup.apiErrors.borgAttachedLong"],
|
||||
["pve_parent_job_id and pve_storage are required for attached", "backup.apiErrors.parentRequired"],
|
||||
["paths must be a list of absolute paths", "backup.apiErrors.absolutePaths"],
|
||||
["custom profile needs at least one path", "backup.apiErrors.customPath"],
|
||||
["could not resolve default profile paths", "backup.apiErrors.defaultPaths"],
|
||||
["retention must be an object", "backup.apiErrors.retentionObject"],
|
||||
["pbs_repository is required", "backup.apiErrors.pbsRepository"],
|
||||
["pbs_encrypt_mode must be 'none', 'new' or 'existing'", "backup.apiErrors.pbsEncryption"],
|
||||
["failed to create PBS encryption keyfile", "backup.apiErrors.pbsKeyFailed"],
|
||||
["failed to create PBS encryption keyfile (is proxmox-backup-client installed?)", "backup.apiErrors.pbsClientMissing"],
|
||||
["borg_repo is required", "backup.apiErrors.borgRepository"],
|
||||
["borg_encrypt_mode must be one of: none, repokey, keyfile, authenticated", "backup.apiErrors.borgEncryption"],
|
||||
["encrypt_mode must be one of: none, repokey, keyfile, authenticated", "backup.apiErrors.encryption"],
|
||||
["borg_passphrase is required when encryption is enabled (save it on the destination or pass one in the request)", "backup.apiErrors.borgPassphrase"],
|
||||
["passphrase is required when encryption is enabled", "backup.apiErrors.passphrase"],
|
||||
["password is required", "backup.apiErrors.password"],
|
||||
["confirm_device must match device exactly (typo / safety check)", "backup.apiErrors.deviceConfirmation"],
|
||||
["invalid device path", "backup.apiErrors.invalidDevice"],
|
||||
["absolute mountpoint required", "backup.apiErrors.absoluteMountpoint"],
|
||||
["archive layout not recognized — no rootfs/ found", "backup.apiErrors.archiveLayout"],
|
||||
["archive not found", "backup.apiErrors.archiveMissing"],
|
||||
["archive path is not a regular file", "backup.apiErrors.archiveNotFile"],
|
||||
["invalid name", "backup.apiErrors.invalidName"],
|
||||
["invalid staging_path", "backup.apiErrors.invalidStaging"],
|
||||
["job name cannot be changed (delete + recreate to rename)", "backup.apiErrors.jobRename"],
|
||||
["key_path must be absolute", "backup.apiErrors.absoluteKey"],
|
||||
["mode must be \"local\" or \"ssh\"", "backup.apiErrors.mode"],
|
||||
["name is required (letters, digits, _ or -)", "backup.apiErrors.nameRequired"],
|
||||
["no log available for this job yet", "backup.apiErrors.noLog"],
|
||||
["no manual PBS configs file", "backup.apiErrors.noPbsConfig"],
|
||||
["output file missing", "backup.apiErrors.outputMissing"],
|
||||
["parser output was not valid JSON", "backup.apiErrors.parserJson"],
|
||||
["run_restore output was not valid JSON", "backup.apiErrors.restoreJson"],
|
||||
["repo is required", "backup.apiErrors.repoRequired"],
|
||||
["repo_name and snapshot are required", "backup.apiErrors.repoSnapshot"],
|
||||
["repo_name and snapshot are required for borg", "backup.apiErrors.borgRepoSnapshot"],
|
||||
["repo_name and snapshot are required for pbs", "backup.apiErrors.pbsRepoSnapshot"],
|
||||
["repo_name is required", "backup.apiErrors.repoName"],
|
||||
["restore tooling not installed on this host", "backup.apiErrors.restoreMissing"],
|
||||
["server and datastore are required", "backup.apiErrors.serverDatastore"],
|
||||
["snapshot is required", "backup.apiErrors.snapshotRequired"],
|
||||
["source must be pbs, borg, or local", "backup.apiErrors.source"],
|
||||
["ssh_port must be an integer between 1 and 65535", "backup.apiErrors.sshPort"],
|
||||
["ssh_user, ssh_host and ssh_remote_path are required for ssh mode", "backup.apiErrors.sshRequired"],
|
||||
["target has no repository path", "backup.apiErrors.repositoryPath"],
|
||||
["task not found or expired", "backup.apiErrors.taskExpired"],
|
||||
["invalid expression", "backup.apiErrors.invalidExpression"],
|
||||
["no report emitted", "backup.apiErrors.noReport"],
|
||||
["parse_manifest exited non-zero", "backup.apiErrors.parseManifestFailed"],
|
||||
])
|
||||
|
||||
const parameterized: ReadonlyArray<readonly [RegExp, string]> = [
|
||||
[new RegExp("^job already exists: ([\\s\\S]+)$"), "backup.apiErrors.jobExists"],
|
||||
[new RegExp("^path does not exist on this host: ([\\s\\S]+)$"), "backup.apiErrors.pathMissing"],
|
||||
[new RegExp("^device does not exist: ([\\s\\S]+)$"), "backup.apiErrors.deviceMissing"],
|
||||
[new RegExp("^not a mountpoint: ([\\s\\S]+)$"), "backup.apiErrors.mountpoint"],
|
||||
[new RegExp("^local archive not found: ([\\s\\S]+)$"), "backup.apiErrors.localArchiveMissing"],
|
||||
[new RegExp("^Borg destination '([\\s\\S]+)' not found$"), "backup.apiErrors.borgDestination"],
|
||||
[new RegExp("^PBS destination '([\\s\\S]+)' not found$"), "backup.apiErrors.pbsDestination"],
|
||||
[new RegExp("^no password available for PBS repo '([\\s\\S]+)'$"), "backup.apiErrors.pbsPassword"],
|
||||
[new RegExp("^Borg repo \"([\\s\\S]+)\" not configured$"), "backup.apiErrors.borgRepoMissing"],
|
||||
[new RegExp("^PBS repo \"([\\s\\S]+)\" not configured$"), "backup.apiErrors.pbsRepoMissing"],
|
||||
[new RegExp("^Borg target not found: ([\\s\\S]+)$"), "backup.apiErrors.borgTargetMissing"],
|
||||
[new RegExp("^PBS repo not found: ([\\s\\S]+)$"), "backup.apiErrors.pbsTargetMissing"],
|
||||
[new RegExp("^PVE vzdump job not found: ([\\s\\S]+)$"), "backup.apiErrors.pveJobMissing"],
|
||||
[new RegExp("^cannot persist state: ([\\s\\S]+)$"), "backup.apiErrors.statePersist"],
|
||||
[new RegExp("^cannot prepare ssh dir: ([\\s\\S]+)$"), "backup.apiErrors.sshDirectory"],
|
||||
[new RegExp("^cannot read log: ([\\s\\S]+)$"), "backup.apiErrors.logRead"],
|
||||
[new RegExp("^could not update \\.env: ([\\s\\S]+)$"), "backup.apiErrors.envUpdate"],
|
||||
[new RegExp("^failed to persist PBS config: ([\\s\\S]+)$"), "backup.apiErrors.pbsConfigWrite"],
|
||||
[new RegExp("^failed to persist: ([\\s\\S]+)$"), "backup.apiErrors.persist"],
|
||||
[new RegExp("^failed to remove archive: ([\\s\\S]+)$"), "backup.apiErrors.archiveRemove"],
|
||||
[new RegExp("^failed to start: ([\\s\\S]+)$"), "backup.apiErrors.start"],
|
||||
[new RegExp("^failed to update borg-targets\\.txt: ([\\s\\S]+)$"), "backup.apiErrors.borgTargetsUpdate"],
|
||||
[new RegExp("^failed to update pbs-manual-configs\\.txt: ([\\s\\S]+)$"), "backup.apiErrors.pbsTargetsUpdate"],
|
||||
[new RegExp("^failed to update systemd units: ([\\s\\S]+)$"), "backup.apiErrors.unitsUpdate"],
|
||||
[new RegExp("^failed to write borg-targets\\.txt: ([\\s\\S]+)$"), "backup.apiErrors.borgTargetsWrite"],
|
||||
[new RegExp("^failed to write job files: ([\\s\\S]+)$"), "backup.apiErrors.jobWrite"],
|
||||
[new RegExp("^failed to write local-target\\.conf: ([\\s\\S]+)$"), "backup.apiErrors.localTargetWrite"],
|
||||
[new RegExp("^failed to write systemd units: ([\\s\\S]+)$"), "backup.apiErrors.unitsWrite"],
|
||||
[new RegExp("^format failed: ([\\s\\S]+)$"), "backup.apiErrors.format"],
|
||||
[new RegExp("^invalid on_calendar: ([\\s\\S]+)$"), "backup.apiErrors.calendar"],
|
||||
[new RegExp("^mount failed: ([\\s\\S]+)$"), "backup.apiErrors.mount"],
|
||||
[new RegExp("^parser invocation failed: ([\\s\\S]+)$"), "backup.apiErrors.parserInvocation"],
|
||||
[new RegExp("^preflight invocation failed: ([\\s\\S]+)$"), "backup.apiErrors.preflightInvocation"],
|
||||
[new RegExp("^public key not found after generation: ([\\s\\S]+)$"), "backup.apiErrors.publicKeyMissing"],
|
||||
[new RegExp("^retention\\.([\\s\\S]+) must be an integer$"), "backup.apiErrors.retentionInteger"],
|
||||
[new RegExp("^retention\\.([\\s\\S]+) must be non-negative$"), "backup.apiErrors.retentionNegative"],
|
||||
[new RegExp("^ssh-keygen failed: ([\\s\\S]+)$"), "backup.apiErrors.keygen"],
|
||||
[new RegExp("^systemctl failed: ([\\s\\S]+)$"), "backup.apiErrors.systemctl"],
|
||||
[new RegExp("^umount failed: ([\\s\\S]+)$"), "backup.apiErrors.unmount"],
|
||||
[new RegExp("^task is in state \"([\\s\\S]+)\", not completed$"), "backup.apiErrors.taskIncomplete"],
|
||||
[new RegExp("^unknown kind: ([\\s\\S]+)$"), "backup.apiErrors.unknownKind"],
|
||||
[new RegExp("^unknown mode \"([\\s\\S]+)\"$"), "backup.apiErrors.unknownMode"],
|
||||
[new RegExp("^([\\s\\S]+) is the built-in default and cannot be removed$"), "backup.apiErrors.defaultRemove"],
|
||||
[new RegExp("^([\\s\\S]+) is the built-in default; no need to add it$"), "backup.apiErrors.defaultAdd"],
|
||||
[new RegExp("^pbs_encrypt_mode='existing' but no keyfile is installed at ([\\s\\S]+) — import one first with POST /api/host-backups/pbs-encryption/import$"), "backup.apiErrors.keyImport"],
|
||||
[new RegExp("^PVE job storage mismatch \\(job uses ([\\s\\S]+), payload says ([\\s\\S]+)\\)$"), "backup.apiErrors.storageMismatch"],
|
||||
]
|
||||
|
||||
export function hostBackupErrorText(problem: unknown, t: Translate): string {
|
||||
const message = typeof problem === "string" ? problem
|
||||
: problem && typeof problem === "object" && "message" in problem && typeof problem.message === "string"
|
||||
? problem.message : ""
|
||||
if (!message) return t("backup.apiErrors.requestFailed")
|
||||
const key = fixed.get(message)
|
||||
if (key) return t(key)
|
||||
// Keep unusually large tool output verbatim, without running pattern matching over it.
|
||||
if (message.length > 16384) return message
|
||||
for (const [pattern, patternKey] of parameterized) {
|
||||
const match = pattern.exec(message)
|
||||
if (match) return t(patternKey, { value: match[1], other: match[2] ?? "" })
|
||||
}
|
||||
return message
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
type Translate = (key: string, params?: Record<string, string | number>) => string
|
||||
|
||||
// Exact lxc_apps validation/persistence messages; never change their API contract.
|
||||
// Anchored patterns leave unknown diagnostics intact, including paths and commands.
|
||||
const RULES: [RegExp, string, string[]][] = [
|
||||
[/^an OCI operation on this container is running$/, "ociBusy", []],
|
||||
[/^([\s\S]+) is not installed via dpkg$/, "dpkgMissing", ["package"]],
|
||||
[/^dpkg\-query failed$/, "dpkgFailed", []],
|
||||
[/^no version matched in dpkg output$/, "dpkgNoVersion", []],
|
||||
[/^apk info failed$/, "apkFailed", []],
|
||||
[/^no version matched in apk output$/, "apkNoVersion", []],
|
||||
[/^could not read ([\s\S]+)$/, "fileRead", ["path"]],
|
||||
[/^file_regex did not match ([\s\S]+)$/, "fileNoMatch", ["path"]],
|
||||
[/^binary invocation failed$/, "binaryFailed", []],
|
||||
[/^no version matched in binary output$/, "binaryNoVersion", []],
|
||||
[/^python \-c importlib\.metadata failed$/, "pythonFailed", []],
|
||||
[/^no version matched in Python distribution output$/, "pythonNoVersion", []],
|
||||
[/^could not resolve the container's installed image$/, "dockerImageResolve", []],
|
||||
[/^docker inspect failed$/, "dockerInspectFailed", []],
|
||||
[/^container has no ([\s\S]+) label$/, "dockerLabelMissing", ["label"]],
|
||||
[/^docker label reports ([\s\S]+) \(mutable tag, not a version\)$/, "dockerLabelMutable", ["tag"]],
|
||||
[/^no version matched in docker label output$/, "dockerLabelNoVersion", []],
|
||||
[/^docker exec failed$/, "dockerExecFailed", []],
|
||||
[/^no version matched in docker exec output$/, "dockerExecNoVersion", []],
|
||||
[/^command_argv is empty$/, "commandEmpty", []],
|
||||
[/^command failed$/, "commandFailed", []],
|
||||
[/^no version matched in command output$/, "commandNoVersion", []],
|
||||
[/^unsupported method: ([\s\S]+)$/, "unsupportedMethod", ["method"]],
|
||||
[/^unknown upstream_type: ([\s\S]+)$/, "unknownUpstream", ["type"]],
|
||||
[/^([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+): not found$/, "githubNotFound", ["repo"]],
|
||||
[/^github rejected the request \(403\)$/, "githubRejected", []],
|
||||
[/^github error (\d+)$/, "githubError", ["code"]],
|
||||
[/^no tag \/ release name in response$/, "noRelease", []],
|
||||
[/^tag_regex did not match '([\s\S]+)'$/, "tagNoMatch", ["tag"]],
|
||||
[/^http error (\d+)$/, "httpError", ["code"]],
|
||||
[/^invalid JSON response: ([\s\S]+)$/, "invalidJson", ["detail"]],
|
||||
[/^json_path '([\s\S]+)' did not resolve$/, "jsonPathUnresolved", ["path"]],
|
||||
[/^empty value at json_path$/, "jsonEmpty", []],
|
||||
[/^no tags matched \(docker_hub\)$/, "dockerNoTags", []],
|
||||
[/^tag_regex matched only moving tags; use Docker image digest tracking$/, "dockerMovingOnly", []],
|
||||
[/^docker hub error (\d+)$/, "dockerHubError", ["code"]],
|
||||
[/^invalid docker hub JSON: ([\s\S]+)$/, "dockerHubJson", ["detail"]],
|
||||
[/^no tags returned by docker hub$/, "dockerHubEmpty", []],
|
||||
[/^payload must be a JSON object$/, "payload", []],
|
||||
[/^name is required and must be 1\-64 chars of letters\/digits\/spaces\/\._\+\-\(\)\/$/, "name", []],
|
||||
[/^installed_via must be one of: ([\s\S]+) or empty$/, "method", ["values"]],
|
||||
[/^package is required \(letters\/digits\/\._\+@:\/ up to 127 chars\)$/, "package", []],
|
||||
[/^file_path is required and must be an absolute path$/, "filePath", []],
|
||||
[/^file_regex is required$/, "fileRegex", []],
|
||||
[/^file_regex is not a valid regex: ([\s\S]+)$/, "fileRegexInvalid", ["detail"]],
|
||||
[/^binary_path is required and must be an absolute path$/, "binaryPath", []],
|
||||
[/^python_path is required and must be an absolute path$/, "pythonPath", []],
|
||||
[/^distribution is required \(PEP 503 name\)$/, "distribution", []],
|
||||
[/^container_name is required \(docker naming rules\)$/, "container", []],
|
||||
[/^label is required \(OCI label naming rules\)$/, "label", []],
|
||||
[/^binary_path is required \(absolute path or bare command\)$/, "dockerBinary", []],
|
||||
[/^installed_version is required for the manual method$/, "manualVersion", []],
|
||||
[/^installed_version exceeds (\d+) chars$/, "versionLength", ["max"]],
|
||||
[/^installed_version contains a forbidden control character$/, "versionControl", []],
|
||||
[/^installed_regex is not a valid regex: ([\s\S]+)$/, "installedRegexInvalid", ["detail"]],
|
||||
[/^upstream_type must be one of: ([\s\S]+)$/, "upstreamType", ["values"]],
|
||||
[/^tag_regex is not a valid regex: ([\s\S]+)$/, "tagRegexInvalid", ["detail"]],
|
||||
[/^repo is required for upstream_type=github$/, "repoRequired", []],
|
||||
[/^repo must be 'owner\/name'$/, "repoFormat", []],
|
||||
[/^github_source must be one of: ([\s\S]+)$/, "githubSource", ["values"]],
|
||||
[/^upstream_url is required for upstream_type=http_json$/, "upstreamUrlRequired", []],
|
||||
[/^upstream_url must be an http\(s\) URL$/, "upstreamUrl", []],
|
||||
[/^upstream_url exceeds (\d+) chars$/, "upstreamUrlLength", ["max"]],
|
||||
[/^upstream_json_path is required for upstream_type=http_json$/, "jsonPathRequired", []],
|
||||
[/^upstream_json_path exceeds (\d+) chars$/, "jsonPathLength", ["max"]],
|
||||
[/^upstream_json_path uses forbidden characters$/, "jsonPathCharacters", []],
|
||||
[/^docker_image is required for upstream_type=docker_hub$/, "dockerImageRequired", []],
|
||||
[/^docker_image exceeds (\d+) chars$/, "dockerImageLength", ["max"]],
|
||||
[/^docker_image must match Docker Hub naming \(owner\/name or name\)$/, "dockerImageFormat", []],
|
||||
[/^health_path must be a valid URL path \(max 255 chars\)$/, "healthPath", []],
|
||||
[/^logo_url must be an http\(s\) URL or this VM's cached local icon$/, "logoUrl", []],
|
||||
[/^invalid discovery_id$/, "discoveryId", []],
|
||||
[/^helper_slug must be a lowercase slug \(letters\/digits\/\._\-\)$/, "helperSlug", []],
|
||||
[/^update_via only accepts 'docker'$/, "updateVia", []],
|
||||
[/^update_via=docker requires a docker_label or docker_exec detector$/, "dockerDetector", []],
|
||||
[/^update_via=docker cannot be combined with ([\s\S]+)$/, "dockerConflict", ["field"]],
|
||||
[/^update_command must be a string$/, "updateCommandType", []],
|
||||
[/^update_command exceeds (\d+) chars$/, "updateCommandLength", ["max"]],
|
||||
[/^update_command contains a null byte$/, "updateCommandNull", []],
|
||||
[/^update_method must be none, helper or custom$/, "updateMethod", []],
|
||||
[/^update_command is required for update_method=custom$/, "updateCommandRequired", []],
|
||||
[/^update_command is only allowed for update_method=custom$/, "updateCommandOnlyCustom", []],
|
||||
[/^a supported helper_slug is required for update_method=helper$/, "helperRequired", []],
|
||||
[/^Docker\-delegated apps update through their image, not a separate app updater$/, "dockerUpdater", []],
|
||||
[/^command_argv is required \(non\-empty list\)$/, "commandRequired", []],
|
||||
[/^command_argv must be a non\-empty list of strings$/, "commandList", []],
|
||||
[/^command_argv accepts at most (\d+) entries$/, "commandCount", ["max"]],
|
||||
[/^command_argv\[(\d+)\] must be a non\-empty string$/, "commandEntry", ["index"]],
|
||||
[/^command_argv\[(\d+)\] exceeds (\d+) chars$/, "commandEntryLength", ["index", "max"]],
|
||||
[/^command_argv\[(\d+)\] contains a forbidden control character$/, "commandEntryControl", ["index"]],
|
||||
[/^binary_args must be a list of strings$/, "binaryArgsList", []],
|
||||
[/^binary_args accepts at most (\d+) entries$/, "binaryArgsCount", ["max"]],
|
||||
[/^binary_args\[(\d+)\] must be a non\-empty string$/, "binaryArgEntry", ["index"]],
|
||||
[/^binary_args\[(\d+)\] exceeds (\d+) chars$/, "binaryArgLength", ["index", "max"]],
|
||||
[/^binary_args\[(\d+)\] contains a forbidden control character$/, "binaryArgControl", ["index"]],
|
||||
[/^ports must be a list of \{port, description, scheme\}$/, "portsList", []],
|
||||
[/^ports\[(\d+)\] must be an object$/, "portObject", ["index"]],
|
||||
[/^ports\[(\d+)\]\.port is required$/, "portRequired", ["index"]],
|
||||
[/^ports\[(\d+)\]\.port must be an integer$/, "portInteger", ["index"]],
|
||||
[/^ports\[(\d+)\]\.port must be 1\-65535$/, "portRange", ["index"]],
|
||||
[/^port (\d+) appears more than once for this app$/, "portDuplicate", ["port"]],
|
||||
[/^ports\[(\d+)\]\.description has invalid characters$/, "portDescription", ["index"]],
|
||||
[/^ports\[(\d+)\]\.scheme must be 'http' or 'https'$/, "portScheme", ["index"]],
|
||||
[/^ports\[(\d+)\]\.web_path must be a valid URL path \(max 255 chars\)$/, "portPath", ["index"]],
|
||||
[/^ports\[(\d+)\]\.logo_url must be an http\(s\) URL \(max 512 chars\)$/, "portLogo", ["index"]],
|
||||
[/^ports\[(\d+)\]\.category has invalid characters or is too long$/, "portCategory", ["index"]],
|
||||
[/^ports\[(\d+)\]\.custom_url must be an http\(s\) URL \(max 512 chars\)$/, "portCustomUrl", ["index"]],
|
||||
[/^tag_regex exceeds 512 chars$/, "tagRegexLength", []],
|
||||
[/^could not persist sidecar \(permission\?\)$/, "persistPermission", []],
|
||||
[/^could not persist sidecar$/, "persist", []],
|
||||
[/^no apps registered for this vmid$/, "noApps", []],
|
||||
[/^app_id '([\s\S]+)' not found$/, "appMissing", ["appId"]],
|
||||
[/^app not found$/, "appNotFound", []],
|
||||
]
|
||||
|
||||
export function localizeAppEditorError(message: string | null | undefined, t: Translate): string {
|
||||
if (!message) return ""
|
||||
for (const [pattern, key, names] of RULES) {
|
||||
const match = pattern.exec(message)
|
||||
if (!match) continue
|
||||
const params = Object.fromEntries(names.map((name, index) => [name, match[index + 1]]))
|
||||
return t(`vmLxc.appEditor.errors.${key}`, params)
|
||||
}
|
||||
return message
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
type Translate = (key: string, params?: Record<string, string | number>) => string
|
||||
|
||||
const ERROR_KEYS: Record<string, string> = {
|
||||
monitor_link_address_required: "addressRequired",
|
||||
monitor_link_invalid_address: "invalidAddress",
|
||||
monitor_link_address_credentials: "addressCredentials",
|
||||
monitor_link_invalid_port: "invalidPort",
|
||||
monitor_link_invalid_request: "invalidRequest",
|
||||
monitor_link_duplicate: "duplicate",
|
||||
monitor_link_not_found: "notFound",
|
||||
monitor_link_detected_remove: "detectedRemove",
|
||||
}
|
||||
|
||||
// fetchApi preserves the structured response on Error.body. Never expose raw
|
||||
// server/network messages or treat arbitrary error codes as translation keys.
|
||||
export function monitorLinkError(error: unknown, t: Translate): string {
|
||||
const fallback = () => t("monitorLinks.saveFailed")
|
||||
if (!error || typeof error !== "object" || !("body" in error)) return fallback()
|
||||
const body = error.body
|
||||
if (!body || typeof body !== "object" || !("code" in body) || typeof body.code !== "string") return fallback()
|
||||
if (body.code === "monitor_link_limit") {
|
||||
const params = "params" in body ? body.params : null
|
||||
const count = params && typeof params === "object" && "count" in params ? params.count : null
|
||||
return typeof count === "number" && Number.isSafeInteger(count) && count > 0
|
||||
? t("monitorLinks.errors.limit", { count }) : fallback()
|
||||
}
|
||||
return Object.prototype.hasOwnProperty.call(ERROR_KEYS, body.code)
|
||||
? t(`monitorLinks.errors.${ERROR_KEYS[body.code]}`) : fallback()
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
type Translate = (key: string, params?: Record<string, string | number>) => string
|
||||
|
||||
const SOURCE_TOOLS = new Set([
|
||||
"subscription_banner", "time_sync", "apt_languages", "journald", "logrotate",
|
||||
"system_limits", "memory_settings", "zram_swap", "kernel_panic", "apt_ipv4",
|
||||
"kexec", "rpc", "motd", "system_utils", "network_optimization", "bashrc_custom",
|
||||
"figurine", "fastfetch", "log2ram", "zfs_autotrim", "amd_fixes", "persistent_network",
|
||||
"vfio_iommu", "lvm_repair", "repo_cleanup", "proxmox_repos", "entropy",
|
||||
])
|
||||
|
||||
export function optimizationSourceName(tool: string, fallback: string, t: Translate): string {
|
||||
return SOURCE_TOOLS.has(tool) ? t(`settings.optimizations.sourceToolNames.${tool}`) : fallback
|
||||
}
|
||||
|
||||
export interface OptimizationSourceError {
|
||||
kind: "unavailable" | "loadFailed"
|
||||
detail?: string
|
||||
}
|
||||
|
||||
export interface OptimizationSourceResponse {
|
||||
success?: unknown
|
||||
source?: unknown
|
||||
function?: unknown
|
||||
script?: unknown
|
||||
deprecated?: unknown
|
||||
error?: unknown
|
||||
}
|
||||
|
||||
// Keep the blueprint's English API contract intact; translate only its display.
|
||||
export function optimizationSourceError(error: OptimizationSourceError, t: Translate): string {
|
||||
const detail = error.detail || ""
|
||||
const key = "settings.optimizations."
|
||||
if (detail === "Function not found in available scripts") return t(key + "sourceFunctionMissing")
|
||||
if (detail.startsWith("Unknown tool: ")) return t(key + "sourceUnknownTool", { tool: detail.slice(14) })
|
||||
if (detail.startsWith("No function mapping for ")) return t(key + "sourceNoMapping", { tool: detail.slice(24) })
|
||||
if (detail === "Source code not available") return t(key + "sourceUnavailable")
|
||||
const message = t(key + (error.kind === "unavailable" ? "sourceUnavailable" : "sourceLoadFailed"))
|
||||
return detail ? `${message} ${detail}` : message
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
type Translate = (key: string, params?: Record<string, string | number>) => string
|
||||
|
||||
const ERROR_KEYS: Record<string, string> = {
|
||||
pbs_config_read: "pbs.errors.configRead", pbs_invalid_config: "pbs.errors.invalidConfig",
|
||||
pbs_invalid_field: "pbs.errors.invalidField", pbs_invalid_url: "pbs.errors.invalidUrl",
|
||||
pbs_name_required: "pbs.errors.nameRequired", pbs_token_id_format: "pbs.errors.tokenIdFormat",
|
||||
pbs_token_secret: "pbs.errors.tokenSecret", pbs_invalid_token_id: "pbs.errors.invalidTokenId",
|
||||
pbs_invalid_fingerprint: "pbs.errors.invalidFingerprint", pbs_fingerprint_mismatch: "pbs.errors.fingerprintMismatch",
|
||||
pbs_token_rejected: "pbs.errors.tokenRejected", pbs_invalid_response: "pbs.errors.invalidResponse",
|
||||
pbs_invalid_json: "pbs.errors.invalidJson", pbs_connect_failed: "pbs.errors.connectFailed",
|
||||
pbs_invalid_id: "pbs.errors.invalidId", pbs_invalid_datastores: "pbs.errors.invalidDatastores",
|
||||
pbs_username: "pbs.errors.username", pbs_password: "pbs.errors.password",
|
||||
pbs_login_rejected: "pbs.errors.loginRejected", pbs_invalid_login: "pbs.errors.invalidLogin",
|
||||
pbs_token_create_denied: "pbs.errors.tokenCreateDenied", pbs_invalid_token: "pbs.errors.invalidToken",
|
||||
pbs_audit_role_denied: "pbs.errors.auditRoleDenied", pbs_not_found: "pbs.errors.notFound",
|
||||
pbs_read_failed: "pbs.errors.readFailed", pbs_invalid_list: "pbs.errors.invalidList",
|
||||
pbs_local_unavailable: "pbs.errors.localUnavailable", pbs_auth_required: "pbs.errors.authRequired",
|
||||
pbs_save_failed: "pbs.saveFailed", pbs_remove_failed: "pbs.removeFailed",
|
||||
pbs_saved_refresh_failed: "pbs.savedRefreshFailed", pbs_removed_refresh_failed: "pbs.removedRefreshFailed",
|
||||
pbs_datastore_offline: "pbs.errors.datastoreOffline", pbs_datastore_unavailable: "pbs.errors.datastoreUnavailable",
|
||||
}
|
||||
|
||||
// Accept structured codes, never arbitrary error text as a translation key.
|
||||
function pbsErrorTranslation(error: unknown): { key: string; params?: { count: number } } | null {
|
||||
const body = error && typeof error === "object" && "body" in error ? error.body : error
|
||||
if (!body || typeof body !== "object" || !("code" in body) || typeof body.code !== "string") return null
|
||||
if (body.code === "pbs_connection_limit") {
|
||||
const params = "params" in body ? body.params : null
|
||||
const count = params && typeof params === "object" && "count" in params ? params.count : null
|
||||
return typeof count === "number" && Number.isSafeInteger(count) && count > 0
|
||||
? { key: "pbs.errors.connectionLimit", params: { count } } : null
|
||||
}
|
||||
return Object.prototype.hasOwnProperty.call(ERROR_KEYS, body.code) ? { key: ERROR_KEYS[body.code] } : null
|
||||
}
|
||||
|
||||
export function pbsIsKnownError(error: unknown): boolean {
|
||||
return pbsErrorTranslation(error) !== null
|
||||
}
|
||||
|
||||
export function pbsErrorText(error: unknown, t: Translate, fallback = "pbs.loadFailed"): string {
|
||||
const translated = pbsErrorTranslation(error)
|
||||
return translated ? t(translated.key, translated.params) : t(fallback)
|
||||
}
|
||||
|
||||
export function pbsDiagnostic(error: unknown): string | undefined {
|
||||
if (!error || typeof error !== "object") return undefined
|
||||
const body = "body" in error ? error.body : null
|
||||
if (body && typeof body === "object" && "error" in body && typeof body.error === "string") return body.error
|
||||
return "message" in error && typeof error.message === "string" ? error.message : undefined
|
||||
}
|
||||
|
||||
const TASK_TYPES: Record<string, string> = {
|
||||
backup: "backup", reader: "restore", verify: "verify", verificationjob: "verify",
|
||||
verify_group: "verifyGroup", verify_snapshot: "verifySnapshot", prune: "prune", prunejob: "prune",
|
||||
garbage_collection: "garbageCollection", sync: "sync", syncjob: "sync", aptupdate: "updateCheck",
|
||||
termproxy: "terminal", dircreate: "createDatastore", "create-datastore": "createDatastore",
|
||||
}
|
||||
export function pbsTaskType(type: string | undefined, t: Translate): string {
|
||||
return type && Object.prototype.hasOwnProperty.call(TASK_TYPES, type)
|
||||
? t(`pbs.taskTypes.${TASK_TYPES[type]}`) : t("pbs.taskTypes.other", { type: type || "—" })
|
||||
}
|
||||
|
||||
export function pbsResultText(status: string | undefined, t: Translate): string {
|
||||
if (!status || status === "unknown") return t("pbs.states.unknown")
|
||||
if (status.toLowerCase() === "ok") return t("pbs.states.ok")
|
||||
const warnings = /^WARNINGS:\s*(\d+)\s*$/i.exec(status)
|
||||
if (warnings && Number.isSafeInteger(Number(warnings[1]))) return t("pbs.states.warnings", { count: Number(warnings[1]) })
|
||||
return /^warn/i.test(status) ? t("pbs.states.warning") : t("pbs.states.error")
|
||||
}
|
||||
|
||||
export function pbsServiceState(state: string, t: Translate): string {
|
||||
const known = ["active", "inactive", "failed", "activating", "deactivating", "unknown"]
|
||||
return t(`pbs.serviceStates.${known.includes(state) ? state : "unknown"}`)
|
||||
}
|
||||
|
||||
export function formatPBSDuration(seconds: number, t: Translate): string {
|
||||
if (!Number.isFinite(seconds) || seconds < 0) return "—"
|
||||
const unit = (key: string, count: number) => t(`pbs.durationUnits.${key}`, { count })
|
||||
if (seconds < 60) return unit("seconds", Math.round(seconds))
|
||||
const minutes = Math.floor(seconds / 60)
|
||||
if (minutes < 60) return `${unit("minutes", minutes)} ${unit("seconds", Math.round(seconds % 60))}`
|
||||
const hours = Math.floor(minutes / 60)
|
||||
return hours < 24 ? `${unit("hours", hours)} ${unit("minutes", minutes % 60)}`
|
||||
: `${unit("days", Math.floor(hours / 24))} ${unit("hours", hours % 24)}`
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Native OCI `manage` reserves this code for explicit user cancellation.
|
||||
export const OCI_CANCELLED_EXIT_CODE = 6
|
||||
|
||||
export function scriptCompletion(exitCode: number, cancelledExitCode?: number) {
|
||||
if (exitCode === 0) return { kind: "success", color: "32" } as const
|
||||
if (cancelledExitCode !== undefined && exitCode === cancelledExitCode) {
|
||||
return { kind: "cancelled", color: "36" } as const
|
||||
}
|
||||
return { kind: "error", color: "31" } as const
|
||||
}
|
||||
|
||||
export function shouldRefreshOciAfterCompletion(exitCode?: number) {
|
||||
return exitCode !== OCI_CANCELLED_EXIT_CODE
|
||||
}
|
||||
|
||||
export function scriptCompletionOutput(exitCode: number, message: string, cancelledExitCode?: number) {
|
||||
const completion = scriptCompletion(exitCode, cancelledExitCode)
|
||||
// Queue RIS with the result so earlier ncurses writes finish before reset.
|
||||
// A synchronous terminal.reset() could be overwritten by that pending output.
|
||||
const reset = completion.kind === "cancelled" ? "\x1bc" : ""
|
||||
return `${reset}\x1b[${completion.color}m${message}\x1b[0m`
|
||||
}
|
||||
Reference in New Issue
Block a user