Appdata Cleanup Plus targets Unraid 7.0.0 and later. Supporting older Unraid releases requires a separate PHP, DockerClient, installer, theme, and browser compatibility matrix; the minimum version has not been lowered based on another plugin's declaration.
Historical ZFS audit exports alias dataset, child-dataset and snapshot identifiers independently of the current scan. A first pass collects identifiers across the history before sanitizing result/entry messages and impact summaries. Server collection and browser download regressions verify historical-only datasets, parent/child overlap, snapshot counts, consistent aliases, and unchanged stored audit records. Browser exports also sanitize audit history received from older servers.
Diagnostics schema 5 additionally distinguishes verified-empty lock storage, missing runtime storage and refused inspection, using fixed ownership/permission/link/readability reasons. Runtime JSON health uses the secured reader with a 5 MiB bound. Cache diagnostics identify runtime storage, the TTL and current/expired/invalid entry counts; unavailable counts are null, and a missing disposable cache is informational. Neither cache keys nor stored paths are exported. Scan metrics record mount-check counts (not unique-folder counts), with separate unavailable/empty/invalid inventory and filesystem-metadata reasons; row decisions retain the relevant blocker. A timestamped export-time mount-inventory probe is separate from the latest scan and cannot overwrite its evidence. Older metrics report mount inspection as not checked. Runtime and browser regressions exercise these fields, refused inspection severity, privacy and read-only collection without repairing permissions, creating locks or rebuilding/pruning cache data.
Runtime directories retain their existing location and lock inodes across updates. Writes validate every ancestor, reject symlinks, unexpected owners and unsafe write permissions, and make trusted runtime directories/files private. Read-only status calls do not create, chmod or recover locks. A root-owned sticky temporary parent is permitted; an unsafe existing runtime child is refused without being repaired. Persistent settings, audit history, quarantine records and backups keep their existing storage. The disposable path-stat cache now lives in runtime storage, retaining its 15-minute expiry and 512-entry limit; the old persistent cache is ignored and left untouched. A reboot rebuilds the cache on demand.
Mount checks distinguish verified directories, verified mounts and unknown results. Missing, empty or malformed mount information and failed filesystem metadata checks block actions, including candidates with an exact ZFS mapping. Native tree inspection still detects same-device and nested mounts. tests/runtime_safety.php exercises planted runtime symlinks/hard links, unsafe permissions, status read purity, cache rebuilding, and mount failures with synthetic data. Owner-run checks must confirm upgrade/reboot behavior, concurrent UI/cron locking, permissions and mount failures on Unraid. If runtime validation refuses an existing path, stop active plugin work and inspect its ownership, permissions and symlink status before correcting it; never recursively chmod or delete an unverified path.
Broad container access is disclosed in Details and omitted from the Source column to keep candidate rows compact. It remains informational and does not change ownership or selection rules. Browser tests verify its absence from Source, expansion in Details across themes and locales, mobile width, selection behavior and escaping.
Repository CI checks PHP and JavaScript syntax, direct plugin function resolution, shell scripts, packaging and canonical URLs, diagnostics privacy, destructive confirmations, isolated filesystem behavior, localization catalogs, and rendered UI message flows. Dedicated ownership and template tests cover Docker list success/failure, mount overlap, action-time ownership changes, supported Compose syntax, template revision changes, verified backups, and restore collisions. Test data is synthetic and never points at a live Unraid server.
The Playwright layout suite runs in CI and exercises desktop/mobile rendering for the bundled locales. Passing catalog and rendering tests does not establish native-speaker review: the catalogs are machine translated. The human review process tracks exact safety phrases and rejects stale approvals independently of structural coverage.
tests/recovery_backups.php checks cross-process lock/status recovery, partial outcomes, duplicate operation IDs, reminder retention, private backup export/import checksums, malformed/unsafe XML, filenames, collision-safe restore, and explicit backup removal. tests/recovery_ui.cjs exercises the new recovery and private backup surfaces in all 42 locales at desktop/mobile widths, including lost action responses, stale callbacks, confirmation checkboxes, and separation from diagnostics. Runtime progress is volatile across reboot; persistent audit/quarantine data survives uninstall. Real-server connection-loss and uninstall/reinstall acceptance remain owner-run checks.
Modal regression coverage uses 24 synthetic quarantine entries and repeats Quarantine -> Help -> Appdata Sources on desktop and mobile. It checks that every entry and disclosure stays inside its replaceable container, previous content disappears, the source list scrolls, and Done receives pointer clicks. Linux behavior tests exercise configured source links to dedicated pool folders, rejected links outside /mnt or to pool roots, and preservation of external targets behind linked fixture children and marker files.
Diagnostics schema 5 includes a path-free serverDiagnostics.state.fixtureRoot summary: root availability, candidate and symlink counts, and fixed reason counts (ready, missing, not_writable, unsafe_symlink, unsafe_path, no_sources). uiState.fixtureTools records the last browser status and whether its request failed. Recent failures identify fixture status, create, or remove requests and an allowlisted root reason when supplied by the server. These fields contain no response bodies, source names, or root paths; unknown future status fields are omitted. Exporting this evidence does not create fixtures or scan Docker/ZFS. A usable fixture root and an orphaned scan candidate are separate concepts.
tests/theme_ui.cjs runs in CI using pinned Playwright and jQuery test dependencies. It loads official Unraid stylesheets pinned to 7.0 commit 25bc9e2d16525e4e4f9796c9c64c1a3e1c63402d and 7.3 commit 41e8d5ad5db20b0e9aeea4334b39bea56729579a, and checks Black, White, Azure, and Gray at desktop/mobile widths. It compares native page/input colors and backgrounds, verifies the plugin's flat button styling, exercises Help, Details, sources, ZFS mappings, Tools, quarantine, history, confirmation, progress, results, and restore conflicts, and verifies reused dialogs and live theme changes. Stylesheet fetch failures fail the test. The reference CSS and browser dependencies are not shipped in the plugin. For local runs, set ACP_BROWSER_MODULES to the tooling node_modules directory; optional ACP_NATIVE_THEME_DIR supplies downloaded reference files under 7.0/ and 7.3/ (replace / in filenames with -).
The plugin consumes Unraid's semantic surface and text variables directly, while retaining its own flat, rounded buttons with neutral text and subtle borders. Theme tests check that native gradients cannot override plugin buttons, including hover, disabled, and dialog states; native buttons outside the plugin remain unchanged. Scoped native 7.0 palette fallbacks cover releases before those variables existed. White and Azure are light themes; Black and Gray are dark themes. Page and modal tokens share a single definition, and modal reuse copies theme identity rather than freezing individual colors. Status colors adapt to light/dark surfaces, while component layout remains scoped to the plugin.
Local fixtures and CI do not establish successful behavior on every real pool or Compose layout. The owner should confirm the install/update cycle, secondary pools, stopped Compose stacks, nested/broad container mounts, template archive/restore, and ZFS destroy previews on suitable recoverable data. Real dataset destruction requires separate owner-run validation. Never treat an unattended test suite as authorization to act on server data.
Docker ownership comes from a successful /containers/json?all=1 request, including stopped containers. Cleanup rechecks that inventory and Compose references at action time. A failure or incomplete inventory blocks cleanup. Docker can still change independently during any filesystem operation; plugin locks serialize plugin operations, not external Docker changes.
Broad access is a mount strictly above every configured appdata source containing the candidate. It is informational, not ownership. Mounts of the source itself, exact candidate paths, and parents/children within a source remain protective. Nested source settings cannot relax protection of an outer source. Unknown source roots remain conservative. Docker scan rows, Details, action-time revalidation, and Compose filtering share this boundary. Regressions cover the four-viewer /mnt/user and /mnt scenario, real fixture folder deletion, quarantine/delete previews, synthetic ZFS destruction, and a specific mapping added after scanning.
Nameless dead entries retain their known mounts without invalidating healthy containers. Explicit null mount collections are normalized; missing or malformed fields trigger at most 32 inspect lookups per inventory. Requests use unchunked responses and validate the complete JSON body with a 16 MiB acceptance limit. Malformed JSON must not be mistaken for a successful empty inventory. Known mappings remain protective when another record cannot be verified. Incomplete scans label their remaining rows Unverified; enabling permanent deletion does not bypass ownership verification.
Diagnostics schema 4 reports the recorded inventory reason code, request success, accepted/rejected counts, dead entries, normalized null collections, inspect recoveries, collector limits, and each candidate-filter stage. A successful request and a verified inventory are separate fields. Blocked action results also include aggregate ownership diagnostics. Exports use allowlists, omit raw Docker payloads and identifiers, and preserve fixed schema keys even when a private folder shares their wording. Generating diagnostics reads existing telemetry; it does not rescan Docker or perform cleanup.
Row exports distinguish blocking mountEvidence from informational broadMountEvidence. Each category includes at most 50 containers and 50 paths per container, with export-scoped aliases and only allowlisted name/path fields. The final recursive privacy scrub still applies; unknown future mount fields are discarded.
not_checked means the bundle predates collection or has no saved scan. request_failed identifies a request/transport failure; invalid_json and invalid_list identify unusable response bodies; incomplete_inventory includes fixed issue counts such as invalid_names, invalid_mounts, invalid_mount_source, unsupported_mount, inspect_failed, and inspect_limit. Rescan after addressing an underlying condition. These are support codes, not container names or raw errors.
Compose protection covers Compose Manager project files, indirect projects, supported short/long bind syntax, .env variables (including export), and override files. Relative ./ and . bind sources resolve against the base Compose file's directory, including sources in overrides and values expanded from .env. Directory and file binds retain their complete paths and protect candidate parents. It is a conservative scanner, not a complete YAML/Compose interpreter. Unresolved variables, traversal, relative binds without a usable absolute base or through unsafe symlink segments, and detected include/extends/inline-volume forms block cleanup until the configuration can be inspected safely. Additional syntax requires regression fixtures before claiming support.
Indirect projects support directory targets and explicit Compose file targets, including custom filenames and legacy file targets without an indirect_mode marker. File targets use their parent directory for .env and relative binds; unrelated default base filenames in that directory are not loaded. Adjacent and project-side overrides remain conservatively protective, using the same base directory. The selected file is required: missing/unreadable targets or invalid/contradictory mode metadata keep cleanup blocked. Regression tests cover relative directory/file binds, overrides, ignored default files, scan metrics, owned-path filtering, unrelated actionable candidates, and action-time blocking when ownership changes or a selected file disappears. Unreadable-file permission coverage requires non-root POSIX PHP and reports a skip on Windows or when run as root. On Unraid, confirm these flows with a stopped file-mode stack and verify that its appdata remains protected after reload and rescan.
A maintainer can apply the test-build label to a pull request. PR test package builds that exact head SHA with a read-only token and uploads a 14-day artifact whose name includes the PR number and SHA. Remove and reapply the label to request a new build after changes. Review the PR's normal CI results before installing its package.
Download the artifact from the workflow run and follow INSTALL.txt. Its standalone local .plg embeds the package built by pkg_build.sh, verifies the package checksum before installation, and retains the canonical dev update URL. It does not depend on an unpublished archive URL. No release, tag, branch push, or PR comment is created by this workflow.
After testing, reinstall the published dev manifest to return to the regular channel. Confirm the installed version and hard-refresh the webGUI. Test packages can have a version ahead of the currently published dev build, so use the explicit install command in INSTALL.txt rather than waiting for an update notification.