* fix: MySQL-compatible case-insensitive model merge and disabled model matching
Root cause: MySQL uses utf8mb4_general_ci collation by default, making the
unique index on (account_id, model_name) case-insensitive. When different
tokens discover the same model with different casing (e.g., 'GPT-4o' vs
'gpt-4o'), the case-sensitive JavaScript Set allowed both, but the MySQL
batch INSERT failed with a duplicate key error — causing the entire model
refresh to silently fail and leaving stale routes (including disabled models).
Fixes:
1. mergeDiscoveredModels: change Set<string> to Map<lowercase, original>
for cross-scan dedup, matching normalizeModels' existing behavior.
2. isModelDisabledForSite: compare with toLowerCase() so disabled models
are filtered regardless of case variants across discovery sources.
3. tokens.ts /api/routes/rebuild: add missing await on
rebuildTokenRoutesFromAvailability() so errors propagate correctly
instead of being silently swallowed.
* fix: add missing await on rebuildTokenRoutesFromAvailability in stats.ts
The POST handler for /api/models/check/:accountId called
rebuildTokenRoutesFromAvailability() without await, returning a Promise
object in the response instead of the actual rebuild result.
- Sync blockedBrands local state after save to prevent stale chips
- Defer getBrandList from blocking Settings bootstrap
- Guard model management UI on brandGroups instead of availableModels
- Deduplicate single-model add in disabled models
- Skip read-only/zero-channel routes in enabledCounts and filter
- Add sequence counter to loadCandidates to prevent force race condition
- Use stableAddChannel for mobile add-channel button
Replace closure-captured editor check with loadingModelsSiteIdRef so the
stale-response guard works correctly across rapid site switches. Also
gate setDisabledModelsLoading(false) behind the same ref check.
- settings.ts: trigger route rebuild after globalBlockedBrands changes in both
import and PUT handler; reject non-array payloads with 400 instead of silently
defaulting to empty array
- sites.ts: trim model names before deduplication so whitespace variants merge
- brandMatcher.ts: align boundary regex with frontend delimiter class [/:_\-\s]
- Settings.tsx: add role="switch" and aria-checked to brand filter chips; make
getBrandList best-effort so its failure doesn't block page load
- Sites.tsx: build brandGroups from union of availableModels and disabledModels
so legacy/removed models remain visible; add stale-response guard and error
logging for disabled/available model fetches
- TokenRoutes.tsx: use baseFilteredRoutes.length for totalRouteCount to keep
enabled/disabled counts in sync with status filter; add candidatesVersionRef
to invalidate lazy caches on payload change; add per-channel in-flight guard
on toggle; invalidate channel cache after rebuild and site-block operations