diff --git a/PLAN.md b/PLAN.md index 3171e81..0e0fab5 100644 --- a/PLAN.md +++ b/PLAN.md @@ -184,6 +184,14 @@ something long in one, switch away and back, refresh — it's all still there. several stacks run through a queue one at a time rather than recreating half the machine at once, each with its task output on screen. A row that succeeds says "updated just now" instead of re-checking, since a check is a registry request. +- **Recreate, and the Updates tab at full width** (2026-09-26): a stack's actions gained + **Recreate** (`up -d --force-recreate`), the only way out of a container Docker refuses to + start — one pinned to a network that has since been recreated answers "network … not + found" to Deploy and Update alike, because as far as compose is concerned nothing about it + changed. Found the hard way when a SiYuan update landed in exactly that state. The Updates + tab also stopped rendering as a list-and-detail pair: it is a page, not a list, so on a + wide window it now sits under the tab strip at full width instead of being squeezed into + the right-hand half beside an empty column. - **Cron in English** (`crontext.ts`, 2026-09-26): schedules read "daily at 3:15am" rather than `15 3 * * *`, with the expression kept beside it. Handles lists, ranges, steps, day and month names, `@daily` and friends, weekdays/weekends, midnight and noon — and declines diff --git a/internal/api/stacks.go b/internal/api/stacks.go index d2ee5ea..ec74085 100644 --- a/internal/api/stacks.go +++ b/internal/api/stacks.go @@ -825,6 +825,12 @@ func (s *Server) stackAction(w http.ResponseWriter, r *http.Request) { script = base("up -d --remove-orphans") + remember case "update": script = base("pull") + " && " + base("up -d --remove-orphans") + remember + case "recreate": + // The way out of a container Docker will no longer start — most often one pinned to a + // network that has since been recreated, which answers "network … not found" to every + // ordinary deploy. `up -d` leaves such a container alone because its definition has + // not changed; only --force-recreate builds it again. + script = base("up -d --force-recreate --remove-orphans") + remember case "restart": script = base("restart") case "stop": @@ -832,7 +838,7 @@ func (s *Server) stackAction(w http.ResponseWriter, r *http.Request) { case "down": // remove its containers and networks; its files, volumes and bind-mounted data stay script = base("down --remove-orphans") default: - jsonErr(w, 400, "deploy, update, restart, stop or down") + jsonErr(w, 400, "deploy, update, recreate, restart, stop or down") return } if t, busy := s.tasks.RunningOf("stack:" + st.Name); busy { diff --git a/web/src/Docker.tsx b/web/src/Docker.tsx index ee002ad..6841eff 100644 --- a/web/src/Docker.tsx +++ b/web/src/Docker.tsx @@ -347,7 +347,7 @@ function StackDetail({ name, startTask, onBack, onChanged, onDuplicate }: { name try { const r = await api.deleteStack(name); toast(`${name} deleted — its files are in ${r.trash}`); setUnsaved(''); onChanged(); onBack() } catch (e) { toast(e instanceof Error ? e.message : 'failed', 'err') } } const duplicate = async () => { if (!st) return; try { onDuplicate({ name, text: file === 0 ? orig : (await api.stackFile(name, 0)).text, hasEnv: !!st.has_env, files: st.host_files?.length ?? 1 }) } catch (e) { toast(e instanceof Error ? e.message : 'failed', 'err') } } - const run = (a: 'deploy' | 'update' | 'restart' | 'stop' | 'down') => async () => { try { setTask((await api.stackAction(name, a)).id) } catch (e) { toast(e instanceof Error ? e.message : 'failed', 'err') } } + const run = (a: 'deploy' | 'update' | 'recreate' | 'restart' | 'stop' | 'down') => async () => { try { setTask((await api.stackAction(name, a)).id) } catch (e) { toast(e instanceof Error ? e.message : 'failed', 'err') } } if (err && !st) return