Commit Graph
3296 Commits
Author SHA1 Message Date
Will Miao 0be3ea7d98 fix(download): accept mirror/www Civitai hosts and clarify linked-file errors
Follow-up to #1152:
- Accept civitai.green and www-prefixed hosts in isSupportedCivitaiPageHost,
  aligning with py/utils/civitai_utils.py and extensionReimportBridge.js, so
  download URLs on those hosts resolve too
- Drop the duplicate civitaiModelByVersion entry from the LoRA-specific
  endpoint map; the shared getApiEndpoints map now provides it for all types
- Report a distinct error when a linked fileId exists in the version but is
  not a downloadable model weight file (new linkedFileNotWeightFile locale key)
2026-10-21 21:53:59 +08:00
pixelpaws 8033292ad8 Merge pull request #1152 from ergs0204/fix/civitai-download-links
fix: accept Civitai download URLs and preserve file selection
2026-10-21 21:47:18 +08:00
ergs0204 b290cb8314 fix: preserve page version fallback and batch file availability 2026-10-21 19:39:45 +08:00
Will Miao 6c4caab6e9 refactor(preview): extract ChunkedFileResponse into py.utils.file_responses
Move the Windows-safe chunked FileResponse out of PreviewHandler into a
reusable py/utils/file_responses.py module, with platform_file_response_class()
selecting the transport per platform. Static routes (add_static) can adopt it
later if they also need to bypass native sendfile on Windows.

