Skip to content

Stabilize stack lifecycle, improve update accuracy, and expand E2E safety coverage - #139

Open
mstrhakr wants to merge 84 commits into
mainfrom
dev
Open

Stabilize stack lifecycle, improve update accuracy, and expand E2E safety coverage#139
mstrhakr wants to merge 84 commits into
mainfrom
dev

Conversation

@mstrhakr

@mstrhakr mstrhakr commented Aug 18, 2026

Copy link
Copy Markdown
Owner

Summary

This PR merges dev into main with a reliability-focused update across stack operations, update detection, settings UX, and test coverage.

The goal is to reduce stale or false states, make automation safer, clarify operator controls, and increase confidence through broader regression coverage.

Changes

1. Stack lifecycle reliability

  • Hardened compose command execution and exit-code propagation.
  • Improved stack delete/update flows to reduce stale UI/runtime state.
  • Centralized menu visibility and related page-state handling.
  • Stopped rendering placeholder/error rows for project folders that don't contain a valid compose config; such folders are now silently skipped instead of showing a "Failed to load" row.

2. Update detection and runtime behavior

  • Improved auto-update digest/change detection to avoid false "no update" results.
  • Added running-container guard behavior so automated updates align with manual expectations.
  • Added plugin-local update checking logic for better update accuracy.
  • Improved compose info publishing/WebSocket handling for hidden tabs and reduced nchan pressure.
  • Tightened stack refresh behavior to avoid stale post-update status after manual operations.

3. Settings page modernization

  • Reorganized settings sections and help text for clearer operator intent.
  • Added change counting/global actions for settings management.
  • Expanded auto-check options with mode selection and custom interval support.

4. Test strategy expansion

  • Added Playwright E2E coverage for smoke and mutation-safe lifecycle flows.
  • Added new operation-flow helpers/specs and supporting docs/scripts.
  • Reworked unit test coverage to target current behavior and reduce brittle legacy assertions.

Bug Fixes

Scope

  • 34 commits from dev not in main
  • 48 files changed
  • +5088 / -2189

Validation

  • Updated unit coverage around compose execution and stack behavior.
  • Added Playwright E2E coverage for isolated lifecycle and read-only smoke flows.
  • Updated supporting docs/scripts for E2E use and setup.

…Test

- Simplified IconTest by consolidating setup and teardown processes, removing unnecessary comments, and streamlining icon file handling.
- Removed SettingsBackupTest, SettingsPageTest, and StackInfoSourceTest as they were deemed unnecessary.
- Introduced ComposeListEndpointTest to validate the rendering of Docker Compose stacks and ensure proper handling of empty states.
- Created package.json and package-lock.json for e2e tests with Playwright dependencies.
- Added Playwright configuration file (playwright.config.ts) for test setup.
- Implemented script to create storage state for authentication (create-storage-state.mjs).
- Developed comprehensive end-to-end tests for Compose Manager lifecycle and read-only checks.
- Included TypeScript configuration for test files.
mstrhakr and others added 30 commits August 29, 2026 23:02
…radio

Replace the two adjacent External Compose Path and External Compose
File inputs with a single Compose Source radio group with three
options — Project folder (default), External folder, Specific compose
file — and reveal only the picker input relevant to the selected mode.

This makes the mutual exclusion structural. Switching modes clears the
other input's value so no state is retained across choices, and
consequently the save-time "Set either External Compose Path or
External Compose File, not both" swal is removed.

- ComposeManager.php: new radio group inside Compose Sources & Files.
  Each picker input is wrapped in a display-toggle div and reused by
  the existing setStackSettings save flow (same field IDs, same POST
  keys). A stable settings-external-compose-file-warning element is
  moved into the HTML so the inline "inside project folder" warning
  has a consistent location.
- composeManagerMain.js: new setComposeSource() / normalizeComposeSourceMode()
  helpers wired to a delegated change handler on the radio group.
  loadSettingsData now derives the initial mode from response state
  (externalComposeFilePath -> file, externalComposePath -> folder,
  invalidIndirectPath -> preserve indirectMode, else project) and
  calls setComposeSource with change tracking suppressed. Reset and
  fail paths also normalize back to project mode.

The still-valid "file is inside stack project folder" save-time swal
is kept as belt-and-suspenders until slice 6 tightens inline
validation across the modal.
Replace the ambiguous "Use Docker Compose default file discovery
(no explicit -f flags)" checkbox with a computed discovery-mode
badge and a single explicit toggle button. The prior UI silently
unchecked itself when Env File Path, External Compose File, or
Additional Compose Files were set — hiding what happened behind a
warning banner. The new UI shows current mode as a badge at all
times and shows a Lock reason when overrides are active.

