validateSystemConfiguration ran `bash -n` over every file two levels deep
under configs/, so a data file in a directory with no .category marker was
reported as "does not parse as shell" — a configuration problem about a file
nothing executes, pointing whoever read it at the wrong thing.
Apply the same rule the loader uses: a file in a SUBDIRECTORY is judged only
when that directory carries .category. Files directly in configs/ are checked
as before.
No behaviour change for any real config — every category (webui, general,
security, backup, network) carries the marker.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The main sweep — ~260 call sites across ~100 files move from string
concatenation on a single root to appDir/storageAppDirs/storageAppConfigs.
On a single-root install the resolved paths are identical, so this is a
no-op until a location is registered.
Enumerators were the interesting half. `for d in "$containers_dir"/*/`
appears in the menus, the registry/artifact scanners and the DNS setup —
and a shell glob cannot list a rootless 751 tree at all, which is the
same bug config_find_file.sh already documents in a comment. Routing them
through storageAppDirs (which enumerates as the owning user) fixes that
alongside the multi-root work.
Three places needed judgement rather than substitution:
db_app_scan.sh deletes database rows and port allocations for apps whose
folder is missing, and reaps "empty" app dirs. With a storage location
unmounted, every app on it looks exactly like that. Each of those
branches now gates on appStorageAvailable first — an app on an unplugged
drive is skipped with a notice, never deleted.
instance_create.sh rewrites cloned hooks so an instance touches its own
directory instead of the base app's. Its sed matched ${containers_dir}<type>,
which this sweep just replaced with $(appDir <type>) — so it would have
silently stopped redirecting, and an instance would have written to the
original's files (the adguard auth adapter case its own comment warns
about). Now matches both appDir forms, verified against bare, quoted,
unrelated-app, legacy and prose cases.
peer_shell/peer_pull streamed and extracted relative to the primary root.
Both now use the app's own root, and peer_shell keeps a single-root
fallback since it runs as a restricted SSH shell with no LibrePortal env.
Also fixes a pre-existing bug found on the way: webui_app_config.sh
tested "$containers_dir/frontend/data/last_update", one level short of the
real tree under the libreportal app dir, so the WebUI refresh trigger
after a config update has never once fired.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Speedtest's config held one password while its container ran another, so the
WebUI credentials card advertised a login that could not work. Validation only
caught it by accident: the rename left a stale tag behind, and the tag-name check
fired on that. Had the rename kept the name, the divergence would have been
invisible — and it is the divergence, not the tag, that actually breaks someone's
login.
So compare them directly: for every app-prefixed tag in the DEPLOYED compose,
check the substituted value against the deployed config's. Live files only —
in the templates one side is a placeholder and the other a RANDOMIZED token, so
they could never agree.
Resolves the slot rather than giving up: a compose written before a key gained
its _<n> suffix still carries the old tag, so fall back to the numbered variant
and compare anyway. That is warned about, not passed over — the compose is due a
re-template — but the warning is separate from the failure, so a stale name with
matching values reports only the warning.
Verified both ways against a fixture of speedtest's real pre-fix state (warning
plus failure), the same fixture with values agreed (warning only), and the live
install across 39 apps (silent).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two faults only visible by running the real command rather than the harness.
validateAppConfiguration never built the source index — that happened in
validateAllConfigurations. Called on its own the index was empty, so every tag
filled by a hook instead of a CFG key read as unbacked: `validation app matrix`
reported MATRIX_RUN_UID_TAG and MATRIX_RUN_GID_TAG as failures that
`validation all` correctly passed. A validator that contradicts itself depending
on how it is invoked is worse than one that is merely wrong.
It also printed nothing on success, so a clean single-app run looked identical to
one that never ran. It now reports either way, while the all-apps loop marks
itself so the per-app summary stays out of the bulk output.
Verified against the live install: matrix and mattermost both clean per-app, 39
apps clean under `all`, and running any subcommand mutates nothing (the
deliberately-kept AUTH_PROFILE orphans from configBackfillAllApps survive it).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The app-scoped Tasks tab never sorted. It rendered straight from
tasksManager.tasks and relied on loadTasks() having ordered it, so any path that
appends after the load — a task arriving from the event bus, a retry, a queue
merge — put that task wherever it happened to land rather than at the top. Sort
where the list is rendered instead of trusting it from three callers away.
Honest note on the reported symptom: a list_users task appearing mid-list could
not be reproduced from the stored records — replaying the sort over all 96 task
files puts the newest tool tasks first. What is demonstrably wrong is the
missing sort above, and a second latent fault it would mask: 8 of those 96
records carry a null createdAt (cron-created backups), and `new Date(null)` is
the epoch, so they sort as if from 1970 rather than as unknown.
Adds window.taskSortTime for that: createdAt when it parses, otherwise the
timestamp already embedded in the task id — the WebUI mints
task_<epoch_ms>_<rand> and the backend task_<epoch_s>_<hex>, distinguishable by
digit count. All three sorts now use it, so the global list, the app list and
the loader agree.
The filter bar is client-side over the already-loaded per-app array, so it is
instant and needs no reload: status chips (built from the statuses actually
present, with counts, so a chip can never return zero) plus a search over the
command and the task id — the id being what a deep link and a log URL both
carry, so pasting one finds it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The duplicate-value check strips quotes off the value, then looked the keys back
up with grep -F "=$value" while the file stores ="$value" — so the lookup never
matched and the failure read "these keys share one value: — a secret should never
be reused", naming nothing. A failure report that cannot tell you which keys
collided is barely better than no check.
Found while confirming the check still holds now that configBackfillAllApps
(741edfd) resolves RANDOMIZED<n> during an update as well as an install, which
gives a shared placeholder a second way to reach a deployed config.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`libreportal validation app|system|all|status` dispatched to four functions that
were never defined anywhere and were absent from the manifest, so every
subcommand failed. They exist now.
The checks are the ones that would have caught the bugs found while auditing the
credential rework, all of which were invisible at runtime — a mis-declared key
does not crash, it silently stops working:
* two keys sharing one RANDOMIZED<n>, which gave Gitea's metrics token and its
admin password the same value
* a generated key with no slot number
* an annotation whose value is absent from its line body, so the tag can never
substitute — how 0.1.0 Mastodon shipped a placeholder as its live password
* an auth adapter persisting a key the config does not declare, making every
password reset a silent no-op
* duplicate keys, keys under the wrong app prefix, and compose tags with
nothing to fill them
Verified both directions: clean across all 39 apps today, and each of the seven
bug classes above is caught when reintroduced into a scratch copy of the catalog
(including the real 0.1.0 mastodon compose pulled from git history).
Version tags are exempt from the backing-key check: the updater builds both the
CFG name and the tag name from the slug at runtime, so neither literal exists to
find.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>