feat(early-access): implement EA filtering and UI improvements

Add Early Access version support with filtering and improved UI:

Backend:
- Add is_early_access and early_access_ends_at fields to ModelVersionRecord
- Implement two-phase EA detection (bulk API + single API enrichment)
- Add hide_early_access_updates setting to filter EA updates
- Update has_update() and has_updates_bulk() to respect EA filter setting
- Add _enrich_early_access_details() for precise EA time fetching
- Fix setting propagation through base_model_service and model_update_service

Frontend:
- Add smart relative time display for EA (in Xh, in Xd, or date)
- Replace EA label with clock icon in metadata (fa-clock)
- Show Download button with bolt icon for EA versions (fa-bolt)
- Change EA badge color to #F59F00 (CivitAI Buzz theme)
- Fix toggle UI for hide_early_access_updates setting
- Add translation keys for EA time formatting

Tests:
- Update all tests to pass with new EA functionality
- Add test coverage for EA filtering logic

Closes #815
This commit is contained in:
Will Miao
2026-02-20 10:32:51 +08:00
parent e8b37365a6
commit 67869f19ff
22 changed files with 506 additions and 31 deletions

View File

@@ -426,6 +426,10 @@
"any": "Jede verfügbare Aktualisierung markieren"
}
},
"hideEarlyAccessUpdates": {
"label": "[TODO: Translate] Hide Early Access Updates",
"help": "[TODO: Translate] When enabled, models with only early access updates will not show 'Update available' badge"
},
"misc": {
"includeTriggerWords": "Trigger Words in LoRA-Syntax einschließen",
"includeTriggerWordsHelp": "Trainierte Trigger Words beim Kopieren der LoRA-Syntax in die Zwischenablage einschließen"
@@ -1031,12 +1035,19 @@
},
"labels": {
"unnamed": "Unbenannte Version",
"noDetails": "Keine zusätzlichen Details"
"noDetails": "Keine zusätzlichen Details",
"earlyAccess": "[TODO: Translate] Early Access"
},
"eaTime": {
"endingSoon": "[TODO: Translate] ending soon",
"hours": "[TODO: Translate] in {count}h",
"days": "[TODO: Translate] in {count}d"
},
"badges": {
"current": "Aktuelle Version",
"inLibrary": "In der Bibliothek",
"newer": "Neuere Version",
"earlyAccess": "[TODO: Translate] Early Access",
"ignored": "Ignoriert"
},
"actions": {
@@ -1044,6 +1055,7 @@
"delete": "Löschen",
"ignore": "Ignorieren",
"unignore": "Ignorierung aufheben",
"earlyAccessTooltip": "[TODO: Translate] Requires early access purchase",
"resumeModelUpdates": "Aktualisierungen für dieses Modell fortsetzen",
"ignoreModelUpdates": "Aktualisierungen für dieses Modell ignorieren",
"viewLocalVersions": "Alle lokalen Versionen anzeigen",

View File

@@ -426,6 +426,10 @@
"any": "Flag any available update"
}
},
"hideEarlyAccessUpdates": {
"label": "Hide Early Access Updates",
"help": "When enabled, models with only early access updates will not show 'Update available' badge"
},
"misc": {
"includeTriggerWords": "Include Trigger Words in LoRA Syntax",
"includeTriggerWordsHelp": "Include trained trigger words when copying LoRA syntax to clipboard"
@@ -1031,12 +1035,19 @@
},
"labels": {
"unnamed": "Untitled Version",
"noDetails": "No additional details"
"noDetails": "No additional details",
"earlyAccess": "EA"
},
"eaTime": {
"endingSoon": "ending soon",
"hours": "in {count}h",
"days": "in {count}d"
},
"badges": {
"current": "Current Version",
"inLibrary": "In Library",
"newer": "Newer Version",
"earlyAccess": "Early Access",
"ignored": "Ignored"
},
"actions": {
@@ -1044,6 +1055,7 @@
"delete": "Delete",
"ignore": "Ignore",
"unignore": "Unignore",
"earlyAccessTooltip": "Requires early access purchase",
"resumeModelUpdates": "Resume updates for this model",
"ignoreModelUpdates": "Ignore updates for this model",
"viewLocalVersions": "View all local versions",

View File

@@ -426,6 +426,10 @@
"any": "Marcar cualquier actualización disponible"
}
},
"hideEarlyAccessUpdates": {
"label": "[TODO: Translate] Hide Early Access Updates",
"help": "[TODO: Translate] When enabled, models with only early access updates will not show 'Update available' badge"
},
"misc": {
"includeTriggerWords": "Incluir palabras clave en la sintaxis de LoRA",
"includeTriggerWordsHelp": "Incluir palabras clave entrenadas al copiar la sintaxis de LoRA al portapapeles"
@@ -1031,12 +1035,19 @@
},
"labels": {
"unnamed": "Versión sin nombre",
"noDetails": "Sin detalles adicionales"
"noDetails": "Sin detalles adicionales",
"earlyAccess": "[TODO: Translate] Early Access"
},
"eaTime": {
"endingSoon": "[TODO: Translate] ending soon",
"hours": "[TODO: Translate] in {count}h",
"days": "[TODO: Translate] in {count}d"
},
"badges": {
"current": "Versión actual",
"inLibrary": "En la biblioteca",
"newer": "Versión más reciente",
"earlyAccess": "[TODO: Translate] Early Access",
"ignored": "Ignorada"
},
"actions": {
@@ -1044,6 +1055,7 @@
"delete": "Eliminar",
"ignore": "Ignorar",
"unignore": "Dejar de ignorar",
"earlyAccessTooltip": "[TODO: Translate] Requires early access purchase",
"resumeModelUpdates": "Reanudar actualizaciones para este modelo",
"ignoreModelUpdates": "Ignorar actualizaciones para este modelo",
"viewLocalVersions": "Ver todas las versiones locales",

View File

@@ -426,6 +426,10 @@
"any": "Signaler nimporte quelle mise à jour disponible"
}
},
"hideEarlyAccessUpdates": {
"label": "[TODO: Translate] Hide Early Access Updates",
"help": "[TODO: Translate] When enabled, models with only early access updates will not show 'Update available' badge"
},
"misc": {
"includeTriggerWords": "Inclure les mots-clés dans la syntaxe LoRA",
"includeTriggerWordsHelp": "Inclure les mots-clés d'entraînement lors de la copie de la syntaxe LoRA dans le presse-papiers"
@@ -1031,12 +1035,19 @@
},
"labels": {
"unnamed": "Version sans nom",
"noDetails": "Aucun détail supplémentaire"
"noDetails": "Aucun détail supplémentaire",
"earlyAccess": "[TODO: Translate] Early Access"
},
"eaTime": {
"endingSoon": "[TODO: Translate] ending soon",
"hours": "[TODO: Translate] in {count}h",
"days": "[TODO: Translate] in {count}d"
},
"badges": {
"current": "Version actuelle",
"inLibrary": "Dans la bibliothèque",
"newer": "Version plus récente",
"earlyAccess": "[TODO: Translate] Early Access",
"ignored": "Ignorée"
},
"actions": {
@@ -1044,6 +1055,7 @@
"delete": "Supprimer",
"ignore": "Ignorer",
"unignore": "Ne plus ignorer",
"earlyAccessTooltip": "[TODO: Translate] Requires early access purchase",
"resumeModelUpdates": "Reprendre les mises à jour pour ce modèle",
"ignoreModelUpdates": "Ignorer les mises à jour pour ce modèle",
"viewLocalVersions": "Voir toutes les versions locales",

View File

@@ -426,6 +426,10 @@
"any": "תוויות לכל עדכון זמין"
}
},
"hideEarlyAccessUpdates": {
"label": "[TODO: Translate] Hide Early Access Updates",
"help": "[TODO: Translate] When enabled, models with only early access updates will not show 'Update available' badge"
},
"misc": {
"includeTriggerWords": "כלול מילות טריגר בתחביר LoRA",
"includeTriggerWordsHelp": "כלול מילות טריגר מאומנות בעת העתקת תחביר LoRA ללוח"
@@ -1031,12 +1035,19 @@
},
"labels": {
"unnamed": "גרסה ללא שם",
"noDetails": "אין פרטים נוספים"
"noDetails": "אין פרטים נוספים",
"earlyAccess": "[TODO: Translate] Early Access"
},
"eaTime": {
"endingSoon": "[TODO: Translate] ending soon",
"hours": "[TODO: Translate] in {count}h",
"days": "[TODO: Translate] in {count}d"
},
"badges": {
"current": "גרסה נוכחית",
"inLibrary": "בספרייה",
"newer": "גרסה חדשה יותר",
"earlyAccess": "[TODO: Translate] Early Access",
"ignored": "התעלם"
},
"actions": {
@@ -1044,6 +1055,7 @@
"delete": "מחיקה",
"ignore": "התעלם",
"unignore": "בטל התעלמות",
"earlyAccessTooltip": "[TODO: Translate] Requires early access purchase",
"resumeModelUpdates": "המשך עדכונים עבור מודל זה",
"ignoreModelUpdates": "התעלם מעדכונים עבור מודל זה",
"viewLocalVersions": "הצג את כל הגרסאות המקומיות",

View File

@@ -426,6 +426,10 @@
"any": "利用可能な更新すべてを表示"
}
},
"hideEarlyAccessUpdates": {
"label": "[TODO: Translate] Hide Early Access Updates",
"help": "[TODO: Translate] When enabled, models with only early access updates will not show 'Update available' badge"
},
"misc": {
"includeTriggerWords": "LoRA構文にトリガーワードを含める",
"includeTriggerWordsHelp": "LoRA構文をクリップボードにコピーする際、学習済みトリガーワードを含めます"
@@ -1031,12 +1035,19 @@
},
"labels": {
"unnamed": "名前のないバージョン",
"noDetails": "追加情報なし"
"noDetails": "追加情報なし",
"earlyAccess": "[TODO: Translate] Early Access"
},
"eaTime": {
"endingSoon": "[TODO: Translate] ending soon",
"hours": "[TODO: Translate] in {count}h",
"days": "[TODO: Translate] in {count}d"
},
"badges": {
"current": "現在のバージョン",
"inLibrary": "ライブラリにあります",
"newer": "新しいバージョン",
"earlyAccess": "[TODO: Translate] Early Access",
"ignored": "無視中"
},
"actions": {
@@ -1044,6 +1055,7 @@
"delete": "削除",
"ignore": "無視",
"unignore": "無視を解除",
"earlyAccessTooltip": "[TODO: Translate] Requires early access purchase",
"resumeModelUpdates": "このモデルの更新を再開",
"ignoreModelUpdates": "このモデルの更新を無視",
"viewLocalVersions": "ローカルの全バージョンを表示",

View File

@@ -426,6 +426,10 @@
"any": "사용 가능한 모든 업데이트 표시"
}
},
"hideEarlyAccessUpdates": {
"label": "[TODO: Translate] Hide Early Access Updates",
"help": "[TODO: Translate] When enabled, models with only early access updates will not show 'Update available' badge"
},
"misc": {
"includeTriggerWords": "LoRA 문법에 트리거 단어 포함",
"includeTriggerWordsHelp": "LoRA 문법을 클립보드에 복사할 때 학습된 트리거 단어를 포함합니다"
@@ -1031,12 +1035,19 @@
},
"labels": {
"unnamed": "이름 없는 버전",
"noDetails": "추가 정보 없음"
"noDetails": "추가 정보 없음",
"earlyAccess": "[TODO: Translate] Early Access"
},
"eaTime": {
"endingSoon": "[TODO: Translate] ending soon",
"hours": "[TODO: Translate] in {count}h",
"days": "[TODO: Translate] in {count}d"
},
"badges": {
"current": "현재 버전",
"inLibrary": "라이브러리에 있음",
"newer": "최신 버전",
"earlyAccess": "[TODO: Translate] Early Access",
"ignored": "무시됨"
},
"actions": {
@@ -1044,6 +1055,7 @@
"delete": "삭제",
"ignore": "무시",
"unignore": "무시 해제",
"earlyAccessTooltip": "[TODO: Translate] Requires early access purchase",
"resumeModelUpdates": "이 모델 업데이트 재개",
"ignoreModelUpdates": "이 모델 업데이트 무시",
"viewLocalVersions": "로컬 버전 모두 보기",

View File

@@ -426,6 +426,10 @@
"any": "Отмечать любые доступные обновления"
}
},
"hideEarlyAccessUpdates": {
"label": "[TODO: Translate] Hide Early Access Updates",
"help": "[TODO: Translate] When enabled, models with only early access updates will not show 'Update available' badge"
},
"misc": {
"includeTriggerWords": "Включать триггерные слова в синтаксис LoRA",
"includeTriggerWordsHelp": "Включать обученные триггерные слова при копировании синтаксиса LoRA в буфер обмена"
@@ -1031,12 +1035,19 @@
},
"labels": {
"unnamed": "Версия без названия",
"noDetails": "Дополнительная информация отсутствует"
"noDetails": "Дополнительная информация отсутствует",
"earlyAccess": "[TODO: Translate] Early Access"
},
"eaTime": {
"endingSoon": "[TODO: Translate] ending soon",
"hours": "[TODO: Translate] in {count}h",
"days": "[TODO: Translate] in {count}d"
},
"badges": {
"current": "Текущая версия",
"inLibrary": "В библиотеке",
"newer": "Более новая версия",
"earlyAccess": "[TODO: Translate] Early Access",
"ignored": "Игнорируется"
},
"actions": {
@@ -1044,6 +1055,7 @@
"delete": "Удалить",
"ignore": "Игнорировать",
"unignore": "Перестать игнорировать",
"earlyAccessTooltip": "[TODO: Translate] Requires early access purchase",
"resumeModelUpdates": "Возобновить обновления для этой модели",
"ignoreModelUpdates": "Игнорировать обновления для этой модели",
"viewLocalVersions": "Показать все локальные версии",

View File

@@ -426,6 +426,10 @@
"any": "显示任何可用更新"
}
},
"hideEarlyAccessUpdates": {
"label": "[TODO: Translate] Hide Early Access Updates",
"help": "[TODO: Translate] When enabled, models with only early access updates will not show 'Update available' badge"
},
"misc": {
"includeTriggerWords": "复制 LoRA 语法时包含触发词",
"includeTriggerWordsHelp": "复制 LoRA 语法到剪贴板时包含训练触发词"
@@ -1031,12 +1035,19 @@
},
"labels": {
"unnamed": "未命名版本",
"noDetails": "暂无更多信息"
"noDetails": "暂无更多信息",
"earlyAccess": "[TODO: Translate] Early Access"
},
"eaTime": {
"endingSoon": "[TODO: Translate] ending soon",
"hours": "[TODO: Translate] in {count}h",
"days": "[TODO: Translate] in {count}d"
},
"badges": {
"current": "当前版本",
"inLibrary": "已在库中",
"newer": "较新的版本",
"earlyAccess": "[TODO: Translate] Early Access",
"ignored": "已忽略"
},
"actions": {
@@ -1044,6 +1055,7 @@
"delete": "删除",
"ignore": "忽略",
"unignore": "取消忽略",
"earlyAccessTooltip": "[TODO: Translate] Requires early access purchase",
"resumeModelUpdates": "继续跟踪该模型的更新",
"ignoreModelUpdates": "忽略该模型的更新",
"viewLocalVersions": "查看所有本地版本",

View File

@@ -426,6 +426,10 @@
"any": "顯示任何可用更新"
}
},
"hideEarlyAccessUpdates": {
"label": "[TODO: Translate] Hide Early Access Updates",
"help": "[TODO: Translate] When enabled, models with only early access updates will not show 'Update available' badge"
},
"misc": {
"includeTriggerWords": "在 LoRA 語法中包含觸發詞",
"includeTriggerWordsHelp": "複製 LoRA 語法到剪貼簿時包含訓練觸發詞"
@@ -1031,12 +1035,19 @@
},
"labels": {
"unnamed": "未命名版本",
"noDetails": "沒有其他資訊"
"noDetails": "沒有其他資訊",
"earlyAccess": "[TODO: Translate] Early Access"
},
"eaTime": {
"endingSoon": "[TODO: Translate] ending soon",
"hours": "[TODO: Translate] in {count}h",
"days": "[TODO: Translate] in {count}d"
},
"badges": {
"current": "目前版本",
"inLibrary": "已在庫中",
"newer": "較新版本",
"earlyAccess": "[TODO: Translate] Early Access",
"ignored": "已忽略"
},
"actions": {
@@ -1044,6 +1055,7 @@
"delete": "刪除",
"ignore": "忽略",
"unignore": "取消忽略",
"earlyAccessTooltip": "[TODO: Translate] Requires early access purchase",
"resumeModelUpdates": "恢復追蹤此模型的更新",
"ignoreModelUpdates": "忽略此模型的更新",
"viewLocalVersions": "檢視所有本地版本",