fix(quarantine+quality): persistent group toggle, better alt-row UI, quality upgrade default scope

Quarantine grouping:
- First candidate shown as normal row; others hidden under a "▾ N more"
  button inline in the actions bar — no separate header row
- Group open state tracked in _verifQuarOpenGroups (Set), survives
  periodic re-renders so the list no longer auto-collapses

Quality Upgrade Finder:
- Default scope changed from 'watchlist' to 'all' so it scans the whole
  library when no scope is explicitly configured
- _get_settings rewritten to read the full settings dict at once
  (same pattern as QualityUpgradeScannerJob) to fix silent read failures

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
dev 2026-06-24 15:26:41 +02:00
parent e4ba27d8b3
commit ef0b68a973
3 changed files with 48 additions and 36 deletions

View file

@ -476,25 +476,26 @@ class QualityUpgradeJob(RepairJob):
icon = 'repair-icon-lossy'
default_enabled = False
default_interval_hours = 168
default_settings = {'scope': 'watchlist', 'min_confidence': 0.7, 'deep_audio_verify': False, 'require_top_target': False}
setting_options = {'scope': ['watchlist', 'all'], 'deep_audio_verify': [True, False], 'require_top_target': [True, False]}
default_settings = {'scope': 'all', 'min_confidence': 0.7, 'deep_audio_verify': False, 'require_top_target': False}
setting_options = {'scope': ['all', 'watchlist'], 'deep_audio_verify': [True, False], 'require_top_target': [True, False]}
auto_fix = False
def _get_settings(self, context: JobContext) -> Dict[str, Any]:
cfg = context.config_manager
scope = 'watchlist'
min_conf = 0.7
deep_verify = False
require_top = False
if cfg:
scope = cfg.get(self.get_config_key('settings.scope'), 'watchlist') or 'watchlist'
merged = dict(self.default_settings)
if context.config_manager:
try:
min_conf = float(cfg.get(self.get_config_key('settings.min_confidence'), 0.7))
except (TypeError, ValueError):
min_conf = 0.7
deep_verify = cfg.get(self.get_config_key('settings.deep_audio_verify'), False) is True
require_top = cfg.get(self.get_config_key('settings.require_top_target'), False) is True
return {'scope': scope, 'min_confidence': min_conf, 'deep_audio_verify': deep_verify, 'require_top_target': require_top}
cfg = context.config_manager.get(f'repair.jobs.{self.job_id}.settings', {})
if isinstance(cfg, dict):
merged.update(cfg)
except Exception as e:
logger.debug("settings read failed: %s", e)
try:
merged['min_confidence'] = float(merged.get('min_confidence', 0.7))
except (TypeError, ValueError):
merged['min_confidence'] = 0.7
merged['deep_audio_verify'] = merged.get('deep_audio_verify') is True
merged['require_top_target'] = merged.get('require_top_target') is True
return merged
def _load_tracks(self, db: Any, scope: str) -> List[dict]:
conn = db._get_connection()

View file

