keep every open terminal panel alive and sort sub-minute uptimes correctly

This commit is contained in:
Jarek Krochmalski committed 2026-09-06 17:03:48 +05:30
1 parent e35b825e38
commit 8862054ce4
3 files changed
+106 -58

No files matched your search

+30
View File
@@ -0,0 +1,30 @@
/**
* Parse a human-readable relative time ("2 hours", "45 seconds", "Less than a
* second") into seconds, for sorting containers by uptime. Pure; unit-tested.
*
* Only the literal "less than a second" collapses to 1s - a value like "45 seconds"
* must keep its real magnitude so sub-minute uptimes sort against each other (#1395).
*/
export function parseTimeStringToSeconds(timeStr: string): number {
const str = timeStr.toLowerCase();
if (str.includes('less than a second')) return 1;
if (str.includes('less than a minute') || str.includes('about a minute')) return 60;
const match = str.match(/(\d+)\s*(second|minute|hour|day|week|month|year)/);
if (!match) return 0;
const value = parseInt(match[1], 10);
const unit = match[2];
switch (unit) {
case 'second': return value;
case 'minute': return value * 60;
case 'hour': return value * 3600;
case 'day': return value * 86400;
case 'week': return value * 604800;
case 'month': return value * 2592000;
case 'year': return value * 31536000;
default: return 0;
}
}
+36 -58
View File
@@ -92,6 +92,7 @@
import { canAccess } from '$lib/stores/auth';
import { vulnerabilityCriteriaIcons } from '$lib/utils/update-steps';
import { compareIps } from '$lib/utils/ip';
import { parseTimeStringToSeconds } from '$lib/utils/parse-uptime';
import { formatHostPortUrl } from '$lib/utils/url';
import { parseCustomUrl } from '$lib/utils/custom-url';
import { extractTraefikUrls } from '$lib/utils/traefik-urls';
@@ -1321,31 +1322,6 @@
return -Infinity;
}
function parseTimeStringToSeconds(timeStr: string): number {
// Parse strings like "2 hours", "3 days", "About a minute", "Less than a second"
const str = timeStr.toLowerCase();
if (str.includes('less than a second')) return 1;
if (str.includes('less than a minute') || str.includes('about a minute')) return 60;
const match = str.match(/(\d+)\s*(second|minute|hour|day|week|month|year)/);
if (!match) return 0;
const value = parseInt(match[1], 10);
const unit = match[2];
switch (unit) {
case 'second': return value;
case 'minute': return value * 60;
case 'hour': return value * 3600;
case 'day': return value * 86400;
case 'week': return value * 604800;
case 'month': return value * 2592000;
case 'year': return value * 31536000;
default: return 0;
}
}
function getHealthVariant(health?: string): 'default' | 'destructive' | 'secondary' | 'outline' {
switch (health) {
case 'healthy':
@@ -2447,24 +2423,26 @@
{/if}
{/if}
<!-- Current Terminal Panel -->
{#if currentTerminalContainerId}
{@const activeTerminal = activeTerminals.find(t => t.containerId === currentTerminalContainerId)}
{#if activeTerminal}
<div class="flex-1 min-h-0">
<TerminalPanel
containerId={activeTerminal.containerId}
containerName={activeTerminal.containerName}
shell={activeTerminal.shell}
user={activeTerminal.user}
visible={true}
envId={envId}
fillHeight={true}
onClose={() => closeTerminal(activeTerminal.containerId)}
/>
</div>
{/if}
{/if}
<!-- Terminal panels: render EVERY open session so each keeps its own
live WebSocket/xterm; only the current one is visible. Switching
just toggles visibility, so a running process (e.g. top) in a
backgrounded session stays alive and its shell stays its own. -->
{#each activeTerminals as t (t.containerId)}
<!-- Do NOT display:none a backgrounded panel; TerminalPanel hides
itself off-screen while keeping its size so xterm can fit. -->
<div class="min-h-0" class:flex-1={t.containerId === currentTerminalContainerId}>
<TerminalPanel
containerId={t.containerId}
containerName={t.containerName}
shell={t.shell}
user={t.user}
visible={t.containerId === currentTerminalContainerId}
envId={envId}
fillHeight={true}
onClose={() => closeTerminal(t.containerId)}
/>
</div>
{/each}
</div>
{/if}
</div>
@@ -2485,21 +2463,21 @@
{/if}
{/if}
<!-- Show current terminal panel -->
{#if currentTerminalContainerId}
{@const activeTerminal = activeTerminals.find(t => t.containerId === currentTerminalContainerId)}
{#if activeTerminal}
<TerminalPanel
containerId={activeTerminal.containerId}
containerName={activeTerminal.containerName}
shell={activeTerminal.shell}
user={activeTerminal.user}
visible={true}
envId={envId}
onClose={() => closeTerminal(activeTerminal.containerId)}
/>
{/if}
{/if}
<!-- Terminal panels: render every open session (each keeps its own live
WebSocket/xterm); only the current one is visible. -->
{#each activeTerminals as t (t.containerId)}
<!-- TerminalPanel hides a backgrounded session off-screen (keeps size for
xterm fit); do not display:none it or a background xterm gets 0x0. -->
<TerminalPanel
containerId={t.containerId}
containerName={t.containerName}
shell={t.shell}
user={t.user}
visible={t.containerId === currentTerminalContainerId}
envId={envId}
onClose={() => closeTerminal(t.containerId)}
/>
{/each}
{/if}
{/if}
</div>