Skip to content

Commit 294c904

Browse files
bogdanpricopclaude
andcommitted
fix(templates): yaml-based parser + persistent error dialog + UniFi root creds (v8.7.2)
Three related fixes for the template-deploy path: 1) PARSER (latent bug since forever): the Docker-API deploy returned "(HTTP code 400) bad parameter - no command specified" for any template with a top-level volumes: block — i.e. every template with a named volume (postgres, redis, mariadb, mongo, every LSIO image). The hand- rolled line parser treated ANY 2-space-indented name: as a service, so volumes: -> redis-data: produced a phantom service with empty Image. Rewritten to use the yaml package — extracts only doc.services.*, ignores volumes/configs/networks. Also now supports map-form environment, long-form ports, string|array command, container_name, and IP-prefixed ports ("127.0.0.1:2375:2375"). Imageless services produce a clear 400 instead of the cryptic generic Docker error. 2) UNIFI TEMPLATE: the v8.6.2 inline configs: content: script for Mongo user creation is a Compose-CLI-only feature the API deploy cannot apply. Switched to root Mongo credentials (MONGO_INITDB_ROOT_USERNAME / PASSWORD + MONGO_AUTHSOURCE=admin) so the stack deploys cleanly via the API. 3) PERSISTENT ERROR DIALOG: Toast.error fades in 4s so deploy failures were undebuggable. Replaced with a persistent sub-modal showing full Docker error + failing service/container + partial deploys + the compose YAML sent, with a Copy button. Backend now returns structured { error, service, containerName, partial } on failure; api.js attaches .body and .status to the thrown Error so the dialog has all of it. Browser-verified: deploys lsio-prowlarr cleanly (response {ok:true, containers:[{name:"prowlarr",...}]}, 0 console errors); error modal renders correctly with full block + Copy. Suite green 1444. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent d5d0e48 commit 294c904

9 files changed

Lines changed: 226 additions & 96 deletions

File tree

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,27 @@
22

33
All notable changes to Docker Dash are documented here.
44

5+
## [8.7.2] - 2026-05-27 — Template deploy: parser rewrite + persistent error dialog + UniFi simplified
6+
7+
### Fixed (long-standing latent bug)
8+
The Docker-API template deploy was returning **"(HTTP code 400) bad parameter - no command specified"** for any template with a top-level `volumes:` (or `configs:` / `networks:`) block — that includes **every template with a named volume** (postgres, redis, mariadb, mongo, every LSIO template, …). The hand-rolled line parser at `_parseComposeServices` treated any 2-space-indented `name:` as a service, so `volumes:\n redis-data:` produced a phantom "redis-data" service with empty `Image`, which Docker rejected with the cryptic "no command specified".
9+
10+
- **Parser rewritten** to use the project's `yaml` package — now correctly extracts only `doc.services.*`, ignoring `volumes` / `configs` / `networks` top-level blocks. Supports both array and map forms of `environment`, both short-form strings and long-form `{ target, published, protocol }` for ports, both string and array forms of `command`, and `container_name` overrides.
11+
- **`command:` is honored** in `createOpts.Cmd` (string `command:` runs via `/bin/sh -c`, array form passes through). Templates that relied on a `command:` override (litellm) now actually use it.
12+
- **IP-prefixed ports** (e.g. `"127.0.0.1:2375:2375"` in `lsio-socket-proxy`) now parsed and bound to the right interface via `HostConfig.PortBindings[…].HostIp`.
13+
- Imageless services produce a **clear 400** with the offending service name, instead of letting Docker return the cryptic generic error.
14+
15+
### Persistent deploy-error dialog
16+
Replaces the 4-second `Toast.error` (which made deploy failures essentially undebuggable) with a **persistent sub-modal** showing the full Docker error, the failing service + container, any partial deploys, and the compose YAML that was sent — with a **Copy** button. Closes only when the user clicks Close or the X.
17+
18+
The backend now returns structured `{ error, service, containerName, partial: [...] }` on failure, and `api.js` attaches `.body` and `.status` to the thrown `Error` so the dialog can show all of it.
19+
20+
### UniFi template simplified
21+
The v8.6.2 UniFi template used Compose `configs:` with `content:` for the inline Mongo init script — a Compose-CLI feature the API-based deploy can't apply. Switched to **root Mongo credentials** (`MONGO_INITDB_ROOT_USERNAME` / `MONGO_INITDB_ROOT_PASSWORD` on mongo + `MONGO_AUTHSOURCE=admin` on the app) so the stack now deploys cleanly via the Docker API without any external file or healthcheck. The first-start race is handled by `restart: unless-stopped` (unifi may crash once before Mongo is ready, then restarts).
22+
23+
### Verified
24+
End-to-end browser test deploys `lsio-prowlarr` cleanly through the new code path (response: `{ ok: true, containers: [{ name: "prowlarr", ... }] }`, zero console errors); the persistent error modal renders correctly with the full error block + Copy/Close. Suite green (1444), template-tests pass, every template's YAML round-trips through the new parser.
25+
526
## [8.7.1] - 2026-05-27 — Templates dialog: search field + intersect filter
627