@ -2611,6 +2611,7 @@ let _verifQuarLoading = false;
// Expanded 🔍 detail panels, keyed by quarantine entry id — survives the
// polling re-render (which rebuilds the rows every few seconds).
const _verifQuarOpenDetails = new Set();
const _verifQuarOpenGroups = new Set(); // group keys whose alt-members are expanded
// null = not fetched yet (assume enabled). Without an AcoustID API key
// nothing ever gets a verification status, so the review queue collapses
// to quarantine-only.
@ -2668,7 +2669,7 @@ const _VERIF_QUAR_TRIGGERS = {
bit_depth: ['BIT DEPTH FILTER', 'verif-rb-int'],
};
function _verifQuarRowHtml(q, idx) {
function _verifQuarRowHtml(q, idx, extraAction = '') {
const title = _adlEsc(q.expected_track || q.original_filename || q.filename || 'Unknown file');
const meta = [_adlEsc(q.expected_artist || ''), _adlEsc(q.original_filename || '')].filter(Boolean).join(' — ');
const [trigLabel, trigClass] = _VERIF_QUAR_TRIGGERS[q.trigger] || ['QUARANTINED', 'verif-rb-unv'];
@ -2702,10 +2703,21 @@ function _verifQuarRowHtml(q, idx) {
<button class="verif-act" onclick="verifQuarAudit(${idx})" title="Open the audit trail for this quarantined file (details, embedded tags, lyrics)">🔍</button>
${approveBtn}
<button class="verif-act verif-act-del" onclick="verifQuarDelete(${idx}, this)" title="Delete the quarantined file permanently">🗑</button>
${extraAction}
</div>
</div>`;
}
function _verifQuarToggleGroup(btn) {
const key = btn.dataset.groupKey;
const open = !_verifQuarOpenGroups.has(key);
if (open) _verifQuarOpenGroups.add(key); else _verifQuarOpenGroups.delete(key);
const wrapper = btn.closest('.verif-quar-alt-wrapper');
if (wrapper) wrapper.querySelector('.verif-quar-alt-members')?.classList.toggle('vqg-open', open);
btn.classList.toggle('open', open);
btn.textContent = open ? `${btn.dataset.altCount} more` : `${btn.dataset.altCount} more`;
}
function _verifQuarRows() {
if (!_verifQuarLoaded) return '<div class="adl-section-header">Loading quarantine…</div>';
if (!_verifQuarEntries.length) return '';
@ -2718,20 +2730,20 @@ function _verifQuarRows() {
if (group.members.length === 1) {
html += _verifQuarRowHtml(group.members[0], idxById.get(group.members[0].id));
} else {
// First member shown as normal row; rest hidden under a toggle button.
const first = group.members[0];
const title = _adlEsc(first.expected_track || first.original_filename || 'Unknown');
const artist = first.expected_artist ? `${_adlEsc(first.expected_artist)}` : '';
const n = group.members.length;
const groupId = `vqg-${_adlEsc(first.id)}`;
html += `<div class="verif-quar-group">
<div class="verif-quar-group-hdr" onclick="document.getElementById('${groupId}').classList.toggle('vqg-open')" title="${n} alternative candidates quarantined for this track">
<span class="verif-quar-group-lbl">${title}${artist}</span>
<span class="verif-quar-group-cnt">${n} alternatives &#x25BE;</span>
</div>
<div class="verif-quar-group-members" id="${groupId}">
${group.members.map(m => _verifQuarRowHtml(m, idxById.get(m.id))).join('')}
</div>
</div>`;
const firstIdx = idxById.get(first.id);
const altCount = group.members.length - 1;
const groupKey = group.key || first.id;
const isOpen = _verifQuarOpenGroups.has(groupKey);
const altBtn = `<button class="verif-quar-alt-btn${isOpen ? ' open' : ''}" data-group-key="${_adlEsc(groupKey)}" data-alt-count="${altCount}" onclick="_verifQuarToggleGroup(this)" title="Show ${altCount} more alternative candidate${altCount === 1 ? '' : 's'} for this track">${isOpen ? '▴' : '▾'} ${altCount} more</button>`;
html += `<div class="verif-quar-alt-wrapper">`;
html += _verifQuarRowHtml(first, firstIdx, altBtn);
html += `<div class="verif-quar-alt-members${isOpen ? ' vqg-open' : ''}">`;
for (let i = 1; i < group.members.length; i++) {
html += _verifQuarRowHtml(group.members[i], idxById.get(group.members[i].id));
}
html += `</div></div>`;
}
}
return html;

View file

@ -68215,13 +68215,12 @@ body.em-scroll-lock { overflow: hidden; }
.verif-banner-spacer { flex: 1; }
.verif-bulk-danger { border-color: rgba(248,113,113,0.4) !important; color: #f87171 !important; }
.verif-quar-group { margin-bottom: 4px; }
.verif-quar-group-hdr { display: flex; align-items: center; gap: 8px; padding: 6px 12px; background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); border-radius: 6px; cursor: pointer; user-select: none; }
.verif-quar-group-hdr:hover { background: rgba(255,255,255,0.07); }
.verif-quar-group-lbl { flex: 1; font-size: 13px; font-weight: 500; color: rgba(255,255,255,0.85); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.verif-quar-group-cnt { font-size: 12px; color: rgba(255,255,255,0.45); white-space: nowrap; }
.verif-quar-group-members { display: none; padding-left: 12px; padding-top: 2px; }
.verif-quar-group-members.vqg-open { display: block; }
.verif-quar-alt-wrapper { display: contents; }
.verif-quar-alt-btn { border: 1px solid rgba(255,255,255,0.15); background: rgba(255,255,255,0.05); color: rgba(255,255,255,0.5); border-radius: 6px; padding: 2px 7px; font-size: 11px; cursor: pointer; white-space: nowrap; line-height: 18px; }
.verif-quar-alt-btn:hover { background: rgba(255,255,255,0.12); color: rgba(255,255,255,0.8); }
.verif-quar-alt-btn.open { color: rgba(255,255,255,0.75); border-color: rgba(255,255,255,0.25); }
.verif-quar-alt-members { display: none; padding-left: 20px; border-left: 2px solid rgba(255,255,255,0.07); margin-left: 8px; margin-bottom: 2px; }
.verif-quar-alt-members.vqg-open { display: block; }
/* ── Security settings: grouped sub-sections + dependency visuals ── */
.security-subgroup {