fix(usage-control): enrich usageControl from CivitAI by-hash API for all model types

The model-level API (GET /api/v1/models/{id}) does not include usageControl
on version entries, causing generation-only models to show as downloadable.

Backend changes:
- Add get_model_versions_by_hashes() to CivitaiClient (POST by-hash batch)
- Propagate through all provider classes including RateLimitRetryingProvider
- Add _enrich_version_entries() pipeline: extract SHA256 from files[].hashes,
  batch-call by-hash endpoint, inject usageControl+earlyAccessEndsAt in-place
- Wire enrichment into both bulk (_fetch_model_versions_bulk) and individual
  (_refresh_single_model) refresh paths
- Fix _build_record_from_remote dropping usage_control field
- Fix POST by-hash request format (plain JSON array, not {hashes:[...]} object)

Frontend changes:
- Fix disabled download button tooltip: wrap in <span> since HTML title
  attribute does not fire on disabled elements
This commit is contained in:
Will Miao
2026-05-07 08:56:19 +08:00
parent 908464bc0a
commit 682e964f89
5 changed files with 263 additions and 2 deletions

View File

@@ -241,7 +241,7 @@ function buildActionButton(label, variant, action, options = {}) {
if (action) {
attributes.push(`data-version-action="${escapeHtml(action)}"`);
}
if (options.title) {
if (!options.disabled && options.title) {
attributes.push(`title="${escapeHtml(options.title)}"`);
attributes.push(`aria-label="${escapeHtml(options.title)}"`);
}
@@ -251,7 +251,11 @@ function buildActionButton(label, variant, action, options = {}) {
if (options.extraAttributes) {
attributes.push(options.extraAttributes);
}
return `<button ${attributes.join(' ')}>${options.iconMarkup || ''}${escapeHtml(label)}</button>`;
const buttonHtml = `<button ${attributes.join(' ')}>${options.iconMarkup || ''}${escapeHtml(label)}</button>`;
if (options.disabled && options.title) {
return `<span class="version-action-disabled-wrapper" title="${escapeHtml(options.title)}" aria-label="${escapeHtml(options.title)}">${buttonHtml}</span>`;
}
return buttonHtml;
}
const DISPLAY_FILTER_MODES = Object.freeze({