728
With 84 built-in templates now in **Containers → Templates**, scanning the grid for a specific image was painful. Adds a live search field above the category buttons.

docker-compose.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ services:
44
context: .
55
dockerfile: Dockerfile
66
args:
7-
APP_VERSION: "${APP_VERSION:-8.7.1}"
8-
image: docker-dash:${APP_VERSION:-8.7.1}
7+
APP_VERSION: "${APP_VERSION:-8.7.2}"
8+
image: docker-dash:${APP_VERSION:-8.7.2}
99
container_name: docker-dash
1010
restart: unless-stopped
1111
env_file:
@@ -54,7 +54,7 @@ services:
5454
dd-egress-filter:
5555
build:
5656
context: ./docker/egress-filter
57-
image: docker-dash-egress-filter:${APP_VERSION:-8.7.1}
57+
image: docker-dash-egress-filter:${APP_VERSION:-8.7.2}
5858
container_name: dd-egress-filter
5959
restart: unless-stopped
6060
# Uses the default bridge so target containers on the default bridge can

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "docker-dash",
3-
"version": "8.7.1",
3+
"version": "8.7.2",
44
"description": "Full-featured Docker management dashboard",
55
"main": "src/server.js",
66
"scripts": {

public/js/api.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,10 @@ const Api = {
7474
? await res.json()
7575
: await res.text();
7676
if (!res.ok) {
77-
throw new Error(data?.error || data?.message || `HTTP ${res.status}`);
77+
const err = new Error(data?.error || data?.message || `HTTP ${res.status}`);
78+
err.status = res.status;
79+
err.body = data;
80+
throw err;
7881
}
7982
return data;
8083
} catch (err) {

public/js/pages/containers.js

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1848,7 +1848,7 @@ const ContainersPage = {
18481848
Toast.success(i18n.t('pages.containers.templatesDeplyed', { name: tmpl.name }));
18491849
await this._loadList();
18501850
} catch (err) {
1851-
Toast.error(i18n.t('pages.containers.templatesDeployFailed', { message: err.message }));
1851+
this._showDeployError(tmpl, err, compose);
18521852
}
18531853
},
18541854
onCancel: () => {
@@ -1905,11 +1905,60 @@ const ContainersPage = {
19051905
Toast.success(i18n.t('pages.containers.templatesDeplyed', { name: tmpl.name }));
19061906
await this._loadList();
19071907
} catch (err) {
1908-
Toast.error(i18n.t('pages.containers.templatesDeployFailed', { message: err.message }));
1908+
this._showDeployError(tmpl, err, tmpl.compose);
19091909
}
19101910
}
19111911
},
19121912

