This commit is contained in:
jarek committed 2026-01-18 09:56:38 +01:00
1 parent c185d00dc3
commit 6483cea6c6
5 files changed
+93 -61

No files matched your search

+11
View File
@@ -1,4 +1,15 @@
[
{
"version": "1.0.10",
"date": "2026-01-18",
"changes": [
{ "type": "fix", "text": "Fix docker socket access for custom PUID/PGID" },
{ "type": "fix", "text": "Fix stack creation with deploy failing when no env vars provided" },
{ "type": "fix", "text": "Fix env var validation flagging variables in commented lines as missing" },
{ "type": "fix", "text": "Show stop button for stacks in restart loop" }
],
"imageTag": "fnsys/dockhand:v1.0.10"
},
{
"version": "1.0.9",
"date": "2026-01-17",
+6 -6
View File
@@ -124,14 +124,14 @@ export const POST: RequestHandler = async ({ request, url, cookies }) => {
return json({ success: true, started: false });
}
// ALWAYS save compose file first - deployStack expects it to exist
await saveStackComposeFile(name, compose, true, envIdNum, {
composePath: composePath || undefined,
envPath: envPath || undefined
});
// Save environment variables BEFORE deploying so they're available during start
if (rawEnvContent || (envVars && Array.isArray(envVars) && envVars.length > 0)) {
// First ensure the stack directory exists by saving compose file
await saveStackComposeFile(name, compose, true, envIdNum, {
composePath: composePath || undefined,
envPath: envPath || undefined
});
// - rawEnvContent: non-secret vars with comments → .env file
// - envVars: ALL vars → DB (secrets stored for shell injection, non-secrets for metadata)
if (rawEnvContent) {
+36 -25
View File
@@ -16,42 +16,53 @@ interface ValidationResult {
/**
* Extract environment variables from compose YAML content.
* Matches ${VAR_NAME} and ${VAR_NAME:-default} patterns.
* Ignores variables in commented lines (lines starting with #).
* Returns { required: [...], optional: [...] }
*/
function extractComposeVars(yaml: string): { required: string[]; optional: string[] } {
const required: string[] = [];
const optional: string[] = [];
// Match ${VAR_NAME} (required) and ${VAR_NAME:-default} or ${VAR_NAME-default} (optional)
const regex = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?:(:-?)[^}]*)?\}/g;
let match;
// Process line by line to skip commented lines
const lines = yaml.split('\n');
for (const line of lines) {
// Skip lines that are comments (start with # after optional whitespace)
const trimmedLine = line.trim();
if (trimmedLine.startsWith('#')) {
continue;
}
while ((match = regex.exec(yaml)) !== null) {
const varName = match[1];
const hasDefault = match[2] !== undefined;
// Match ${VAR_NAME} (required) and ${VAR_NAME:-default} or ${VAR_NAME-default} (optional)
const regex = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?:(:-?)[^}]*)?\}/g;
let match;
if (hasDefault) {
if (!optional.includes(varName) && !required.includes(varName)) {
optional.push(varName);
}
} else {
// Move from optional to required if we find a non-default usage
const optIdx = optional.indexOf(varName);
if (optIdx !== -1) {
optional.splice(optIdx, 1);
}
if (!required.includes(varName)) {
required.push(varName);
while ((match = regex.exec(line)) !== null) {
const varName = match[1];
const hasDefault = match[2] !== undefined;
if (hasDefault) {
if (!optional.includes(varName) && !required.includes(varName)) {
optional.push(varName);
}
} else {
// Move from optional to required if we find a non-default usage
const optIdx = optional.indexOf(varName);
if (optIdx !== -1) {
optional.splice(optIdx, 1);
}
if (!required.includes(varName)) {
required.push(varName);
}
}
}
}
// Also match $VAR_NAME (simple variable substitution)
const simpleRegex = /\$([A-Za-z_][A-Za-z0-9_]*)(?![{A-Za-z0-9_])/g;
while ((match = simpleRegex.exec(yaml)) !== null) {
const varName = match[1];
if (!required.includes(varName) && !optional.includes(varName)) {
required.push(varName);
// Also match $VAR_NAME (simple variable substitution)
const simpleRegex = /\$([A-Za-z_][A-Za-z0-9_]*)(?![{A-Za-z0-9_])/g;
while ((match = simpleRegex.exec(line)) !== null) {
const varName = match[1];
if (!required.includes(varName) && !optional.includes(varName)) {
required.push(varName);
}
}
}
+6 -6
View File
@@ -390,7 +390,7 @@
);
// Count by status for selected stacks
const selectedRunning = $derived(selectedInFilter.filter(s => s.status === 'running' || s.status === 'partial'));
const selectedRunning = $derived(selectedInFilter.filter(s => s.status === 'running' || s.status === 'partial' || s.status === 'restarting'));
const selectedStopped = $derived(selectedInFilter.filter(s => s.status === 'stopped' || s.status === 'not deployed'));
function toggleSelectAll() {
@@ -1413,7 +1413,7 @@
<div class="text-right">
{#if stats}
<span class="text-xs font-mono {stats.cpuPercent > 80 ? 'text-red-500' : stats.cpuPercent > 50 ? 'text-yellow-500' : 'text-muted-foreground'}">{stats.cpuPercent.toFixed(1)}%</span>
{:else if stack.status === 'running' || stack.status === 'partial'}
{:else if stack.status === 'running' || stack.status === 'partial' || stack.status === 'restarting'}
<span class="text-xs text-muted-foreground/50">...</span>
{:else}
<span class="text-gray-400 dark:text-gray-600 text-xs">-</span>
@@ -1424,7 +1424,7 @@
<div class="text-right">
{#if stats}
<span class="text-xs font-mono text-muted-foreground" title="{formatBytes(stats.memoryUsage)} / {formatBytes(stats.memoryLimit)}">{formatBytes(stats.memoryUsage)}</span>
{:else if stack.status === 'running' || stack.status === 'partial'}
{:else if stack.status === 'running' || stack.status === 'partial' || stack.status === 'restarting'}
<span class="text-xs text-muted-foreground/50">...</span>
{:else}
<span class="text-gray-400 dark:text-gray-600 text-xs">-</span>
@@ -1437,7 +1437,7 @@
<span class="text-xs font-mono text-muted-foreground" title="↓{formatBytes(stats.networkRx)} received / ↑{formatBytes(stats.networkTx)} sent">
<span class="text-2xs text-blue-400">↓</span>{formatBytes(stats.networkRx, 0)} <span class="text-2xs text-orange-400">↑</span>{formatBytes(stats.networkTx, 0)}
</span>
{:else if stack.status === 'running' || stack.status === 'partial'}
{:else if stack.status === 'running' || stack.status === 'partial' || stack.status === 'restarting'}
<span class="text-xs text-muted-foreground/50">...</span>
{:else}
<span class="text-gray-400 dark:text-gray-600 text-xs">-</span>
@@ -1450,7 +1450,7 @@
<span class="text-xs font-mono text-muted-foreground" title="↓{formatBytes(stats.blockRead)} read / ↑{formatBytes(stats.blockWrite)} written">
<span class="text-2xs text-green-400">r</span>{formatBytes(stats.blockRead, 0)} <span class="text-2xs text-yellow-400">w</span>{formatBytes(stats.blockWrite, 0)}
</span>
{:else if stack.status === 'running' || stack.status === 'partial'}
{:else if stack.status === 'running' || stack.status === 'partial' || stack.status === 'restarting'}
<span class="text-xs text-muted-foreground/50">...</span>
{:else}
<span class="text-gray-400 dark:text-gray-600 text-xs">-</span>
@@ -1559,7 +1559,7 @@
<div class="p-1">
<Loader2 class="w-3 h-3 animate-spin text-muted-foreground" />
</div>
{:else if stack.status === 'running' || stack.status === 'partial'}
{:else if stack.status === 'running' || stack.status === 'partial' || stack.status === 'restarting'}
{#if $canAccess('stacks', 'restart')}
<ConfirmPopover
open={false}