0.36.0: per-stack image-update indicator on the Stacks overview

Shows an amber "Update" pill next to a stack's status (and highlights the
inline Update button) when any of the stack's images has a newer digest in
the registry. Reuses the existing background image-update check — a new
update_service.stacks_update_summary() reads the cached digests in a single
container sweep (no extra registry calls), exposed as GET /api/stacks/updates
and proxied per agent at GET /api/agents/{id}/stacks/updates. The Stacks page
and each remote-host section poll it every 60s.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
menzelj
2026-06-16 18:52:51 +00:00
co-authored by Claude Opus 4.8
parent a4b1bbcdd1
commit cd15cdc75e
12 changed files with 115 additions and 9 deletions
@@ -33,6 +33,12 @@ export function AgentStacksSection({ agent, isAdmin }: { agent: Agent; isAdmin:
enabled: online,
refetchInterval: 30000,
});
const updates = useQuery({
queryKey: ["agent-stack-updates", agent.id],
queryFn: () => agentsApi.stackUpdates(agent.id),
enabled: online,
refetchInterval: 60000,
});
const run = async (action: string, label: string, id: string) => {
setBusyId(id);
@@ -72,6 +78,7 @@ export function AgentStacksSection({ agent, isAdmin }: { agent: Agent; isAdmin:
<StacksTable
stacks={stacks.data}
stats={stats.data}
updates={updates.data}
hostCpus={sys.data?.cpu_cores ?? 0}
hostMem={sys.data?.mem_total ?? 0}
isAdmin={isAdmin}
+31 -4
View File
@@ -8,13 +8,14 @@ import { ConfirmDialog } from "@/components/ui/ConfirmDialog";
import { formatBytes } from "@/lib/utils";
import { stacksApi } from "@/api/stacks";
import { apiErrorMessage } from "@/api/client";
import type { StackStats, StackSummary } from "@/types";
import type { StackStats, StackSummary, StackUpdateInfo } from "@/types";
/** Stack list rendered as a table with live CPU/memory usage meters and inline
* start/stop/restart, shared by the Dashboard and the Stacks page. */
export function StacksTable({
stacks,
stats,
updates,
hostCpus,
hostMem,
isAdmin,
@@ -31,6 +32,7 @@ export function StacksTable({
}: {
stacks: StackSummary[] | undefined;
stats: Record<string, StackStats> | undefined;
updates?: Record<string, StackUpdateInfo>;
hostCpus: number;
hostMem: number;
isAdmin: boolean;
@@ -70,6 +72,7 @@ export function StacksTable({
key={s.id}
stack={s}
stats={stats?.[s.id]}
update={updates?.[s.id]}
hostCpus={hostCpus}
hostMem={hostMem}
isAdmin={isAdmin}
@@ -92,6 +95,7 @@ export function StacksTable({
function StackRow({
stack,
stats,
update,
hostCpus,
hostMem,
isAdmin,
@@ -106,6 +110,7 @@ function StackRow({
}: {
stack: StackSummary;
stats?: StackStats;
update?: StackUpdateInfo;
hostCpus: number;
hostMem: number;
isAdmin: boolean;
@@ -120,6 +125,7 @@ function StackRow({
}) {
const qc = useQueryClient();
const running = stack.running_count > 0;
const updateAvailable = update?.update_available ?? false;
const canDelete = showDelete && !stack.agent_id;
const [confirming, setConfirming] = useState(false);
const [deleting, setDeleting] = useState(false);
@@ -149,6 +155,18 @@ function StackRow({
<span className="text-xs text-slate-400">
{stack.running_count}/{stack.service_count} svc
</span>
{updateAvailable && (
<span
title={
update?.stale_images?.length
? `Image update available:\n${update.stale_images.join("\n")}`
: "An image update is available for this stack"
}
className="inline-flex items-center gap-1 rounded-pill border border-amber-500/40 bg-amber-500/10 px-2 py-0.5 text-[11px] font-semibold text-amber-600 dark:text-amber-400"
>
<ArrowUpCircle className="h-3 w-3" /> Update
</span>
)}
</Link>
</td>
<td className="px-4 py-2.5">
@@ -201,13 +219,22 @@ function StackRow({
</IconBtn>
)}
{onUpdate && (
<IconBtn
title="Update (pull latest images & recreate)"
<button
title={
updateAvailable
? "Image update available — pull latest images & recreate"
: "Update (pull latest images & recreate)"
}
onClick={() => onUpdate(stack.id)}
disabled={busy}
className={
updateAvailable
? "rounded-lg p-1.5 text-amber-600 hover:bg-amber-100 disabled:opacity-40 dark:text-amber-400 dark:hover:bg-amber-500/20"
: "rounded-lg p-1.5 text-slate-500 hover:bg-slate-100 disabled:opacity-40 dark:hover:bg-slate-700"
}
>
<ArrowUpCircle className="h-4 w-4" />
</IconBtn>
</button>
)}
{showEdit && (
<Link