1913+
/**
1914+
* Persistent error dialog for template deploy failures. The Docker error
1915+
* (and any structured fields the backend returned — service name, container
1916+
* name, partial successes) is shown in a modal the user has to close, with
1917+
* a Copy button so the message can be pasted into an issue. Replaces the
1918+
* 4-second Toast.error which made debugging impossible.
1919+
*/
1920+
_showDeployError(tmpl, err, composeText) {
1921+
const body = (err && err.body) || {};
1922+
const message = (err && err.message) || 'Unknown error';
1923+
const status = (err && err.status) ? ` (HTTP ${err.status})` : '';
1924+
const detailLines = [];
1925+
if (body.service) detailLines.push(`service: ${body.service}`);
1926+
if (body.containerName) detailLines.push(`container: ${body.containerName}`);
1927+
if (Array.isArray(body.partial) && body.partial.length) {
1928+
detailLines.push(`partial deploy: ${body.partial.length} container(s) created before the failure:`);
1929+
body.partial.forEach(p => detailLines.push(` - ${p.name}${p.started === false ? ' (created, NOT started)' : ''}`));
1930+
}
1931+
const details = detailLines.length ? detailLines.join('\n') + '\n\n' : '';
1932+
const errorBlock = `Template: ${tmpl.id} (${tmpl.name})\nError${status}: ${message}\n\n${details}--- Compose YAML sent ---\n${composeText || tmpl.compose || ''}`;
1933+
1934+
const html = `
1935+
<div class="modal-header">
1936+
<h3 style="color:var(--red)"><i class="fas fa-circle-exclamation" style="margin-right:8px"></i>Deploy failed — ${Utils.escapeHtml(tmpl.name)}</h3>
1937+
<button class="modal-close-btn" id="deploy-err-x"><i class="fas fa-times"></i></button>
1938+
</div>
1939+
<div class="modal-body">
1940+
<p class="text-muted text-sm" style="margin-bottom:8px">The Docker API returned an error. The full message is below — copy it before closing if you want to debug or report it.</p>
1941+
${body.partial && body.partial.length ? `<div class="empty-msg is-warning" style="margin-bottom:10px"><i class="fas fa-triangle-exclamation"></i><p><strong>Partial deploy:</strong> ${body.partial.length} container(s) were created before the failure. Stop/remove them from the Containers page if you need to start clean.</p></div>` : ''}
1942+
<pre class="inspect-json" style="max-height:60vh;overflow:auto;white-space:pre-wrap;font-size:12px;border:1px solid var(--red);border-radius:4px;padding:10px">${Utils.escapeHtml(errorBlock)}</pre>
1943+
</div>
1944+
<div class="modal-footer">
1945+
<button class="btn btn-secondary" id="deploy-err-copy"><i class="fas fa-copy"></i> ${i18n.t('common.copy')}</button>
1946+
<button class="btn btn-primary" id="deploy-err-ok">${i18n.t('common.close')}</button>
1947+
</div>
1948+
`;
1949+
1950+
// If a modal is already open (e.g. the template configurator), layer this on top via openSub.
1951+
const useSub = !!(Modal._content && Modal._content.parentElement && getComputedStyle(Modal._content.parentElement).display !== 'none');
1952+
const opts = { width: 'min(900px, 95vw)' };
1953+
const root = useSub ? Modal.openSub(html, opts) : (Modal.open(html, opts), Modal._content);
1954+
const close = () => { if (useSub) Modal.closeSub(); else Modal.close(); };
1955+
root.querySelector('#deploy-err-x').addEventListener('click', close);
1956+
root.querySelector('#deploy-err-ok').addEventListener('click', close);
1957+
root.querySelector('#deploy-err-copy').addEventListener('click', () => {
1958+
Utils.copyToClipboard(errorBlock).then(() => Toast.success(i18n.t('common.copied')));
1959+
});
1960+
},
1961+
19131962
// ─── Health Check Logs Viewer ──────────────────
19141963

19151964
_showActionsGuide() {

public/js/pages/whatsnew.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@ const WhatsNewPage = {
99
// Add new releases at the TOP of this array.
1010
// Types: feature, fix, improvement, security, breaking
1111
_releases: [
12+
{
13+
version: '8.7.2',
14+
date: '2026-05-27',
15+
title: 'Template deploy: parser rewrite + persistent error dialog',
16+
changes: [
17+
{ type: 'fix', text: 'Fixed a long-standing latent bug: the Docker-API template deploy returned "no command specified" for every template with a top-level volumes: block (postgres, redis, mariadb, every LSIO template…). The hand-rolled line parser treated any 2-space-indented name: as a service, so volumes: → redis-data: created a phantom service with no image. Rewritten to use the yaml package — extracts only doc.services.*, correctly ignores top-level volumes/configs/networks blocks.' },
18+
{ type: 'fix', text: 'Deploy errors no longer flash in a toast that disappears in 4 seconds. Failures now open a persistent sub-modal with the full Docker error, the failing service + container name, any partial deploys, the compose YAML that was sent, and a Copy button. Closes only on user action so you can actually read and debug what went wrong.' },
19+
{ type: 'improvement', text: 'Template deploy now honors command: (string form via /bin/sh -c, array form passed through), container_name: overrides, and IP-prefixed ports like "127.0.0.1:2375:2375" (lsio-socket-proxy). Imageless services produce a clear 400 with the offending service name instead of the cryptic generic Docker error. UniFi template simplified to root Mongo credentials so it deploys cleanly via the API (the prior inline configs script was a Compose-CLI-only feature).' },
20+
],
21+
},
1222
{
1323
version: '8.7.1',
1424
date: '2026-05-27',

0 commit comments

Comments
 (0)