fix: address bug review findings

- Debounce MutationObserver (200ms) and scope to #modelGrid when available
- Sanitize data.updated via parseInt before innerHTML to prevent XSS
- Guard pagination against page_size=0 (division by zero) and negative values
- Reject non-POST requests on /api/lm-extra/fetch-stats (state-mutating)
- Early return in upsert_batch for empty rows list

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-15 14:48:14 +01:00
parent f6e39df157
commit b39ea7a647
3 changed files with 18 additions and 7 deletions
+4 -2
View File
@@ -284,8 +284,8 @@ async def _proxy_list_with_stats_sort(
"""Fetch all items from remote, sort by stats locally, paginate."""
from urllib.parse import urlencode
page = int(request.query.get("page", "1"))
page_size = int(request.query.get("page_size", "20"))
page = max(1, int(request.query.get("page", "1")))
page_size = max(1, int(request.query.get("page_size", "20")))
sort_key, _, order = sort_by.partition(":")
order = order or "desc"
@@ -342,6 +342,8 @@ async def _proxy_list_with_stats_sort(
async def _handle_fetch_stats(request: web.Request) -> web.Response:
"""Bulk fetch CivitAI stats for all models."""
if request.method != "POST":
return web.json_response({"error": "POST required"}, status=405)
try:
from .stats_service import StatsFetchService
except ImportError:
+12 -5
View File
@@ -174,7 +174,8 @@
const resp = await _origFetch("/api/lm-extra/fetch-stats", { method: "POST" });
const data = await resp.json();
if (data.success) {
btn.innerHTML = `<i class="fas fa-check"></i> <span>${data.updated} updated</span>`;
const count = parseInt(data.updated, 10) || 0;
btn.innerHTML = `<i class="fas fa-check"></i> <span>${count} updated</span>`;
setTimeout(() => {
btn.innerHTML = '<i class="fas fa-chart-bar"></i> <span>Fetch Stats</span>';
btn.disabled = false;
@@ -205,13 +206,19 @@
addFetchStatsButton();
patchCards();
let debounceTimer = null;
const observer = new MutationObserver(() => {
patchCards();
patchSortDropdown();
addFetchStatsButton();
if (debounceTimer) return;
debounceTimer = setTimeout(() => {
debounceTimer = null;
patchCards();
patchSortDropdown();
addFetchStatsButton();
}, 200);
});
observer.observe(document.body, { childList: true, subtree: true });
const grid = document.getElementById("modelGrid");
observer.observe(grid || document.body, { childList: true, subtree: true });
}
// ── Init ───────────────────────────────────────────────────────
+2
View File
@@ -74,6 +74,8 @@ class StatsDB:
def upsert_batch(self, rows: list[tuple[str, dict]]) -> None:
"""Insert or update stats for multiple models."""
if not rows:
return
conn = self._ensure_conn()
now = time.time()
conn.executemany(