Docker: new stacks and containers, stack down/delete, container remove, image pull/remove/prune

A new stack is a folder in a home (where stacks are already kept: Dockhand's group here,
~/stacks elsewhere) with compose.yaml and an optional root-only .env, validated by compose
before it is kept and deployed the way its neighbours were — inside dockhand, or to a node
over tcp from the hub. New container is the same dialog on a form that writes the compose.
Homes are scanned, so undeployed stacks stay listed. Delete moves a stack's files to
.hamlab-trash; nothing is forced, and volumes and data are never touched.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-09-20 07:50:43 -07:00
co-authored by Claude Fable 5.1
parent df6c1dcebf
commit c017b422d5
8 changed files with 605 additions and 17 deletions
+9 -2
View File
@@ -104,6 +104,7 @@ export interface SmartInfo { device: string; available: boolean; supported: bool
export interface DockerContainer { id: string; name: string; image: string; state: string; status: string; health: string; ports: string; stack: string; service: string; created: string; mem: number }
export interface DockerInfo { available: boolean; message?: string; tool?: string; containers: DockerContainer[]; images: Record<string, unknown>[]; volumes: Record<string, unknown>[]; networks: Record<string, unknown>[] }
export interface Stack { name: string; containers: string[]; running: number; file: string; host_file: string; files: string[]; host_files: string[] | null; images: string[] | null; via: string; remote: string; held_by: string; editable: boolean; note?: string; has_env: boolean }
export interface StackHome { dir: string; inner: string; via: string; remote: string; held_by: string; stacks: number; default: boolean }
export interface ImageCheck { image: string; state: 'current' | 'update' | 'unknown'; detail?: string }
export interface StackDiff { name: string; base: '' | 'deployed' | 'saved'; old: string; new: string }
export interface AlertRule { id: string; name: string; sensor: string; op: string; value: number | boolean | string; for_sec: number; severity: 'info' | 'warn' | 'crit'; enabled: boolean; machines: string }
@@ -192,15 +193,21 @@ export const api = {
tokens: () => req<{ name: string; created: string; scope?: string; sensors?: string[] }[]>('GET', '/api/tokens', undefined, true),
createReadToken: (name: string, sensors: string[] = []) => req<{ token: string; name: string }>('POST', '/api/tokens/read', { name, sensors }, true),
docker: () => req<DockerInfo>('GET', '/api/docker'),
dockerAction: (id: string, action: 'start' | 'stop' | 'restart') => req<void>('POST', `/api/docker/containers/${encodeURIComponent(id)}/${action}`),
dockerAction: (id: string, action: 'start' | 'stop' | 'restart' | 'remove') => req<void>('POST', `/api/docker/containers/${encodeURIComponent(id)}/${action}`),
dockerLogs: (id: string, since = '') => req<{ lines: { t: string; m: string }[] }>('GET', `/api/docker/containers/${encodeURIComponent(id)}/logs${since ? `?since=${encodeURIComponent(since)}` : '?n=300'}`),
dockerInspect: (id: string) => req<Record<string, unknown>>('GET', `/api/docker/containers/${encodeURIComponent(id)}/inspect`),
stacks: () => req<{ stacks: Stack[] }>('GET', '/api/docker/stacks'),
stackHomes: () => req<{ homes: StackHome[] }>('GET', '/api/docker/stacks/homes'),
createStack: (p: { name: string; text: string; env: string; home: string }) => req<{ stack: Stack }>('POST', '/api/docker/stacks', p),
deleteStack: (name: string) => req<{ trash: string; moved: number }>('POST', `/api/docker/stacks/${encodeURIComponent(name)}/delete`, { confirm: name }),
pullImage: (image: string) => req<TaskInfo>('POST', '/api/docker/images/pull', { image }),
removeImage: (image: string) => req<void>('POST', '/api/docker/images/remove', { image }),
pruneImages: () => req<{ result: string }>('POST', '/api/docker/images/prune'),
stackDiff: (name: string) => req<{ files: StackDiff[] }>('GET', `/api/docker/stacks/${encodeURIComponent(name)}/diff`),
stackUpdates: (name: string, refresh = false) => req<{ images: ImageCheck[]; checked: number }>('GET', `/api/docker/stacks/${encodeURIComponent(name)}/updates${refresh ? '?refresh=1' : ''}`),
stackFile: (name: string, file: number | 'env' = 0) => req<{ stack: Stack; text: string; sum: string }>('GET', `/api/docker/stacks/${encodeURIComponent(name)}?file=${file}`),
saveStack: (name: string, file: number | 'env', text: string, sum: string) => req<void>('PUT', `/api/docker/stacks/${encodeURIComponent(name)}?file=${file}`, { text, sum }),
stackAction: (name: string, action: 'deploy' | 'update' | 'restart' | 'stop') => req<TaskInfo>('POST', `/api/docker/stacks/${encodeURIComponent(name)}/${action}`),
stackAction: (name: string, action: 'deploy' | 'update' | 'restart' | 'stop' | 'down') => req<TaskInfo>('POST', `/api/docker/stacks/${encodeURIComponent(name)}/${action}`),
alerts: () => req<AlertsInfo>('GET', '/api/alerts', undefined, true),
saveAlerts: (p: { rules?: AlertRule[]; channels?: AlertChannel[] }) => req<void>('PUT', '/api/alerts', p, true),
testAlerts: () => req<void>('POST', '/api/alerts/test', undefined, true),