Add a platform-independent test for ChunkedFileResponse so Linux CI exercises
the chunked transport; the merged Windows-only test never runs there.
2026-10-21 16:48:13 +08:00
Wellington NunesandClaude Sonnet 5.5 c77540d171 fix(preview): serve previews in chunks instead of sendfile on Windows (#1151)
aiohttp's native sendfile (TransmitFile over IOCP) is suspected of wedging
the event loop and leaving an unkillable process when clients abort
transfers on Windows. Send previews through FileResponse's chunked fallback
there, keeping Range, ETag and header handling. Linux and macOS keep
sendfile.

Drop the unused _stream_file and _VIDEO_EXTENSIONS.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 16:44:48 +08:00
Will Miao 3c18dd6531 fix(delete): keep the cache in step when deleting duplicate models
Deleting a duplicate could remove the file from disk while leaving the
entry in the cache, so the duplicate kept showing after a refresh and
only Rebuild Cache cleared it (issue #1154). Three paths produced that
state:

* ModelHashIndex.remove_by_path() deleted _hash_to_path[hash_val] with no
  guard. The delete flow passes the cache row's sha256 as a hint; once
  the row and the index had drifted apart (a hash corrected in place, a
  partially applied cache update) the hint was not in the index and the
  KeyError aborted _batch_update_cache_for_deleted_models(), so every
  deleted model stayed in memory and in SQLite.
* The same cache update was skipped outright when the scanner's
  cancellation flag was set, turning files staged before the cancellation
  into ghost entries.
* A file that was already gone from disk could never be cleaned up:
  staging found no artifacts, the hard delete reported nothing, and the
  row survived as a permanent "No files deleted" failure.

Make the hash hint advisory and guard the index deletions; drop the
path-keyed entries (filename duplicates, autov2/autov3) even when no hash
mapping exists. Always apply the cache update after a delete, purge stale
index paths that never reached the cache, and treat a missing file under
a reachable folder as deleted while still keeping unreachable roots
offline. bulk_delete_models() reports success: false when nothing was
removed instead of presenting a no-op as a completed delete.

The duplicates UI now surfaces what it used to hide: per-file failures
from `results`, and a cache-update failure that needs Rebuild Cache.
2026-10-20 21:14:57 +08:00
Will Miao 90067de621 fix(move): keep drag-drop move alive when dragend clears the drag state
The #1148 fix made performDragMove() resolve the library root from
GET roots before reading the dragged paths. The browser dispatches
dragend right after drop, and handleCardDragEnd() calls
resetDragState(), so by the time the roots response arrived
draggedFilePaths was null and `this.draggedFilePaths.length` threw:

  TypeError: Cannot read properties of null (reading 'length')
  at SidebarManager.performDragMove (SidebarManager.js:711)

The throw happened before the try block, so no toast was shown and no
move request was ever sent -- drag-drop silently did nothing. The same
race hit the draggedRootPath fallback (aborting the move when the roots
request failed) and the draggedFromBulk cleanup (bulk mode stayed on
after a bulk move).

Snapshot filePaths / fromBulk / fallbackRoot before the first await and
use only the snapshot afterwards; _resolveMoveRoot takes the fallback
root as an argument instead of reading this.draggedRootPath.
2026-10-20 08:48:47 +08:00
Will Miao 6d9391672a fix(controls): keep model type dropdown hidden until it is usable
PageControls.syncExcludedViewState() un-hid #modelTypeDropdown whenever
the excluded view was inactive, racing the dropdown's async init. On
pages with a single sub-type (e.g. checkpoints with only "checkpoint"),
init returns early, leaving a bare caret trigger with no label and no
event handlers.

Let ModelTypeDropdown flag data-model-type-available after a successful
init and restore visibility from that flag, so single-type pages stay
hidden while excluded-view transitions still hide and restore an actual
dropdown. Add regression tests for both the single-type and excluded-view
paths.
2026-10-19 17:33:30 +08:00
ergs0204 a113b14f85 fix: support Civitai version download URLs with file IDs 2026-10-19 15:45:32 +08:00
willmiao b073025842 docs: auto-update supporters list in README 2026-10-19 06:32:44 +00:00
Will Miao 1fb1d5bb89 chore(release): bump version to v1.2.6 v1.2.6 2026-10-19 14:32:29 +08:00
Will Miao 3937ad37d0 feat(download): opt-in file-type routing for bundled component files
When file_params explicitly selects a file whose Civitai type maps to an
other-model sub_type (VAE/Text Encoder/CLIPVision/ControlNet/Upscaler)
and the caller passes file_type_routing=true, reroute the download to
the other-model pipeline: sub_type resolved from the file type, full
other-models gating (other_models_disabled / other_sub_type_disabled /
other_no_default_root), storage under default_other_roots, and
file-level dedup in the other scanner.

Default behavior is unchanged: without the flag, file type never
overrides the mapped model.type.
2026-10-19 11:56:11 +08:00
Will Miao 3f93683231 fix(settings): validate download base-model settings against merged vocabulary
The download_skip_base_models and download_root_rules normalizers validated
against the hardcoded SUPPORTED_DOWNLOAD_SKIP_BASE_MODELS frozenset only, so
base models known solely via the remote CivitAI snapshot (e.g. Qwen 3,
Flux.3) were silently dropped on save, on startup, and again at download
time.

CivitaiBaseModelService now persists the fetched snapshot to
{settings_dir}/cache/civitai_base_models.json and exposes a synchronous
get_known_base_models() (hardcoded + latest remote snapshot, stale snapshots
accepted on purpose) for validation contexts. The settings normalizers use
it with a hardcoded fallback, and DownloadManager drops its redundant
hardcoded membership check since the exclusion list is already validated.
2026-10-19 09:21:42 +08:00
Will Miao e95ff360cf fix(download): decode percent-encoded stored filenames before saving
CivitAI stores non-ASCII file names percent-encoded; the mini endpoint's
raw fileName (preferred since #1100) returned that encoded string, so it
landed on disk literally as "%E7%8A%AC..." and the path-budget
truncation could cut mid-escape. Decode to valid non-ASCII UTF-8 names
before the on-disk name is fixed; literal % (stored as %25) and invalid
escapes pass through unchanged.
2026-10-19 08:15:03 +08:00
Will Miao b5d313113c fix(recipes): strip legacy trailing ellipsis from prompts on modal open
Old companion-extension builds scraped Civitai's collapsed "...Show more"
toggle as part of the prompt text, so recipes imported through them can end
with a stray "..." on prompt and/or negative_prompt.

Repair it where the recipe modal already loads its detail payload: GET
/api/lm/recipe/{id} now strips trailing "..." runs from both fields, persists
the cleaned gen_params through recipe_scanner.update_recipe_metadata (recipe
JSON, memory and persistent caches, FTS index, preview EXIF) and returns the
cleaned values. The repair is unconditional — no source or API check — since
the existing library damage all came from that scraper. Values without a
trailing "..." are returned untouched, and a failed write logs and falls back
to the stored value so the detail response never breaks. Once persisted the
value no longer matches, so later opens are no-ops.
2026-10-18 21:59:47 +08:00
Will Miao f0615f769d i18n: translate the count tooltip and folder-removal strings
Fill in the two keys added by 965bfd33 (the always-on hover text of the counted
filter chips, All Types options and Refresh scope rows) and sweep the four
leftover sidebar.deleteFolder* placeholders from the folder-removal flow
(missingMessage, removeFromList, forgotten, exists), so no [TODO: Translate]
remains in any locale.

"models" reuses each locale's loras.controls.refresh.rootModels noun and
"recipes" the R4 noun in its plain plural (fr keeps "Recipes"); the separator
follows the sidebar.folderRoot.*Status precedent (CJK ——, all others —), and
ko puts the count after the noun as in rootModels. The folder strings mirror
each locale's existing deleteFolderResult.success / .missing renderings.

Terminology recorded in docs/i18n-translation-guidelines.md §2.
2026-10-18 18:35:01 +08:00
Will Miao 965bfd33cd feat(controls): keep model counts readable in large libraries
Counts in the filter panel chips (base models, tags, model types), the All Types
dropdown and the Refresh scope rows were rendered as raw digits, so a library with
10k+ models turned every chip into a five digit pill that dominated the row it sat
in — and the Refresh menu grouped its numbers while the filter panel did not.

One formatter now renders all of them (`formatCount`): exact and locale-grouped
below 10,000, abbreviated to three significant digits above it, and abbreviated
only where the locale's Intl compact form is a real unit and narrower than the
plain number — German keeps 18.475 instead of the rounded 18.500, Russian keeps
18 475 instead of the wider 18,5 тыс.

Their hover text is always present and always exact ({name} — {count} models, and
... recipes on the Recipes page): an affordance that only exists above a threshold
is undiscoverable, and the tooltip is also what reveals a chip label that CSS has
ellipsized.

Chips gain a two-slot layout (the name ellipsizes, the count keeps a tabular
right-aligned slot) so a long tag name can no longer push the count out of the
chip; Refresh rows move from float:right to a flex row, which keeps the counts in
a right-hand column and lets a long root label ellipsize. The All Types group
tooltip moves onto its trigger so it stops leaking onto the menu options.

The two new strings ship as [TODO: Translate] placeholders in the nine locales;
the translation pass is the next commit.
2026-10-18 18:34:16 +08:00
Will Miao 915cec9b67 i18n(controls): translate the model type dropdown strings
Fill in the nine locales for the three keys added by 3d65e044 (model
sub-type quick-switch dropdown): the trigger tooltip, the "All Types"
option and the "{count} types" multi-selection label.

Reuses each locale's existing `header.filter.modelTypes` rendering of
"model type"; the count fragment follows the locale's usual count form
(zh 种/種, ja タイプ, ko 유형 {count}개, fr/de/es plain plural, ru the
count-agnostic тип(ов), he סוגים).
2026-10-18 16:54:51 +08:00
Will Miao 6b3566a894 i18n(workflow): translate the send-to-workflow exclusion strings
Fill in the nine locales for the keys added by f8ecc00f (send-to-workflow
target filtering): the settings section header, the two exclusion-list
labels/placeholders/help texts, the quick-exclude toast and the
node-selector tooltip.

New terminology: "node type" (zh-CN 节点类型 / zh-TW 節點類型, ja
ノードタイプ, ko 노드 유형, fr type de nœud, de Knotentyp, es tipo de nodo,
ru тип узла, he סוג צומת) and "send targets" (zh-CN 发送目标 / zh-TW
傳送目標, ja 送信ターゲット, ko 전송 대상, fr cibles d'envoi, de Sendeziele,
es destinos de envío, ru цели отправки, he יעדי שליחה), both reusing each
locale's existing node/target/workflow renderings.

The literal ComfyUI menu label "Mark as" (hardcoded English in
web/comfyui/node_marker.js) stays untranslated in every locale, per
docs/i18n-translation-guidelines.md §6.
2026-10-18 16:53:31 +08:00
Will Miao 6835880e0b style(controls): drop decorative icons from toolbar action buttons
Width pressure on the controls bar is real (see the max-width: 1500px
tightening in layout.css), and the icons on text-labeled action buttons
are redundant encoding. Remove them from Refresh / Fetch / Download /
Bulk / Duplicates / Doctor and the recipes-page Import / Batch Import.

Kept: state-carrying icons (favorites star, updates exclamation,
exit-duplicates fa-times, badges) and structural icons (carets, layout
toggle, dropdown menu items). ModelDuplicatesManager rebuilds the
duplicates button on mode exit, so its restore string is updated too.
2026-10-18 16:46:44 +08:00
Will Miao 3d65e044f8 feat(controls): add model sub-type quick-switch dropdown to the controls bar
Single-select Model Type dropdown next to the sort control on the loras,
checkpoints and other pages (hidden when only one sub-type exists).
Selection maps to filters.modelTypes, persists via the existing filter
storage, and stays in sync with the filter panel's multi-select chips.
2026-10-18 16:16:59 +08:00
Will Miao 6ab4b041c0 style(card): unify quant/hl badge height with version-name chip
Match font-size, line-height, and vertical padding of .quant-badge and
.hl-badge to .version-name across base/medium/compact densities so all
chips in .badge-version-unit share the same box height. Color remains
the sole semantic differentiator.
2026-10-18 11:58:41 +08:00
Will Miao f8ecc00f6b feat(workflow): user-controlled send-to-workflow target filtering
- Global exclusion lists (node classes + title patterns) in settings,
  applied at the shared filterRegistryNodes choke point; settings UI
  section is hidden in standalone mode
- Per-node "Exclude from Send Targets" context-menu toggle persisted
  in the workflow JSON (lm_exclude_send)
- Quick-exclude button on node selector items writes the comfy class
  to the exclusion list
- Precedence: per-node exclusion > explicit marker > global lists
- interceptModeChange now supports multiple listeners; registry hooks
  mode changes so mute/bypass refreshes candidates immediately
2026-10-18 09:42:44 +08:00
Will Miao 43b26f7cc9 fix(paths): cap filename components to the filesystem NAME_MAX byte limit 2026-10-18 07:11:44 +08:00
Will Miao 72705c570e fix(move): stop drag-and-drop from nesting the target under the source dir (#1148)
The sidebar drag-and-drop derived the library root by stripping the card's
cached folder off its file path. Any disagreement between the two -- a stale
or wrong-cased folder recorded by an older version -- made the strip fail and
silently returned the file's own directory as the root, so the move created
<current dir>/<target path> on disk and physically nested the library.

Two defenses:

* SidebarManager now resolves the move root from the authoritative library
  roots (longest prefix match) and only falls back to the card heuristic,
  which compares case-insensitively and aborts the move on a real mismatch
  instead of fabricating a root.
* Reconcile repairs existing entries whose folder no longer matches where
  their file_path sits. Refresh never rewrote existing rows, so folders
  recorded wrong by 1.2.2-1.2.5 kept poisoning moves forever; now a plain
  Refresh heals them (no manual Rebuild Cache needed).
2026-10-17 19:39:37 +08:00
Will Miao f940daae29 feat(sidebar): compact drag image, hover auto-expand, edge auto-scroll for card drags
Replace the browser's default full-card drag snapshot with a small chip
(thumbnail + name + count badge) anchored below-right of the cursor, so
the folder tree stays visible while aiming. Collapsed folders now
auto-expand after a short hover during a drag, and the tree auto-scrolls
when the pointer nears its top/bottom edge.
2026-10-17 18:20:42 +08:00
Will Miao 1276673e0e fix(loaders): migrate backslash values in subgraphs and clear the error flag (#1149)
The follow-up report found three gaps in the value migration:

* applyToAllNodes only walked the root graph, so loader nodes inside
  subgraphs kept their backslash values. It now recurses through
  node.subgraph (Subgraph extends LGraph, so .nodes works at every level).
  Promoted combo widgets share the interior widget's store-backed value,
  so migrating the interior widget fixes the subgraph boundary too.
* Pasted nodes never hit the graph-level configure hook. A per-node
  onConfigure wrapper now runs the same sanitize + migrate pass after
  widget values restore.
* The corrected value kept its stale "value not in list" flag. The
  migration now fires the node's onWidgetChanged hook, which the
  frontend's error-clearing chain uses to drop value_not_in_list errors.
2026-10-17 17:12:41 +08:00
Will Miao be0133a699 fix(loaders): accept backslash-spelled model names from old workflows (#1149)
The forward-slash paths fix changed the checkpoint/unet combo lists on
Windows from "dir\model.safetensors" to "dir/model.safetensors", so
workflows saved before it loaded with a value no option matches: the
front-end extension reset the widget to the first model, or ComfyUI
flagged it "value not in list" when the widget was wired.

Two layers:

* random_loader_control.js migrates the widget value to its
  separator-normalized form when that form is a known model, before the
  reset-to-first fallback and before the wired early return.
* The loader nodes declare VALIDATE_INPUTS for the model name input,
  lifting ComfyUI's strict list check for it; the shared helper resolves
  separator-normalized names against the cache, so API-submitted or
  programmatic values validate too. get_checkpoint_info_absolute now
  normalizes backslashes explicitly instead of via os.sep, which was a
  no-op on Linux.
2026-10-17 10:19:53 +08:00
Will Miao 27275049b2 style(modals): redesign re-link and link-source dialogs
Compact single-field dialog layout: narrower content (640px), header with
accent icon and in-flow close button whose hover outline no longer crosses
the header divider, warning/info banner with theme-token side accents,
monospace URL example chips, and error text that only takes space when
present. Buttons switch to the standard secondary/primary pair.
2026-10-16 16:43:19 +08:00
Will Miao 034168d04e fix(cache): re-freeze GC after cache rebuilds (#1147)
A full rebuild or library switch replaces the frozen cache with freshly
allocated, GC-tracked objects, so ComfyUI's post-prompt gc.collect() would
slow down again until the next restart. Schedule a debounced re-freeze
(10 s settle delay) after ModelScanner full rebuilds and after model/recipe
library-change re-initialization, so repeated rebuilds and multi-scanner
library switches freeze once, after follow-up work settles.
2026-10-16 08:19:33 +08:00
Will Miao 6ef20bd33e fix(hash): run calculate_sha256 in a thread executor
The async calculate_sha256 performed its full-file read+hash loop
synchronously, freezing the single aiohttp event loop for the whole
duration of a multi-GB checkpoint hash — every HTTP endpoint, websocket,
and the extension's progress polling hung with no error surfaced.

Move the blocking loop into run_in_executor so hashing occupies a worker
thread instead of the event loop. Fixes all call sites at once (download
finalize, checkpoint lazy hash, scanners, metadata manager).
2026-10-15 23:29:40 +08:00
Will Miao 85d27dfa36 feat(metadata): link foreign-ecosystem models to their real source via CivArchive hints
When CivArchive's hash index only knows a file as a foreign-ecosystem
entry (e.g. a civision mirror of a ModelScope upload), record the
entry's source URL during the lookup. On a not-found fetch the sync
service now resolves that URL through the model-source registry, proves
via a strict sha256 gate (ModelScopeSource.fetch_card_context_by_hash)
that the repository really publishes this exact file, then persists the
source_url/source_platform link and runs the download-time hydration
path, yielding the correct name, base model, tags, trigger words and
example images from the actual source site.

Models already linked to an external source are never re-linked, and
rate-limited runs skip the extra fetches entirely.
2026-10-15 23:12:52 +08:00
Will Miao 35bc21a5bb fix(civarchive): don't resolve foreign-ecosystem file IDs into CivitAI metadata
CivArchive's /sha256 endpoint also indexes files from other ecosystems
(e.g. civision/ModelScope) whose model_id/model_version_id belong to a
different ID space. _resolve_version_from_files fed those IDs into the
CivitAI-ID-based /models/{id} endpoint, which resolves modelVersionId
globally, so a colliding ID silently linked the local file to an
unrelated CivitAI model (hash f3a5370b... resolved to CivitAI model
119656 via version id 130022).

Two guards: skip file entries whose source marks a foreign ecosystem,
and require the resolved version's files to contain the queried SHA256
before accepting it.
2026-10-15 22:49:06 +08:00
Will Miao 740b42db3f test(rate-limit): epsilon the cooldown upper bounds against float noise
Arming computes now + backoff and remaining_seconds subtracts a later
monotonic reading, so the result can round an epsilon above the armed
window (Windows CI reported 60.00000000000003 for a 60s bound). Same
fix pattern as 946809f4.
2026-10-15 22:11:44 +08:00
Will Miao 7deb0c9285 fix(config): skip already-live roots when admitting configured roots
admit_configured_roots() re-probed every configured root on every scan
(os.path.exists + realpath per root), so even a single-root refresh
statted every configured drive first — enough to spin up sleeping disks
and stall the scan the user actually asked for.

Admission only ever appends, so roots already in the live lists can
never gain entries from re-preparation. Probe only the configured roots
that are not live yet; the plug-in-a-drive-later behavior is unchanged
(a missing root is re-probed on each call until admitted).
2026-10-15 21:16:14 +08:00
Will Miao f4d9a6ccd6 fix(download): don't let a CivArchive cooldown block CivitAI-served downloads
A CivArchive API 429 armed a civarchive.com cooldown that the pre-flight
gate then applied to every download tagged source=civarchive — even when
the file bytes would have come from CivitAI (the common case of clicking
download on a CivArchive page for a model that still lives on CivitAI).

- Narrow the pre-flight gate to the CivitAI hosts; the CivArchive legs
  keep their own protection (metadata fetch via wait_for_slot, mirrors
  via the new deprioritization).
- Fall back to the default (CivitAI) metadata provider when the
  CivArchive fetch is rate-limited; re-raise the structured 429 only
  when CivitAI has nothing either (archive-only model), so the queue
  re-queues instead of failing.
- Deprioritize mirrors whose host is in cooldown once the concrete
  download URLs are known (also on the aria2 resume path), so a cooled
  mirror never shadows a ready one.
2026-10-15 21:16:03 +08:00
Will Miao cf3877bbc9 fix(scanner): keep folder casing when slicing the relative path (#1148)
85f79283 made _calculate_folder compare root identities, but sliced the
relative folder out of the identity itself -- and normalize_root_identity
folds case on Windows (or any drive-letter path), so every move recorded
the folder in lowercase. The sidebar then grew a lowercase twin branch
("Krea 2" vs "krea 2") on each move until the next disk walk healed it.

Slice from normalize_root_spelling instead, a case-preserving counterpart
that folds only separators and redundancies, with a length guard for the
rare Unicode case-fold that changes the string length.
2026-10-15 21:10:41 +08:00
Will Miao a1fc5eb825 i18n: translate showQuantizationOnCard keys into 9 locales 2026-10-15 11:21:17 +08:00
Will Miao 19abf7ed03 feat(ui): show quantization badge on model cards
Distinguish same-version quantized models (e.g. GGUF Q4_K_M vs Q8_0) on
cards, which previously showed only the identical CivitAI version name.

Detection is content-based and stored as a three-state sidecar field
(absent = unchecked, "" = checked-unavailable, value = canonical name),
mirroring the autov3 pipeline:

- GGUF: minimal pure-struct header parser reads general.file_type
- safetensors: dominant tensor dtype F8_* -> FP8
- CivitAI files[].metadata.fp == "fp8" fills in as a fallback only
- A one-time backfill service covers existing libraries; each file
  costs only a few-hundred-byte header read

The footer renders the badge after the version name (suppressed in
grouped mode), gated by a new show_quantization_on_card setting that
follows the show_version_on_card toggle pattern.
2026-10-15 11:18:06 +08:00
Will Miao d1586c4c61 test(paths): make the path-budget tests portable to Windows
Two platform assumptions broke the Windows informational job: the
alongside-sidecar fixture went through abspath, which prepends the drive
letter on Windows and skewed the asserted lengths by two; and the
download-rename trim test patched a fixed path limit that left no room
for a viable stem under the runner's long temp dir. The fixture now uses
plain dirname, and the limit is patched relative to the actual tmp_path.
2026-10-15 08:54:43 +08:00
Will Miao 260b21766b fix(startup): freeze hydrated caches out of GC tracking (#1147)
ComfyUI runs a full gc.collect() after every prompt, and its cost scales
with the number of GC-tracked objects. With a large library the hydrated
model/recipe caches account for ~1M long-lived objects, so each post-prompt
collection held the GIL for seconds and froze the whole server.

After all scanner init tasks finish, run gc.collect() + gc.freeze() once to
move the long-lived objects into the permanent generation, which the
collector no longer scans. Matches the workaround the reporter verified
manually.
2026-10-15 08:43:29 +08:00
Will Miao 7a5c1da9b5 fix(paths): trim model filenames to the real full-path budget (#1142)
The per-component caps (150/100/50) do not add up to a guarantee: a deep
model root plus template folders plus a max-length stem still overflows
the Windows MAX_PATH limit, and raw download filenames carried no cap at
all. fit_stem_to_path_budget() now measures the actual destination —
the model path and the sidecar location resolved through sidecar_paths
(the centralized mirror tree is typically deeper) — and trims the stem
to fit, reserving room for companion suffixes (.metadata.json.bak,
previews, .aria2).

Applied at all four name/path decision points: bulk filename-template
rename, post-download template rename, the initial download save path
(file_info rewritten to the on-disk name, matching the #1100 raw-name
precedent), and an auto-organize pre-check that skips with an
actionable message instead of failing opaquely on Windows.
2026-10-15 08:36:49 +08:00
Will Miao dabaf32858 docs(update): mark the threshold material in §10-§11 as historical
The status header still advertised "P5 (alerts panel) planned" while §11 opened
with "P5a implemented", so a user reading the plan to learn the UI could not
tell that the panel, the numeric threshold, the alerts query, the two alert
endpoints and getPriceAlerts were built on a branch and removed in e42d649d,
before v1.2.5 shipped. §10 is stale in the same way: it lists the alerts query,
price_alert_* and getPriceAlerts among what shipped.

The header now states the released behaviour (no independent surface, no
threshold, no price-drop alert), §10 and §11 carry a superseded note pointing at
§12.4/§12.7, and the §11 "Owner decision" paragraph is marked reversed by
§12.3 D1. Documentation only; no code or behaviour changes.
2026-10-14 09:19:28 +08:00
Will Miao d4a01c8d6d test(download): compare rule-routed save dirs as paths, not strings
The feat/download-root-rules merge added two tests asserting the joined
save_dir against a str(Path) expectation. `_calculate_relative_path` returns a
forward-slashed relative path that `os.path.join` appends to a native-spelled
root, so on Windows the value is "...\unet-archive\Flux.1 D/fantasy" while the
expectation is "...\unet-archive\Flux.1 D\fantasy" -- purely a separator
difference. Only those two assertions failed (3844 passed), and the Windows job
was green at the merge's first parent (b197793b).

Capture save_dir as a Path, like every other fake in this file, and compare
Path objects so the assertions pin the routed root -- their actual subject --
instead of the host separator.
2026-10-13 11:35:39 +08:00
Will Miao e27500c80f Merge branch 'feat/download-root-rules'
Download Root Rules: route downloads to a non-default root by ordered
base-model/tag rules per model type (loras/checkpoints/unet/embeddings),
with a rules editor in Settings > Organization, a default-root cue, and
the matched rule shown in the download modal. Translated in all locales.

Conflict note: settings_manager.py adopted the shared
utils.root_paths.normalize_root_identity from main (issue #1146).
2026-10-13 09:23:35 +08:00
Will Miao b197793b10 fix(rate-limit): do not leave the gate on an early timer wake
`wait_for_slot` trusted a single `asyncio.sleep(delay)`. The event-loop timer can
fire before the delay elapsed -- Windows granularity is ~15ms, which the new
Windows job caught -- and the method then returned while the cooldown was still
armed, so the caller sent a request inside the window the gate exists to enforce.

It now re-checks the fixed `wake_at` and sleeps again, which also restores the
strict `elapsed >= 0.14` cooldown bounds the earlier relaxation had weakened (a
substring replace had turned two of them into `>= 0.09`), and a regression test
fails the gate if it ever returns on an early wake.
2026-10-12 11:59:10 +08:00
Will Miao 946809f47c test(rate-limit): tolerate the timer on Windows, and restore two bounds
The last Windows failure was float noise on an upper bound: a 0.15s cooldown
window reports 0.15000000000000568 remaining, so the assertion missed by 6e-15.
The upper bound now carries an epsilon.

The two `elapsed >= 0.14` cooldown assertions had been weakened to `>= 0.09` by
a substring replace while relaxing the pacing bound -- that would have let a
missing wait pass. They are back at 0.12 (0.15s minus Windows timer rounding),
which still fails if the cooldown is not awaited.
2026-10-12 11:51:48 +08:00
Will Miao d25f170ffe test: fold the last Windows path and label assumptions
Second pass over the informational Windows job (91 failures -> 11 -> 0):

* `_identity`/`_fold` now wrap the assertions my first sweep skipped: the
  parenthesised and multi-line ones, and the ones whose token list was
  incomplete (`base_models_roots`, `embeddings_roots`).
* The reconciler's `current_name` and `active_roots` carry the drive-prefixed
  label on Windows ("C: loras"), so the last direct comparison goes through
  `_root_label_names` too.
* `open_settings_location` reveals the settings *file* with
  `explorer /select,` on Windows instead of opening the containing folder, so
  that test asserts the platform's call instead of the POSIX one.
* Three more expectations are written in the business form the code returns
  (metadata payload, extracted download paths, hydrated preview URL).
2026-10-12 11:46:13 +08:00
Will Miao c16de68640 test: make the backend suite pass on Windows
The new informational Windows job surfaced 91 failures: four real bugs (fixed in
the previous two commits) and a long tail of POSIX assumptions in the tests
themselves. None of them assert behaviour that differs on Windows, so they are
fixed rather than skipped wherever the behaviour is the same:

* Path spelling: expectations built with `str(tmp_path)` are compared through an
  identity helper (`normalize_root_identity`) or the platform's separator, since
  the code hands out forward-slashed business paths while `str()` is native.
* Root labels: `_root_display_labels` prefixes the drive on Windows ("C: loras"),
  so label assertions compare the final path component or derive the expected
  label from the same helper.
* `~` expansion reads USERPROFILE (ntpath), not HOME; timer granularity rounds
  0.1s down; a case-only rename counts as "already named" via `os.path.normcase`.
* Genuinely POSIX-only strategies are skipped with a reason instead of failing:
  the headless clipboard fallback, dbus file-manager selection, xdg-open, and
  the case-fold index test that simulates a case-sensitive filesystem.
2026-10-12 11:35:33 +08:00
Will Miao 85f79283b6 fix(paths): keep business paths forward-slashed on Windows
Four places produced the host's separator (or a mixed path) where the app's
business form is forward-slashed, so on Windows the value could not be compared
with the one the rest of the app stores:

* `ModelScanner._calculate_folder` matched `file_path.startswith(root)` as raw
  strings, so every forward-slashed path resolved to the library root and a
  moved model was filed under the root instead of its folder. It now compares
  root identities and slices the relative part off.
* `normalize_relative_folder` ran `os.path.normpath`, and ntpath folds forward
  slashes back to backslashes: every folder parameter (scoped scan, create,
  rename, delete) came back host-spelled and mismatched the stored folders.
* `_format_model_name_for_comfyui` returned `os.path.relpath` unchanged, so the
  checkpoint/unet nodes and the metadata node emitted `unet\base.safetensors`
  on Windows -- a name no ComfyUI widget list matches.
* `resolve_centralized_dir_for_dir` passed a component that spans two
  directories ("<library>/<component>") whole to `os.path.join`, leaving a mixed
  path ("...\comfyui/loras-1a2b3c4d\sub") that no native path equals.

Also hands the rename re-key callback business paths, like every other absolute
path the move service returns.
2026-10-12 11:35:33 +08:00