From 48aec3f6f30c340bc94277e2cc93cfc2fa6e584d Mon Sep 17 00:00:00 2001 From: Antti Kettunen Date: Sun, 26 Apr 2026 19:39:18 +0300 Subject: [PATCH] Remove legacy issues shell code - Delete the static issues page renderer and detail modal helpers - Keep the React issues route as the only implementation - Drop the dead mobile CSS and troubleshooter hook that only targeted the removed shell --- webui/static/helper.js | 11 - webui/static/init.js | 6 - webui/static/mobile.css | 38 +- webui/static/stats-automations.js | 1030 +--------------------- webui/static/style.css | 1316 +---------------------------- 5 files changed, 22 insertions(+), 2379 deletions(-) diff --git a/webui/static/helper.js b/webui/static/helper.js index 7384c970..6eb516a2 100644 --- a/webui/static/helper.js +++ b/webui/static/helper.js @@ -4536,17 +4536,6 @@ const TROUBLESHOOT_RULES = [ 'Consider switching to iTunes temporarily to continue working' ] }, - { - selector: '.issue-card.status-open, .issues-stat-open', - title: 'Open Issues in Library', - steps: [ - 'Open issues have been reported for tracks in your library', - 'Go to the Issues page to review and resolve them', - 'Common issues: wrong track downloaded, bad metadata, low audio quality', - 'Each issue has fix suggestions and action buttons' - ], - action: { label: 'View Issues', fn: () => navigateToPage('issues') } - }, ]; function activateTroubleshootMode() { diff --git a/webui/static/init.js b/webui/static/init.js index f7f84e63..7e94f3fc 100644 --- a/webui/static/init.js +++ b/webui/static/init.js @@ -2198,9 +2198,6 @@ function initApp() { // Start always-on download polling (batched, minimal overhead) startGlobalDownloadPolling(); - // Load issues badge count - loadIssuesBadge(); - // Load initial data loadInitialData(); @@ -2499,9 +2496,6 @@ async function loadPageData(pageId) { case 'automations': await loadAutomations(); break; - case 'issues': - await loadIssuesPage(); - break; case 'help': initializeDocsPage(); break; diff --git a/webui/static/mobile.css b/webui/static/mobile.css index 75ff8337..8f292d98 100644 --- a/webui/static/mobile.css +++ b/webui/static/mobile.css @@ -3264,42 +3264,6 @@ } } -/* ====================================== - ISSUES PAGE — Mobile (supplement) - ====================================== */ - -@media (max-width: 768px) { - .issues-header-right { - width: 100%; - flex-wrap: wrap; - } - - .issues-filter-select { - flex: 1; - min-width: 0; - } - - .issues-stats { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 8px; - } - - .issues-stat-card { - min-width: 0; - padding: 10px; - } - - .issues-stat-value { - font-size: 18px; - } - - .issue-card-right { - flex-wrap: wrap; - gap: 6px; - } -} - /* ====================================== HELP / DOCS PAGE — Mobile ====================================== */ @@ -3737,4 +3701,4 @@ font-size: 12px; padding: 9px 12px; } -} \ No newline at end of file +} diff --git a/webui/static/stats-automations.js b/webui/static/stats-automations.js index 60b395a5..7df4ab4b 100644 --- a/webui/static/stats-automations.js +++ b/webui/static/stats-automations.js @@ -6388,1034 +6388,24 @@ function _autoInsertVar(textareaId, variable) { el.focus(); } -// ===== ISSUES PAGE ===== - -const ISSUE_CATEGORIES = { - wrong_track: { label: 'Wrong Track', icon: '❌', description: 'This file plays a completely different song than expected', applies: ['track'] }, - wrong_metadata: { label: 'Wrong Metadata', icon: '✎', description: 'Title, artist, year, or other tags are incorrect', applies: ['track', 'album'] }, - wrong_cover: { label: 'Wrong Cover Art', icon: '📷', description: 'The album artwork is wrong or missing', applies: ['album'] }, - wrong_artist: { label: 'Wrong Artist', icon: '👤', description: 'This track is filed under the wrong artist', applies: ['track'] }, - duplicate_tracks: { label: 'Duplicate Tracks', icon: '🔁', description: 'The same track appears more than once in this album', applies: ['album'] }, - missing_tracks: { label: 'Missing Tracks', icon: '❓', description: 'Tracks that should be here are missing from this album', applies: ['album'] }, - audio_quality: { label: 'Audio Quality', icon: '🎵', description: 'Audio has quality issues — clipping, low bitrate, silence, etc.', applies: ['track'] }, - wrong_album: { label: 'Wrong Album', icon: '💿', description: 'This track belongs to a different album', applies: ['track'] }, - incomplete_album: { label: 'Incomplete Album', icon: '⚠', description: 'Album is partially downloaded — some tracks present, others not', applies: ['album'] }, - other: { label: 'Other', icon: '💬', description: 'Any other issue not listed above', applies: ['track', 'album'] }, -}; - -const ISSUE_STATUS_META = { - open: { label: 'Open', cls: 'issue-status-open' }, - in_progress: { label: 'In Progress', cls: 'issue-status-progress' }, - resolved: { label: 'Resolved', cls: 'issue-status-resolved' }, - dismissed: { label: 'Dismissed', cls: 'issue-status-dismissed' }, -}; - -let _issuesPageState = { loaded: false }; - -function _issueHeaders(extra) { - const h = { 'X-Profile-Id': String(currentProfile ? currentProfile.id : 1) }; - if (extra) Object.assign(h, extra); - return h; -} - -async function loadIssuesPage() { - const admin = isEnhancedAdmin(); - const subtitle = document.getElementById('issues-subtitle'); - if (subtitle) { - subtitle.textContent = admin ? 'Manage and resolve reported library problems' : 'Track and resolve library problems'; - } - await Promise.all([loadIssuesList(), loadIssuesCounts()]); -} - -async function loadIssuesCounts() { - try { - const resp = await fetch('/api/issues/counts', { headers: _issueHeaders() }); - const data = await resp.json(); - if (!data.success) return; - const counts = data.counts; - const statsEl = document.getElementById('issues-stats'); - if (!statsEl) return; - const total = (counts.open || 0) + (counts.in_progress || 0) + (counts.resolved || 0) + (counts.dismissed || 0); - statsEl.innerHTML = ` -
-
${counts.open || 0}
-
Open
-
-
-
${counts.in_progress || 0}
-
In Progress
-
-
-
${counts.resolved || 0}
-
Resolved
-
-
-
${counts.dismissed || 0}
-
Dismissed
-
-
-
${total}
-
Total
-
- `; - // Update nav badge - const badge = document.getElementById('issues-nav-badge'); - if (badge) { - const openCount = counts.open || 0; - badge.textContent = openCount; - badge.classList.toggle('hidden', openCount === 0); - } - } catch (e) { - console.error('Failed to load issue counts:', e); - } -} - -async function loadIssuesList() { - const listEl = document.getElementById('issues-list'); - if (!listEl) return; - listEl.innerHTML = '
Loading issues...
'; - - const statusFilter = document.getElementById('issues-filter-status')?.value || ''; - const categoryFilter = document.getElementById('issues-filter-category')?.value || ''; - - let url = '/api/issues?'; - if (statusFilter) url += `status=${encodeURIComponent(statusFilter)}&`; - if (categoryFilter) url += `category=${encodeURIComponent(categoryFilter)}&`; - - try { - const profileId = currentProfile ? currentProfile.id : 1; - const resp = await fetch(url, { headers: { 'X-Profile-Id': String(profileId) } }); - const data = await resp.json(); - if (!data.success || !data.issues || data.issues.length === 0) { - listEl.innerHTML = ` -
-
🔍
-
No issues found
-
${statusFilter || categoryFilter ? 'Try adjusting your filters' : 'No issues have been reported yet'}
-
- `; - return; - } - listEl.innerHTML = ''; - data.issues.forEach(issue => { - listEl.appendChild(renderIssueCard(issue)); - }); - } catch (e) { - console.error('Failed to load issues:', e); - listEl.innerHTML = '
Failed to load issues
'; - } -} - -function renderIssueCard(issue) { - const card = document.createElement('div'); - card.className = 'issue-card'; - card.dataset.issueId = issue.id; - card.onclick = () => showIssueDetailModal(issue.id); - - const catMeta = ISSUE_CATEGORIES[issue.category] || ISSUE_CATEGORIES.other; - const statusMeta = ISSUE_STATUS_META[issue.status] || ISSUE_STATUS_META.open; - const admin = isEnhancedAdmin(); - - let snapshot = {}; - try { snapshot = typeof issue.snapshot_data === 'string' ? JSON.parse(issue.snapshot_data || '{}') : (issue.snapshot_data || {}); } catch (e) { } - - const entityLabel = issue.entity_type === 'track' ? 'Track' : (issue.entity_type === 'album' ? 'Album' : 'Artist'); - const entityName = snapshot.title || snapshot.name || `${entityLabel} #${issue.entity_id}`; - const artistName = snapshot.artist_name || ''; - const albumName = snapshot.album_title || ''; - const thumbUrl = snapshot.thumb_url || snapshot.album_thumb || ''; - - const createdDate = issue.created_at ? new Date(issue.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : ''; - const createdTime = issue.created_at ? new Date(issue.created_at).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }) : ''; - - // Priority indicator - const priorityCls = issue.priority === 'high' ? 'issue-priority-high' : (issue.priority === 'low' ? 'issue-priority-low' : 'issue-priority-normal'); - - let thumbHtml = ''; - if (thumbUrl) { - thumbHtml = ``; - } else { - thumbHtml = `
${catMeta.icon}
`; - } - - let metaLine = ''; - if (issue.entity_type === 'track') { - metaLine = [artistName, albumName].filter(Boolean).map(s => _esc(s)).join(' — '); - } else if (issue.entity_type === 'album') { - metaLine = artistName ? _esc(artistName) : ''; - } - - let profileBadge = ''; - if (admin && issue.reporter_name) { - profileBadge = `by ${_esc(issue.reporter_name)}`; - } - - let adminResponseIndicator = ''; - if (issue.admin_response) { - adminResponseIndicator = '💬'; - } - - card.innerHTML = ` -
- ${thumbHtml} -
-
-
- ${catMeta.icon} - ${_esc(issue.title)} - ${adminResponseIndicator} -
-
- ${_esc(entityLabel)} - ${_esc(entityName)} - ${metaLine ? `${metaLine}` : ''} -
- ${issue.description ? `
${_esc(issue.description)}
` : ''} - -
-
- ${_esc(statusMeta.label)} - -
- `; - return card; -} - -// --- Report Issue Modal --- - -let _reportIssueState = {}; +// --- Report Issue Helper --- function showReportIssueModal(entityType, entityId, entityName, artistName, albumTitle) { - _reportIssueState = { entityType, entityId, entityName, artistName, albumTitle: albumTitle || '' }; - const overlay = document.getElementById('report-issue-overlay'); - const titleEl = document.getElementById('report-issue-title'); - const body = document.getElementById('report-issue-body'); - if (!overlay || !body) return; - - const entityLabel = entityType === 'track' ? 'Track' : (entityType === 'album' ? 'Album' : 'Artist'); - titleEl.textContent = `Report Issue — ${entityLabel}`; - - body.innerHTML = ` -
-
${_esc(entityName)}
- ${artistName ? `
${_esc(artistName)}${albumTitle ? ' — ' + _esc(albumTitle) : ''}
` : ''} -
-
- -
- ${Object.entries(ISSUE_CATEGORIES) - .filter(([, cat]) => !cat.applies || cat.applies.includes(entityType)) - .map(([key, cat]) => ` -
-
${cat.icon}
-
${_esc(cat.label)}
-
${_esc(cat.description)}
-
- `).join('')} -
-
- - `; - - _reportIssueState.selectedCategory = null; - _reportIssueState.selectedPriority = 'normal'; - const submitBtn = document.getElementById('report-issue-submit-btn'); - if (submitBtn) submitBtn.disabled = true; - - overlay.classList.remove('hidden'); -} - -function selectIssueCategory(el, category) { - document.querySelectorAll('.report-issue-category-card').forEach(c => c.classList.remove('selected')); - el.classList.add('selected'); - _reportIssueState.selectedCategory = category; - - const detailsSection = document.getElementById('report-issue-details-section'); - if (detailsSection) detailsSection.style.display = ''; - - // Auto-generate title based on category - const titleInput = document.getElementById('report-issue-input-title'); - const catMeta = ISSUE_CATEGORIES[category]; - if (titleInput && !titleInput._userEdited) { - const entityName = _reportIssueState.entityName || ''; - titleInput.value = `${catMeta.label}: ${entityName}`; - } - - const submitBtn = document.getElementById('report-issue-submit-btn'); - if (submitBtn) submitBtn.disabled = false; -} - -function selectIssuePriority(el, priority) { - document.querySelectorAll('.report-issue-priority-btn').forEach(b => b.classList.remove('selected')); - el.classList.add('selected'); - _reportIssueState.selectedPriority = priority; -} - -function closeReportIssueModal() { - const overlay = document.getElementById('report-issue-overlay'); - if (overlay) overlay.classList.add('hidden'); - _reportIssueState = {}; -} - -async function submitIssue() { - if (_reportIssueState._submitting) return; - const category = _reportIssueState.selectedCategory; - if (!category) { - showToast('Please select an issue category', 'error'); - return; - } - - const titleInput = document.getElementById('report-issue-input-title'); - const descInput = document.getElementById('report-issue-input-desc'); - const title = (titleInput?.value || '').trim(); - const description = (descInput?.value || '').trim(); - - if (!title) { - showToast('Please provide a title for the issue', 'error'); - return; - } - - _reportIssueState._submitting = true; - const submitBtn = document.getElementById('report-issue-submit-btn'); - if (submitBtn) { - submitBtn.disabled = true; - submitBtn.textContent = 'Submitting...'; - } - - try { - const resp = await fetch('/api/issues', { - method: 'POST', - headers: _issueHeaders({ 'Content-Type': 'application/json' }), - body: JSON.stringify({ - profile_id: currentProfile ? currentProfile.id : 1, - entity_type: _reportIssueState.entityType, - entity_id: String(_reportIssueState.entityId), - category: category, - title: title, - description: description, - priority: _reportIssueState.selectedPriority || 'normal', - }), - }); - const data = await resp.json(); - if (data.success) { - showToast('Issue reported successfully', 'success'); - closeReportIssueModal(); - // Refresh issues page if visible - const issuesPage = document.getElementById('issues-page'); - if (issuesPage && issuesPage.classList.contains('active')) { - loadIssuesPage(); - } - // Update badge - loadIssuesBadge(); - } else { - showToast(data.error || 'Failed to submit issue', 'error'); - } - } catch (e) { - console.error('Failed to submit issue:', e); - showToast('Failed to submit issue', 'error'); - } finally { - _reportIssueState._submitting = false; - if (submitBtn) { - submitBtn.disabled = false; - submitBtn.textContent = 'Submit Issue'; - } - } -} - -// --- Issue Detail Modal --- - -async function showIssueDetailModal(issueId) { - const overlay = document.getElementById('issue-detail-overlay'); - const body = document.getElementById('issue-detail-body'); - const footer = document.getElementById('issue-detail-footer'); - const titleEl = document.getElementById('issue-detail-title'); - if (!overlay || !body) return; - - body.innerHTML = '
Loading...
'; - footer.innerHTML = ''; - overlay.classList.remove('hidden'); - - try { - const resp = await fetch(`/api/issues/${issueId}`, { headers: _issueHeaders() }); - const data = await resp.json(); - if (!data.success || !data.issue) { - body.innerHTML = '
Issue not found
'; - return; - } - renderIssueDetail(data.issue, body, footer, titleEl); - } catch (e) { - console.error('Failed to load issue:', e); - body.innerHTML = '
Failed to load issue
'; - } -} - -function renderIssueDetail(issue, body, footer, titleEl) { - const admin = isEnhancedAdmin(); - const catMeta = ISSUE_CATEGORIES[issue.category] || ISSUE_CATEGORIES.other; - const statusMeta = ISSUE_STATUS_META[issue.status] || ISSUE_STATUS_META.open; - - let snapshot = {}; - try { snapshot = typeof issue.snapshot_data === 'string' ? JSON.parse(issue.snapshot_data || '{}') : (issue.snapshot_data || {}); } catch (e) { } - - const entityLabel = issue.entity_type === 'track' ? 'Track' : (issue.entity_type === 'album' ? 'Album' : 'Artist'); - const entityName = snapshot.title || snapshot.name || `${entityLabel} #${issue.entity_id}`; - const artistName = snapshot.artist_name || (issue.entity_type === 'artist' ? snapshot.name : '') || ''; - const albumTitle = issue.entity_type === 'album' ? (snapshot.title || '') : (snapshot.album_title || ''); - const artistId = issue.entity_type === 'artist' ? snapshot.id : snapshot.artist_id; - - // Resolve image URLs — album art and artist photo - let artistThumb = ''; - let albumThumb = ''; - if (issue.entity_type === 'album') { - albumThumb = snapshot.thumb_url || ''; - artistThumb = snapshot.artist_thumb || ''; - } else if (issue.entity_type === 'track') { - albumThumb = snapshot.album_thumb || ''; - artistThumb = snapshot.artist_thumb || ''; - } else { - // Artist issue - artistThumb = snapshot.thumb_url || ''; - } - - // Determine the album-level Spotify ID for download/wishlist actions - const spotifyAlbumId = snapshot.spotify_album_id || ''; - - console.log('Issue detail snapshot:', { entityType: issue.entity_type, albumThumb, artistThumb, spotifyAlbumId, snapshotKeys: Object.keys(snapshot) }); - - const createdDate = issue.created_at ? new Date(issue.created_at).toLocaleString() : 'Unknown'; - const resolvedDate = issue.resolved_at ? new Date(issue.resolved_at).toLocaleString() : ''; - - titleEl.textContent = `Issue #${issue.id}`; - - // --- Build external links chips --- - function _extLinks(snap) { - const links = []; - if (snap.spotify_artist_id) links.push({ svc: 'Spotify', type: 'Artist', url: `https://open.spotify.com/artist/${snap.spotify_artist_id}`, cls: 'ext-spotify' }); - if (snap.spotify_album_id) links.push({ svc: 'Spotify', type: 'Album', url: `https://open.spotify.com/album/${snap.spotify_album_id}`, cls: 'ext-spotify' }); - if (snap.spotify_track_id) links.push({ svc: 'Spotify', type: 'Track', url: `https://open.spotify.com/track/${snap.spotify_track_id}`, cls: 'ext-spotify' }); - if (snap.artist_musicbrainz_id) links.push({ svc: 'MusicBrainz', type: 'Artist', url: `https://musicbrainz.org/artist/${snap.artist_musicbrainz_id}`, cls: 'ext-mb' }); - if (snap.musicbrainz_release_id) links.push({ svc: 'MusicBrainz', type: 'Release', url: `https://musicbrainz.org/release/${snap.musicbrainz_release_id}`, cls: 'ext-mb' }); - if (snap.musicbrainz_recording_id) links.push({ svc: 'MusicBrainz', type: 'Recording', url: `https://musicbrainz.org/recording/${snap.musicbrainz_recording_id}`, cls: 'ext-mb' }); - if (snap.artist_deezer_id) links.push({ svc: 'Deezer', type: 'Artist', url: `https://www.deezer.com/artist/${snap.artist_deezer_id}`, cls: 'ext-deezer' }); - if (snap.album_deezer_id) links.push({ svc: 'Deezer', type: 'Album', url: `https://www.deezer.com/album/${snap.album_deezer_id}`, cls: 'ext-deezer' }); - if (snap.track_deezer_id) links.push({ svc: 'Deezer', type: 'Track', url: `https://www.deezer.com/track/${snap.track_deezer_id}`, cls: 'ext-deezer' }); - if (snap.artist_tidal_id) links.push({ svc: 'Tidal', type: 'Artist', url: `https://listen.tidal.com/artist/${snap.artist_tidal_id}`, cls: 'ext-tidal' }); - if (snap.album_tidal_id) links.push({ svc: 'Tidal', type: 'Album', url: `https://listen.tidal.com/album/${snap.album_tidal_id}`, cls: 'ext-tidal' }); - if (snap.artist_qobuz_id) links.push({ svc: 'Qobuz', type: 'Artist', cls: 'ext-qobuz', id: snap.artist_qobuz_id }); - if (snap.album_qobuz_id) links.push({ svc: 'Qobuz', type: 'Album', cls: 'ext-qobuz', id: snap.album_qobuz_id }); - return links; - } - - const extLinks = _extLinks(snapshot); - let extLinksHtml = ''; - if (extLinks.length > 0) { - const chips = extLinks.map(l => { - if (l.url) { - return `${_esc(l.svc)} ${_esc(l.type)}`; - } - return `${_esc(l.svc)} ${_esc(l.type)}`; - }).join(''); - extLinksHtml = `
${chips}
`; - } - - // --- Build enhanced-library-style album/track widget --- - // Determine which album data to show (for album issues it's the entity, for track issues it's the parent) - const showAlbumWidget = (issue.entity_type === 'album' || issue.entity_type === 'track'); - const albumName = issue.entity_type === 'album' ? (snapshot.title || '') : (snapshot.album_title || ''); - const albumYear = snapshot.year || ''; - const albumLabel = snapshot.label || ''; - const albumType = snapshot.record_type || ''; - const albumTrackCount = issue.entity_type === 'album' ? (snapshot.track_count || '') : (snapshot.album_track_count || ''); - const albumGenres = snapshot.genres || []; - - // --- Build the hero section (artist photo + album art + info) --- - let heroHtml = ''; - if (showAlbumWidget) { - // Genre tags - let genreTagsHtml = ''; - if (Array.isArray(albumGenres) && albumGenres.length > 0) { - genreTagsHtml = `
${albumGenres.slice(0, 5).map(g => `${_esc(g)}`).join('')}
`; - } - - // Album meta line - const albumMetaParts = []; - if (albumYear) albumMetaParts.push(String(albumYear)); - if (albumType) albumMetaParts.push(albumType.charAt(0).toUpperCase() + albumType.slice(1)); - if (albumTrackCount) albumMetaParts.push(albumTrackCount + ' tracks'); - if (albumLabel) albumMetaParts.push(albumLabel); - - // For track issues, show the track title under the album - const trackNameLine = issue.entity_type === 'track' && entityName - ? `
♫ ${_esc(entityName)}
` : ''; - - heroHtml = ` -
-
- ${artistThumb ? `` : ''} - ${albumThumb ? `` : ''} -
${catMeta.icon}
-
-
- ${artistName ? `
${_esc(artistName)}
` : ''} -
${_esc(albumName)}
- ${trackNameLine} - ${albumMetaParts.length > 0 ? `
${_esc(albumMetaParts.join(' \u00B7 '))}
` : ''} - ${genreTagsHtml} - ${extLinksHtml} -
-
- `; - } else { - // Artist-level issue — simpler hero - heroHtml = ` -
-
- ${artistThumb ? `` : `
${catMeta.icon}
`} -
-
-
${_esc(entityName)}
- ${extLinksHtml} -
-
- `; - } - - // --- Issue info bar --- - let issueInfoHtml = ` -
-
- ${_esc(statusMeta.label)} - - ${catMeta.icon} ${_esc(catMeta.label)} -
-
- Reported ${_esc(createdDate)} - ${issue.reporter_name && admin ? `by ${_esc(issue.reporter_name)}` : ''} - ${resolvedDate ? `Resolved ${_esc(resolvedDate)}` : ''} -
-
- `; - - // --- Issue description --- - let descriptionHtml = ` -
-
Issue
-
${_esc(issue.title)}
- ${issue.description ? `
${_esc(issue.description)}
` : '
No additional details provided
'} -
- `; - - // --- Action buttons (Download Album / Add to Wishlist) for admin --- - let actionButtonsHtml = ''; - if (admin && (issue.entity_type === 'album' || issue.entity_type === 'track')) { - actionButtonsHtml = ` -
- - -
- `; - } - - // --- Metadata grid for track-level issues --- - let metaGridHtml = ''; - if (issue.entity_type === 'track') { - const metaItems = []; - if (snapshot.track_number) metaItems.push({ icon: '#', label: 'Track', value: String(snapshot.track_number) }); - if (snapshot.duration) metaItems.push({ icon: '◷', label: 'Duration', value: typeof snapshot.duration === 'number' ? formatDurationMs(snapshot.duration) : String(snapshot.duration) }); - if (snapshot.format) metaItems.push({ icon: '💾', label: 'Format', value: snapshot.format }); - if (snapshot.bitrate) metaItems.push({ icon: '🎶', label: 'Bitrate', value: snapshot.bitrate + ' kbps' }); - if (snapshot.bpm) metaItems.push({ icon: '♫', label: 'BPM', value: String(snapshot.bpm) }); - if (snapshot.quality) metaItems.push({ icon: '★', label: 'Quality', value: snapshot.quality }); - if (metaItems.length > 0) { - metaGridHtml = ` -
-
Track Details
-
- ${metaItems.map(m => ` -
- ${m.icon} - ${_esc(m.label)} - ${_esc(m.value)} -
- `).join('')} -
-
- `; - } - } - - // --- File path display for tracks --- - let filePathHtml = ''; - if (snapshot.file_path) { - filePathHtml = ` -
-
File Path
-
${_esc(snapshot.file_path)}
-
- `; - } - - // --- Enhanced-library-style track listing --- - let trackListHtml = ''; - if (snapshot.tracks && Array.isArray(snapshot.tracks) && snapshot.tracks.length > 0) { - let lastDisc = null; - let rows = ''; - const hasMultiDisc = snapshot.tracks.some(tr => (tr.disc_number || 1) > 1); - snapshot.tracks.forEach(t => { - const disc = t.disc_number || 1; - if (hasMultiDisc && disc !== lastDisc) { - rows += `
Disc ${disc}
`; - lastDisc = disc; - } - const fmt = t.format || (t.file_path ? t.file_path.split('.').pop().toUpperCase() : ''); - const fmtLower = fmt.toLowerCase(); - const fmtClass = fmtLower === 'flac' ? 'flac' : (fmtLower === 'mp3' ? 'mp3' : 'other'); - const br = t.bitrate ? parseInt(t.bitrate) : 0; - const brClass = br >= 320 || fmtLower === 'flac' ? 'high' : (br >= 192 ? 'medium' : 'low'); - const durStr = t.duration && typeof t.duration === 'number' ? formatDurationMs(t.duration) : ''; - - rows += ` -
- ${_esc(String(t.track_number || '-'))} - ${_esc(t.title || 'Unknown')} - ${durStr ? `${durStr}` : ''} - - ${fmt ? `${_esc(fmt)}` : ''} - ${br ? `${br}k` : ''} - -
- `; + const bridge = window.SoulSyncIssueDomain; + if (bridge && typeof bridge.openReportIssue === 'function') { + bridge.openReportIssue({ + entityType, + entityId, + entityName, + artistName, + albumTitle: albumTitle || '', }); - trackListHtml = ` -
-
Track Listing ${snapshot.tracks.length} tracks
-
${rows}
-
- `; - } - - // --- Admin response section --- - let adminResponseHtml = ''; - if (admin) { - adminResponseHtml = ` -
-
Admin Response
- -
- `; - } else if (issue.admin_response) { - adminResponseHtml = ` -
-
Admin Response
-
${_esc(issue.admin_response)}
-
- `; - } - - body.innerHTML = ` - ${heroHtml} - ${issueInfoHtml} - ${actionButtonsHtml} - ${descriptionHtml} - ${metaGridHtml} - ${filePathHtml} - ${trackListHtml} - ${adminResponseHtml} - `; - - // --- Footer with status action buttons --- - const safeId = parseInt(issue.id, 10); - let footerHtml = ''; - - if (admin) { - if (issue.status === 'open' || issue.status === 'in_progress') { - if (issue.status === 'open') { - footerHtml += ``; - } - footerHtml += ``; - footerHtml += ``; - } else { - footerHtml += ``; - } - footerHtml += ``; - } else { - if (issue.status === 'open') { - footerHtml += ``; - } - } - - footer.innerHTML = footerHtml; - - // --- Attach action button handlers --- - const dlBtn = document.getElementById('issue-action-download'); - if (dlBtn) { - dlBtn.onclick = () => issueDownloadAlbum(spotifyAlbumId, artistName, albumName); - } - const wlBtn = document.getElementById('issue-action-wishlist'); - if (wlBtn) { - wlBtn.onclick = () => issueAddToWishlist(spotifyAlbumId, artistName, albumName); - } -} - -// --- Issue Action: Download Album --- -async function issueDownloadAlbum(spotifyAlbumId, artistName, albumName) { - const btn = document.getElementById('issue-action-download'); - if (!spotifyAlbumId && (!artistName || !albumName)) { - showToast('No album ID or artist/album info available for download', 'warning'); return; } - try { - if (btn) { btn.disabled = true; btn.textContent = 'Loading...'; } - - let response; - if (spotifyAlbumId) { - const albumParams = new URLSearchParams({ name: albumName || '', artist: artistName || '' }); - response = await fetch(`/api/spotify/album/${encodeURIComponent(spotifyAlbumId)}?${albumParams}`); - } else { - // No Spotify album ID — search for the album by name - const query = `${artistName} ${albumName}`; - const searchResp = await fetch('/api/enhanced-search', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ query }) - }); - if (!searchResp.ok) throw new Error('Album search failed'); - const searchData = await searchResp.json(); - const foundAlbum = searchData.spotify_albums?.[0]; - if (!foundAlbum || !foundAlbum.id) { - showToast(`Could not find "${albumName}" by ${artistName}`, 'warning'); - return; - } - const albumParams = new URLSearchParams({ name: foundAlbum.name || albumName, artist: foundAlbum.artist || artistName }); - response = await fetch(`/api/spotify/album/${encodeURIComponent(foundAlbum.id)}?${albumParams}`); - } - - if (!response.ok) { - if (response.status === 401) throw new Error('Spotify not authenticated'); - throw new Error(`Failed to load album: ${response.status}`); - } - - const albumData = await response.json(); - if (!albumData || !albumData.tracks || albumData.tracks.length === 0) { - showToast(`No tracks available for "${albumName}"`, 'warning'); - return; - } - - // Close the issue modal first - closeIssueDetailModal(); - - const resolvedAlbumId = albumData.id || spotifyAlbumId || Date.now(); - const virtualPlaylistId = `issue_download_${resolvedAlbumId}`; - - // Enrich tracks with album metadata - const enrichedTracks = albumData.tracks.map(track => ({ - ...track, - album: { - name: albumData.name, - id: albumData.id, - album_type: albumData.album_type || 'album', - images: albumData.images || [], - release_date: albumData.release_date, - total_tracks: albumData.total_tracks - } - })); - - const playlistName = `[${artistName}] ${albumData.name}`; - const artistObject = { id: `issue_${artistName}`, name: artistName, image_url: '' }; - const fullAlbumObject = { - name: albumData.name, - id: albumData.id, - album_type: albumData.album_type || 'album', - images: albumData.images || [], - image_url: albumData.images?.[0]?.url || null, - release_date: albumData.release_date, - total_tracks: albumData.total_tracks, - artists: albumData.artists || [{ name: artistName }] - }; - - await openDownloadMissingModalForArtistAlbum( - virtualPlaylistId, playlistName, enrichedTracks, fullAlbumObject, artistObject, true - ); - // Register download bubble so it appears on the dashboard - const albumType = fullAlbumObject.album_type || 'album'; - registerArtistDownload(artistObject, fullAlbumObject, virtualPlaylistId, albumType); - - } catch (error) { - console.error('Issue download error:', error); - showToast(`Error: ${error.message}`, 'error'); - } finally { - if (btn) { btn.disabled = false; btn.innerHTML = ' Download Album'; } - } + console.warn('Report issue bridge is unavailable'); } -// --- Redownload Library Album (Enhanced View) --- -async function redownloadLibraryAlbum(album, artistName, btn) { - const albumName = album.title || ''; - const spotifyAlbumId = album.spotify_album_id || ''; - - if (!spotifyAlbumId && !albumName) { - showToast('No album ID or name available for redownload', 'warning'); - return; - } - - const origText = btn ? btn.innerHTML : ''; - try { - if (btn) { btn.disabled = true; btn.textContent = 'Loading...'; } - - let response; - if (spotifyAlbumId) { - const params = new URLSearchParams({ name: albumName, artist: artistName || '' }); - response = await fetch(`/api/spotify/album/${encodeURIComponent(spotifyAlbumId)}?${params}`); - } - - // Fallback: search by name if no ID or direct fetch failed - if (!response || !response.ok) { - const query = `${artistName || ''} ${albumName}`.trim(); - const searchResp = await fetch('/api/enhanced-search', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ query }) - }); - if (!searchResp.ok) throw new Error('Album search failed'); - const searchData = await searchResp.json(); - const found = searchData.spotify_albums?.[0] || searchData.itunes_albums?.[0]; - if (!found || !found.id) { - showToast(`Could not find "${albumName}" by ${artistName || 'unknown'}`, 'warning'); - return; - } - const params = new URLSearchParams({ name: found.name || albumName, artist: found.artist || artistName || '' }); - response = await fetch(`/api/spotify/album/${encodeURIComponent(found.id)}?${params}`); - } - - if (!response.ok) throw new Error(`Failed to load album: ${response.status}`); - - const albumData = await response.json(); - if (!albumData || !albumData.tracks || albumData.tracks.length === 0) { - showToast(`No tracks found for "${albumName}"`, 'warning'); - return; - } - - const resolvedId = albumData.id || spotifyAlbumId || album.id; - const virtualPlaylistId = `library_redownload_${resolvedId}`; - const playlistName = `[${artistName || 'Unknown'}] ${albumData.name}`; - - const enrichedTracks = albumData.tracks.map(track => ({ - ...track, - album: { - name: albumData.name, - id: albumData.id, - album_type: albumData.album_type || 'album', - images: albumData.images || [], - release_date: albumData.release_date, - total_tracks: albumData.total_tracks - } - })); - - const enhancedArtist = artistDetailPageState.enhancedData?.artist; - const artistObject = { - id: artistDetailPageState.currentArtistId || `library_${artistName || album.id}`, - name: artistName || '', - image_url: enhancedArtist?.thumb_url || '' - }; - const fullAlbumObject = { - name: albumData.name, - id: albumData.id, - album_type: albumData.album_type || 'album', - images: albumData.images || [], - image_url: albumData.images?.[0]?.url || null, - release_date: albumData.release_date, - total_tracks: albumData.total_tracks, - artists: albumData.artists || [{ name: artistName || '' }] - }; - - await openDownloadMissingModalForArtistAlbum( - virtualPlaylistId, playlistName, enrichedTracks, fullAlbumObject, artistObject, true - ); - - // Register download bubble so it appears on the dashboard - const albumType = fullAlbumObject.album_type || 'album'; - registerArtistDownload(artistObject, fullAlbumObject, virtualPlaylistId, albumType); - - } catch (error) { - console.error('Redownload album error:', error); - showToast(`Error: ${error.message}`, 'error'); - } finally { - if (btn) { btn.disabled = false; btn.innerHTML = origText; } - } -} - -// --- Issue Action: Add to Wishlist --- -async function issueAddToWishlist(spotifyAlbumId, artistName, albumName) { - const btn = document.getElementById('issue-action-wishlist'); - if (!spotifyAlbumId && (!artistName || !albumName)) { - showToast('No album ID or artist/album info available', 'warning'); - return; - } - try { - if (btn) { btn.disabled = true; btn.textContent = 'Loading...'; } - - let response; - if (spotifyAlbumId) { - const albumParams = new URLSearchParams({ name: albumName || '', artist: artistName || '' }); - response = await fetch(`/api/spotify/album/${encodeURIComponent(spotifyAlbumId)}?${albumParams}`); - } else { - // No Spotify album ID — search for the album by name - const query = `${artistName} ${albumName}`; - const searchResp = await fetch('/api/enhanced-search', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ query }) - }); - if (!searchResp.ok) throw new Error('Album search failed'); - const searchData = await searchResp.json(); - const foundAlbum = searchData.spotify_albums?.[0]; - if (!foundAlbum || !foundAlbum.id) { - showToast(`Could not find "${albumName}" by ${artistName}`, 'warning'); - return; - } - const albumParams = new URLSearchParams({ name: foundAlbum.name || albumName, artist: foundAlbum.artist || artistName }); - response = await fetch(`/api/spotify/album/${encodeURIComponent(foundAlbum.id)}?${albumParams}`); - } - - if (!response.ok) throw new Error(`Failed to load album: ${response.status}`); - - const albumData = await response.json(); - if (!albumData || !albumData.tracks || albumData.tracks.length === 0) { - showToast(`No tracks available for "${albumName}"`, 'warning'); - return; - } - - // Close issue modal and open wishlist modal - closeIssueDetailModal(); - - const albumArtists = albumData.artists || [{ name: artistName }]; - const album = { - name: albumData.name, - id: albumData.id, - album_type: albumData.album_type || 'album', - images: albumData.images || [], - release_date: albumData.release_date, - total_tracks: albumData.total_tracks, - artists: albumArtists - }; - const artist = { id: null, name: artistName }; - - // Enrich tracks with album metadata — use album artist for wishlist grouping - // (Spotify returns per-track artists which can differ on compilations/soundtracks) - const tracks = albumData.tracks.map(t => ({ - ...t, - artists: albumArtists, - album: album - })); - - await openAddToWishlistModal(album, artist, tracks, albumData.album_type || 'album'); - - } catch (error) { - console.error('Issue wishlist error:', error); - showToast(`Error: ${error.message}`, 'error'); - } finally { - if (btn) { btn.disabled = false; btn.innerHTML = ' Add to Wishlist'; } - } -} - -async function updateIssueStatus(issueId, newStatus) { - const payload = { status: newStatus }; - - // Include admin response if present - const responseInput = document.getElementById('issue-detail-response-input'); - if (responseInput) { - payload.admin_response = responseInput.value.trim(); - } - - try { - const resp = await fetch(`/api/issues/${issueId}`, { - method: 'PUT', - headers: _issueHeaders({ 'Content-Type': 'application/json' }), - body: JSON.stringify(payload), - }); - const data = await resp.json(); - if (data.success) { - showToast(`Issue ${newStatus === 'resolved' ? 'resolved' : newStatus === 'dismissed' ? 'dismissed' : newStatus === 'in_progress' ? 'marked in progress' : 'reopened'}`, 'success'); - closeIssueDetailModal(); - // Refresh if on issues page - const issuesPage = document.getElementById('issues-page'); - if (issuesPage && issuesPage.classList.contains('active')) { - loadIssuesPage(); - } - loadIssuesBadge(); - } else { - showToast(data.error || 'Failed to update issue', 'error'); - } - } catch (e) { - console.error('Failed to update issue:', e); - showToast('Failed to update issue', 'error'); - } -} - -async function deleteIssue(issueId) { - if (!confirm('Are you sure you want to delete this issue?')) return; - try { - const resp = await fetch(`/api/issues/${issueId}`, { method: 'DELETE', headers: _issueHeaders() }); - const data = await resp.json(); - if (data.success) { - showToast('Issue deleted', 'success'); - closeIssueDetailModal(); - const issuesPage = document.getElementById('issues-page'); - if (issuesPage && issuesPage.classList.contains('active')) { - loadIssuesPage(); - } - loadIssuesBadge(); - } else { - showToast(data.error || 'Failed to delete issue', 'error'); - } - } catch (e) { - console.error('Failed to delete issue:', e); - showToast('Failed to delete issue', 'error'); - } -} - -function closeIssueDetailModal() { - const overlay = document.getElementById('issue-detail-overlay'); - if (overlay) overlay.classList.add('hidden'); -} - -async function loadIssuesBadge() { - try { - const resp = await fetch('/api/issues/counts', { headers: _issueHeaders() }); - const data = await resp.json(); - if (!data.success) return; - const badge = document.getElementById('issues-nav-badge'); - if (badge) { - const openCount = data.counts.open || 0; - badge.textContent = openCount; - badge.classList.toggle('hidden', openCount === 0); - } - } catch (e) { } -} - -// ===== END ISSUES PAGE ===== - // --- Helpers --- function _esc(str) { diff --git a/webui/static/style.css b/webui/static/style.css index 71ee3be7..ff3537e0 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -49123,1318 +49123,24 @@ tr.tag-diff-same { color: #ffa500; } -/* ===== Issues Page ===== */ - -.issues-container { - max-width: 1000px; - margin: 0 auto; - padding: 24px 20px; -} - -.issues-header { - display: flex; - align-items: flex-start; - justify-content: space-between; - margin-bottom: 20px; - flex-wrap: wrap; - gap: 16px; -} - -.issues-header-left { - flex: 1; - min-width: 200px; -} - -.issues-title { - font-size: 24px; - font-weight: 600; - color: #fff; - margin: 0 0 4px 0; -} - -.issues-subtitle { - font-size: 13px; - color: rgba(255, 255, 255, 0.5); - margin: 0; -} - -.issues-header-right { - display: flex; - align-items: center; - gap: 10px; -} - -.issues-filters { - display: flex; - gap: 8px; -} - -.issues-filter-select { - background: rgba(255, 255, 255, 0.06); - border: 1px solid rgba(255, 255, 255, 0.1); - color: #fff; - padding: 7px 12px; - border-radius: 8px; - font-size: 13px; - cursor: pointer; - outline: none; - transition: border-color 0.2s; - appearance: none; - -webkit-appearance: none; - color-scheme: dark; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' fill='%23888'%3E%3Cpath d='M0 0l5 6 5-6z'/%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: right 10px center; - padding-right: 28px; - min-width: 130px; -} - -.issues-filter-select:focus { - border-color: rgba(var(--accent-light-rgb), 0.5); -} - -.issues-filter-select option { - background: #1a1a2e; - color: #fff; -} - -.issues-filter-select optgroup { - background: #1a1a2e; - color: #fff; -} - -/* Issues Stats */ - -.issues-stats { - display: flex; - gap: 10px; - margin-bottom: 20px; - flex-wrap: wrap; -} - -.issues-stat-card { - flex: 1; - min-width: 100px; - background: rgba(255, 255, 255, 0.04); - border: 1px solid rgba(255, 255, 255, 0.06); - border-radius: 10px; - padding: 14px 16px; - text-align: center; - transition: border-color 0.2s; -} +/* Nav Badge */ -.issues-stat-number { - font-size: 22px; +.issues-nav-badge { + position: absolute; + top: 4px; + right: 4px; + background: #ffa500; + color: #000; + font-size: 10px; font-weight: 700; - color: #fff; - line-height: 1.2; -} - -.issues-stat-label { - font-size: 11px; - color: rgba(255, 255, 255, 0.45); - text-transform: uppercase; - letter-spacing: 0.5px; - margin-top: 4px; -} - -.issues-stat-open { border-left: 3px solid #ffa500; } -.issues-stat-progress { border-left: 3px solid #4da6ff; } -.issues-stat-resolved { border-left: 3px solid #4ade80; } -.issues-stat-dismissed { border-left: 3px solid #888; } -.issues-stat-total { border-left: 3px solid rgba(var(--accent-light-rgb), 0.6); } - -/* Issues List */ - -.issues-list { - display: flex; - flex-direction: column; - gap: 8px; -} - -.issues-loading { - display: flex; - align-items: center; - justify-content: center; - gap: 10px; - padding: 40px; - color: rgba(255, 255, 255, 0.5); - font-size: 14px; -} - -.issues-spinner { - width: 20px; - height: 20px; - border: 2px solid rgba(255, 255, 255, 0.1); - border-top-color: rgba(var(--accent-light-rgb), 0.7); - border-radius: 50%; - animation: spin 0.8s linear infinite; -} - -@keyframes spin { - to { transform: rotate(360deg); } -} - -.issues-empty { - text-align: center; - padding: 60px 20px; - color: rgba(255, 255, 255, 0.4); -} - -.issues-empty-icon { - font-size: 40px; - margin-bottom: 12px; - opacity: 0.5; -} - -.issues-empty-title { - font-size: 16px; - font-weight: 500; - color: rgba(255, 255, 255, 0.6); - margin-bottom: 6px; -} - -.issues-empty-text { - font-size: 13px; - color: rgba(255, 255, 255, 0.35); -} - -/* Issue Card */ - -.issue-card { - display: flex; - align-items: stretch; - background: rgba(255, 255, 255, 0.03); - border: 1px solid rgba(255, 255, 255, 0.06); - border-radius: 12px; - padding: 14px 16px; - cursor: pointer; - transition: all 0.2s ease; - gap: 14px; -} - -.issue-card:hover { - background: rgba(255, 255, 255, 0.06); - border-color: rgba(255, 255, 255, 0.12); - transform: translateY(-1px); -} - -.issue-card-left { - flex-shrink: 0; - display: flex; - align-items: center; -} - -.issue-card-thumb { - width: 52px; - height: 52px; - border-radius: 8px; - object-fit: cover; -} - -.issue-card-thumb-placeholder { - width: 52px; - height: 52px; - border-radius: 8px; - background: rgba(255, 255, 255, 0.05); - display: flex; - align-items: center; - justify-content: center; - font-size: 22px; -} - -.issue-card-center { - flex: 1; - min-width: 0; - display: flex; - flex-direction: column; - justify-content: center; - gap: 4px; -} - -.issue-card-title-row { - display: flex; - align-items: center; - gap: 6px; -} - -.issue-card-category-icon { - font-size: 14px; - flex-shrink: 0; -} - -.issue-card-title { - font-size: 14px; - font-weight: 500; - color: #fff; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.issue-card-responded { - font-size: 13px; - flex-shrink: 0; - opacity: 0.7; -} - -.issue-card-entity { - display: flex; - align-items: center; - gap: 6px; - font-size: 12px; - color: rgba(255, 255, 255, 0.5); -} - -.issue-card-entity-type { - background: rgba(255, 255, 255, 0.08); - padding: 1px 6px; - border-radius: 4px; - font-size: 10px; - text-transform: uppercase; - letter-spacing: 0.5px; - flex-shrink: 0; -} - -.issue-card-entity-name { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.issue-card-meta-line { - color: rgba(255, 255, 255, 0.35); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.issue-card-description { - font-size: 12px; - color: rgba(255, 255, 255, 0.4); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - max-width: 500px; -} - -.issue-card-footer { - display: flex; - align-items: center; - gap: 8px; - font-size: 11px; - color: rgba(255, 255, 255, 0.3); -} - -.issue-card-profile { - background: rgba(255, 255, 255, 0.06); - padding: 1px 6px; - border-radius: 4px; -} - -.issue-card-date { - opacity: 0.8; -} - -.issue-card-right { - flex-shrink: 0; - display: flex; - flex-direction: column; - align-items: flex-end; - justify-content: center; - gap: 8px; -} - -/* Status Badges */ - -.issue-status-badge { - padding: 3px 10px; - border-radius: 20px; - font-size: 11px; - font-weight: 500; - letter-spacing: 0.3px; - white-space: nowrap; -} - -.issue-status-open { - background: rgba(255, 165, 0, 0.15); - color: #ffa500; - border: 1px solid rgba(255, 165, 0, 0.25); -} - -.issue-status-progress { - background: rgba(77, 166, 255, 0.15); - color: #4da6ff; - border: 1px solid rgba(77, 166, 255, 0.25); -} - -.issue-status-resolved { - background: rgba(74, 222, 128, 0.15); - color: #4ade80; - border: 1px solid rgba(74, 222, 128, 0.25); -} - -.issue-status-dismissed { - background: rgba(136, 136, 136, 0.15); - color: #888; - border: 1px solid rgba(136, 136, 136, 0.25); -} - -/* Priority Dots */ - -.issue-priority-dot { - width: 8px; - height: 8px; - border-radius: 50%; - display: inline-block; -} - -.issue-priority-low, .issue-priority-dot.issue-priority-low { - background: #666; -} - -.issue-priority-normal, .issue-priority-dot.issue-priority-normal { - background: #4da6ff; -} - -.issue-priority-high, .issue-priority-dot.issue-priority-high { - background: #ff4d4d; - box-shadow: 0 0 6px rgba(255, 77, 77, 0.4); -} - -/* Nav Badge */ - -.issues-nav-badge { - position: absolute; - top: 4px; - right: 4px; - background: #ffa500; - color: #000; - font-size: 10px; - font-weight: 700; - min-width: 16px; - height: 16px; - line-height: 16px; + min-width: 16px; + height: 16px; + line-height: 16px; text-align: center; border-radius: 8px; padding: 0 4px; } -/* ===== Report Issue Modal ===== */ - -.report-issue-modal { - max-width: 580px; - width: 95vw; -} - -.report-issue-entity-info { - background: rgba(255, 255, 255, 0.04); - border: 1px solid rgba(255, 255, 255, 0.08); - border-radius: 10px; - padding: 14px 16px; - margin-bottom: 18px; -} - -.report-issue-entity-name { - font-size: 15px; - font-weight: 500; - color: #fff; -} - -.report-issue-entity-artist { - font-size: 12px; - color: rgba(255, 255, 255, 0.45); - margin-top: 3px; -} - -.report-issue-section { - margin-bottom: 16px; -} - -.report-issue-label { - display: block; - font-size: 12px; - font-weight: 500; - color: rgba(255, 255, 255, 0.6); - text-transform: uppercase; - letter-spacing: 0.5px; - margin-bottom: 10px; -} - -.report-issue-optional { - font-weight: 400; - text-transform: none; - letter-spacing: 0; - color: rgba(255, 255, 255, 0.3); -} - -.report-issue-category-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); - gap: 8px; -} - -.report-issue-category-card { - background: rgba(255, 255, 255, 0.03); - border: 1px solid rgba(255, 255, 255, 0.08); - border-radius: 10px; - padding: 12px 10px; - cursor: pointer; - transition: all 0.2s ease; - text-align: center; -} - -.report-issue-category-card:hover { - background: rgba(255, 255, 255, 0.06); - border-color: rgba(255, 255, 255, 0.15); -} - -.report-issue-category-card.selected { - background: rgba(var(--accent-light-rgb), 0.1); - border-color: rgba(var(--accent-light-rgb), 0.4); -} - -.report-issue-category-icon { - font-size: 22px; - margin-bottom: 6px; -} - -.report-issue-category-label { - font-size: 12px; - font-weight: 500; - color: #fff; - margin-bottom: 3px; -} - -.report-issue-category-desc { - font-size: 10px; - color: rgba(255, 255, 255, 0.35); - line-height: 1.4; -} - -.report-issue-input { - width: 100%; - background: rgba(255, 255, 255, 0.05); - border: 1px solid rgba(255, 255, 255, 0.1); - color: #fff; - padding: 10px 12px; - border-radius: 8px; - font-size: 14px; - outline: none; - transition: border-color 0.2s; - box-sizing: border-box; -} - -.report-issue-input:focus { - border-color: rgba(var(--accent-light-rgb), 0.5); -} - -.report-issue-textarea { - width: 100%; - background: rgba(255, 255, 255, 0.05); - border: 1px solid rgba(255, 255, 255, 0.1); - color: #fff; - padding: 10px 12px; - border-radius: 8px; - font-size: 13px; - outline: none; - resize: vertical; - min-height: 80px; - font-family: inherit; - transition: border-color 0.2s; - box-sizing: border-box; -} - -.report-issue-textarea:focus { - border-color: rgba(var(--accent-light-rgb), 0.5); -} - -.report-issue-priority-row { - display: flex; - gap: 8px; -} - -.report-issue-priority-btn { - flex: 1; - padding: 8px 12px; - background: rgba(255, 255, 255, 0.04); - border: 1px solid rgba(255, 255, 255, 0.1); - color: rgba(255, 255, 255, 0.6); - border-radius: 8px; - cursor: pointer; - font-size: 13px; - transition: all 0.2s ease; -} - -.report-issue-priority-btn:hover { - background: rgba(255, 255, 255, 0.08); -} - -.report-issue-priority-btn.selected { - background: rgba(var(--accent-light-rgb), 0.12); - border-color: rgba(var(--accent-light-rgb), 0.4); - color: rgb(var(--accent-light-rgb)); -} - -/* ===== Issue Detail Modal ===== */ - -.issue-detail-modal { - max-width: 750px; - width: 95vw; - max-height: 85vh; -} - -.issue-detail-modal .enhanced-bulk-modal-body { - overflow-y: auto; - max-height: calc(85vh - 120px); -} - -/* Issue Detail — Hero Section (Enhanced Library Style) */ - -.issue-hero { - display: flex; - gap: 16px; - padding-bottom: 16px; - border-bottom: 1px solid rgba(255, 255, 255, 0.06); - margin-bottom: 14px; -} - -.issue-hero-art-group { - position: relative; - flex-shrink: 0; -} - -.issue-hero-artist-thumb { - width: 48px; - height: 48px; - border-radius: 50%; - object-fit: cover; - position: absolute; - top: -6px; - left: -10px; - border: 2px solid rgba(30, 30, 30, 0.9); - z-index: 1; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4); -} - -.issue-hero-album-art { - width: 140px; - height: 140px; - border-radius: 10px; - object-fit: cover; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4); -} - -.issue-hero-album-placeholder { - width: 140px; - height: 140px; - border-radius: 10px; - background: rgba(255, 255, 255, 0.04); - border: 1px solid rgba(255, 255, 255, 0.06); - display: flex; - align-items: center; - justify-content: center; - font-size: 40px; - color: rgba(255, 255, 255, 0.15); -} - -.issue-hero-info { - flex: 1; - min-width: 0; - display: flex; - flex-direction: column; - justify-content: center; - gap: 4px; -} - -.issue-hero-artist { - font-size: 12px; - color: rgba(255, 255, 255, 0.5); - text-transform: uppercase; - letter-spacing: 0.5px; - font-weight: 500; -} - -.issue-hero-album { - font-size: 20px; - font-weight: 700; - color: #fff; - line-height: 1.2; -} - -.issue-hero-track-name { - font-size: 14px; - color: rgba(255, 255, 255, 0.7); - font-weight: 500; - margin-top: 2px; -} - -.issue-hero-meta { - font-size: 12px; - color: rgba(255, 255, 255, 0.4); - margin-top: 2px; -} - -.issue-hero-genres { - display: flex; - flex-wrap: wrap; - gap: 4px; - margin-top: 4px; -} - -.issue-hero-genre-tag { - font-size: 10px; - padding: 2px 8px; - border-radius: 10px; - background: rgba(255, 255, 255, 0.06); - color: rgba(255, 255, 255, 0.55); - border: 1px solid rgba(255, 255, 255, 0.08); -} - -/* Issue Detail — Info Bar */ - -.issue-detail-info-bar { - display: flex; - align-items: center; - justify-content: space-between; - flex-wrap: wrap; - gap: 8px; - padding: 10px 0; - border-bottom: 1px solid rgba(255, 255, 255, 0.05); - margin-bottom: 14px; -} - -.issue-detail-info-left { - display: flex; - align-items: center; - gap: 10px; -} - -.issue-detail-info-right { - display: flex; - align-items: center; - gap: 10px; - font-size: 12px; -} - -/* Issue Detail — Action Buttons */ - -.issue-action-buttons { - display: flex; - gap: 8px; - margin-bottom: 14px; -} - -.issue-action-btn { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 8px 16px; - border-radius: 8px; - font-size: 13px; - font-weight: 500; - cursor: pointer; - border: 1px solid transparent; - transition: all 0.2s ease; - font-family: inherit; -} - -.issue-action-btn:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.issue-action-download { - background: rgba(var(--accent-light-rgb), 0.12); - color: rgb(var(--accent-light-rgb)); - border-color: rgba(var(--accent-light-rgb), 0.25); -} - -.issue-action-download:hover:not(:disabled) { - background: rgba(var(--accent-light-rgb), 0.22); - border-color: rgba(var(--accent-light-rgb), 0.4); -} - -.issue-action-wishlist { - background: rgba(255, 165, 0, 0.1); - color: #ffa500; - border-color: rgba(255, 165, 0, 0.2); -} - -.issue-action-wishlist:hover:not(:disabled) { - background: rgba(255, 165, 0, 0.2); - border-color: rgba(255, 165, 0, 0.35); -} - -/* Issue Detail — Track Duration */ - -.issue-detail-tracklist-dur { - font-size: 11px; - color: rgba(255, 255, 255, 0.3); - flex-shrink: 0; - min-width: 36px; - text-align: right; -} - - -.issue-detail-section { - margin-bottom: 16px; -} - -.issue-detail-section-title { - font-size: 11px; - text-transform: uppercase; - letter-spacing: 0.6px; - color: rgba(255, 255, 255, 0.4); - margin-bottom: 8px; - font-weight: 500; -} - -.issue-detail-meta-row { - display: flex; - align-items: center; - flex-wrap: wrap; - gap: 12px; - font-size: 12px; - color: rgba(255, 255, 255, 0.5); -} - -.issue-detail-category { - font-size: 13px; - color: rgba(255, 255, 255, 0.7); -} - -.issue-detail-date { - color: rgba(255, 255, 255, 0.35); -} - -.issue-detail-profile { - background: rgba(255, 255, 255, 0.06); - padding: 2px 8px; - border-radius: 4px; - font-size: 11px; -} - -.issue-detail-title-text { - font-size: 15px; - font-weight: 500; - color: #fff; - margin-bottom: 8px; -} - -.issue-detail-description { - font-size: 13px; - color: rgba(255, 255, 255, 0.65); - line-height: 1.6; - white-space: pre-wrap; -} - -.issue-detail-no-desc { - font-size: 13px; - color: rgba(255, 255, 255, 0.25); - font-style: italic; -} - -/* Issue Detail — Snapshot */ - -.issue-detail-snapshot { - background: rgba(255, 255, 255, 0.03); - border: 1px solid rgba(255, 255, 255, 0.06); - border-radius: 10px; - padding: 12px 14px; -} - -.issue-detail-snapshot-row { - display: flex; - justify-content: space-between; - align-items: center; - padding: 4px 0; - border-bottom: 1px solid rgba(255, 255, 255, 0.03); -} - -.issue-detail-snapshot-row:last-child { - border-bottom: none; -} - -.issue-detail-snapshot-label { - font-size: 12px; - color: rgba(255, 255, 255, 0.4); - flex-shrink: 0; - min-width: 70px; -} - -.issue-detail-snapshot-value { - font-size: 12px; - color: rgba(255, 255, 255, 0.75); - text-align: right; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - max-width: 350px; -} - -.issue-detail-snapshot-tracklist-header { - font-size: 11px; - text-transform: uppercase; - letter-spacing: 0.5px; - color: rgba(255, 255, 255, 0.35); - margin-top: 10px; - margin-bottom: 6px; - padding-top: 8px; - border-top: 1px solid rgba(255, 255, 255, 0.06); -} - -.issue-detail-snapshot-track { - font-size: 12px; - color: rgba(255, 255, 255, 0.6); - padding: 2px 0; -} - -.issue-detail-snapshot-track-meta { - color: rgba(255, 255, 255, 0.3); - font-size: 11px; - margin-left: 6px; -} - - -/* Issue Detail — Metadata Grid */ - -.issue-detail-meta-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); - gap: 8px; -} - -.issue-meta-item { - display: flex; - align-items: center; - gap: 8px; - padding: 8px 10px; - background: rgba(255, 255, 255, 0.03); - border: 1px solid rgba(255, 255, 255, 0.05); - border-radius: 8px; -} - -.issue-meta-icon { - font-size: 14px; - opacity: 0.5; - flex-shrink: 0; - width: 18px; - text-align: center; -} - -.issue-meta-label { - font-size: 10px; - text-transform: uppercase; - letter-spacing: 0.4px; - color: rgba(255, 255, 255, 0.35); - flex-shrink: 0; -} - -.issue-meta-value { - font-size: 13px; - color: rgba(255, 255, 255, 0.85); - margin-left: auto; - font-weight: 500; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - max-width: 120px; -} - -/* Issue Detail — Album Context (for track issues) */ - -.issue-detail-album-context { - display: flex; - align-items: center; - gap: 12px; - padding: 10px 12px; - background: rgba(255, 255, 255, 0.03); - border: 1px solid rgba(255, 255, 255, 0.06); - border-radius: 10px; -} - -.issue-detail-album-context-thumb { - width: 44px; - height: 44px; - border-radius: 6px; - object-fit: cover; - flex-shrink: 0; -} - -.issue-detail-album-context-info { - flex: 1; - min-width: 0; -} - -.issue-detail-album-context-title { - font-size: 13px; - font-weight: 500; - color: #fff; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.issue-detail-album-context-meta { - font-size: 11px; - color: rgba(255, 255, 255, 0.4); - margin-top: 2px; -} - -/* Issue Detail — File Path */ - -.issue-detail-filepath { - font-size: 12px; - color: rgba(255, 255, 255, 0.5); - background: rgba(0, 0, 0, 0.2); - padding: 8px 10px; - border-radius: 6px; - font-family: monospace; - word-break: break-all; - border: 1px solid rgba(255, 255, 255, 0.04); -} - -/* Issue Detail — Track Listing (redesigned) */ - -.issue-detail-tracklist { - background: rgba(255, 255, 255, 0.02); - border: 1px solid rgba(255, 255, 255, 0.05); - border-radius: 10px; - padding: 6px 0; - max-height: 240px; - overflow-y: auto; -} - -.issue-detail-tracklist-disc { - font-size: 10px; - text-transform: uppercase; - letter-spacing: 0.5px; - color: rgba(255, 255, 255, 0.35); - padding: 8px 14px 4px; - border-top: 1px solid rgba(255, 255, 255, 0.04); -} - -.issue-detail-tracklist-disc:first-child { - border-top: none; - padding-top: 4px; -} - -.issue-detail-tracklist-row { - display: flex; - align-items: center; - padding: 4px 14px; - gap: 10px; - transition: background 0.15s; -} - -.issue-detail-tracklist-row:hover { - background: rgba(255, 255, 255, 0.03); -} - -.issue-detail-tracklist-num { - font-size: 12px; - color: rgba(255, 255, 255, 0.3); - min-width: 22px; - text-align: right; - flex-shrink: 0; -} - -.issue-detail-tracklist-title { - font-size: 13px; - color: rgba(255, 255, 255, 0.75); - flex: 1; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.issue-detail-tracklist-meta { - display: flex; - align-items: center; - gap: 6px; - flex-shrink: 0; -} - - -.issue-detail-section-count { - font-weight: 400; - color: rgba(255, 255, 255, 0.3); - font-size: 10px; - text-transform: none; - letter-spacing: 0; - margin-left: 6px; -} - -/* Issue Detail — External Links */ - -.issue-ext-chips { - display: flex; - flex-wrap: wrap; - gap: 6px; -} - -.issue-ext-chip { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 4px 10px; - border-radius: 6px; - font-size: 12px; - font-weight: 500; - text-decoration: none; - cursor: pointer; - transition: all 0.2s ease; - border: 1px solid transparent; -} - -.issue-ext-chip-type { - font-weight: 400; - font-size: 10px; - opacity: 0.7; -} - -.issue-ext-chip.ext-spotify { - background: rgba(30, 215, 96, 0.1); - color: #1ed760; - border-color: rgba(30, 215, 96, 0.2); -} - -.issue-ext-chip.ext-spotify:hover { - background: rgba(30, 215, 96, 0.2); -} - -.issue-ext-chip.ext-mb { - background: rgba(186, 81, 163, 0.1); - color: #d4a0cb; - border-color: rgba(186, 81, 163, 0.2); -} - -.issue-ext-chip.ext-mb:hover { - background: rgba(186, 81, 163, 0.2); -} - -.issue-ext-chip.ext-deezer { - background: rgba(160, 54, 255, 0.1); - color: #c88eff; - border-color: rgba(160, 54, 255, 0.2); -} - -.issue-ext-chip.ext-deezer:hover { - background: rgba(160, 54, 255, 0.2); -} - -.issue-ext-chip.ext-tidal { - background: rgba(0, 255, 255, 0.08); - color: #66ffff; - border-color: rgba(0, 255, 255, 0.15); -} - -.issue-ext-chip.ext-tidal:hover { - background: rgba(0, 255, 255, 0.15); -} - -.issue-ext-chip.ext-qobuz { - background: rgba(0, 130, 200, 0.1); - color: #5bb8e8; - border-color: rgba(0, 130, 200, 0.2); -} - -.issue-ext-chip.ext-qobuz:hover { - background: rgba(0, 130, 200, 0.2); -} - -/* Issue Detail — Responsive */ - -@media (max-width: 768px) { - .issue-hero { - flex-direction: column; - align-items: center; - text-align: center; - } - - .issue-hero-album-art, - .issue-hero-album-placeholder { - width: 120px; - height: 120px; - } - - .issue-hero-artist-thumb { - width: 40px; - height: 40px; - } - - .issue-hero-info { - align-items: center; - } - - .issue-hero-genres { - justify-content: center; - } - - .issue-hero-album { - font-size: 17px; - } - - .issue-detail-info-bar { - flex-direction: column; - align-items: flex-start; - } - - .issue-action-buttons { - flex-direction: column; - } - - .issue-action-btn { - justify-content: center; - } - - .issue-detail-meta-grid { - grid-template-columns: repeat(2, 1fr); - } - - .issue-meta-value { - max-width: 80px; - } - - .issue-ext-chips { - gap: 4px; - justify-content: center; - } - - .issue-ext-chip { - font-size: 11px; - padding: 3px 8px; - } -} - -/* Issue Detail — Admin Response */ - -.issue-detail-response-textarea { - width: 100%; - background: rgba(255, 255, 255, 0.05); - border: 1px solid rgba(255, 255, 255, 0.1); - color: #fff; - padding: 10px 12px; - border-radius: 8px; - font-size: 13px; - outline: none; - resize: vertical; - min-height: 70px; - font-family: inherit; - transition: border-color 0.2s; - box-sizing: border-box; -} - -.issue-detail-response-textarea:focus { - border-color: rgba(var(--accent-light-rgb), 0.5); -} - -.issue-detail-admin-response { - background: rgba(74, 222, 128, 0.06); - border: 1px solid rgba(74, 222, 128, 0.15); - border-radius: 10px; - padding: 12px 14px; - font-size: 13px; - color: rgba(255, 255, 255, 0.75); - line-height: 1.6; - white-space: pre-wrap; -} - -/* Issue Detail — Footer Action Buttons */ - -.issue-btn-progress { - background: rgba(77, 166, 255, 0.15) !important; - border-color: rgba(77, 166, 255, 0.3) !important; - color: #4da6ff !important; -} - -.issue-btn-progress:hover { - background: rgba(77, 166, 255, 0.25) !important; -} - -.issue-btn-resolve { - background: rgba(74, 222, 128, 0.15) !important; - border-color: rgba(74, 222, 128, 0.3) !important; - color: #4ade80 !important; -} - -.issue-btn-resolve:hover { - background: rgba(74, 222, 128, 0.25) !important; -} - -.issue-btn-dismiss { - background: rgba(136, 136, 136, 0.15) !important; - border-color: rgba(136, 136, 136, 0.3) !important; - color: #aaa !important; -} - -.issue-btn-dismiss:hover { - background: rgba(136, 136, 136, 0.25) !important; -} - -.issue-btn-reopen { - background: rgba(255, 165, 0, 0.15) !important; - border-color: rgba(255, 165, 0, 0.3) !important; - color: #ffa500 !important; -} - -.issue-btn-reopen:hover { - background: rgba(255, 165, 0, 0.25) !important; -} - -.issue-btn-delete { - background: rgba(255, 77, 77, 0.1) !important; - border-color: rgba(255, 77, 77, 0.2) !important; - color: #ff6b6b !important; -} - -.issue-btn-delete:hover { - background: rgba(255, 77, 77, 0.2) !important; -} - -/* ===== Issues Page — Responsive ===== */ - -@media (max-width: 768px) { - .issues-header { - flex-direction: column; - } - - .issues-stats { - flex-wrap: wrap; - } - - .issues-stat-card { - min-width: 80px; - } - - .issue-card { - flex-wrap: wrap; - padding: 12px; - } - - .issue-card-left { - display: none; - } - - .issue-card-description { - max-width: 100%; - } - - .report-issue-category-grid { - grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); - } - - .issue-detail-header-info { - flex-direction: column; - } - - .issue-detail-snapshot-value { - max-width: 200px; - } -} - /* ============================================ */ /* == METADATA CACHE BROWSER == */ /* ============================================ */