feat: add search result timestamps, enhanced cache clearing, and maintenance UI
Add searched_at timestamps to search results so cached entries are accurately tracked and preserved across merges. Expand the clear-search-cache command to also invalidate popular databases, model list, and the new result timestamp store. Frontend gains new cache-clearing methods and a full resolver cache reset for maintenance. The search panel and missing browser now show source result dates and toggle the search button between 'Search' and 'Search Again' when results already exist.
This commit is contained in:
+84
-2
@@ -31,6 +31,7 @@ class ModelResolverExtension:
|
||||
self.routes_setup = False
|
||||
self.logger = create_module_logger(__name__)
|
||||
self.analysis_progress = {}
|
||||
self.search_result_timestamps = {}
|
||||
|
||||
def initialize(self):
|
||||
"""Initialize the extension and set up API routes."""
|
||||
@@ -73,7 +74,7 @@ class ModelResolverExtension:
|
||||
apply_resolution,
|
||||
search_local_matches,
|
||||
)
|
||||
from .core.scanner import get_model_files
|
||||
from .core.scanner import get_model_files, invalidate_model_files_cache
|
||||
except ImportError as e:
|
||||
self.logger.error(f"Model Resolver: Could not import core modules: {e}")
|
||||
return False
|
||||
@@ -90,12 +91,14 @@ class ModelResolverExtension:
|
||||
from .core.sources.popular import (
|
||||
get_popular_model_url,
|
||||
search_popular_models,
|
||||
reload_databases as reload_popular_databases,
|
||||
)
|
||||
from .core.sources.model_list import (
|
||||
search_model_list,
|
||||
search_model_list_multiple,
|
||||
get_model_list_update_status,
|
||||
update_model_list_from_remote,
|
||||
reload_model_list,
|
||||
)
|
||||
from .core.sources.huggingface import (
|
||||
search_huggingface_for_file,
|
||||
@@ -962,6 +965,79 @@ class ModelResolverExtension:
|
||||
"searched_sources": sorted(normalized_sources),
|
||||
}
|
||||
|
||||
def current_search_timestamp():
|
||||
from datetime import datetime, timezone
|
||||
|
||||
return (
|
||||
datetime.now(timezone.utc)
|
||||
.replace(microsecond=0)
|
||||
.isoformat()
|
||||
)
|
||||
|
||||
def get_search_result_signature(source_key, result):
|
||||
if isinstance(result, list):
|
||||
return "|".join(
|
||||
get_search_result_signature(source_key, item)
|
||||
for item in result
|
||||
)
|
||||
if not isinstance(result, dict):
|
||||
return ""
|
||||
|
||||
parts = [
|
||||
source_key,
|
||||
result.get("download_url")
|
||||
or result.get("url")
|
||||
or result.get("model_url")
|
||||
or "",
|
||||
result.get("filename") or result.get("path") or "",
|
||||
result.get("repo_id") or result.get("repo") or "",
|
||||
result.get("model_id") or "",
|
||||
result.get("version_id") or "",
|
||||
result.get("name") or "",
|
||||
]
|
||||
return "::".join(str(part).strip() for part in parts)
|
||||
|
||||
def stamp_search_result(source_key, result):
|
||||
if isinstance(result, list):
|
||||
return [
|
||||
stamp_search_result(source_key, item)
|
||||
for item in result
|
||||
]
|
||||
if not isinstance(result, dict):
|
||||
return result
|
||||
|
||||
signature = get_search_result_signature(source_key, result)
|
||||
if not signature:
|
||||
return result
|
||||
|
||||
timestamp = (
|
||||
result.get("searched_at")
|
||||
or result.get("searchedAt")
|
||||
or self.search_result_timestamps.get(signature)
|
||||
)
|
||||
if not timestamp:
|
||||
timestamp = current_search_timestamp()
|
||||
self.search_result_timestamps.setdefault(
|
||||
signature, timestamp
|
||||
)
|
||||
result["searched_at"] = timestamp
|
||||
return result
|
||||
|
||||
def stamp_search_results(payload):
|
||||
for source_key in (
|
||||
"popular",
|
||||
"model_list",
|
||||
"huggingface",
|
||||
"civitai",
|
||||
"civarchive",
|
||||
"lora_manager_archive",
|
||||
):
|
||||
if payload.get(source_key):
|
||||
payload[source_key] = stamp_search_result(
|
||||
source_key, payload[source_key]
|
||||
)
|
||||
return payload
|
||||
|
||||
def search_local_sources():
|
||||
source_results = {"popular": None, "model_list": None}
|
||||
source_found = False
|
||||
@@ -1254,6 +1330,7 @@ class ModelResolverExtension:
|
||||
log_info(
|
||||
f"Search summary: found={results['found']}, searched_sources={results['searched_sources']}"
|
||||
)
|
||||
stamp_search_results(results)
|
||||
return web.json_response(results)
|
||||
|
||||
except Exception as e:
|
||||
@@ -1268,8 +1345,12 @@ class ModelResolverExtension:
|
||||
clear_civitai_search_cache()
|
||||
clear_civarchive_search_cache()
|
||||
clear_lora_manager_archive_search_cache()
|
||||
reload_popular_databases()
|
||||
reload_model_list()
|
||||
invalidate_model_files_cache()
|
||||
self.search_result_timestamps.clear()
|
||||
log_info("Cleared backend search caches")
|
||||
return web.json_response({"success": True})
|
||||
return web.json_response({"success": True, "cleared": "all"})
|
||||
except Exception as e:
|
||||
log_exception(f"Clear search cache error: {e}")
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
@@ -1376,6 +1457,7 @@ class ModelResolverExtension:
|
||||
clear_civitai_search_cache()
|
||||
clear_civarchive_search_cache()
|
||||
clear_lora_manager_archive_search_cache()
|
||||
self.search_result_timestamps.clear()
|
||||
return web.json_response(result)
|
||||
except Exception as e:
|
||||
log_exception(f"Model list update error: {e}")
|
||||
|
||||
@@ -45,9 +45,11 @@ CIVITAI_TYPE_MAP = {
|
||||
|
||||
|
||||
def clear_search_cache():
|
||||
"""Clear cached CivitAI search results."""
|
||||
global _search_cache
|
||||
"""Clear cached CivitAI search, URN, and hash results."""
|
||||
global _search_cache, _urn_cache, _hash_cache
|
||||
_search_cache.clear()
|
||||
_urn_cache.clear()
|
||||
_hash_cache.clear()
|
||||
|
||||
|
||||
def check_civitai_session_token(session_token: Optional[str]) -> Dict[str, Any]:
|
||||
|
||||
@@ -40,9 +40,11 @@ _author_index_lock = threading.RLock()
|
||||
|
||||
|
||||
def clear_search_cache():
|
||||
"""Clear cached HuggingFace search results."""
|
||||
global _search_cache
|
||||
"""Clear cached HuggingFace search results and in-memory indexes."""
|
||||
global _search_cache, _author_index_cache
|
||||
_search_cache.clear()
|
||||
with _author_index_lock:
|
||||
_author_index_cache.clear()
|
||||
|
||||
|
||||
def check_huggingface_token(token: Optional[str]) -> Dict[str, Any]:
|
||||
|
||||
@@ -44,9 +44,10 @@ GENERIC_FILENAME_TOKENS = {
|
||||
|
||||
|
||||
def clear_search_cache():
|
||||
"""Clear cached LoRA Manager archive search results."""
|
||||
global _search_cache
|
||||
"""Clear cached LoRA Manager archive search results and DB path."""
|
||||
global _search_cache, _db_path_cache
|
||||
_search_cache.clear()
|
||||
_db_path_cache = None
|
||||
|
||||
|
||||
def get_lora_manager_archive_db_path() -> Optional[str]:
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1956,11 +1956,28 @@
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mr-search-source-cell {
|
||||
display: inline-grid;
|
||||
max-width: 100%;
|
||||
gap: 3px;
|
||||
justify-items: start;
|
||||
}
|
||||
.mr-search-source-pill > span {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.mr-search-source-date {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
color: var(--mr-text-muted);
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
line-height: 1.15;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mr-search-source-icon {
|
||||
display: block;
|
||||
flex: 0 0 12px;
|
||||
|
||||
@@ -949,7 +949,9 @@ export const resolveDownloadMethods = {
|
||||
|
||||
const found = this.hasSearchResults(data);
|
||||
anyFound = anyFound || found;
|
||||
state.results = this.mergeSearchResults(state.results, data);
|
||||
state.results = this.mergeSearchResults(state.results, data, {
|
||||
searchedAt: new Date().toISOString()
|
||||
});
|
||||
state.lastAttemptSources = Array.from(attemptedSources);
|
||||
state.lastAttemptFound = anyFound;
|
||||
this.clearSearchProgressTimer(searchRunId, source);
|
||||
@@ -1314,7 +1316,8 @@ export const resolveDownloadMethods = {
|
||||
downloadUrl: popular.url,
|
||||
downloadFilename: popularFilename,
|
||||
category: popular.directory || missing.category,
|
||||
openUrl: this.getModelCardUrl(popular.url)
|
||||
openUrl: this.getModelCardUrl(popular.url),
|
||||
searchedAt: this.getSearchResultTimestamp(popular)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1330,7 +1333,8 @@ export const resolveDownloadMethods = {
|
||||
downloadUrl: modelListResult.url,
|
||||
downloadFilename: modelListResult.filename,
|
||||
category: modelListResult.directory || missing.category,
|
||||
openUrl: this.getModelCardUrl(modelListResult.url)
|
||||
openUrl: this.getModelCardUrl(modelListResult.url),
|
||||
searchedAt: this.getSearchResultTimestamp(modelListResult)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1348,7 +1352,8 @@ export const resolveDownloadMethods = {
|
||||
downloadUrl: hfResult.url,
|
||||
downloadFilename: hfResult.filename,
|
||||
category: missing.category,
|
||||
openUrl: hfModelUrl
|
||||
openUrl: hfModelUrl,
|
||||
searchedAt: this.getSearchResultTimestamp(hfResult)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1367,7 +1372,8 @@ export const resolveDownloadMethods = {
|
||||
downloadUrl: civarchiveResult.download_url,
|
||||
downloadFilename: archiveFilename,
|
||||
category: missing.category,
|
||||
openUrl: civarchiveResult.url
|
||||
openUrl: civarchiveResult.url,
|
||||
searchedAt: this.getSearchResultTimestamp(civarchiveResult)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1386,7 +1392,8 @@ export const resolveDownloadMethods = {
|
||||
downloadUrl: loraManagerArchiveResult.download_url || '',
|
||||
downloadFilename: archiveFilename,
|
||||
category: missing.category,
|
||||
openUrl: loraManagerArchiveResult.url
|
||||
openUrl: loraManagerArchiveResult.url,
|
||||
searchedAt: this.getSearchResultTimestamp(loraManagerArchiveResult)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1406,7 +1413,8 @@ export const resolveDownloadMethods = {
|
||||
downloadUrl: civitaiResult.download_url,
|
||||
downloadFilename,
|
||||
category: missing.category,
|
||||
openUrl: modelUrl
|
||||
openUrl: modelUrl,
|
||||
searchedAt: this.getSearchResultTimestamp(civitaiResult)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -533,16 +533,19 @@ export const downloadTargetMethods = {
|
||||
}
|
||||
},
|
||||
|
||||
async clearSearchCaches() {
|
||||
clearFrontendSearchCaches() {
|
||||
for (const state of this.searchResultCache.values()) {
|
||||
state.activeSearchRunId = null;
|
||||
}
|
||||
this.clearAllSearchProgressTimers();
|
||||
this.backgroundSearchJobs?.clear();
|
||||
this.searchResultCache.clear();
|
||||
const key = this.getWorkflowScopedQueueKey();
|
||||
if (key) {
|
||||
this.workflowSearchResultCaches.delete(key);
|
||||
}
|
||||
this.workflowSearchResultCaches.clear();
|
||||
this.urnResolvePromises.clear();
|
||||
this.urnLocalMatchPromises.clear();
|
||||
},
|
||||
|
||||
async clearBackendSearchCaches({ throwOnError = false } = {}) {
|
||||
try {
|
||||
const response = await api.fetchApi('/model_resolver/clear-search-cache', {
|
||||
method: 'POST'
|
||||
@@ -550,8 +553,41 @@ export const downloadTargetMethods = {
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to clear backend search cache');
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Model Resolver: Clear search cache error:', error);
|
||||
if (throwOnError) {
|
||||
throw error;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
async clearSearchCaches() {
|
||||
this.clearFrontendSearchCaches();
|
||||
await this.clearBackendSearchCaches();
|
||||
},
|
||||
|
||||
async clearAllResolverCaches() {
|
||||
this.clearFrontendSearchCaches();
|
||||
|
||||
this.workflowAnalysisCaches.clear();
|
||||
this.workflowLoadedModelCaches.clear();
|
||||
this.cachedAnalysisData = null;
|
||||
this.cachedWorkflowSignature = null;
|
||||
this.cachedLoadedModelsData = null;
|
||||
this.cachedLoadedModelsSignature = null;
|
||||
this.allModels = null;
|
||||
this.downloadDirectories = null;
|
||||
this.capabilities = null;
|
||||
this.downloadSubfolders.clear();
|
||||
this._analysisProgressToken = null;
|
||||
|
||||
await this.ensureCapabilitiesLoaded();
|
||||
this.refreshMissingListStats?.();
|
||||
this.updateBatchFooterButtons?.();
|
||||
this.updateDownloadAllButtonState?.();
|
||||
|
||||
await this.clearBackendSearchCaches({ throwOnError: true });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -137,14 +137,62 @@ export const searchPanelMethods = {
|
||||
* Merge new search results into cached per-source results.
|
||||
* Empty responses do not delete previously found results.
|
||||
*/
|
||||
mergeSearchResults(existingResults = {}, newResults = {}) {
|
||||
mergeSearchResults(existingResults = {}, newResults = {}, { searchedAt = null } = {}) {
|
||||
const pickResult = (source) => {
|
||||
if (newResults[source]) {
|
||||
const existingTimestamp = this.getSearchResultTimestamp(existingResults[source]);
|
||||
const resultTimestamp = this.areSearchResultsSame(existingResults[source], newResults[source])
|
||||
? existingTimestamp
|
||||
: null;
|
||||
return this.withSearchResultTimestamp(
|
||||
newResults[source],
|
||||
resultTimestamp || searchedAt
|
||||
);
|
||||
}
|
||||
return existingResults[source] || null;
|
||||
};
|
||||
|
||||
return {
|
||||
popular: newResults.popular || existingResults.popular || null,
|
||||
model_list: newResults.model_list || existingResults.model_list || null,
|
||||
huggingface: newResults.huggingface || existingResults.huggingface || null,
|
||||
civitai: newResults.civitai || existingResults.civitai || null,
|
||||
civarchive: newResults.civarchive || existingResults.civarchive || null,
|
||||
lora_manager_archive: newResults.lora_manager_archive || existingResults.lora_manager_archive || null
|
||||
popular: pickResult('popular'),
|
||||
model_list: pickResult('model_list'),
|
||||
huggingface: pickResult('huggingface'),
|
||||
civitai: pickResult('civitai'),
|
||||
civarchive: pickResult('civarchive'),
|
||||
lora_manager_archive: pickResult('lora_manager_archive')
|
||||
};
|
||||
},
|
||||
|
||||
getSearchResultSignature(result) {
|
||||
if (Array.isArray(result)) {
|
||||
return result.map(item => this.getSearchResultSignature(item)).join('|');
|
||||
}
|
||||
if (!result || typeof result !== 'object') return '';
|
||||
|
||||
return [
|
||||
result.download_url || result.url || result.model_url || '',
|
||||
result.filename || result.path || '',
|
||||
result.repo_id || result.repo || '',
|
||||
result.model_id || '',
|
||||
result.version_id || '',
|
||||
result.name || ''
|
||||
].map(value => String(value || '').trim()).join('::');
|
||||
},
|
||||
|
||||
areSearchResultsSame(previousResult, nextResult) {
|
||||
const previousSignature = this.getSearchResultSignature(previousResult);
|
||||
const nextSignature = this.getSearchResultSignature(nextResult);
|
||||
return Boolean(previousSignature && nextSignature && previousSignature === nextSignature);
|
||||
},
|
||||
|
||||
withSearchResultTimestamp(result, searchedAt = null) {
|
||||
if (!result || !searchedAt) return result;
|
||||
if (Array.isArray(result)) {
|
||||
return result.map(item => this.withSearchResultTimestamp(item, searchedAt));
|
||||
}
|
||||
if (typeof result !== 'object') return result;
|
||||
return {
|
||||
...result,
|
||||
searchedAt: result.searchedAt || result.searched_at || searchedAt
|
||||
};
|
||||
},
|
||||
|
||||
@@ -269,6 +317,12 @@ export const searchPanelMethods = {
|
||||
return !!(data.popular || data.model_list || data.huggingface || data.civitai || data.civarchive || data.lora_manager_archive);
|
||||
},
|
||||
|
||||
hasSearchResultsForMissing(missing = {}) {
|
||||
if (missing?.download_source?.url) return true;
|
||||
const state = this.searchResultCache?.get(this.getMissingSearchKey(missing));
|
||||
return this.hasSearchResults(state?.results || {});
|
||||
},
|
||||
|
||||
/**
|
||||
* Convert source ids to readable labels
|
||||
*/
|
||||
@@ -641,14 +695,46 @@ export const searchPanelMethods = {
|
||||
return icons[sourceKey] || 'globe';
|
||||
},
|
||||
|
||||
renderSearchSourcePill(sourceKey, sourceLabel) {
|
||||
getSearchResultTimestamp(result = {}) {
|
||||
if (Array.isArray(result)) {
|
||||
return this.getSearchResultTimestamp(result[0] || {});
|
||||
}
|
||||
return result?.searchedAt
|
||||
|| result?.searched_at
|
||||
|| result?.cachedAt
|
||||
|| result?.cached_at
|
||||
|| result?.updatedAt
|
||||
|| result?.updated_at
|
||||
|| '';
|
||||
},
|
||||
|
||||
formatSearchResultTimestamp(value, { compact = false } = {}) {
|
||||
if (!value) return '';
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return String(value);
|
||||
|
||||
const monthNames = compact
|
||||
? ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
|
||||
: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
||||
const pad = (number) => String(number).padStart(2, '0');
|
||||
const time = `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
||||
|
||||
return `${monthNames[date.getMonth()]} ${date.getDate()}, ${date.getFullYear()} ${time}`;
|
||||
},
|
||||
|
||||
renderSearchSourcePill(sourceKey, sourceLabel, searchedAt = '') {
|
||||
const iconName = this.getSearchSourceIconName(sourceKey);
|
||||
const iconHtml = getSvgIcon(iconName, 'currentColor', 'mr-search-source-icon');
|
||||
const sourceDate = this.formatSearchResultTimestamp(searchedAt, { compact: true });
|
||||
const sourceDateTitle = this.formatSearchResultTimestamp(searchedAt);
|
||||
return `
|
||||
<span class="mr-search-source-pill mr-search-source-${sourceKey}" data-tooltip="${this.escapeHtml(sourceLabel)}">
|
||||
${iconHtml}
|
||||
<span>${this.escapeHtml(sourceLabel)}</span>
|
||||
</span>
|
||||
<div class="mr-search-source-cell">
|
||||
<span class="mr-search-source-pill mr-search-source-${sourceKey}" data-tooltip="${this.escapeHtml(sourceLabel)}">
|
||||
${iconHtml}
|
||||
<span>${this.escapeHtml(sourceLabel)}</span>
|
||||
</span>
|
||||
${sourceDate ? `<small class="mr-search-source-date" data-tooltip="Result saved ${this.escapeHtml(sourceDateTitle)}">${this.escapeHtml(sourceDate)}</small>` : ''}
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
|
||||
@@ -675,7 +761,7 @@ export const searchPanelMethods = {
|
||||
return Math.max(max, count);
|
||||
}, 1);
|
||||
|
||||
const sourcePx = textWidth('x'.repeat(maxSourceLabel), 6, 34, 100) + 52;
|
||||
const sourcePx = Math.max(118, textWidth('x'.repeat(maxSourceLabel), 6, 34, 100) + 52);
|
||||
const matchPx = textWidth('x'.repeat(maxMatchLabel), 7, 34, 76) + 22;
|
||||
const sizePx = textWidth('x'.repeat(maxSizeLabel), 6.5, 28, 72) + 22;
|
||||
const actionsPx = Math.max(60, (maxActions * 24) + (Math.max(0, maxActions - 1) * 6) + 22);
|
||||
@@ -762,7 +848,8 @@ export const searchPanelMethods = {
|
||||
downloadUrl: downloadSource.url,
|
||||
downloadFilename,
|
||||
category: downloadSource.directory || downloadSource.category || missing.category || 'checkpoints',
|
||||
openUrl: modelUrl
|
||||
openUrl: modelUrl,
|
||||
searchedAt: this.getSearchResultTimestamp(downloadSource)
|
||||
};
|
||||
},
|
||||
|
||||
@@ -818,7 +905,7 @@ export const searchPanelMethods = {
|
||||
for (const row of rows) {
|
||||
const sourceKey = String(row.sourceKey || '').replace(/[^a-z0-9_-]/gi, '');
|
||||
const sourceLabel = row.sourceLabel || row.sourceKey || 'Source';
|
||||
const sourcePill = this.renderSearchSourcePill(sourceKey, sourceLabel);
|
||||
const sourcePill = this.renderSearchSourcePill(sourceKey, sourceLabel, row.searchedAt || row.sourceDate || '');
|
||||
const rawModel = row.model || row.filename || 'Model';
|
||||
const rawVersion = row.version || '';
|
||||
const model = this.escapeHtml(rawModel);
|
||||
@@ -886,9 +973,10 @@ export const searchPanelMethods = {
|
||||
const searchSourcesId = `search-sources-${missing.node_id}-${missing.widget_index}`;
|
||||
const searchSourceSelectId = `search-source-select-${missing.node_id}-${missing.widget_index}`;
|
||||
const searchSourceListId = `search-source-list-${missing.node_id}-${missing.widget_index}`;
|
||||
const buttonText = options.buttonText || 'Search';
|
||||
const state = this.getSearchState(missing);
|
||||
const selectedSource = state.selectedSource || 'all';
|
||||
const buttonText = options.buttonText
|
||||
|| (this.hasSearchResultsForMissing(missing) ? 'Search Again' : 'Search');
|
||||
|
||||
let html = `<div id="${searchSourcesId}" class="mr-search-source-bar">`;
|
||||
html += `<button id="search-${missing.node_id}-${missing.widget_index}" class="mr-btn mr-btn-link">`;
|
||||
|
||||
@@ -848,7 +848,9 @@ export const missingBrowserMethods = {
|
||||
} else if (perfectMatches.length > 0) {
|
||||
// Has perfect local match - download not needed, but allow online re-check.
|
||||
html += `<div class="mr-download-section">`;
|
||||
html += this.renderSearchControls(missing, { buttonText: 'Search Online' });
|
||||
html += this.renderSearchControls(missing, {
|
||||
buttonText: this.hasSearchResultsForMissing(missing) ? 'Search Again' : 'Search Online'
|
||||
});
|
||||
html += this.renderDownloadTargetControls(missing, missing.category || 'checkpoints');
|
||||
html += `</div>`;
|
||||
html += `<div id="search-results-${missing.node_id}-${missing.widget_index}" class="mr-search-results"></div>`;
|
||||
|
||||
@@ -76,6 +76,18 @@ export const optionsMethods = {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mr-options-sidebar-group">
|
||||
<div class="mr-options-sidebar-label">System</div>
|
||||
<div class="mr-options-nav">
|
||||
<button type="button" class="mr-options-nav-btn" data-target="mr-options-section-maintenance">
|
||||
<span class="mr-options-nav-main">
|
||||
<span class="mr-options-nav-icon" aria-hidden="true">${getSvgIcon('database')}</span>
|
||||
<span>Maintenance</span>
|
||||
</span>
|
||||
<span class="mr-options-nav-meta">05</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mr-options-actions">
|
||||
<div id="mr-options-status" class="mr-options-status">Saved only on this machine.</div>
|
||||
<button id="mr-options-save" class="mr-btn mr-btn-primary mr-footer-btn">Save</button>
|
||||
@@ -287,6 +299,24 @@ export const optionsMethods = {
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="mr-options-section-maintenance" class="mr-options-card mr-options-section is-hidden">
|
||||
<div class="mr-options-section-head">
|
||||
<h4 class="mr-options-section-title">Maintenance</h4>
|
||||
</div>
|
||||
<div class="mr-options-grid">
|
||||
<div class="mr-options-panel">
|
||||
<div class="mr-options-stack">
|
||||
<div class="mr-options-db-message">
|
||||
Clears only Model Resolver's temporary memory: remembered workflow analysis, search results, provider lookups, source status, folder lists, and subfolder suggestions. It does not delete downloaded models, metadata files, saved options, or API keys.
|
||||
</div>
|
||||
<div class="mr-options-db-actions">
|
||||
<button id="mr-options-clear-all-cache" type="button" class="mr-btn mr-btn-secondary">${getSvgIcon('refreshCw')} Clear Frontend and Backend Cache</button>
|
||||
</div>
|
||||
<div id="mr-options-clear-cache-status" class="mr-options-db-message">Use this when search results look stale or the resolver gets into an inconsistent state. Your model files stay untouched.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -328,6 +358,8 @@ export const optionsMethods = {
|
||||
const hfIndexStateEl = this.contentElement.querySelector('#mr-options-hf-index-state');
|
||||
const hfIndexMessageEl = this.contentElement.querySelector('#mr-options-hf-index-message');
|
||||
const hfIndexRefreshBtn = this.contentElement.querySelector('#mr-options-hf-index-refresh');
|
||||
const clearAllCacheBtn = this.contentElement.querySelector('#mr-options-clear-all-cache');
|
||||
const clearAllCacheStatus = this.contentElement.querySelector('#mr-options-clear-cache-status');
|
||||
const navButtons = Array.from(this.contentElement.querySelectorAll('.mr-options-nav-btn'));
|
||||
const optionSections = Array.from(this.contentElement.querySelectorAll('.mr-options-section'));
|
||||
const trackedInputs = [
|
||||
@@ -573,6 +605,25 @@ export const optionsMethods = {
|
||||
}
|
||||
};
|
||||
|
||||
const clearAllCachesFromOptions = async () => {
|
||||
try {
|
||||
if (clearAllCacheBtn) clearAllCacheBtn.disabled = true;
|
||||
if (clearAllCacheStatus) clearAllCacheStatus.textContent = 'Clearing frontend and backend cache...';
|
||||
await this.clearAllResolverCaches();
|
||||
if (clearAllCacheStatus) clearAllCacheStatus.textContent = 'Frontend and backend cache cleared.';
|
||||
this.showNotification('Resolver cache cleared', 'success');
|
||||
if (this.activeTab === 'options') {
|
||||
this.displayOptions();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Model Resolver: clear all cache error:', error);
|
||||
if (clearAllCacheStatus) clearAllCacheStatus.textContent = error.message || 'Failed to clear cache.';
|
||||
this.showNotification('Cache clear failed', 'error');
|
||||
} finally {
|
||||
if (clearAllCacheBtn) clearAllCacheBtn.disabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
const setActiveNav = (targetId) => {
|
||||
navButtons.forEach((btn) => {
|
||||
btn.classList.toggle('is-active', btn.dataset.target === targetId);
|
||||
@@ -656,6 +707,12 @@ export const optionsMethods = {
|
||||
});
|
||||
}
|
||||
|
||||
if (clearAllCacheBtn) {
|
||||
clearAllCacheBtn.addEventListener('click', () => {
|
||||
clearAllCachesFromOptions();
|
||||
});
|
||||
}
|
||||
|
||||
if (civitaiSessionCheckBtn) {
|
||||
civitaiSessionCheckBtn.addEventListener('click', () => {
|
||||
checkCredential({
|
||||
|
||||
Reference in New Issue
Block a user