stacks page: env-var counts from sources, icon batching, and an in-order restart
This commit is contained in:
9 files changed
+229
-80
No files matched your search
@@ -3,6 +3,7 @@
|
||||
import { Box } from 'lucide-svelte';
|
||||
import { appSettings } from '$lib/stores/settings';
|
||||
import { selfhstMatcher, ensureSelfhstMatcher } from '$lib/stores/selfhst-refs';
|
||||
import { selfhstIcons, requestSelfhst } from '$lib/stores/selfhst-icons';
|
||||
import { isSelfhstIcon, selfhstRef, isCustomIcon, getStackIconComponent } from '$lib/utils/icons';
|
||||
|
||||
interface Props {
|
||||
@@ -84,33 +85,30 @@
|
||||
|
||||
// Reactive match: recomputes when the matcher store loads or the image changes.
|
||||
const ref = $derived(enabled ? $selfhstMatcher(image, name) : null);
|
||||
// If the <img> can't load at all (a transport error - the endpoint serves a neutral
|
||||
// placeholder SVG rather than a 404 for an unresolved icon), fall back to the glyph.
|
||||
let imgFailed = $state(false);
|
||||
$effect(() => {
|
||||
// reset the failure flag when the resolved ref changes
|
||||
void ref;
|
||||
imgFailed = false;
|
||||
});
|
||||
|
||||
// Show the fallback glyph when the toggle is on (no logo matched) or a caller that
|
||||
// always had an icon asked for it while off - but never on dense inline spots.
|
||||
const showFallback = $derived(!hideWhenNoMatch && (enabled || showFallbackWhenOff));
|
||||
|
||||
// Resolve selfh.st refs (override OR auto-matched) through the shared batch store so a
|
||||
// list of containers makes ONE icon request, not one per row. `resolved`: data URI when
|
||||
// ready, '' if unresolvable, undefined while the batch is in flight.
|
||||
const activeSelfhst = $derived(hasOverride ? overrideSelfhst : enabled ? ref : null);
|
||||
$effect(() => {
|
||||
if (activeSelfhst) requestSelfhst(activeSelfhst);
|
||||
});
|
||||
const resolvedSelfhst = $derived(activeSelfhst ? $selfhstIcons[activeSelfhst] : undefined);
|
||||
</script>
|
||||
|
||||
{#if hasOverride && overrideSelfhst}
|
||||
<img src="/api/icons/selfhst/{overrideSelfhst}" alt="" class="{className} object-contain shrink-0" />
|
||||
{#if activeSelfhst && resolvedSelfhst}
|
||||
<img src={resolvedSelfhst} alt="" class="{className} object-contain shrink-0" />
|
||||
{:else if activeSelfhst && resolvedSelfhst === undefined}
|
||||
<!-- batch in flight: hold the space so the row doesn't jump -->
|
||||
<span class="{className} shrink-0"></span>
|
||||
{:else if hasOverride && overrideCustom && overrideCustomUrl}
|
||||
<img src={overrideCustomUrl} alt="" class="{className} object-contain shrink-0" />
|
||||
{:else if hasOverride && OverrideLucide}
|
||||
<OverrideLucide class="{className} shrink-0" />
|
||||
{:else if enabled && ref && !imgFailed}
|
||||
<img
|
||||
src="/api/icons/selfhst/{ref}"
|
||||
alt=""
|
||||
class="{className} object-contain shrink-0"
|
||||
onerror={() => (imgFailed = true)}
|
||||
/>
|
||||
{:else if showFallback}
|
||||
<!-- Generic icon so the layout stays consistent even with no match. -->
|
||||
<FallbackIcon class="{className} shrink-0 {fallbackClass}" />
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Box } from 'lucide-svelte';
|
||||
import { getStackIconComponent, isSelfhstIcon, selfhstRef, isCustomIcon } from '$lib/utils/icons';
|
||||
import { selfhstIcons, requestSelfhst } from '$lib/stores/selfhst-icons';
|
||||
|
||||
interface Props {
|
||||
/** Icon value: a lucide name, 'selfhst:<ref>', 'custom:<file>', or null/empty. */
|
||||
@@ -23,12 +25,26 @@
|
||||
const custom = $derived(isCustomIcon(icon));
|
||||
// Lucide is the fallback for any non-selfhst, non-custom value (incl. null).
|
||||
const LucideIcon = $derived(!isSelfhstIcon(icon) && !custom ? getStackIconComponent(icon || '') : null);
|
||||
|
||||
// Resolve the selfh.st icon via the shared batch store (one request for the whole
|
||||
// list) instead of a per-icon <img src>. `resolved`: data URI when ready, '' when it
|
||||
// could not resolve, undefined while the batch is still in flight.
|
||||
$effect(() => {
|
||||
if (selfhst) requestSelfhst(selfhst);
|
||||
});
|
||||
const resolved = $derived(selfhst ? $selfhstIcons[selfhst] : undefined);
|
||||
</script>
|
||||
|
||||
{#if selfhst}
|
||||
<img src="/api/icons/selfhst/{selfhst}" alt="" class="{className} object-contain shrink-0" />
|
||||
{#if selfhst && resolved}
|
||||
<img src={resolved} alt="" class="{className} object-contain shrink-0" />
|
||||
{:else if selfhst && resolved === undefined}
|
||||
<!-- batch in flight: keep space so the row doesn't jump -->
|
||||
<span class="{className} shrink-0"></span>
|
||||
{:else if custom && customUrl}
|
||||
<img src={customUrl} alt="" class="{className} object-contain shrink-0" />
|
||||
{:else if LucideIcon}
|
||||
<LucideIcon class={className} />
|
||||
{:else if selfhst}
|
||||
<!-- a selfh.st ref that could not resolve (resolved === '') - show a generic box -->
|
||||
<Box class="{className} shrink-0" />
|
||||
{/if}
|
||||
@@ -1,4 +1,16 @@
|
||||
[
|
||||
{
|
||||
"version": "1.0.46",
|
||||
"date": "",
|
||||
"comingSoon": true,
|
||||
"changes": [
|
||||
{ "type": "feature", "text": "restart a stack in depends_on order (#1480)" },
|
||||
{ "type": "fix", "text": "cache app icons so the containers and stacks pages load faster" },
|
||||
{ "type": "fix", "text": "upgrade svelte to 5.56.10 (#1476, PR#1477, @ThanatosDi)" },
|
||||
{ "type": "fix", "text": "refresh the bundled docker-compose (5.5.0-r2)" }
|
||||
],
|
||||
"imageTag": "fnsys/dockhand:v1.0.46"
|
||||
},
|
||||
{
|
||||
"version": "1.0.45",
|
||||
"date": "2026-08-27",
|
||||
|
||||
@@ -25831,7 +25831,7 @@
|
||||
"tags": [
|
||||
"stacks"
|
||||
],
|
||||
"summary": "Restart a stack (mode=restart) or recreate its containers (mode=recreate); progress and the final result stream over Server-Sent Events",
|
||||
"summary": "Restart a stack (mode=restart|ordered|recreate); progress and the final result stream over Server-Sent Events",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "name",
|
||||
@@ -25858,7 +25858,7 @@
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Restart mode — \"recreate\" recreates containers, anything else performs a plain restart"
|
||||
"description": "Restart mode — \"recreate\" recreates containers (new IDs, re-pull), \"ordered\" does a stop+start honoring depends_on ordering (same IDs), anything else is a plain in-place restart"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -26631,7 +26631,7 @@
|
||||
"tags": [
|
||||
"stacks"
|
||||
],
|
||||
"summary": "List stack source records (their stored compose/env paths and source type)",
|
||||
"summary": "List stack source records (their stored compose/env paths and source type, plus an env-var count)",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "env",
|
||||
|
||||
@@ -490,6 +490,55 @@ export async function findStackDir(stackName: string, envId?: number | null): Pr
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Count the env vars GET /api/stacks/[name]/env would return for a stack, without
|
||||
* reading values - just for the list badge. Mirrors that endpoint's build EXACTLY
|
||||
* (same env param, same source lookup, git = all DB rows, internal = .env keys +
|
||||
* DB secret rows) so the badge count equals what the env editor shows. Returns 0 on
|
||||
* any error (a missing badge is harmless). */
|
||||
export async function countStackEnvVars(stackName: string, envId?: number | null): Promise<number> {
|
||||
try {
|
||||
// Same three lookups GET /env does, with the same env param.
|
||||
const dbVars = await getStackEnvVars(stackName, envId, true);
|
||||
const src = await getStackSource(stackName, envId);
|
||||
|
||||
if (src?.sourceType === 'git') {
|
||||
// Git stacks: ALL vars (overrides + secrets) come from the DB.
|
||||
return dbVars.length;
|
||||
}
|
||||
|
||||
// Internal/adopted: non-secrets from the .env file + secrets from the DB.
|
||||
let count = dbVars.filter((v) => v.isSecret).length;
|
||||
|
||||
let envFilePath: string | null = null;
|
||||
if (src?.envPath === '') envFilePath = null;
|
||||
else if (src?.envPath) envFilePath = src.envPath;
|
||||
else if (src?.composePath) envFilePath = join(dirname(src.composePath), '.env');
|
||||
else {
|
||||
const stackDir = await findStackDir(stackName, envId);
|
||||
if (stackDir) envFilePath = join(stackDir, '.env');
|
||||
}
|
||||
if (envFilePath && existsSync(envFilePath)) {
|
||||
try {
|
||||
// Same parse GET /env uses (key=value lines, skip blanks/comments). Inlined
|
||||
// to count keys without the verbose git-env parser's per-stack logging.
|
||||
const keys = new Set<string>();
|
||||
for (const line of readFileSync(envFilePath, 'utf-8').split('\n')) {
|
||||
const t = line.trim();
|
||||
if (!t || t.startsWith('#')) continue;
|
||||
const eq = t.indexOf('=');
|
||||
if (eq > 0) keys.add(t.slice(0, eq).trim());
|
||||
}
|
||||
count += keys.size;
|
||||
} catch {
|
||||
// ignore file read errors, mirror GET /env
|
||||
}
|
||||
}
|
||||
return count;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// COMPOSE FILE MANAGEMENT
|
||||
// =============================================================================
|
||||
@@ -2362,19 +2411,22 @@ export async function stopStack(
|
||||
}
|
||||
|
||||
/**
|
||||
* Restart a stack using docker compose restart or stop+up (recreate mode).
|
||||
* Restart a stack using docker compose restart, stop+start (ordered), or stop+up (recreate).
|
||||
*
|
||||
* mode='restart' (default): Uses 'docker compose restart' — fast, in-place restart
|
||||
* that preserves container IDs but won't fix stale network_mode references.
|
||||
* that preserves container IDs but does NOT honor depends_on startup ordering.
|
||||
* mode='ordered': Uses 'docker compose stop' then 'docker compose start' — an in-place
|
||||
* restart that respects depends_on ordering (start builds the dependency graph) while
|
||||
* keeping the same container IDs and NOT re-pulling images.
|
||||
* mode='recreate': Uses 'docker compose stop' then 'docker compose up -d' —
|
||||
* recreates containers, fixing network_mode: service:<container> dependencies.
|
||||
* recreates containers (new IDs, re-pulls newer images), fixing network_mode: service:<container>.
|
||||
*
|
||||
* Falls back to individual container restart for stacks without compose files.
|
||||
*/
|
||||
export async function restartStack(
|
||||
stackName: string,
|
||||
envId?: number | null,
|
||||
mode: 'restart' | 'recreate' = 'restart'
|
||||
mode: 'restart' | 'ordered' | 'recreate' = 'restart'
|
||||
): Promise<StackOperationResult> {
|
||||
const result = await requireComposeFile(stackName, envId);
|
||||
|
||||
@@ -2398,6 +2450,11 @@ export async function restartStack(
|
||||
await executeComposeCommand('stop', opts, result.content!, result.nonSecretVars, result.secretVars);
|
||||
await applyProviderSecretsToComposeResult(result, stackName, envId, `[Stack:${stackName}]`);
|
||||
composeResult = await executeComposeCommand('up', { ...opts, forceRecreate: true }, result.content!, result.nonSecretVars, result.secretVars);
|
||||
} else if (mode === 'ordered') {
|
||||
// Stop everything, then start in depends_on order (compose start honors the
|
||||
// dependency graph). Same container IDs, no recreate, no re-pull.
|
||||
await executeComposeCommand('stop', opts, result.content!, result.nonSecretVars, result.secretVars);
|
||||
composeResult = await executeComposeCommand('start', opts, result.content!, result.nonSecretVars, result.secretVars);
|
||||
} else {
|
||||
composeResult = await executeComposeCommand('restart', opts, result.content!, result.nonSecretVars, result.secretVars);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Resolve selfh.st app icons through the BATCH endpoint instead of one
|
||||
* `<img src="/api/icons/selfhst/<ref>">` request per icon. A list with dozens of
|
||||
* stacks/containers otherwise fires dozens of separate icon requests (which a WAF
|
||||
* flags as crawling, #1467, and which is just slow). Components call `requestSelfhst(ref)`
|
||||
* to register the refs they need and read the resolved `data:` URI from `selfhstIcons`;
|
||||
* this store collects refs within a microtask window and resolves them in ONE POST.
|
||||
*/
|
||||
import { writable, get } from 'svelte/store';
|
||||
|
||||
/** ref -> resolved `data:image/svg+xml;base64,...` URI (or '' if it could not resolve). */
|
||||
export const selfhstIcons = writable<Record<string, string>>({});
|
||||
|
||||
const pending = new Set<string>();
|
||||
const requested = new Set<string>(); // refs already resolved or in flight (don't re-fetch)
|
||||
let flushScheduled = false;
|
||||
|
||||
const BATCH_MAX = 200; // matches the endpoint cap
|
||||
|
||||
async function flush() {
|
||||
flushScheduled = false;
|
||||
const refs = [...pending].slice(0, BATCH_MAX);
|
||||
if (refs.length === 0) return;
|
||||
for (const r of refs) {
|
||||
pending.delete(r);
|
||||
requested.add(r);
|
||||
}
|
||||
// More than BATCH_MAX refs registered this tick: drain the rest in another batch.
|
||||
if (pending.size > 0 && !flushScheduled) {
|
||||
flushScheduled = true;
|
||||
queueMicrotask(flush);
|
||||
}
|
||||
try {
|
||||
const res = await fetch('/api/icons/selfhst/batch', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ refs })
|
||||
});
|
||||
const data: { icons?: Record<string, string> } = res.ok ? await res.json() : {};
|
||||
const icons = data.icons ?? {};
|
||||
// Store resolved icons; mark unresolved refs as '' so we don't re-request them.
|
||||
selfhstIcons.update((cur) => {
|
||||
const next = { ...cur };
|
||||
for (const r of refs) next[r] = icons[r] ?? '';
|
||||
return next;
|
||||
});
|
||||
} catch {
|
||||
// leave refs unresolved; a component can fall back to a placeholder
|
||||
selfhstIcons.update((cur) => {
|
||||
const next = { ...cur };
|
||||
for (const r of refs) if (!(r in next)) next[r] = '';
|
||||
return next;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a selfh.st ref for resolution. Returns immediately; the resolved data URI
|
||||
* appears in the `selfhstIcons` store once the next batch completes. Already-resolved or
|
||||
* in-flight refs are skipped.
|
||||
*/
|
||||
export function requestSelfhst(ref: string | null | undefined): void {
|
||||
if (!ref) return;
|
||||
if (requested.has(ref) || pending.has(ref)) return;
|
||||
if (ref in get(selfhstIcons)) return;
|
||||
pending.add(ref);
|
||||
if (!flushScheduled) {
|
||||
flushScheduled = true;
|
||||
// Coalesce all refs registered in this tick into one request.
|
||||
queueMicrotask(flush);
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,10 @@ import type { RequestHandler } from './$types';
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* summary: Restart a stack (mode=restart) or recreate its containers (mode=recreate); progress and the final result stream over Server-Sent Events
|
||||
* summary: Restart a stack (mode=restart|ordered|recreate); progress and the final result stream over Server-Sent Events
|
||||
* path: name:string! Stack name (from GET /api/stacks)
|
||||
* query: env:integer Environment ID the stack belongs to (from GET /api/environments)
|
||||
* query: mode:string Restart mode — "recreate" recreates containers, anything else performs a plain restart
|
||||
* query: mode:string Restart mode — "recreate" recreates containers (new IDs, re-pull), "ordered" does a stop+start honoring depends_on ordering (same IDs), anything else is a plain in-place restart
|
||||
* resp-200: Server-Sent-Events job stream with a final result event ({success, output})
|
||||
* resp-403: Permission denied (requires stacks:restart, or environment access denied on enterprise)
|
||||
*/
|
||||
@@ -31,7 +31,8 @@ export const POST: RequestHandler = async (event) => {
|
||||
return json({ error: 'Access denied to this environment' }, { status: 403 });
|
||||
}
|
||||
|
||||
const mode = url.searchParams.get('mode') === 'recreate' ? 'recreate' : 'restart';
|
||||
const modeParam = url.searchParams.get('mode');
|
||||
const mode = modeParam === 'recreate' ? 'recreate' : modeParam === 'ordered' ? 'ordered' : 'restart';
|
||||
|
||||
return createJobResponse(async (send) => {
|
||||
try {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { getStackSources } from '$lib/server/db';
|
||||
import { countStackEnvVars } from '$lib/server/stacks';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* summary: List stack source records (their stored compose/env paths and source type)
|
||||
* summary: List stack source records (their stored compose/env paths and source type, plus an env-var count)
|
||||
* query: env:integer Filter to a single environment id
|
||||
* resp-403: Permission denied (needs stacks:view)
|
||||
* resp-500: Failed to list stack sources
|
||||
@@ -33,17 +34,32 @@ export const GET: RequestHandler = async ({ url, cookies }) => {
|
||||
repository?: any;
|
||||
secretProviderId?: number | null;
|
||||
icon?: string | null;
|
||||
envVarCount?: number;
|
||||
}
|
||||
> = {};
|
||||
for (const source of sources) {
|
||||
// Count env vars server-side (one local read per stack) so the list badge does
|
||||
// not need a /env fetch per stack. GET /env resolves its env param as
|
||||
// `envId ? parseInt : null`, so pass the SAME null-when-absent value (not
|
||||
// undefined - getStackEnvVars scopes differently for null vs undefined) so the
|
||||
// count matches that endpoint's variable list exactly.
|
||||
const countEnvId = envIdNum ?? null;
|
||||
const counts = await Promise.all(
|
||||
sources.map((s) =>
|
||||
s.sourceType === 'internal' || s.sourceType === 'git'
|
||||
? countStackEnvVars(s.stackName, countEnvId)
|
||||
: Promise.resolve(0)
|
||||
)
|
||||
);
|
||||
sources.forEach((source, i) => {
|
||||
sourceMap[source.stackName] = {
|
||||
sourceType: source.sourceType,
|
||||
composePath: source.composePath,
|
||||
repository: source.repository,
|
||||
secretProviderId: source.secretProviderId,
|
||||
icon: source.icon ?? null,
|
||||
envVarCount: counts[i],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return json(sourceMap);
|
||||
} catch (error) {
|
||||
|
||||
@@ -905,9 +905,15 @@
|
||||
|
||||
stacks = dockerStacks;
|
||||
|
||||
// Fetch env var counts for internal and git stacks (in background, don't block UI)
|
||||
const allStackNames = stacks.map(s => s.name);
|
||||
fetchEnvVarCounts(allStackNames, sourcesData);
|
||||
// Env-var counts come from /api/stacks/sources (envVarCount per stack) - no
|
||||
// per-stack /env fetch needed for the list badge.
|
||||
const counts: Record<string, number> = {};
|
||||
for (const [name, src] of Object.entries(sourcesData as Record<string, { envVarCount?: number }>)) {
|
||||
if (src && typeof src.envVarCount === 'number' && src.envVarCount > 0) {
|
||||
counts[name] = src.envVarCount;
|
||||
}
|
||||
}
|
||||
stackEnvVarCounts = counts;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch stacks:', error);
|
||||
toast.error('Failed to load stacks');
|
||||
@@ -917,39 +923,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchEnvVarCounts(stackNames: string[], sources: Record<string, any>) {
|
||||
// Only fetch for stacks that can have env vars (internal or git)
|
||||
const stacksToFetch = stackNames.filter(name => {
|
||||
const source = sources[name];
|
||||
return source && (source.sourceType === 'internal' || source.sourceType === 'git');
|
||||
});
|
||||
|
||||
if (stacksToFetch.length === 0) {
|
||||
stackEnvVarCounts = {};
|
||||
return;
|
||||
}
|
||||
|
||||
const counts: Record<string, number> = {};
|
||||
|
||||
// Fetch in parallel with error handling
|
||||
await Promise.all(stacksToFetch.map(async (stackName) => {
|
||||
try {
|
||||
const response = await fetch(appendEnvParam(`/api/stacks/${encodeURIComponent(stackName)}/env`, envId));
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const varCount = data.variables?.length || 0;
|
||||
if (varCount > 0) {
|
||||
counts[stackName] = varCount;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore errors for individual stack env var fetches
|
||||
}
|
||||
}));
|
||||
|
||||
stackEnvVarCounts = counts;
|
||||
}
|
||||
|
||||
function getStackSource(stackName: string) {
|
||||
return stackSources[stackName] || { sourceType: 'external' };
|
||||
}
|
||||
@@ -1031,13 +1004,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function restartStack(name: string, mode: 'restart' | 'recreate' = 'restart') {
|
||||
async function restartStack(name: string, mode: 'restart' | 'ordered' | 'recreate' = 'restart') {
|
||||
operationError = null;
|
||||
stackActionLoading = name;
|
||||
try {
|
||||
let url = appendEnvParam(`/api/stacks/${encodeURIComponent(name)}/restart`, envId);
|
||||
if (mode === 'recreate') {
|
||||
url += (url.includes('?') ? '&' : '?') + 'mode=recreate';
|
||||
if (mode === 'recreate' || mode === 'ordered') {
|
||||
url += (url.includes('?') ? '&' : '?') + `mode=${mode}`;
|
||||
}
|
||||
const response = await fetch(url, { method: 'POST' });
|
||||
const data = await readJobResponse(response);
|
||||
@@ -2170,16 +2143,20 @@
|
||||
align="end"
|
||||
sideOffset={8}
|
||||
>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<div class="flex flex-col gap-2 w-60">
|
||||
<span class="text-xs text-muted-foreground">Restart stack <strong>{stack.name.length > 20 ? stack.name.slice(0, 20) + '...' : stack.name}</strong></span>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<Button size="sm" variant="secondary" class="h-6 px-2 text-xs" onclick={() => { restartPopoverOpen[stack.name] = false; restartStack(stack.name, 'restart'); }}>
|
||||
Restart
|
||||
</Button>
|
||||
<Button size="sm" variant="default" class="h-6 px-2 text-xs" onclick={() => { restartPopoverOpen[stack.name] = false; restartStack(stack.name, 'recreate'); }}>
|
||||
Recreate (stop & up)
|
||||
</Button>
|
||||
</div>
|
||||
<button class="flex flex-col items-start gap-0.5 rounded px-2 py-1.5 text-left hover:bg-muted" onclick={() => { restartPopoverOpen[stack.name] = false; restartStack(stack.name, 'restart'); }}>
|
||||
<span class="text-xs font-medium">Restart</span>
|
||||
<span class="text-[11px] text-muted-foreground">Fast in-place restart. Ignores depends_on ordering.</span>
|
||||
</button>
|
||||
<button class="flex flex-col items-start gap-0.5 rounded px-2 py-1.5 text-left hover:bg-muted" onclick={() => { restartPopoverOpen[stack.name] = false; restartStack(stack.name, 'ordered'); }}>
|
||||
<span class="text-xs font-medium">Restart in order</span>
|
||||
<span class="text-[11px] text-muted-foreground">Stop then start in depends_on order. Same container IDs.</span>
|
||||
</button>
|
||||
<button class="flex flex-col items-start gap-0.5 rounded px-2 py-1.5 text-left hover:bg-muted" onclick={() => { restartPopoverOpen[stack.name] = false; restartStack(stack.name, 'recreate'); }}>
|
||||
<span class="text-xs font-medium">Recreate (stop & up)</span>
|
||||
<span class="text-[11px] text-muted-foreground">Recreate containers in order. New IDs, re-pulls newer images.</span>
|
||||
</button>
|
||||
</div>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
|
||||
Reference in new issue
Block a user