When CivArchive returns HTTP 429 with a large retry_after, the bulk
metadata refresh would block for hours because:
1. FallbackMetadataProvider raised RateLimitError instead of continuing
to the next provider (e.g., SQLite archive was never reached).
2. _RateLimitRetryHelper retried long-rate-limit 429s 3 times — all
futile since the hourly cap hasn't reset.
3. The batch loop had no awareness of persistent rate-limiting,
causing 192+ models to each hammer the same rate-limited endpoint.
Changes:
- FallbackMetadataProvider: all 6 methods now continue to next provider
on RateLimitError instead of raising (model_metadata_provider.py)
- fetch_and_update_model: deleted-model path also continues on
RateLimitError so sqlite provider gets a chance (metadata_sync_service.py)
- _RateLimitRetryHelper: when retry_after >= 120s, only 1 attempt is
made — retries are futile for hour-scale rate limits
- BulkMetadataRefreshUseCase: tracks consecutive rate-limit failures
and aborts early after 3 (bulk_metadata_refresh_use_case.py)
Tests: updated test_fallback_respects_retry_limit for new continue
behavior; added tests for large/small retry_after thresholds.
- retry_from_history() and retry_all_failed() now DELETE the original
history entry after re-queuing it. Previously the old entry stayed
in history causing exponential growth on repeated retry→cancel→retry
cycles.
- Add deduplicate() called once on singleton creation to clean up
existing duplicate queue/history entries left by the bug:
1. In-status dedup (keep highest id per model+version+status)
2. Cross-status dedup (prefer completed > failed > canceled)
3. Queue dedup (keep highest rowid per model+version)
4. Orphan queue cleanup (source='retry' entries obsoleted by
terminal history entries)
Chrome does not cache 206 Partial Content responses for <video> elements
without an explicit Cache-Control header. When VirtualScroller recycles
cards and creates new <video> elements with the same URL, Chrome
re-downloads the full video (several MB each) instead of using the cache.
Verified via Chrome DevTools: same .mp4 URL appears 2-3 times in network
trace as separate requests with no cache hit, each returning 206. With
Cache-Control: max-age=86400, the browser will reuse the cached response
for 24 hours across scroll cycles.
Video preview files are ~3.5MB while image previews are ~50-100KB (due
to WebP optimization), making caching especially impactful for videos.
The previous commit (a19ddc14) restored Linux sendfile but kept the
manual streaming path for Windows via sys.platform guard. A Windows
user reports performance is still worse than v1.0.5.
Switch back to web.FileResponse for all files on all platforms as the
default. The IOCP crash is an edge case (fast scrolling through many
video previews) that affects few users, while the Python chunked I/O
performance penalty affects everyone.
_stream_file() is kept as an unused fallback for a future compat
setting toggle.
- Restrict manual video streaming to Windows only (sys.platform == 'win32');
Linux/macOS now uses kernel sendfile (zero-copy DMA) via aiohttp FileResponse
- Add Cache-Control: public, max-age=86400 to streaming responses so browsers
cache video previews across scroll cycles
- Increase chunk size from 256KB to 1MB to reduce async iteration overhead on
Windows where streaming is still required
Build a civitai_image_id → recipe_id mapping once during cache
initialization instead of scanning all recipes on every
check_image_exists and import_from_url call.
- RecipeCache gains an image_id_map field populated by
_build_image_id_map() during cache init
- check_image_exists and import_from_url duplicate detection
now use the precomputed map (O(k) / O(1) vs O(n))
- Map is persisted in SQLite cache_metadata for fast startup
- Incrementally updated on add/remove/bulk_remove paths
- Fix: conn.close() before cache_metadata query (dead connection)
- Add ``` button in sidebar header with dropdown menu
- Add "Hide sidebar on this page" option with per-page localStorage state
- Show edge indicator (14px chevron) on left when hidden per-page
- Show brief toast notification when hiding
- Fix container margin not resetting when sidebar is per-page hidden
- Add i18n translations for all 10 locales
_load_stats() was missing the embeddings section, so on every restart
the embeddings usage tracking hash would start from an empty dict.
This caused all previously saved embedding usage data to appear reset.
Added the missing load path for the 'embeddings' key, parallel to the
existing checkpoints and loras loading logic.
- Fix copy button on embedding cards to copy 'embedding:folder/name' format
- Add send-embedding-to-workflow for Prompt (LoraManager), Text (LoraManager),
and CLIPTextEncode nodes, appending embedding code to text content
- Extend workflow registry to register text-capable nodes by comfyClass
(not generic widget name 'text') to avoid false matches
- Add mode parameter to update_node_widget API/event for append support
- Fix single/bulk context menus: single shows plain 'Send to Workflow',
bulk collapses submenu into direct action for embeddings (append-only)
- Add local file reimport support via _do_reimport_from_local
- Validate source_path BEFORE deleting old recipe (prevent data loss)
- Move delete_recipe after save_recipe (safe ordering)
- Preserve folder location, NSFW level, and carry over user edits
- Remove old timestamp preservation (use current time)
- Add scrollTop reset in resetAndReloadWithVirtualScroll
- Only reload on successful bulk reimport (avoid empty grid)
- Disable preserveScroll for both single and bulk reimport
- Change _get_stats_file_path() to use get_settings_dir()/stats/ instead of
first loras root directory
- Add _migrate_from_old_location() to copy existing stats from loras root
to new location on first access, then clean up old file
- Add 'stats' to update protection skip lists (clean, extract, tracking)
to prevent data loss during ZIP/git upgrades in portable mode
- Add usage_stats entry to backup targets and restore resolver so stats
are included in automatic snapshots
- Single recipe right-click menu: Re-import from Source
- Bulk context menu: Re-import Metadata for Selected
- Progress overlay with LoadingManager for single and bulk operations
- Virtual scroller data lookup (replaces fragile DOM querySelector)
- Fix dynamic import path for resetAndReload on recipe pages
- Add translation keys for all 9 supported languages
Adds POST /api/lm/recipe/{recipe_id}/reimport that atomically:
1. Reads the existing recipe to extract source_url and user edits
2. Deletes the old recipe files and cache entries
3. Re-downloads the image from CivitAI, re-parses EXIF metadata
4. Carries over user edits (title, tags, favorite) and timestamps
When CivitAI API returns meta=null and the optimized CDN image has no
embedded generation parameters (e.g. PNG tEXt chunks stripped by
Cloudflare Images), download the original image as fallback to recover
full recipe metadata (prompt, seed, LoRAs, etc.).
Also fixes Chrome password manager popping up on recipe save by adding
autocomplete="new-password" to the settings API key and proxy password
fields.
- Replace all remaining 'transition: all' with specific token-based transitions
- Replace 80+ hardcoded box-shadow rgba values with semantic tokens
- Add new tokens: --shadow-side, --shadow-elevated, --shadow-dialog, --shadow-inset-top
- Update dark theme overrides for new shadow tokens
- 32 files changed, net +8 lines (more consistent, less duplication)
- Add --surface-subtle (oklch 3% opacity) to replace rgba(0,0,0,0.03)
- Fix info items, creator-info, civitai-view, modal-send-btn, header-actions
to use --surface-subtle instead of --surface-hover
- Keep true hover states on --surface-hover
- Use light #d4a017 / dark #ffc107 for --favorite-color based on theme
- Replace hardcoded #ffc107 and #d4a017 with var(--favorite-color)
- Add .input-hint helper text below textarea guiding multi-URL input
- Update label to CivitAI URL(s): for batch-agnostic hint
- Add urlHint locale key across all 10 languages
- Remove unused url locale key
When batch-downloading different versions of the same model, dedup by
modelId alone discards the second URL. Use modelId:modelVersionId as
the dedup key so users can download, e.g., latest + a specific version.
- Add 'Node 2.0: Maximum visible LoRA entries' setting (default 12)
- Apply max-height to loras container in Vue mode to prevent unbounded growth
- Add enableListWheelScroll: window capture-phase wheel hook so scroll
inside the widget scrolls the list instead of zooming the canvas
The refresh_model_updates handler was calling record.has_update() with
default hide_early_access=False, causing the toast to report early-access
updates that the Updates filter (which uses the user's hide_early_access
setting) would then hide. This resulted in misleading "Found N updates"
toasts followed by an empty Updates view.
Now the handler reads hide_early_access_updates from settings and passes
it to has_update(), matching the behavior of _serialize_record and
_annotate_update_flags.
In Vue/Node 2.0 mode, the AutocompleteTextWidget's textarea wheel events were intercepted by TransformPane @wheel.capture before reaching the @wheel handler, causing canvas zoom instead of text scrolling.
- Add lm-wheel-scrollable class in Vue mode to hook into the window capture-phase handler (enableListWheelScroll) which scrolls the textarea manually before TransformPane can react.
- Add maxHeight prop and container max-height for Lora Loader/Stacker/WanVideo nodes (modelType === 'loras'), matching canvas mode's height cap. Prompt/Text nodes remain uncapped.
Both restore_suffixed_filenames.py and migrate_legacy_metadata.py
hardcoded Path.home() / '.config' / APP_NAME for finding settings.json,
which only works on Linux. On Windows this resolves to the wrong path
(~/.config/ instead of %LOCALAPPDATA%).
Replace the hand-rolled fallback with platformdirs.user_config_dir(),
which correctly resolves to the OS-appropriate config directory on all
platforms (Windows: %%LOCALAPPDATA%%, macOS: ~/Library/Application Support,
Linux: ~/.config). The portable mode check (settings.json in repo root
with use_portable_settings: true) is preserved unchanged.
Build a local_cache from the scanner cache before calling the metadata
parser. When a resource hash is found in the cache, populate the entry
directly from cached civitai metadata instead of calling CivitAI's
/model-versions/by-hash endpoint.
This eliminates redundant API calls and retries for the common case
where the example image only uses the parent model plus a checkpoint.
- refreshRecipes() now accepts fullRebuild param and passes it to scan endpoint
- Use consistent toast.api.refreshComplete / toast.api.refreshFailed keys
- Use loadingManager.show() with progress bar (matching models page style)
- Both Refresh and Rebuild Cache now hit the real /api/lm/recipes/scan endpoint
- Add sidebarManager.refresh() after recipe scan completes
- Backend scan_recipes handler reads full_rebuild query param
Bug: when scrolling down on recipes page, any operation with
preserveScroll: true would fetch only page 1 data then restore
scroll position to beyond the loaded items, leaving the grid empty.
Fix:
- Remove preserveScroll: true from all 7 must-refresh trigger
paths (filter, search, sort, import, settings reload, sync,
rebuild cache, sidebar folder nav)
- Replace full list refresh with updateSingleItem() for repair
and bulk missing-LoRA download operations
- Update tests to match new scroll-free behavior
- Apply CivitaiApiMetadataParser's base_model result to metadata in
_do_import_remote_recipe and _do_import_from_url (was previously discarded)
- Extract baseModel from raw civitai_info before populate_checkpoint_from_civitai
so it's not lost when the type check rejects non-checkpoint model versions
- Only format and save checkpoint entry when it has real data (modelId, versionId,
name, or version), preventing empty {'type': 'checkpoint'} stubs
- Add wildcards and backups to skip_files in all three ZIP upgrade
skip locations: _clean_plugin_folder, copy loop, .tracking generation
- Remove logs from skip_files (logs are transient and rotate automatically)
- Add _prune_old_logs() to session_logging.py: keeps only the 3 newest
session log files, deletes older ones on each standalone startup
On Windows, shutil.rmtree() fails when deleting a directory that contains
an open SQLite database file. The ZIP update path in _download_and_replace_zip()
calls _clean_plugin_folder() which tries to delete the cache/ directory,
but downloaded_versions.sqlite is held open by DownloadedVersionHistoryService.
Fix:
- Add close() method to DownloadedVersionHistoryService to release
the persistent SQLite connection
- Call close() before _clean_plugin_folder() in the ZIP update flow
- Add 'cache' to the skip_files list so the runtime cache directory is
never deleted during plugin updates
When certifi is available, pass its CA bundle path as --ca-certificate
to the aria2c subprocess so that aria2 downloads use the same
certificate store as Python aiohttp downloads. Graceful fallback when
certifi is not installed.
Adds a new bulk operation in the recipes page that allows users to select
multiple recipes and repair their metadata in batch.
Backend:
- New POST /api/lm/recipes/repair-bulk endpoint accepting recipe_ids array
- repair_recipes_bulk handler iterates repair_recipe_by_id for each recipe
- Response includes per-recipe updated data for frontend card refresh
Frontend:
- Bulk context menu: new 'Repair Metadata for Selected' item in Metadata section
- BulkManager.repairSelectedRecipes() with loading/toast flow
- Uses VirtualScroller.updateSingleItem() per repaired recipe (no full reload)
- Visibility controlled via repairMetadata actionConfig flag
Locales:
- Added repairMetadata, repairBulkComplete, repairBulkSkipped, repairBulkFailed
- Translated across all 9 supported languages
Add corruption detection to _repair_single_recipe: if checkpoint.modelVersionId matches any LoRA's modelVersionId, the checkpoint is corrupted (a LoRA was saved as checkpoint). Clear the checkpoint and remove the matching LoRA entry, then let enrichment re-resolve the correct checkpoint from CivitAI metadata.
This fixes the retroactive repair path for the modelVersionIds[0] fallback bug.
When importing a CivitAI image as a recipe, modelVersionIds[0] was blindly used as the checkpoint version ID. This array mixes checkpoints and LoRAs without ordering guarantees, causing LoRAs to be saved as the recipe checkpoint.
Fix by:
1. Removing the modelVersionIds[0] fallback in _download_remote_media
2. Parsing resources entries with type:"model" as the checkpoint
3. Adding model type validation in populate_checkpoint_from_civitai
Also add 2 tests for the new behavior and fix 3 tests whose mocks lacked the required model.type field.
Previously check_pending_models() only skipped models already in
processed_models, so models that had permanently failed (no CivitAI
images available, download errors) were forever reported as "pending".
This caused repeated auto-download cycles with no actual work to do.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When CivitAI returns 404 (ResourceNotFoundError) and a fallback provider
like CivArchive subsequently rate-limits, the ChainedMetadataProvider
now suppresses the RateLimitError instead of propagating it. Previously,
the rate-limit error would bubble up through _refresh_single_model and
cause the outer retry loop to re-process the same model repeatedly,
producing dozens of duplicate "Model X is no longer available" log
messages and wasting API quota.
The model is NOT permanently marked as ignored — its last_checked_at
timestamp is preserved, so it will be retried on the next refresh cycle
when the rate limit has cleared and CivArchive may still have the data.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
CivitaiClient._make_request now retries 5xx/524/network errors up to 3 times with exponential backoff (1s, 2s) before giving up to the fallback provider chain.
get_model_version_info gains an in-memory OrderedDict cache (LRU, max 500 entries) so duplicate lookups of the same version ID within a single import/scan flow return instantly without a redundant API call.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>