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
-
-
- `;
- // 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 = '';
-
- 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 = '';
- }
-}
-
-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) : ''}
` : ''}
-
-
-
What's the problem?
-
- ${Object.entries(ISSUE_CATEGORIES)
- .filter(([, cat]) => !cat.applies || cat.applies.includes(entityType))
- .map(([key, cat]) => `
-
-
${cat.icon}
-
${_esc(cat.label)}
-
${_esc(cat.description)}
-
- `).join('')}
-
-
-
-
Title
-
-
Details (optional)
-
-
Priority
-
- Low
- Normal
- High
-
-
- `;
-
- _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 = '';
- footer.innerHTML = 'Close ';
- 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 = '';
- return;
- }
- renderIssueDetail(data.issue, body, footer, titleEl);
- } catch (e) {
- console.error('Failed to load issue:', e);
- body.innerHTML = '';
- }
-}
-
-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 = `
-
- `;
- }
- }
-
- // --- 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 = `
-
- `;
- } 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 = 'Close ';
-
- if (admin) {
- if (issue.status === 'open' || issue.status === 'in_progress') {
- if (issue.status === 'open') {
- footerHtml += `Mark In Progress `;
- }
- footerHtml += `Resolve `;
- footerHtml += `Dismiss `;
- } else {
- footerHtml += `Reopen `;
- }
- footerHtml += `Delete `;
- } else {
- if (issue.status === 'open') {
- footerHtml += `Withdraw `;
- }
- }
-
- 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 == */
/* ============================================ */