- Rename the extra-compose disclosure summary from
  "External files (advanced)" to "Absolute paths (outside compose
  source folder)" — the word "external" is now used consistently
  for source-mode picking only.
- ComposeManager.php: replace visible checkbox+label with a badge
  (#settings-discovery-mode-badge) and a toggle button
  (#settings-discovery-mode-toggle). Keep #settings-use-default-compose-files
  in the DOM as a hidden source of truth so the existing load/save
  round-trip (originalSettings['use-default-compose-files'], the
  setStackSettings POST field) works unchanged.
- composeManagerMain.js: updateSettingsDefaultComposeDiscoveryState()
  now updates the badge label, shows/hides the toggle button, and
  renders the lock reason instead of silently forcing the checkbox
  off with only a warning banner. Add a delegated click handler on
  the toggle that flips the hidden checkbox and re-renders. When
  overrides lock discovery to explicit, the toggle is hidden and
  the reason line is shown.
Adds an Effective Command readout at the bottom of the per-stack
Compose Sources & Files section that mirrors the exact docker compose
invocation the plugin will use (project directory, -f file flags,
--env-file, --profile flags, project name).

- Exec.php: new previewComposeArgs AJAX action wraps
  StackInfo::buildComposeArgs() and getDefaultProfiles(). Returns
  filePaths, envFilePath, profiles, projectName, projectDirectory,
  and useDefaultFileDiscovery.
- ComposeManager.php: new settings-field with a monospace <pre> block
  plus a discreet "Unsaved changes — apply to refresh." pill.
- composeManagerMain.js: loadEffectiveCommandPreview() fetches from
  the new endpoint; renderEffectiveCommandPreview() formats the
  response as backslash-continued shell lines. Wired to run on
  loadSettingsData success and after each successful save.
  updateEffectiveCommandDirtyIndicator() toggles the pill and is
  called from every settings mutation path (generic input tracker,
  extra-compose files handler, override management change handler).
  Fail/reset paths clear the readout.

The preview reflects the saved state only — precisely matching the
authoritative buildComposeArgs() output — with a visible dirty
indicator whenever unsaved changes could shift it.
Replaces the three save-time swals in saveSettings with inline
per-field errors that gate the Apply button. Users see problems
where they typed them and no longer submit only to be bounced back
by a modal error.

- New helpers setSettingsFieldError() / clearSettingsFieldError()
  and clearAllSettingsValidationErrors() manage a per-field error
  slot (id="settings-<fieldId>-error") and a
  editorModal.settingsValidationErrors Set. updateSaveButtonState()
  now disables Apply when the error Set is non-empty.
- WebUI URL: new inline handler validates on input and reports
  "Invalid URL" or "Bare [PORT] not supported" via the error slot.
- External Compose File: the existing "inside project folder"
  warning is promoted from an ad-hoc dynamic element to the shared
  error system so it also blocks Apply.
- ComposeManager.php: static error slots #settings-webui-url-error
  and #settings-external-compose-file-error. Old dynamic
  -warning element removed; setComposeSource() now calls
  clearSettingsFieldError('external-compose-file') on mode change.
- Errors are cleared on modal open, close, and stack switch, so
  state from a previous stack doesn't leak forward.
- saveSettings drops the three matching swals; only inline
  validation remains as the gate.
Extract the Compose Sources & Files section from the Settings tab
into a dedicated Sources tab placed just before Settings in the tab
bar. Runtime Defaults and Labels & Overrides stay on the Settings
tab.

- ComposeManager.php: new #editor-tab-sources button (Files icon)
  inserted between Labels and Settings, and a new #editor-panel-sources
  panel with the extracted section wrapped in a .settings-panel so the
  same scrolling/layout styles apply. Section removed from the Settings
  panel.
- composeManagerMain.js:
  * switchTab: add 'sources' to validTabs; redirect blocked switches
    to 'sources' instead of 'settings' when path-sensitive changes
    are pending (that's where those fields now live).
  * Arrow-key tab navigation array updated.
  * getEditorSaveTargetPath / getEditorActiveFilePath treat 'sources'
    the same as 'settings' (no active file, saves via stackMeta).
  * updateTabModifiedState splits editorModal.modifiedSettings by
    field ownership via a new SOURCES_TAB_SETTING_FIELDS set so the
    modified indicator lights up the correct tab.
  * saveCurrentTab guard now allows saving from the Sources tab
    when path-sensitive fields are dirty (previously only Settings).
  * enforcePathSettingsExclusivity swal wording updated to reference
    Sources instead of Settings.

Field IDs, POST actions, saved metadata, and validation flow are
unchanged; users on existing stacks see the same behavior with the
new tab layout.
Rebuild the Add New Compose Stack modal to use the same
.settings-section / .settings-field layout as the editor's Sources
tab. Purely visual + naming; behavior is unchanged.

- Group fields into Stack Identity, Compose Sources & Files, and
  Labels & Overrides sections with icon-prefixed titles.
- Rename "Indirect Path" / "Indirect Compose File" to
  "External Compose Path" / "External Compose File" for parity
  with the editor. DOM IDs renamed accordingly
  (compose-stack-indirect{,-file} → compose-stack-external-{path,file}),
  event bindings and the discovery-state helper updated. Backend POST
  fields (stackPath, stackFilePath) unchanged.
- Modal error message about mutual exclusion now says External Compose
  Path / File.
- Rewrite Override File Management help copy to spell out the Labels
  tab side effect (form vs raw YAML editor).
- Widen modal from 560px to 680px and drop the accordion + invalid
  </br> so all options are visible at once.
- Move the shared error banner above the sections.
Merge External Compose Path + External Compose File into a single
Compose Source radio (Project folder / External folder / Specific
compose file) with progressive disclosure — same pattern the editor's
Sources tab uses. Kills the "both set" submit-time modal error since
the two states are now mutually exclusive by construction.

- Refactor setComposeSource into setComposeSourceForScope(scope, ...)
  driven by COMPOSE_SOURCE_SCOPE_CONFIGS, with entries for 'settings'
  (editor) and 'add-stack'. Legacy editor call sites keep working via
  a thin setComposeSource wrapper.
- Add-stack modal: replace the two adjacent input fields with a radio
  group; each picker input lives in its own display-toggle wrap. On
  modal init, bind a change.addStackComposeSource handler and
  initialize to 'project' with change tracking suppressed.
- Remove the submit-time "Set either External Compose Path or
  External Compose File, not both" error (structurally unreachable
  now). Backend check in Exec.php stays as defense in depth.

POST fields (stackPath, stackFilePath) and server-side handling are
unchanged.
Replace the plain "Use Docker Compose default file discovery"
checkbox in the Add New Compose Stack modal with the same status
badge + explicit toggle button the editor's Sources tab now uses.
When External Compose File or ENV path is set, the toggle hides and
a lock-reason line explains why discovery is forced to explicit -f
flags.

- Extract updateSettingsDefaultComposeDiscoveryState and
  updateAddStackDefaultComposeDiscoveryState into a shared
  updateDiscoveryModeUI(scope, suppressChangeTracking) driven by
  DISCOVERY_MODE_SCOPE_CONFIGS. Both call sites become one-line
  delegates that select their scope. The scope config carries the
  target DOM IDs and a per-scope getManualOverrideReasons() so
  add-stack (no Additional Compose Files field) and editor share the
  same rendering logic.
- Add badge (#compose-stack-discovery-mode-badge) and toggle button
  (#compose-stack-discovery-mode-toggle) to the add-stack modal;
  keep #compose-stack-use-default-compose-files in the DOM as a
  hidden source of truth so the submit payload and the getConfig()
  hydration path are unchanged.
- Wire a click.addStackDiscovery handler that flips the hidden
  checkbox and re-renders. Initial render runs on modal open.
Replace the single "Automatic (disable for Manual/raw override mode)"
checkbox with an explicit Automatic/Manual radio pair whose labels
spell out the Labels-tab side effect (form editor vs raw YAML editor)
that the checkbox was hiding.

- Modal template: two radio inputs named compose-stack-override-management
  with values 'automatic' (default) and 'manual', each carrying the
  matching help copy inline.
- getConfig() hydration path now maps NEW_STACK_OVERRIDE_MANAGEMENT_AUTOMATIC
  to the selected radio value instead of a checkbox state.
- submitComposeStackModal reads the checked radio and converts to the
  existing overrideManagementAutomatic 'true'/'false' POST field so
  the server contract is unchanged.
Move stack-name and external-path validation from submit-time to
live inline feedback. The Create button is now disabled until the
form is valid.

- Add per-field error slots #compose-stack-name-error,
  #compose-stack-external-path-error, #compose-stack-external-file-error
  wired via updateAddStackValidity(), which runs on input/change of
  name, external path/file, and the Compose Source radio group.
- Rules enforced live:
    * Stack name required (previously only surfaced after clicking Create).
    * When Compose Source is External folder: path required and must
      be under /mnt/ or /boot/config/.
    * When Compose Source is Specific compose file: path required,
      must be under /mnt/ or /boot/config/, and must end in
      .yml/.yaml. Mirrors the server-side checks in Exec.php:addStack.
- Give the Create button id #compose-stack-create-btn and start it
  disabled; toggle disabled based on validation state.
- Add a Project folder slug preview under Stack Name, computed in JS
  by composeSanitizeProjectSlug() which mirrors
  compose_manager_sanitize_project_name (ProjectNameSanitizer.php)
  so users see the folder name that will be created.
refactor(Exec): remove null coalescing for project name and directory
refactor(pkg_build): enhance checksum handling in download function
- compose_fetch_icon_to_cache(): download/read icon source, normalize to PNG
  via GD when available, write to plugin-owned cache dir; SSRF-safe for http(s)
- compose_seed_docker_manager_icon(): copy cached PNG to both DM cache paths
- compose_icon_to_png_bytes(): GD raster → PNG; raw pass-through when GD absent
- compose_icon_is_safe_host(): block private/loopback hosts on remote fetches
- compose_icon_ext_to_mime(): file-extension → MIME hint
#141)

Call compose_fetch_icon_to_cache + compose_seed_docker_manager_icon after every
composeResolveContainerIcon hit in getStackContainers and checkAllStacksUpdates.
fetch is a no-op on cache hit; seed skips containers already present in DM cache.
After clearing DM cache files, also unlink the plugin's own cached PNG
(keyed by icon URL hash) so the next container-icon resolve re-fetches
a fresh copy.
…mp path (#141)

Tests cover:
- compose_fetch_icon_to_cache: data URI, cache hit, force refresh, SVG rejection,
  path traversal/out-of-allowlist blocking, unsupported schemes
- compose_icon_is_safe_host: loopback + private-range rejection
- compose_icon_ext_to_mime: all known extensions + unknown fallback
- compose_icon_to_png_bytes: raw PNG pass-through (GD absent), SVG null, garbage null
- compose_seed_docker_manager_icon: empty path and invalid name no-ops

GD-dependent cases are guarded with markTestSkipped so CI passes cleanly.
- versions.env: pin resvg v0.48.1 + SHA256 alongside COMPOSE_VERSION
- build.sh / build.ps1: parse RESVG_VERSION + RESVG_SHA256, pass as env vars
- pkg_build.sh: download resvg-linux-x86_64.tar.gz (pinned SHA), install to
  /usr/local/emhttp/plugins/compose.manager/bin/resvg; include in release_info
- Defines.php: add COMPOSE_RESVG_BIN constant
- Util.php: compose_icon_to_png_bytes() tries resvg (stdin→tempfile) for SVG;
  GD path unchanged for raster; resvg not present = null (graceful skip)
- tests.yml: add gd to PHP extensions so GD-gated tests run in CI
- bootstrap.php: pre-define COMPOSE_RESVG_BIN pointing to /tmp/resvg when dev
  binary is present, falling back to plugin install path
- IconCacheTest: split SVG test into resvg-present / resvg-absent variants;
  add testFetchSvgDataUriSucceedsWithResvg integration test
Log at debug level under 'icon-cache' category:
- cache hit (skipped download)
- successful cache write (source, path, byte size)
- conversion failure (source, mime hint)
- Docker Manager seed per target path
- plugin cache entry cleared on clearIconCache
…Ls (#141)

Add IconCache.php endpoint: accepts ?src=<url>, calls compose_fetch_icon_to_cache,
serves cached PNG with 24h browser cache; falls back to 302 redirect on cache miss.

Add compose_icon_browser_url() PHP helper: wraps http(s) sources in proxy URL,
passes data: and local paths through unchanged.

Add composeIconSrc() JS helper: same logic client-side, replaces isValidIconSrc
+ternary pattern at every icon rendering site.

Wire into: ComposeList.php (server-side img src), composeManagerMain.js
(container list, update dialog, labels editor, deleted-service preview),
compose.manager.dashboard.page (container row and stack tile icons).
…ear Icon Cache button (#141)

- imagealphablending(false) + imagesavealpha(true) before imagepng() preserves
  the alpha channel — without these GD composites onto white, breaking transparency
- Add clearAllIconCache Exec action: deletes all *.png from COMPOSE_ICON_CACHE_DIR
- Add 'Clear Icon Cache' button to Settings page (alongside Clear Update Cache),
  with swal confirm + count of files removed in success message
- Add compose_fetch_icon_to_cache(): download/read icon from HTTP URL, data URI,
  or allowed local path; normalize to PNG; persist to plugin cache dir
- Add compose_icon_to_png_bytes(): GD-based raster conversion with alpha
  preservation; SVG path tries bundled resvg then falls back to rsvg-convert
- Add compose_seed_docker_manager_icon(): copies cached PNG into both Docker
  Manager RAM and USB image cache directories so the Docker tab shows the
  correct icon for compose-managed containers
- Wire compose_fetch_icon_to_cache + compose_seed_docker_manager_icon into
  composeResolveContainerIcon (already in place in Exec.php)
- Add helper compose_icon_ext_to_mime() and SSRF-safe compose_icon_is_safe_host()
- Add regression tests: PNG data URI caching, idempotency, rejected paths,
  local file caching, SVG graceful-fail, Docker Manager seeding
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment