diff --git a/eshtek/apps.ts b/eshtek/apps.ts index 045e105..2d74168 100644 --- a/eshtek/apps.ts +++ b/eshtek/apps.ts @@ -144,3 +144,9 @@ interface AppsInstallScriptV2 { } export type AppsInstallScript = AppsInstallScriptV1 | AppsInstallScriptV2; + +export interface InstallScriptCuration { + name: string; + url: string; + script: AppsInstallScript; +} diff --git a/eshtek/errors.ts b/eshtek/errors.ts index f6ebfc5..7f937c3 100644 --- a/eshtek/errors.ts +++ b/eshtek/errors.ts @@ -7,23 +7,28 @@ export enum GlobalErrorCode { GENERIC_SERVER_ERROR = 'GENERIC_SERVER_ERROR', GENERIC_USER_ERROR = 'GENERIC_USER_ERROR', GENERIC_WEBSOCKET_ERROR = 'GENERIC_WEBSOCKET_ERROR', - SERVER_NOT_CONNECTED = 'Server not currently connected', - INSTALL_APP_NO_APP_ID = 'App id not provided in request', - INSTALL_APP_NOT_SUPPORTED_AND_NO_SCRIPT = 'Supported app not found and no install script provided', - INSTALL_APP_NO_TRAIN_PROVIDED = 'Train is required if install script is provided', - INSTALL_APP_DOCKER_HUB_RATE_LIMIT = 'Docker Hub rate limit exceeded, wait a few hours for it to reset', - PROCESS_INSTALL_SCRIPT_FAILED_JSON_PARSE = 'Failed to JSON parse install script', - PROCESS_INSTALL_SCRIPT_INVALID_JSON = 'Install script must be a valid JSON object', - PROCESS_INSTALL_SCRIPT_MISSING_VERSION = 'Install script must have a "version" field', - PROCESS_INSTALL_SCRIPT_UNSUPPORTED_VERSION = 'Unsupported install script version', - PROCESS_INSTALL_SCRIPT_MISSING_APP_VALUES = 'Install script must have an "app_values" field', - PROCESS_INSTALL_SCRIPT_UNKNOWN_LOCATION = 'Install script has an unknown location', - PROCESS_INSTALL_SCRIPT_INVALID_RANDOM_STRING_LENGTH = 'Install script has an invalid random string length', - PROCESS_INSTALL_SCRIPT_FAILED_JSON_PARSE_AFTER = 'Failed to JSON parse install script after processing', - PROCESS_INSTALL_SCRIPT_INVALID_MEMORY_PERCENTAGE = 'Install script has an invalid memory percentage', - PROCESS_INSTALL_SCRIPT_INVALID_MEMORY_VALUE = 'Install script has an invalid memory value', - PROCESS_INSTALL_SCRIPT_MISSING_QUESTION_RESPONSE = 'Install script requires a response for a question', - PROCESS_INSTALL_SCRIPT_INVALID_QUESTION_RESPONSE_TYPE = 'Install script question response has invalid type', - RUN_INSTALL_SCRIPT_UNSUPPORTED_VERSION = 'Install script must have a "version" field', - DDP_CLIENT_DISCONNECTED = 'DDPClient disconnected from DDP server', + SERVER_NOT_CONNECTED = 'SERVER_NOT_CONNECTED', + SERVER_NOT_FOUND = 'SERVER_NOT_FOUND', + SERVER_UPDATE_ALREADY_IN_PROGRESS = 'SERVER_UPDATE_ALREADY_IN_PROGRESS', + SERVER_UPDATE_NOT_AVAILABLE = 'SERVER_UPDATE_NOT_AVAILABLE', + SERVER_UPDATE_NOT_SUPPORTED = 'SERVER_UPDATE_NOT_SUPPORTED', + EMAIL_ACTION_INVALID_KEY = 'EMAIL_ACTION_INVALID_KEY', + INSTALL_APP_NO_APP_ID = 'INSTALL_APP_NO_APP_ID', + INSTALL_APP_NOT_SUPPORTED_AND_NO_SCRIPT = 'INSTALL_APP_NOT_SUPPORTED_AND_NO_SCRIPT', + INSTALL_APP_NO_TRAIN_PROVIDED = 'INSTALL_APP_NO_TRAIN_PROVIDED', + INSTALL_APP_DOCKER_HUB_RATE_LIMIT = 'INSTALL_APP_DOCKER_HUB_RATE_LIMIT', + PROCESS_INSTALL_SCRIPT_FAILED_JSON_PARSE = 'PROCESS_INSTALL_SCRIPT_FAILED_JSON_PARSE', + PROCESS_INSTALL_SCRIPT_INVALID_JSON = 'PROCESS_INSTALL_SCRIPT_INVALID_JSON', + PROCESS_INSTALL_SCRIPT_MISSING_VERSION = 'PROCESS_INSTALL_SCRIPT_MISSING_VERSION', + PROCESS_INSTALL_SCRIPT_UNSUPPORTED_VERSION = 'PROCESS_INSTALL_SCRIPT_UNSUPPORTED_VERSION', + PROCESS_INSTALL_SCRIPT_MISSING_APP_VALUES = 'PROCESS_INSTALL_SCRIPT_MISSING_APP_VALUES', + PROCESS_INSTALL_SCRIPT_UNKNOWN_LOCATION = 'PROCESS_INSTALL_SCRIPT_UNKNOWN_LOCATION', + PROCESS_INSTALL_SCRIPT_INVALID_RANDOM_STRING_LENGTH = 'PROCESS_INSTALL_SCRIPT_INVALID_RANDOM_STRING_LENGTH', + PROCESS_INSTALL_SCRIPT_FAILED_JSON_PARSE_AFTER = 'PROCESS_INSTALL_SCRIPT_FAILED_JSON_PARSE_AFTER', + PROCESS_INSTALL_SCRIPT_INVALID_MEMORY_PERCENTAGE = 'PROCESS_INSTALL_SCRIPT_INVALID_MEMORY_PERCENTAGE', + PROCESS_INSTALL_SCRIPT_INVALID_MEMORY_VALUE = 'PROCESS_INSTALL_SCRIPT_INVALID_MEMORY_VALUE', + PROCESS_INSTALL_SCRIPT_MISSING_QUESTION_RESPONSE = 'PROCESS_INSTALL_SCRIPT_MISSING_QUESTION_RESPONSE', + PROCESS_INSTALL_SCRIPT_INVALID_QUESTION_RESPONSE_TYPE = 'PROCESS_INSTALL_SCRIPT_INVALID_QUESTION_RESPONSE_TYPE', + RUN_INSTALL_SCRIPT_UNSUPPORTED_VERSION = 'RUN_INSTALL_SCRIPT_UNSUPPORTED_VERSION', + DDP_CLIENT_DISCONNECTED = 'DDP_CLIENT_DISCONNECTED', } diff --git a/eshtek/server-schema.ts b/eshtek/server-schema.ts index c8191d1..cd52456 100644 --- a/eshtek/server-schema.ts +++ b/eshtek/server-schema.ts @@ -197,6 +197,14 @@ export const serverHealthSchema = z.object({ actions_available: z.array(serverActionsSchema), }); +export const serverUpgradeInfoSchema = z.object({ + serverName: z.string(), + hostId: z.string(), + currentVersion: z.string(), + targetVersion: z.string(), + updateStatus: z.string(), +}); + const diskTypeSchema = z.any(); const poolStatusSchema = z.any(); diff --git a/eshtek/server.ts b/eshtek/server.ts index 3931ae8..88aafe3 100644 --- a/eshtek/server.ts +++ b/eshtek/server.ts @@ -407,3 +407,11 @@ export interface ServerHealth { warnings: ServerHealthWarning[]; actions_available: ServerActions[]; } + +export interface ServerUpgradeInfo { + serverName: string; + hostId: string; + currentVersion: string; + targetVersion: string; + updateStatus: string; +} diff --git a/eshtek/versions.ts b/eshtek/versions.ts new file mode 100644 index 0000000..1897bf2 --- /dev/null +++ b/eshtek/versions.ts @@ -0,0 +1,87 @@ +export interface VersionParts { + year: number; + month: number; + build: number; + patch: number; +} + +export const parseVersion = (version: string): VersionParts => { + const cleanVersion = version.split('-')[0]; + const parts = cleanVersion.split('.').map(p => { + const num = Number(p); + return isNaN(num) ? 0 : num; + }); + return { + year: parts[0] || 0, + month: parts[1] || 0, + build: parts[2] || 0, + patch: parts[3] || 0, + }; +}; + +export const compareVersions = (a: VersionParts, b: VersionParts): number => { + if (a.year !== b.year) return a.year - b.year; + if (a.month !== b.month) return a.month - b.month; + if (a.build !== b.build) return a.build - b.build; + return a.patch - b.patch; +}; + +export const parseRange = (range: string): { min?: VersionParts; max?: VersionParts; minInclusive: boolean; maxInclusive: boolean } => { + const parts = range.split(/\s+/); + let min: VersionParts | undefined; + let max: VersionParts | undefined; + let minInclusive = false; + let maxInclusive = false; + + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + if (part.startsWith('>=')) { + min = parseVersion(part.substring(2)); + minInclusive = true; + } else if (part.startsWith('>')) { + min = parseVersion(part.substring(1)); + minInclusive = false; + } else if (part.startsWith('<=')) { + max = parseVersion(part.substring(2)); + maxInclusive = true; + } else if (part.startsWith('<')) { + max = parseVersion(part.substring(1)); + maxInclusive = false; + } + } + + return { min, max, minInclusive, maxInclusive }; +}; + +export const isTrueNASVersionInRange = (buildVersion: string, versionRange: string): boolean => { + if (!buildVersion) { + return false; + } + + try { + const version = parseVersion(buildVersion); + const { min, max, minInclusive, maxInclusive } = parseRange(versionRange); + + if (min) { + const cmp = compareVersions(version, min); + if (minInclusive) { + if (cmp < 0) return false; + } else { + if (cmp <= 0) return false; + } + } + + if (max) { + const cmp = compareVersions(version, max); + if (maxInclusive) { + if (cmp > 0) return false; + } else { + if (cmp >= 0) return false; + } + } + + return true; + } catch (error) { + return false; + } +}; diff --git a/package-lock.json b/package-lock.json index e1c57f8..f50155e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "shared", + "name": "hexos-shared", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/truenas/webui/interfaces/api/api-call-directory.interface.ts b/truenas/webui/interfaces/api/api-call-directory.interface.ts index ab3cb11..1f4bf14 100644 --- a/truenas/webui/interfaces/api/api-call-directory.interface.ts +++ b/truenas/webui/interfaces/api/api-call-directory.interface.ts @@ -881,6 +881,17 @@ export interface ApiCallDirectory { params: [ServiceName, { silent: boolean }]; response: boolean; // False indicates that service has been stopped. }; + 'service.reload': { + params: [ + service: string, + options?: { + ha_propagate?: boolean; + silent?: boolean; + timeout?: number | null; + } + ]; + response: boolean; + }; 'service.update': { params: [number | ServiceName, Partial]; response: number }; // Sharing @@ -902,7 +913,20 @@ export interface ApiCallDirectory { response: null | { reason: string }; }; 'sharing.smb.update': { params: [id: number, update: SmbShareUpdate]; response: SmbShare }; - 'sharing.smb.sync_registry': { params: []} + 'sharing.smb.sync_registry': { params: []; response: void }; + + // Etc + 'etc.generate': { + params: [ + name: string, + checkpoint?: 'initial' | 'interface_sync' | 'post_init' | 'pool_import' | 'pre_interface_sync' | null + ]; + response: Array<{ + path: string; + status: 'CHANGED' | 'REMOVED'; + changes: string[]; + }>; + }; // SMART 'smart.config': { params: void; response: SmartConfig };