fix(recipes): distinguish unobtainable LoRAs in recipe status and skip them in syntax

The recipe card pill counted LoRAs deleted from the source (isDeleted) as
available, showing a green 'ready 2/2' for recipes that cannot be fully
reproduced. LoRAs with an unresolvable hash (hashInvalid) were counted as
missing/downloadable even though downloads always fail, and recipe syntax
generation emitted broken tokens for them.

- Four-state status on RecipeCard pill and RecipeTab badge: ready (all in
  library), missing (downloadable, red, keeps the action cue), partial
  (unobtainable entries skipped when used, amber, fa-circle-minus),
  unavailable (nothing usable, gray, fa-ban)
- Pill numerator is now the real in-library count; tooltips spell out
  missing vs unavailable (deleted from source or unresolvable hash)
- get_recipe_syntax_tokens skips hashInvalid entries like deleted ones
  instead of emitting tokens pointing at nonexistent files
- Bulk missing-download manager and recipe context menu exclude
  hashInvalid LoRAs, matching the modal's per-item download block
- New locale keys loraStatus.missingAndUnavailable/partial/noneUsable,
  translated for all 9 non-en locales
This commit is contained in:
Will Miao
2026-08-29 16:41:48 +08:00
parent ebe3df7d22
commit c972c755fc
19 changed files with 258 additions and 40 deletions
+28
View File
@@ -374,6 +374,34 @@ async def test_set_lora_entry_hash_invalid_persists_flag(tmp_path: Path, recipe_
assert cleared_lora["hashInvalid"] is False
@pytest.mark.asyncio
async def test_get_recipe_syntax_tokens_skips_unobtainable_loras(tmp_path: Path, recipe_scanner):
scanner, _ = recipe_scanner
recipes_dir = Path(config.loras_roots[0]) / "recipes"
recipes_dir.mkdir(parents=True, exist_ok=True)
recipe_id = "syntax-skip-1"
recipe_path = recipes_dir / f"{recipe_id}.recipe.json"
recipe_data = {
"id": recipe_id,
"file_path": str(tmp_path / "image.png"),
"title": "Syntax skip",
"modified": 0.0,
"created_date": 0.0,
"loras": [
{"file_name": "usable_lora", "strength": 0.8, "hash": ""},
{"file_name": "deleted_lora", "strength": 1.0, "hash": "", "isDeleted": True},
{"file_name": "invalid_hash_lora", "strength": 1.0, "hash": "", "hashInvalid": True},
],
}
recipe_path.write_text(json.dumps(recipe_data))
await scanner.add_recipe(dict(recipe_data))
tokens = await scanner.get_recipe_syntax_tokens(recipe_id)
assert tokens == ["<lora:usable_lora:0.8>"]
@pytest.mark.asyncio
async def test_load_recipe_rewrites_missing_image_path(tmp_path: Path, recipe_scanner):
scanner, _ = recipe_scanner