Hub stage 2: install, update and uninstall apps on several servers
Writes use only <ID>_ADMIN_TOKEN and need the apps.write capability. Store URLs are resolved from each server's own listing. The config is saved to history before an uninstall. Zip uploads are fanned out to the servers. Every target's result is audited. The console gets a row menu on the matrix, a target picker, a results dialog with Retry failed, the store and install from file in All servers mode, and single-server actions for writers. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
3d8df08fa0
commit
6acd2d656d
27 files changed
+1991
-71
No files matched your search
+50
-8
@@ -61,6 +61,8 @@ export interface StoreApp {
|
||||
installedVersion?: string
|
||||
/** Only when `installed` is true. */
|
||||
updateAvailable?: boolean
|
||||
/** Only behind a hub, in the All servers listing: the servers that have it installed. */
|
||||
installedOn?: string[]
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -88,28 +90,32 @@ export function listApps(
|
||||
|
||||
export function listStoreApps(
|
||||
token: string | null,
|
||||
node = '',
|
||||
): Promise<ApiOutcome<{ response: { storeApps: StoreApp[] } }>> {
|
||||
return apiRequest('apps/listStoreApps', { token })
|
||||
return apiRequest('apps/listStoreApps', { token, node })
|
||||
}
|
||||
|
||||
/* Behind a hub, `node` is the one server the call is for; elsewhere it stays empty and is not sent. */
|
||||
export function downloadAndInstall(
|
||||
token: string | null,
|
||||
name: string,
|
||||
url: string,
|
||||
node = '',
|
||||
): Promise<ApiOutcome<{ response: { installedApp: InstalledApp } }>> {
|
||||
return apiRequest('apps/downloadAndInstall', { token, body: { name, url } })
|
||||
return apiRequest('apps/downloadAndInstall', { token, body: { name, url }, node })
|
||||
}
|
||||
|
||||
export function downloadAndUpdate(
|
||||
token: string | null,
|
||||
name: string,
|
||||
url: string,
|
||||
node = '',
|
||||
): Promise<ApiOutcome<{ response: { updatedApp: InstalledApp } }>> {
|
||||
return apiRequest('apps/downloadAndUpdate', { token, body: { name, url } })
|
||||
return apiRequest('apps/downloadAndUpdate', { token, body: { name, url }, node })
|
||||
}
|
||||
|
||||
export function uninstallApp(token: string | null, name: string): Promise<ApiOutcome> {
|
||||
return apiRequest('apps/uninstall', { token, body: { name } })
|
||||
export function uninstallApp(token: string | null, name: string, node = ''): Promise<ApiOutcome> {
|
||||
return apiRequest('apps/uninstall', { token, body: { name }, node })
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -174,18 +180,20 @@ export function installApp(
|
||||
token: string | null,
|
||||
name: string,
|
||||
file: File,
|
||||
node = '',
|
||||
): Promise<ApiOutcome<{ response: { installedApp: InstalledApp } }>> {
|
||||
const { path, form } = buildUpload('apps/install', name, file)
|
||||
return apiRequest(path, { token, form })
|
||||
return apiRequest(path, { token, form, node })
|
||||
}
|
||||
|
||||
export function updateApp(
|
||||
token: string | null,
|
||||
name: string,
|
||||
file: File,
|
||||
node = '',
|
||||
): Promise<ApiOutcome<{ response: { updatedApp: InstalledApp } }>> {
|
||||
const { path, form } = buildUpload('apps/update', name, file)
|
||||
return apiRequest(path, { token, form })
|
||||
return apiRequest(path, { token, form, node })
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -212,10 +220,44 @@ export interface MatrixApp {
|
||||
}
|
||||
|
||||
export interface AppsMatrixData {
|
||||
servers: { id: string; ok: boolean; error?: string }[]
|
||||
servers: { id: string; ok: boolean; writable: boolean; error?: string }[]
|
||||
apps: MatrixApp[]
|
||||
}
|
||||
|
||||
export function getAppsMatrix(token: string | null): Promise<ApiOutcome<{ response: AppsMatrixData }>> {
|
||||
return apiRequest('hub/apps/matrix', { token })
|
||||
}
|
||||
|
||||
/*
|
||||
App writes across servers (hub/apps-write.ts). The hub resolves a store URL from each
|
||||
server's own store; the client never sends one.
|
||||
*/
|
||||
export type HubRunAction = 'install-store' | 'update-store' | 'uninstall'
|
||||
|
||||
export interface TargetResult {
|
||||
server: string
|
||||
ok: boolean
|
||||
message?: string
|
||||
}
|
||||
|
||||
export type RunOutcome = ApiOutcome<{ response: { results: TargetResult[] } }>
|
||||
|
||||
export function runHubApps(token: string | null, action: HubRunAction, name: string, targets: string[]): Promise<RunOutcome> {
|
||||
return apiRequest('hub/apps/run', { token, method: 'POST', body: { action, name, targets: targets.join(',') } })
|
||||
}
|
||||
|
||||
/* The same zip to several servers: multipart, as the hub's `hub/apps/upload` expects. */
|
||||
export function uploadHubApp(
|
||||
token: string | null,
|
||||
action: 'install' | 'update',
|
||||
name: string,
|
||||
file: File,
|
||||
targets: string[],
|
||||
): Promise<RunOutcome> {
|
||||
const form = new FormData()
|
||||
form.append('action', action)
|
||||
form.append('name', name)
|
||||
form.append('targets', targets.join(','))
|
||||
form.append('fileAppZip', file)
|
||||
return apiRequest('hub/apps/upload', { token, form })
|
||||
}
|
||||
Reference in new issue
Block a user