The Refresh menu can scope a scan to one model root, but the folder a user is
looking at lives in the sidebar's unified tree, which merges every root into one
relative-path namespace. "Scan this folder" therefore addresses the folder, not a
root: the backend walks that relative path under every root that holds it, which
is also what makes the action safe while another drive is switched off.
Backend:
* GET /scan accepts `folder=<rel>` (alone or with `roots=`), rejected together
with full_rebuild=true like the roots parameter. Validation reuses
normalize_relative_folder(), extracted to module scope from ModelMoveService so
the folder operations and the scan endpoint reject the same input (absolute
paths, drive letters, `..` climbing) instead of each carrying its own copy.
* The reconcile summary carries `scope_label` (the folder) for a folder scope, so
the result toast names the folder the user clicked instead of the roots it
happens to live under; the completed WS payload carries it too.
* `folder` is a scope prefix exactly like a root: only that subtree is re-read or
pruned, and an unreachable root keeps the entries that fall inside it.
Frontend:
* The sidebar folder context menu gains "Scan this folder" above "Check for
updates in this folder" (they share the refresh divider); the entry is gated by
the same supportsFolderManagement flag as the other folder operations.
* SidebarManager.scanFolder() resolves the node through the existing
_resolveFolderCandidates() before doing anything: a folder no root holds any
more explains itself ("no longer exists on disk") instead of scanning nothing,
and an unresolvable multi-root node is refused rather than guessed.
* PageControls.refreshModels() and BaseModelApi.refreshModels() forward the folder
scope, and _showRefreshSummary() prefers scope_label over the walked roots.
Verified live on the three-root sandbox with one drive switched off:
GET /scan?folder=pack000 walks drive-G and drive-Y, reports scope_label=pack000,
keeps drive-Z's 6 entries under that folder (kept_unreachable=6) and leaves all
420 models cached. 3704 passed, 7 skipped; frontend 1495 passed (150 files); vue
widgets 96 passed. The 2 new sidebar keys are [TODO: Translate] placeholders
pending the feature owner's go-ahead.
applyTheme() mirrors the active mode onto <body> as data-theme="dark", but
the theme preset is only ever written to <html>. The palette token blocks in
tokens/colors.css and base.css used the bare attribute selector
[data-theme="dark"], so <body> matched them on its own and re-declared the
default dark palette (#1a1a1a / #2d2d2d / ...) directly on the body, where it
shadowed the preset values inherited from <html>. Every non-default preset
therefore painted the selected accent over the default preset's background,
surface, text and border tokens, and flipped into that state ~200ms after
load, when initTheme() first touched <body> — the accent-tinted background
flash seen on reload and nav-tab switches. Reached only in dark mode, since
light mode has no [data-theme="light"] token block.
Scope the palette token blocks to :root so a data-theme attribute on any
descendant (only <body> has one) can no longer re-declare them; descendant
rules such as [data-theme="dark"] .foo still match through <html>. Add a
regression guard that fails on bare attribute token blocks.
`Delete folder` and `Delete Model` rendered as plain menu text: the rule
was `color: var(--danger-color)`, and that token is defined nowhere in
the stylesheet tree. A var() reference to an undefined custom property is
invalid at computed-value time, so the declaration does not fall back to
a default — `color` inherits, and it silently matched the surrounding
menu text.
Points both the label and its icon (which inherits the colour) at
`--lora-error`, the themed error token the delete buttons already use.
The shared hover paints the accent background, which a red label does not
read against, so destructive entries also get their own `--lora-error-bg`
wash.
The same dead token was masked by a hardcoded fallback in the settings
priority-tags validation state; those now use the themed token too.
Fixes all seven destructive entries at once — the four model-card menus,
the exclude/duplicates `delete-all` entry and the folder sidebar menu.
Guard: tests/frontend/regression/contextMenuTokens.test.js fails if any
custom property used by menu.css stops resolving (verified by reverting
the token), and if var(--danger-color) ever comes back.
The folder context menu now reads: the content action (check for updates)
on top, then the folder operations as one group (new subfolder, rename),
then the destructive entry behind its own divider.
Gating the three folder entries per page could leave the menu with
dangling separators — the recipes sidebar hides all of them and keeps
only the update check, which already rendered one stray divider before
this change and would have rendered two after it. Add
_updateContextMenuSeparators: a divider survives only when a visible
entry sits on both sides, and a run of consecutive ones collapses to a
single line. The three per-item display toggles fold into one loop.
The template order is guarded by a regression test that parses
templates/components/context_menu.html, plus behaviour tests for the
divider collapsing.