Phase 2 and 4 of docs/roadmap/storage-locations.md. Apps can now be
placed on a location and moved between them.
CFG_<APP>_STORAGE lands in all 37 app templates, holding a location NAME
rather than a path: names survive a migrate to a host with different
disks, paths do not. The 11 infrastructure apps that other apps reach by
literal path (traefik, prometheus, grafana, adguard, gluetun, crowdsec,
headscale, dashy, pihole, unbound, wireguard) are pinned. libreportal
itself never gets the key — it is pinned structurally by webuiDir.
Pinning needed no second config key. "Pinned" is not a fact about a value,
it is a statement about whether the field may be edited, so it goes in the
comment beside **ADVANCED** and **DEV** as **READONLY**, and the field
factory renders those disabled. That marker earns its keep beyond this
feature: derived fields already warned in prose that editing them does
nothing (crowdsec.config:72) next to a perfectly editable input.
storage_app_config.sh keeps the comment honest — it carries the resolved
path for hand-recovery and regenerates the dropdown from the registry, but
only writes when something actually changed, since the app .config is
user-editable and lives in the container-owned tree.
app move stops the app (a live copy of a running Postgres is a corrupt
copy), snapshots it, copies, verifies, and only then removes the source.
The copy runs in libreportal-ownership because it must: app data holds
rootless sub-UID files the manager can neither read nor recreate.
Verified against two real ext4 filesystems that a cross-device move
preserves uid 231141 and the payload, and that the source survives every
refusal path — unregistered destination, the WebUI app, a traversal in the
app name, and an occupied destination.
Task titles registered in both tables, with a specific rule so a move
renders as "Nextcloud - Move to bigdisk" rather than the generic fallback
dropping the destination. lp-task-names could not be run to confirm — it
borrows the WebUI container's node and no containers are running.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two recurring console errors, each masking real ones.
"Identifier 'BackupAppCard' has already been declared": the app has two
script loaders that could not see each other's work. The boot-time
system loader injects tags with NO id and dedupes by script[src]; the
SPA's loadScript injects with an id and deduped ONLY by that id. So a
file both of them load — backup-app-card.js is on the boot list AND in
the overview Backups tab's asset chain — loaded twice whenever the SPA
loaded second: its getElementById check cannot see an id-less tag. The
class redeclared with an uncaught SyntaxError. The blindness was
one-directional, which is why it looked sporadic. spaClean.loadScript
now also matches by src, closing the asymmetry for every shared file,
not just this one.
"ConfigSidebar: config-categories-list element not found", three to
four times per navigation: renderConfig populated the admin sidebar
unconditionally, but the backup center's Configuration tab EMBEDS that
renderer inside its own pane (renderConfig('backup', <target>)), where
the sidebar element legitimately does not exist — and the refresh
coordinator re-renders that tab on every task event. Populating is now
skipped when a target is passed: an embedded render does not own the
admin shell. The error stays for target-less renders, where a missing
sidebar genuinely is a broken shell.
Verified in a live session: repeated sweeps across /tasks, the overview
Backups tab (Configuration sub-tab included) and a per-app Backups page
produce zero new occurrences of either error — confirmed against a
live console buffer via a sentinel, since the buffer retains the
pre-fix history and re-reading it can look like recurrence.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four rules rendered their bubble with `content: attr(title)` while leaving the
title attribute in place, so the browser drew its native tooltip over the styled
one — two bubbles, offset, one of them unthemed. That is the overlap.
setup-wizard.css already had it right with attr(data-tip). Everything else now
matches: the text moved to data-tip, which the browser does not render, and
aria-label carries it for assistive tech. 24 emitters migrated across 9 files,
including two that assign the text at runtime — those set both attributes so
neither goes stale.
The same sweep found the bubble was defined twice globally. core/forms/css had
the plain one; port-manager.css carried a nicer bordered, wrapping version
commented "Enhanced tooltips for main config help icons" — which never applied
to them, because forms.css loads afterwards at equal specificity and won. So
every help icon outside the port manager had been getting the plain bubble, with
`white-space: nowrap` and no max-width: a sentence-length tooltip rendered as one
unbroken line running past its own background. The wrapping version is now the
one in forms.css, the shadowed copy is gone, and the .port-manager-scoped rule
stays.
Verified on Bookstack's config page: 26 help icons, none carrying a title, all
carrying data-tip, and hovering one flips ::after content from `none` to the text
with a single themed bubble on screen.
No port-descriptor tooltip, deliberately. PORT_N fields are replaced at runtime
by the port manager, whose hidePortFieldLabels() sets the field's help icon to
display:none — anything put there is unreachable, confirmed in the DOM. The
format is already documented column by column by the port manager's own 13 help
icons, several in more detail than a single descriptor string could manage.
When the official catalog is hidden, its "Re-enable" action rendered as a
flat, greyed-out ~27px pill pinned to the top-left of the row — shorter than
the 44px URL field and not obviously clickable (the generic .btn-secondary
loses its nebula-theme polish outside .config-actions, so it fell back to an
unstyled base button).
- The action column now align-self:stretch, so its control matches the URL
field's height and sits inline (also vertically centres the compact × in the
enabled state).
- The Re-enable button gets a dedicated .catalog-reenable-btn treatment: accent
fill + accent border + hover state + an eye icon, filling the row height so it
lines up with the input and clearly reads as a button.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Backups tab's Configuration sub-tab showed only the warning banner and
an orphaned divider -- the actual config form (retention presets, engine,
keep-policies, Save/Reset) never appeared.
Root cause: a duplicate id. The Fleet Overview, the app grid and per-app
detail all render the shared apps-unified-layout, which keeps a hidden
#config-tab > #config-section in the DOM. The Backups Configuration sub-tab
created its OWN #config-section, and configManager.renderConfig() resolved
the target with getElementById('config-section') -- which returns the FIRST
(hidden) node in document order. So the 21KB form painted into the invisible
per-app container while the backup tab's container stayed empty. Because the
/backup routes now redirect into /apps/overview/backups, this fired every
time -- the form had effectively never been visible.
Fix:
- renderConfig(category, target) now accepts an explicit container (element
or id) and falls back to #config-section; the error-retry button re-targets
via configSection.id.
- The Backups Configuration sub-tab uses a unique id (#backup-config-section)
and passes it to renderConfig; retention-preset and engine-details
selectors are scoped to it.
Also: the Refresh button now spins + locks while refreshAll()+render() run,
so the click has visible feedback instead of appearing to do nothing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Feedback: the removal warning was an overwhelming paragraph and made it look like
updates might break. Keep the official catalog removable, but present the effects
as a clear two-part list instead of prose:
✓ Still works: system updates & hotfixes, installed apps, re-enabling any time
✕ What changes: official apps hidden from the App Center "Available" list
Backed by a new optional messageHtml arg on the shared confirmation dialog (dev-
controlled HTML body) + styles for the effect groups. Required tickbox kept.
Signed-off-by: librelad <librelad@digitalangels.vip>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
formatSubcategoryName title-cased naively, so section headers read "Ssh",
"Dns", "Libreportal". Add a whole-word acronym/brand map so they render
professionally as "SSH", "DNS", "LibrePortal" (also covers VPN/API/IP/UI/URL/
HTTP(S)/TLS/SSL/WebUI for future files). Composes with stripCategoryPrefix,
which runs after and is case-insensitive.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
- Info banner: drop the borrowed traefik warning banner (icon sat above the
text, amber-warning styling) for a .catalog-note — icon inline with the text,
neutral boxed look matching the content sections.
- Official row (#1): render the release-host URL in a read-only input (same
element/height as the third-party rows) with the "✓ Official" badge overlaid,
instead of short text — so it shows the URL and its box height matches the
others. URL is read from the loaded config (CFG_RELEASE_BASE_URL).
Signed-off-by: librelad <librelad@digitalangels.vip>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Config sections (Basic, Mail, DNS, Firewall, …) rendered as a bare name. Add a
small data-driven icon before each: config files now carry a `# @icon <emoji>`
directive in their header, the config generator surfaces it as the subcategory's
`icon`, and the config editor prepends it to the section <h3> (covers every
render path — regular/master/toggle/domains/catalogs/whitelist — since they all
drop displaySubcategory straight into the heading).
- webui_generate_configs.sh: parse `# @icon` (first 10 lines) → emit "icon".
- config-manager.js: prepend subcategoryData.icon to the section title.
- configs/**: added an @icon to all 23 base config files (backup/general/network/
security/webui). The live install's config files get the same directive + a
configs.json regen so existing installs show the icons too.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
- Catalog block now shows the official catalog as a pinned row #1 (verified
badge + its URL from the generated data). Removing it opens the shared
confirmation dialog with a REQUIRED tickbox (generalized to accept custom
checkbox text) — it only hides official apps from browsing; system updates are
unaffected. Backed by a new CFG_CATALOG_OFFICIAL_ENABLED flag the scan honors.
- Trimmed the info-banner wording per feedback.
- Config option sections: the body (after the divider, before the buttons) now
sits in a darker boxed panel (bg + border), matching the content-box look used
elsewhere. Applies to regular grids + the block editors; scoped to the main
config editor.
Signed-off-by: librelad <librelad@digitalangels.vip>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the Catalog Sources block to the config editor (General → App Catalogs),
mirroring the domains block UI: add/remove up to 9 catalog URL slots, saved by
the generic config-form (any CFG_* input) → configUpdateBatch, which rebuilds
registry_catalog.json. Official catalog stays source #1 (not editable here);
third-party rows carry an "unverified" note.
- new catalog-manager.js (lean CatalogManager; reuses the domains block CSS,
URL inputs, no DNS checks) + window.addCatalog/deleteCatalog globals.
- config-manager.js routes the general_catalogs subcategory to it.
- system-loader.js loads catalog-manager.js before config-manager.js.
Signed-off-by: librelad <librelad@digitalangels.vip>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Admin Dashboard (OverviewPage) and Admin System (SystemPage) boards each
blanked the whole content pane to a bare headerless lpLoadingBox() on init(),
then rendered the .page-header + panel only once data arrived. So clicking
either tab flashed a full-pane spinner with no indication of which page you'd
landed on, then everything appeared at once.
Extract the page header into a _headerHtml() helper on each page and render it
up front in both the loading state and render(). Now navigating to a board
paints its header (breadcrumb + title + description) instantly and the spinner
is confined to the .admin-panel beneath it — the header stays put and only the
content swaps in when the fetch resolves.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Prepend a boxed app icon (tasks-notification style) to each row of the
System page's Per-app usage table. Multi-instance slugs (<type>_<id>)
resolve to the base type's icon; missing icons fall back to default.svg.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Pages and panels showed inconsistent loading states: the Backup center
and several admin pages (System, SSH, admin Overview), the Overview
Migrate/Peers panels, the per-app updater section and the backup engine
details modal rendered a bare 'Loading…' text line (updater-empty /
backup-empty-state) with no spinner, while Services/Config/Tasks used a
boxed card + spinner.
Add one shared loader — window.lpLoadingBox(message) + .lp-loading CSS in
the core/loading subsystem (the boxed card + accent spinner the good tabs
already use) — and route those bare-text loaders through it. The system
metric graph keeps its absolute overlay but gains the same spinner.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Empty-state 'no data yet' messages rendered at rgba(text, 0.55) (and one at
text-muted), which is hard to read on the dark recessed panels. Switch them to
the theme's --text-secondary muted token so they stay de-emphasized but legible.
Covers .updater-empty, .updater-detail-empty, .sys-detail-empty, .eo-modal-empty.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Clicking Reclaim space fired two notifications: routeAction → executeTask
already shows the rich 'Reclaim Space task started!' toast (icon + bold
LibrePortal + task link), then _reclaim added a second, plain, iconless
'Reclaiming space…' info toast on top of it. The image-removal path doesn't
double-notify like this. Drop the redundant one — the start toast plus the
completion toast give clean feedback on their own.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
.sys-detail-loading and .sys-detail-empty are absolutely-positioned overlays
that JS shows/hides via the `hidden` attribute. Their .sys-detail-* rule sets
`display: flex`, an author declaration that overrides the UA
`[hidden] { display: none }` — so `el.hidden = true` never actually hid them.
Both the 'Loading history…' and 'No samples in this range yet' overlays stayed
painted on top of a fully-populated chart (overlapping into garbled text).
Add `.sys-detail-loading[hidden], .sys-detail-empty[hidden] { display: none }`
(higher specificity than the bare class) so the hidden attribute wins.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
The donut legend collided into the stat cards and the image-row details
were squished on tablet widths — the 220px sidebar leaves the content
cramped while the old breakpoints assumed full-viewport width.
- Headline collapses to a single column at <=1024px (was 800px), the two
stat cards reflow side-by-side, and on phones the donut stacks above its
legend with one stat per row. Legend labels now ellipsis instead of
overflowing into the stats.
- Image rows group the name+pill and the size/shared/age metadata so the
metadata drops onto its own line under the name at <=1024px instead of
competing for width; on phones the Delete button collapses to an icon.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
The Images list on /admin/system/storage now has an All / In use / Unused
segmented filter (with live per-group counts), and the default All view
sorts in-use images to the top — the ones you can't reclaim lead, the
reclaimable ones follow. Select all / Clear All act on the visible rows,
so they honour the active filter.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
The .sys-tasklist panel spaces rows with a flex gap, but each .task-item
also carries margin-bottom: 10px from the shared tasks styles. That margin
only stacks on the last row, so the list had ~8px above the first row and
~18px below the last row, looking lopsided. Zero the row margin inside the
list so spacing is symmetric.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
The fleet Overview area (Overview/Updates/Improvements/Backups/Migrate) now
lives at /apps/overview* instead of /overview*, reflecting that it belongs to
App Center. The Backups tab is therefore /apps/overview/backups, and the old
standalone Backup Center page is removed entirely:
- apps feature owns /apps/overview* (covered by the existing /apps* route); its
mount() dispatches /apps/overview -> fleet Overview before the grid check.
- _legacyRedirect() rewrites old short URLs so bookmarks/links keep working and
the address bar shows the canonical path:
/overview[/tab] -> /apps/overview[/tab]
/backup[/sub] -> /apps/overview/backups[/sub]
/updater, /peers redirects retargeted to /apps/overview*
- Removed the standalone backup feature: components/backup/{index.js,feature.json},
its manifest entry, the /backup route registrations and the dead handleBackup().
The BackupPage classes stay — the Overview Backups tab embeds them.
- Repointed every backups/overview link: the admin dashboard's 'Open backup
center', the app-card 'Open backup center' button + snapshot-overflow link,
the sidebar Overview entry, the improvements deep-link, and the Migrate
'go to locations' deep-link.
Also drop the redundant inline Check button from the Security empty state
(same rationale as Improvements: the host auto-scan repopulates it and the
header carries a manual Check).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Apply the same recessed-panel treatment as the Dashboard/Tasks to the
System page: gauges, Trends, Storage, Host and Per-app now sit inside one
dark rounded box under the header divider. Generalise the Dashboard's
.admin-card-grid-wrap into a shared .admin-panel class so both admin pages
use one source of truth.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Match the fleet Overview's .ov-tab-body treatment: the /admin/dashboard
card grid (under the header divider) and the /tasks list now sit inside a
recessed dark rounded box instead of floating directly on the page
gradient.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
The Backups tab's embedded BackupPage repeated the active section as a
big page header (icon + 'Dashboard' + subtitle) right under the nested
strip that already names it. Embedded-scoped CSS now hides the title
block and flips the header below the body (flex order), so its actions
(Refresh + per-section primary) become a bottom-left footer row — the
same place app-detail tabs keep theirs. The export dropdown flips to
open upward from the footer. The standalone /backup page is untouched.
The Migrate ▸ Peers sub-tab drops its page header (breadcrumb + title
+ blurb) the same way: the peer list/empty state now sit in the shared
recessed .ov-tab-body container with the four actions in a bottom-left
.peers-actions footer.
Signed-off-by: librelad <librelad@digitalangels.vip>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The global tasks page still deep-linked a single task with a ?task=<id>
query while the rest of the SPA moved to path-based permalinks
(/app/<name>, /admin/<…>). Bring it in line: the task is now a path
segment, /tasks/<category>/<id>.
Task ids are guaranteed `task_<digits>_<base36>` (isValidTaskId), so the
redundant `task_` prefix is dropped in the URL and restored on read via a
new window.taskPath / window.taskPartsFromPath helper pair (mirrors
appPath/appPartsFromPath). The parser still accepts the legacy ?task=
query and the full-prefixed id, so old links, bookmarks and notifications
keep resolving.
Updated every builder (tasks-manager updateURL + notification url,
task-id link, task-actions, admin config-form, setup-wizard handoff with
its &from=setup flag) and the notification navigation handler / button
text to recognise the path form.
Signed-off-by: librelad <librelad@digitalangels.vip>
- Restore empty-state 'Open Locations' now deep-links to the backup center's
Locations sub-tab: the embedded center honors /overview/backups/<sub> and
switchTab()s to it after mount (was landing on Dashboard).
- PeersPage.notify + MigratePage.notify use the real window.notificationSystem
(were calling a never-defined window.showNotification → console-only).
- Remove the now-dead Admin config-manager peers branch (Peers left Admin).
- Trim the dead migrate.json/peers fetch + hostnameToPeerName from BackupPage
(no consumer after the migrate removal).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Remove the Admin sidebar Peers entry; /peers and /admin/tools/peers now
redirect to /overview/migrate/peers (its new home next to cross-host Restore).
- Re-skin the embedded Backups center's sub-tab strip from pills to the per-app
Config .tabs-list/.tab-button segmented look (full-width bar, accent underline)
so every nested sub-tab row is consistent.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Instead of a glance + 'Open backup center' button, the Backups tab now mounts
the real BackupPage (dashboard/snapshots/locations/migrate/configuration) inline,
with its sidebar restyled as a nested tab strip and its own header taking over.
- BackupPage gains an embedded mode (opts.embedded): no /backup URL coupling, so
sub-tabs switch in-page under /overview/backups. Backward compatible.
- OverviewManager lazy-loads the backup bundle + fragment on first open, news a
BackupPage({embedded:true}), and disposes it on apps-feature unmount. Colliding
ids (#sidebar/#mobile-overlay) are stripped on inject.
- Revert the Admin backup-config surface — the embedded center (incl.
Configuration) is now the single home for backup settings.
- The updater needs no equivalent: its sections were already unpacked into the
Overview/Updates/Improvements tabs + the per-row expander.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- _legacyRedirect: /updater[/tab] -> /overview[/tab] (security/recovery/history
fold into the Updates expander -> /overview/updates). /backup is intentionally
NOT redirected — it stays the operational backup center (locations/migrate/
snapshots), reached from Overview › Backups.
- Re-point the per-app hotfix chip to /overview/improvements.
- Unhide the existing backup config category in the Admin sidebar so
engine/schedule/retention config lives under Admin (same generated category
the backup center binds, so edits stay in sync).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The teardown audit found the backup-stacking leak class across 4 more feature
modules (12 confirmed leaks); unmount() left document/window listeners, intervals,
and SSE subscriptions firing on stale controllers after navigation:
- admin: overview/ssh/peers/system each leaked a document click listener ->
AbortController + dispose() per page; admin unmount() aborts each.
- dashboard: the 1 Hz update-countdown interval + the LiveSystem view sub ->
stopUpdateCountdown()/detachDashboardLive(), registered via ctx.sub().
- tasks: constructor-started global live-log poller (discarded handle) -> stored
+ idempotent + cleared on unmount + re-armed on mount; per-task monitorTask
window listeners + interval -> tracked in a map, released on unmount.
- apps: app-tabbed reconcile setTimeout loop + watchdog window/document listeners
+ popstate -> per-instance AbortController + dispose() that clears the timer,
resets the guards, and unloads the active tab's Services intervals + log SSE.
All mirror the kernel's MountContext teardown discipline. 12 files, all pass
node --check. Backup (fixed earlier) re-confirmed clean by the audit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
The shipped frontend carried ~600 muted '// console.…' debug statements (and
their multi-line commented continuation lines) left over from development —
clutter across 30 files. Removed them with a guarded pass that ONLY ever deletes
lines starting with // (so it can never alter behaviour), consuming each
commented console opener plus its continuation comment lines until the
string-stripped parens balance.
665 lines removed, 30 files; 0 insertions. Verified every deleted line is a //
comment (no code touched), real prose comments preserved, full node --check
sweep clean.
Signed-off-by: librelad <librelad@digitalangels.vip>
From the feng-shui audit naming findings:
- admin/overview/js/admin-overview.js -> overview-page.js (class AdminOverview ->
OverviewPage, window globals + the 'admin-overview' task-refresh id ->
overview-page, lazy-load path + typeof/new in config-manager.js).
- admin/system/js/admin-system.js -> system-page.js (class AdminSystem ->
SystemPage; now sits beside its -page sub-views system-metric-page.js /
system-storage-page.js).
- tasks/js/tasks-logs-modal.js -> tasks-log-modal.js (singular 'log' to match its
sibling tasks-log-stream.js; single path ref in system-loader.js).
These were the only page controllers breaking the dominant <thing>-page.js /
<Thing>Page convention (ssh-page/peers-page/backup-page/updater-page/
system-metric-page/system-storage-page). Pure renames; node --check clean.
Signed-off-by: librelad <librelad@digitalangels.vip>
Comment-only tidy from the feng-shui audit — no code behavior changes. The
features/ directory was renamed to components/ during modularization, but
several header banners and inline comments still named the old path:
- 6 component module headers (admin/tasks/backup/dashboard/updater/index.js +
updater/js/updater-page.js) now name their real components/<id>/… path
- core/kernel/js/spa.js + core/tasks/js/task-router.js comments
- backend/routes/features.js doc-banner (drop a components/<id>/ folder …)
- core/update-notifier/css/update-notifier.css header (js/update-notifier.js)
Guarded the rewrite so the LIVE /api/features/list endpoint name (feature-
registry.js sources + backend route) is untouched — only stale 'features/<path>'
directory references were updated.
Signed-off-by: librelad <librelad@digitalangels.vip>
The config-category icons sat at admin/config/icons/CONFIG/ — the inner config/
duplicates the subsystem name; they belong in the icons root. Moved all 6
(backup, features, general, network, security, webui) up to
components/admin/config/icons/ and updated the two consumers (config-manager.js
header icon, config-sidebar.js category icons).
Also fixed the backup-engine logos: scripts/backup/engines/{restic,kopia,borg}
.json pointed 'logo' at /icons/config/backup.svg — a path that 404'd on two
counts (missing the components/admin/config prefix AND the now-removed config/
nesting), so the engine-details modal logo silently hid. Repointed to the real
served path /components/admin/config/icons/backup.svg.
(Left the meaningful icon groupings alone — admin/system/icons/{cpu,os} and
apps/core/icons/vpn are vendor/OS/provider logo sets, not redundant nesting.
The backup engines borrowing an admin-config icon is a minor smell; a dedicated
backup-engine icon could replace it later if wanted.)
Signed-off-by: librelad <librelad@digitalangels.vip>
admin had sub-system folders (config/overview/system/ssh/peers) AND a bare
root css/ (admin.css) — inconsistent. A component's shared base belongs in a
core/ sub-folder once it has sub-systems (matching apps/core/), so admin.css
moves to admin/core/css/admin.css. Audit confirms all components are now
consistent: sub-system components (apps, admin) use core/ + sub-systems;
single-page components (backup/dashboard/tasks/updater) stay flat js/css/html
(no sub-systems, so no core/ needed).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
De-clutter each component into sub-system folders (apps: core/ port-manager/
services/ tools/ routing/; admin: config/ overview/ system/ ssh/ peers/) with
the standard js/ css/ html/ icons/ layout inside; single-page components
(backup/dashboard/tasks/updater) get js/ css/ html/. Single-feature icon sets
moved into their sub-system (vpn -> apps/core/icons, config/cpu/os ->
admin/{config,system}/icons); shared app + category icons stay in core/icons.
feature.json + index.js stay at each component root (the scanned descriptor +
entry). Every controller/CSS/fragment/icon path reference rewritten; verified
no stale refs, all JS valid.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>