A Recreate action, and the Updates tab uses the whole window
Recreate is `up -d --force-recreate`. It exists because Deploy and Update both leave a container alone when its definition has not changed, which is precisely the state a container is in when Docker will not start it — pinned to a network that has since been recreated, answering "network … not found" to everything. siyuan landed there today and there was no way out from the console. The Updates tab is a page, not a list with a detail pane, so on a wide window it was being squeezed into the right-hand half with an empty list column taking the left — which read as a phone layout on a desktop. It now sits under the tab strip at full width. The list-and-detail tabs are unchanged. The rule needed :not(.has-sel) to outrank the wide-window rule that grows the list to fill the row. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
94800b8207
commit
4e3c817b2a
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+3
-2
@@ -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 <div className="svc-detail"><CloseBtn onClick={onBack} /><button className="btn back" onClick={onBack}>‹ Back</button><div className="notice">{err}</div></div>
|
||||
if (!st) return <div className="svc-detail"><Loading label="Reading the compose file…" /></div>
|
||||
return (
|
||||
@@ -366,6 +366,7 @@ function StackDetail({ name, startTask, onBack, onChanged, onDuplicate }: { name
|
||||
<Btn className="btn" disabled={dirty} title={dirty ? 'save first' : 'recreate what changed: docker compose up -d'} confirm={`Deploy ${st.name}? Containers whose definition changed are recreated (a few seconds down each).`} onClick={run('deploy')}>Deploy</Btn>
|
||||
{st.containers.length > 0 && <Btn className="btn" disabled={dirty} title="pull newer images, then deploy" confirm={`Pull newer images for ${st.name} and redeploy? Anything with a new image is recreated.`} onClick={run('update')}>Update</Btn>}
|
||||
{st.containers.length > 0 && <Btn className="btn" confirm={`Restart every container in ${st.name}?`} onClick={run('restart')}>Restart</Btn>}
|
||||
{st.containers.length > 0 && <Btn className="btn" disabled={dirty} title="build the containers again from scratch: docker compose up -d --force-recreate" confirm={`Recreate every container in ${st.name}?\n\nThis is the way out of a container Docker refuses to start — one pinned to a network that no longer exists, say, which answers "network … not found" to an ordinary Deploy. Nothing in a volume or a bind mount is touched.`} onClick={run('recreate')}>Recreate</Btn>}
|
||||
{st.running > 0 && <Btn className="btn danger" confirm={`Stop every container in ${st.name}? They stay stopped until deployed or started again.`} onClick={run('stop')}>Stop</Btn>}
|
||||
</div>
|
||||
<div className="svc-actions">
|
||||
@@ -425,7 +426,7 @@ export default function Docker() {
|
||||
const sid = (c: DockerContainer, k: string) => sensors[`container.${c.name.toLowerCase().replace(/[^a-z0-9-]+/g, '_').replace(/^_|_$/g, '')}.${k}`]
|
||||
const imageCols: [string, string][] = [['Repository', 'Repository'], ['Tag', 'Tag'], ['Size', 'Size'], ['CreatedSince', 'Created']]
|
||||
return (
|
||||
<div className={'svc docker' + (((cur || sel) && shownTab === 'containers') || selStack ? ' has-sel' : '')}>
|
||||
<div className={'svc docker' + (((cur || sel) && shownTab === 'containers') || selStack ? ' has-sel' : '') + (shownTab === 'updates' ? ' wide' : '')}>
|
||||
<div className="svc-list">
|
||||
<div className="svc-tools">
|
||||
<div className="chips">{(['containers', 'stacks', 'updates', 'images', 'volumes', 'networks'] as Tab[]).map((t) => <button key={t} className={'chip' + (shownTab === t ? ' on' : '')} onClick={() => { setTab(t); if (sel) setSel('') }}>{t}{t === 'containers' ? ` · ${up}/${d.containers.length}` : t === 'stacks' ? ` · ${stacks?.length ?? '…'}` : t === 'updates' ? (behind > 0 ? ` · ${behind}` : '') : ` · ${d[t].length}`}</button>)}</div>
|
||||
|
||||
+1
-1
@@ -306,7 +306,7 @@ export const api = {
|
||||
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' | 'down') => req<TaskInfo>('POST', `/api/docker/stacks/${encodeURIComponent(name)}/${action}`),
|
||||
stackAction: (name: string, action: 'deploy' | 'update' | 'recreate' | '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),
|
||||
|
||||
@@ -167,6 +167,14 @@ button { font: inherit; color: inherit; background: none; border: 0; cursor: poi
|
||||
.svc:not(.has-sel) .svc-list, .cron:not(.has-sel) .cron-list { width: auto; flex: 1 1 auto; border-right: 0; }
|
||||
.svc:not(.has-sel) .svc-empty, .cron:not(.has-sel) .svc-empty { display: none; }
|
||||
}
|
||||
/* A tab that is a page rather than a list-and-detail — Docker's Updates — stacks: the tab
|
||||
strip on top, the page below at full width. Otherwise the empty list column takes half
|
||||
the window and the page is squeezed into the other half, which looks like a phone. */
|
||||
.svc.wide { flex-direction: column; }
|
||||
/* :not(.has-sel) to outrank the wide-window rule above, which grows the list to fill */
|
||||
.svc.wide:not(.has-sel) .svc-list { width: auto; flex: 0 0 auto; border-right: 0; border-bottom: 1px solid var(--border); }
|
||||
.svc.wide .svc-rows { display: none; }
|
||||
.svc.wide > .svc-detail { flex: 1 1 auto; min-height: 0; }
|
||||
.svc-head { display: flex; align-items: center; gap: 10px; } .svc-head h2 { margin: 0; font-size: 18px; font-weight: 600; word-break: break-all; }
|
||||
.svc-actions { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.props { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 4px 16px; font-size: 13px; }
|
||||
|
||||
Reference in New Issue
Block a user