Show all services on dashboard with click-to-configure (#219)
Dashboard now displays all enrichment services as live-status chips below the core service cards. Each chip shows Running, Idle, Paused, Stopped, or Not Configured state with color-coded left border accents. Unconfigured services appear dimmed with dashed borders — clicking any configurable chip navigates to Settings → Connections and scrolls to the relevant service section. Also fixes the Spotify card always being labeled "Apple Music" when using iTunes fallback — card now always says "Spotify" with an amber "Using iTunes/Deezer" indicator when fallback is active.
This commit is contained in:
parent
ab8e44dafd
commit
32adc66fe3
5 changed files with 379 additions and 17 deletions
|
|
@ -3396,7 +3396,7 @@ def run_service_test(service, test_config):
|
|||
return True, "Spotify connection successful!"
|
||||
else:
|
||||
# Using fallback metadata source
|
||||
fallback_name = 'Deezer' if _get_metadata_fallback_source() == 'deezer' else 'Apple Music'
|
||||
fallback_name = 'Deezer' if _get_metadata_fallback_source() == 'deezer' else 'iTunes'
|
||||
if spotify_configured:
|
||||
return True, f"{fallback_name} connection successful! (Spotify configured but not authenticated)"
|
||||
else:
|
||||
|
|
@ -3866,6 +3866,70 @@ def index():
|
|||
|
||||
# --- API Endpoints ---
|
||||
|
||||
def _get_enrichment_status():
|
||||
"""Get lightweight status for all enrichment services (no DB queries).
|
||||
Reads worker properties directly to avoid expensive get_stats() calls."""
|
||||
services = {}
|
||||
|
||||
# Worker-based enrichment services: (key, display_name, worker_var)
|
||||
workers_info = [
|
||||
('musicbrainz', 'MusicBrainz', lambda: mb_worker),
|
||||
('spotify_enrichment', 'Spotify', lambda: spotify_enrichment_worker),
|
||||
('itunes_enrichment', 'iTunes', lambda: itunes_enrichment_worker),
|
||||
('deezer_enrichment', 'Deezer', lambda: deezer_worker),
|
||||
('tidal_enrichment', 'Tidal', lambda: tidal_enrichment_worker),
|
||||
('qobuz_enrichment', 'Qobuz', lambda: qobuz_enrichment_worker),
|
||||
('lastfm', 'Last.fm', lambda: lastfm_worker),
|
||||
('genius', 'Genius', lambda: genius_worker),
|
||||
('audiodb', 'AudioDB', lambda: audiodb_worker),
|
||||
]
|
||||
|
||||
# Config-based "configured" checks for services that need API keys/credentials
|
||||
configured_checks = {
|
||||
'spotify_enrichment': lambda: bool(config_manager.get('spotify.client_id') and config_manager.get('spotify.client_secret')),
|
||||
'tidal_enrichment': lambda: bool(tidal_client and getattr(tidal_client, 'access_token', None)),
|
||||
'qobuz_enrichment': lambda: bool(qobuz_enrichment_worker and qobuz_enrichment_worker.client and qobuz_enrichment_worker.client.user_auth_token),
|
||||
'lastfm': lambda: bool(config_manager.get('lastfm.api_key', '')),
|
||||
'genius': lambda: bool(config_manager.get('genius.access_token', '')),
|
||||
}
|
||||
|
||||
for key, name, get_worker in workers_info:
|
||||
worker = get_worker()
|
||||
if worker is not None:
|
||||
is_alive = worker.thread is not None and worker.thread.is_alive()
|
||||
try:
|
||||
configured = configured_checks.get(key, lambda: True)()
|
||||
except Exception:
|
||||
configured = False
|
||||
services[key] = {
|
||||
'name': name,
|
||||
'configured': configured,
|
||||
'running': worker.running and is_alive and not worker.paused,
|
||||
'paused': worker.paused,
|
||||
'idle': is_alive and not worker.paused and getattr(worker, 'current_item', None) is None,
|
||||
}
|
||||
else:
|
||||
services[key] = {
|
||||
'name': name,
|
||||
'configured': False,
|
||||
'running': False,
|
||||
'paused': False,
|
||||
'idle': False,
|
||||
}
|
||||
|
||||
# Non-worker services (configured status only)
|
||||
services['acoustid'] = {
|
||||
'name': 'AcoustID',
|
||||
'configured': bool(config_manager.get('acoustid.api_key', '')),
|
||||
}
|
||||
services['listenbrainz'] = {
|
||||
'name': 'ListenBrainz',
|
||||
'configured': bool(config_manager.get('listenbrainz.token', '')),
|
||||
}
|
||||
|
||||
return services
|
||||
|
||||
|
||||
# Status check caching to reduce unnecessary API calls
|
||||
_status_cache = {
|
||||
'spotify': {'connected': False, 'response_time': 0, 'source': 'itunes'},
|
||||
|
|
@ -3963,7 +4027,8 @@ def get_status():
|
|||
'spotify': _status_cache['spotify'],
|
||||
'media_server': _status_cache['media_server'],
|
||||
'soulseek': _status_cache['soulseek'],
|
||||
'active_media_server': active_server
|
||||
'active_media_server': active_server,
|
||||
'enrichment': _get_enrichment_status()
|
||||
}
|
||||
return jsonify(status_data)
|
||||
except Exception as e:
|
||||
|
|
@ -6214,7 +6279,7 @@ def spotify_disconnect():
|
|||
'rate_limit': None
|
||||
}
|
||||
_status_cache_timestamps['spotify'] = time.time()
|
||||
fallback_label = 'Deezer' if fallback_src == 'deezer' else 'Apple Music/iTunes'
|
||||
fallback_label = 'Deezer' if fallback_src == 'deezer' else 'iTunes'
|
||||
add_activity_item("🔌", "Spotify Disconnected", f"Switched to {fallback_label} metadata source", "Now")
|
||||
return jsonify({'success': True, 'message': f'Spotify disconnected. Now using {fallback_label}.'})
|
||||
except Exception as e:
|
||||
|
|
@ -19071,6 +19136,18 @@ def get_version_info():
|
|||
"title": "What's New in SoulSync",
|
||||
"subtitle": f"Version {SOULSYNC_VERSION} — Latest Changes",
|
||||
"sections": [
|
||||
{
|
||||
"title": "📊 Show All Services on Dashboard (#219)",
|
||||
"description": "Dashboard now shows connection status for all external services, not just the core three",
|
||||
"features": [
|
||||
"• Enrichment services shown as color-coded chips below core service cards",
|
||||
"• Status indicators: Running, Idle, Paused, Stopped, or Not Configured with accent-colored left borders",
|
||||
"• Unconfigured services show dashed border — click to jump directly to their Settings section",
|
||||
"• All configurable services clickable — navigates to Settings → Connections and scrolls to the service",
|
||||
"• Spotify card always labeled 'Spotify' — no longer confusingly switches to 'Apple Music'",
|
||||
"• Fallback state (using iTunes/Deezer) shown with amber indicator when Spotify is not connected"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "🔧 Add Qobuz to Connections Tab (#218)",
|
||||
"description": "Qobuz credentials now available on the Connections tab for metadata enrichment",
|
||||
|
|
@ -45284,7 +45361,8 @@ def _build_status_payload():
|
|||
'spotify': spotify_data,
|
||||
'media_server': _status_cache.get('media_server', {}),
|
||||
'soulseek': soulseek_data,
|
||||
'active_media_server': config_manager.get_active_media_server()
|
||||
'active_media_server': config_manager.get_active_media_server(),
|
||||
'enrichment': _get_enrichment_status()
|
||||
}
|
||||
|
||||
def _build_watchlist_count_payload(profile_id=1):
|
||||
|
|
|
|||
|
|
@ -665,6 +665,9 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="enrichment-status-grid" id="enrichment-status-grid">
|
||||
<!-- Dynamically populated by JS -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-section">
|
||||
|
|
|
|||
|
|
@ -3403,6 +3403,7 @@ function closeHelperSearch() {
|
|||
const WHATS_NEW = {
|
||||
'2.1': [
|
||||
// Newest features first
|
||||
{ title: 'All Services on Dashboard', desc: 'Dashboard shows all enrichment services as live-status chips — click unconfigured ones to jump to Settings. Spotify card no longer shows "Apple Music"', page: 'dashboard' },
|
||||
{ title: 'Qobuz on Connections Tab', desc: 'Qobuz credentials now on Settings → Connections for metadata enrichment without needing it as download source' },
|
||||
{ title: 'Fix Enrichment Status Widget', desc: 'Enrichment tooltip now shows Rate Limited or Daily Limit instead of stuck on Running' },
|
||||
{ title: 'Cache Maintenance', desc: 'Cache evictor now cleans junk entities, orphaned searches, and stale MusicBrainz nulls' },
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ let currentStream = {
|
|||
progress: 0,
|
||||
track: null
|
||||
};
|
||||
let currentMusicSourceName = 'Spotify'; // 'Spotify' or 'Apple Music' - updated from status endpoint
|
||||
let currentMusicSourceName = 'Spotify'; // 'Spotify', 'iTunes', or 'Deezer' - updated from status endpoint
|
||||
|
||||
// Streaming state management (enhanced functionality)
|
||||
let streamStatusPoller = null;
|
||||
|
|
@ -382,6 +382,9 @@ function handleServiceStatusUpdate(data) {
|
|||
updateSidebarServiceStatus('media-server', data.media_server);
|
||||
updateSidebarServiceStatus('soulseek', data.soulseek);
|
||||
|
||||
// Update enrichment service cards
|
||||
if (data.enrichment) renderEnrichmentCards(data.enrichment);
|
||||
|
||||
// Spotify rate limit / cooldown / recovery
|
||||
if (data.spotify?.rate_limited && data.spotify.rate_limit) {
|
||||
handleSpotifyRateLimit(data.spotify.rate_limit);
|
||||
|
|
@ -6974,7 +6977,7 @@ async function testConnection(service) {
|
|||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
// Use backend's message which contains dynamic source name (Spotify or Apple Music)
|
||||
// Use backend's message which contains dynamic source name
|
||||
showToast(result.message || `${service} connection successful`, 'success');
|
||||
|
||||
// Load music libraries after successful connection
|
||||
|
|
@ -7141,7 +7144,7 @@ async function testDashboardConnection(service) {
|
|||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
// Use backend's message which contains dynamic source name (Spotify or Apple Music)
|
||||
// Use backend's message which contains dynamic source name
|
||||
showToast(result.message || `${service} service verified`, 'success');
|
||||
// Refresh status indicators immediately so UI reflects the new state
|
||||
fetchAndUpdateServiceStatus();
|
||||
|
|
@ -36048,6 +36051,9 @@ async function fetchAndUpdateServiceStatus() {
|
|||
updateSidebarServiceStatus('media-server', data.media_server);
|
||||
updateSidebarServiceStatus('soulseek', data.soulseek);
|
||||
|
||||
// Update enrichment service cards
|
||||
if (data.enrichment) renderEnrichmentCards(data.enrichment);
|
||||
|
||||
// Check for Spotify rate limit
|
||||
if (data.spotify && data.spotify.rate_limited && data.spotify.rate_limit) {
|
||||
handleSpotifyRateLimit(data.spotify.rate_limit);
|
||||
|
|
@ -36071,7 +36077,7 @@ function updateServiceStatus(service, statusData) {
|
|||
? formatRateLimitDuration(statusData.rate_limit?.remaining_seconds || 0)
|
||||
: formatRateLimitDuration(statusData.post_ban_cooldown);
|
||||
const phase = statusData.rate_limited ? 'paused' : 'recovering';
|
||||
const fallbackLabel = statusData.source === 'deezer' ? 'Deezer' : 'Apple Music';
|
||||
const fallbackLabel = statusData.source === 'deezer' ? 'Deezer' : 'iTunes';
|
||||
statusText.textContent = `${fallbackLabel} (Spotify ${phase} \u2014 ${remaining})`;
|
||||
statusText.className = 'service-card-status-text rate-limited';
|
||||
} else if (statusData.connected) {
|
||||
|
|
@ -36085,16 +36091,29 @@ function updateServiceStatus(service, statusData) {
|
|||
}
|
||||
}
|
||||
|
||||
// Update music source title (Spotify or Apple Music) based on active source
|
||||
// Update music source title and status based on active source
|
||||
if (service === 'spotify' && statusData.source) {
|
||||
const musicSourceTitleElement = document.getElementById('music-source-title');
|
||||
if (musicSourceTitleElement) {
|
||||
const sourceName = statusData.source === 'spotify' ? 'Spotify' : statusData.source === 'deezer' ? 'Deezer' : 'Apple Music';
|
||||
musicSourceTitleElement.textContent = sourceName;
|
||||
// Card title always says "Spotify" — it represents the metadata source slot
|
||||
musicSourceTitleElement.textContent = 'Spotify';
|
||||
// Update global variable for use in discovery modals
|
||||
const sourceName = statusData.source === 'spotify' ? 'Spotify' : statusData.source === 'deezer' ? 'Deezer' : 'iTunes';
|
||||
currentMusicSourceName = sourceName;
|
||||
}
|
||||
|
||||
// When using fallback, update status text to show which fallback is active
|
||||
if (statusData.source !== 'spotify' && !statusData.rate_limited && !statusData.post_ban_cooldown) {
|
||||
const fallbackName = statusData.source === 'deezer' ? 'Deezer' : 'iTunes';
|
||||
if (statusText) {
|
||||
statusText.textContent = `Using ${fallbackName}`;
|
||||
statusText.className = 'service-card-status-text fallback';
|
||||
}
|
||||
if (indicator) {
|
||||
indicator.className = 'service-card-indicator fallback';
|
||||
}
|
||||
}
|
||||
|
||||
// Show/hide Spotify disconnect button based on connection state
|
||||
const disconnectBtn = document.getElementById('spotify-disconnect-btn');
|
||||
if (disconnectBtn) {
|
||||
|
|
@ -36141,12 +36160,20 @@ function updateSidebarServiceStatus(service, statusData) {
|
|||
}
|
||||
}
|
||||
|
||||
// Update music source name (Spotify or Apple Music) based on active source
|
||||
// Update music source name — always "Spotify" in sidebar
|
||||
if (service === 'spotify' && statusData.source) {
|
||||
const musicSourceNameElement = document.getElementById('music-source-name');
|
||||
if (musicSourceNameElement) {
|
||||
const sourceName = statusData.source === 'spotify' ? 'Spotify' : statusData.source === 'deezer' ? 'Deezer' : 'Apple Music';
|
||||
musicSourceNameElement.textContent = sourceName;
|
||||
musicSourceNameElement.textContent = 'Spotify';
|
||||
}
|
||||
|
||||
// Show fallback state in sidebar dot
|
||||
if (statusData.source !== 'spotify' && !statusData.rate_limited && !statusData.post_ban_cooldown) {
|
||||
if (dot) {
|
||||
dot.className = 'status-dot fallback';
|
||||
const fallbackName = statusData.source === 'deezer' ? 'Deezer' : 'iTunes';
|
||||
dot.title = `Using ${fallbackName} fallback`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -36160,6 +36187,74 @@ function updateSidebarServiceStatus(service, statusData) {
|
|||
}
|
||||
}
|
||||
|
||||
function renderEnrichmentCards(enrichment) {
|
||||
const grid = document.getElementById('enrichment-status-grid');
|
||||
if (!grid || !enrichment) return;
|
||||
|
||||
// Service display order
|
||||
const serviceOrder = [
|
||||
'musicbrainz', 'spotify_enrichment', 'itunes_enrichment', 'deezer_enrichment',
|
||||
'tidal_enrichment', 'qobuz_enrichment', 'lastfm', 'genius', 'audiodb',
|
||||
'acoustid', 'listenbrainz'
|
||||
];
|
||||
|
||||
// Map service keys to their settings page selector for click-to-configure
|
||||
const settingsSelectors = {
|
||||
'spotify_enrichment': '.spotify-title',
|
||||
'tidal_enrichment': '.tidal-title',
|
||||
'qobuz_enrichment': '.qobuz-title',
|
||||
'lastfm': '.lastfm-title',
|
||||
'genius': '.genius-title',
|
||||
'acoustid': '.acoustid-title',
|
||||
'listenbrainz': '.listenbrainz-title',
|
||||
};
|
||||
|
||||
const chips = [];
|
||||
for (const key of serviceOrder) {
|
||||
const svc = enrichment[key];
|
||||
if (!svc) continue;
|
||||
|
||||
// Determine status class and text
|
||||
let statusClass, statusLabel;
|
||||
if ('running' in svc) {
|
||||
if (!svc.configured) {
|
||||
statusClass = 'not-configured';
|
||||
statusLabel = 'Set up';
|
||||
} else if (svc.paused) {
|
||||
statusClass = 'paused';
|
||||
statusLabel = 'Paused';
|
||||
} else if (svc.running) {
|
||||
statusClass = svc.idle ? 'idle' : 'running';
|
||||
statusLabel = svc.idle ? 'Idle' : 'Running';
|
||||
} else {
|
||||
statusClass = 'stopped';
|
||||
statusLabel = 'Stopped';
|
||||
}
|
||||
} else {
|
||||
statusClass = svc.configured ? 'running' : 'not-configured';
|
||||
statusLabel = svc.configured ? 'Ready' : 'Set up';
|
||||
}
|
||||
|
||||
const selector = settingsSelectors[key];
|
||||
const clickAttr = selector
|
||||
? `onclick="navigateToPage('settings'); setTimeout(() => { switchSettingsTab('connections'); setTimeout(() => { const el = document.querySelector('${selector}'); if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' }); }, 100); }, 50);"`
|
||||
: '';
|
||||
const titleAttr = selector && statusClass === 'not-configured'
|
||||
? 'title="Click to configure in Settings"'
|
||||
: `title="${svc.name} — ${statusLabel}"`;
|
||||
|
||||
chips.push(`
|
||||
<div class="enrichment-chip status-${statusClass}" ${clickAttr} ${titleAttr}>
|
||||
<span class="enrichment-chip-dot"></span>
|
||||
<span class="enrichment-chip-name">${svc.name}</span>
|
||||
<span class="enrichment-chip-status">${statusLabel}</span>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
grid.innerHTML = chips.join('');
|
||||
}
|
||||
|
||||
async function fetchAndUpdateSystemStats() {
|
||||
if (socketConnected) return; // WebSocket handles this
|
||||
if (document.hidden) return; // Skip polling when tab is not visible
|
||||
|
|
@ -38939,7 +39034,7 @@ function buildLibraryArtistCardHTML(artist, index) {
|
|||
if (artist.soul_id && !artist.soul_id.startsWith('soul_unnamed_')) badges.push({ logo: '/static/trans2.png', fb: 'SS', title: `SoulID: ${artist.soul_id}`, url: null });
|
||||
|
||||
// Watchlist badge
|
||||
const hasActiveSourceId = currentMusicSourceName === 'Apple Music'
|
||||
const hasActiveSourceId = currentMusicSourceName === 'iTunes'
|
||||
? (artist.itunes_artist_id || artist.spotify_artist_id)
|
||||
: (artist.spotify_artist_id || artist.itunes_artist_id);
|
||||
let watchBadgeHTML = '';
|
||||
|
|
@ -39047,7 +39142,7 @@ function showLibraryEmpty(show) {
|
|||
async function openWatchAllUnwatchedModal() {
|
||||
if (document.getElementById('watch-all-modal-overlay')) return;
|
||||
|
||||
const sourceIdField = currentMusicSourceName === 'Apple Music' ? 'itunes_artist_id'
|
||||
const sourceIdField = currentMusicSourceName === 'iTunes' ? 'itunes_artist_id'
|
||||
: currentMusicSourceName === 'Deezer' ? 'deezer_id' : 'spotify_artist_id';
|
||||
const sourceName = currentMusicSourceName || 'Spotify';
|
||||
|
||||
|
|
@ -39260,7 +39355,7 @@ async function toggleLibraryCardWatchlist(btn, artist) {
|
|||
|
||||
try {
|
||||
// Use the ID matching the active metadata source
|
||||
const artistId = currentMusicSourceName === 'Apple Music'
|
||||
const artistId = currentMusicSourceName === 'iTunes'
|
||||
? (artist.itunes_artist_id || artist.spotify_artist_id)
|
||||
: (artist.spotify_artist_id || artist.itunes_artist_id);
|
||||
if (!artistId) throw new Error('No iTunes or Spotify ID available for this artist');
|
||||
|
|
|
|||
|
|
@ -2657,6 +2657,12 @@ body.helper-mode-active #dashboard-activity-feed:hover {
|
|||
content: '';
|
||||
}
|
||||
|
||||
.status-dot.fallback {
|
||||
background: rgba(245, 166, 35, 0.7);
|
||||
border: none;
|
||||
box-shadow: 0 0 4px rgba(245, 166, 35, 0.3);
|
||||
}
|
||||
|
||||
.status-dot.rate-limited {
|
||||
background: rgba(250, 204, 21, 0.8);
|
||||
border: none;
|
||||
|
|
@ -7360,6 +7366,19 @@ body.helper-mode-active #dashboard-activity-feed:hover {
|
|||
text-shadow: 0 0 8px rgba(255, 68, 68, 0.5), 0 0 16px rgba(255, 68, 68, 0.2);
|
||||
}
|
||||
|
||||
.service-card-indicator.fallback {
|
||||
color: #f5a623;
|
||||
text-shadow: 0 0 8px rgba(245, 166, 35, 0.4);
|
||||
}
|
||||
|
||||
.service-card-status-text.fallback {
|
||||
color: #f5a623;
|
||||
}
|
||||
|
||||
.service-card:has(.service-card-indicator.fallback)::before {
|
||||
background: linear-gradient(90deg, transparent, rgba(245, 166, 35, 0.4), transparent);
|
||||
}
|
||||
|
||||
.service-card-indicator.rate-limited {
|
||||
color: #facc15;
|
||||
text-shadow: 0 0 8px rgba(250, 204, 21, 0.5);
|
||||
|
|
@ -7443,6 +7462,155 @@ body.helper-mode-active #dashboard-activity-feed:hover {
|
|||
}
|
||||
|
||||
|
||||
/* Enrichment Services Grid */
|
||||
.enrichment-status-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.enrichment-chip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 14px 8px 10px;
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
cursor: default;
|
||||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.enrichment-chip[onclick] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.enrichment-chip::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 15%;
|
||||
bottom: 15%;
|
||||
width: 3px;
|
||||
border-radius: 0 3px 3px 0;
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
.enrichment-chip[onclick]:hover {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Status: Running / Configured */
|
||||
.enrichment-chip.status-running::before {
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 8px rgba(var(--accent-rgb), 0.4);
|
||||
}
|
||||
|
||||
.enrichment-chip.status-running .enrichment-chip-dot {
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 6px rgba(var(--accent-rgb), 0.5);
|
||||
}
|
||||
|
||||
/* Status: Idle */
|
||||
.enrichment-chip.status-idle::before {
|
||||
background: var(--accent);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.enrichment-chip.status-idle .enrichment-chip-dot {
|
||||
background: var(--accent);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Status: Paused */
|
||||
.enrichment-chip.status-paused::before {
|
||||
background: #f5a623;
|
||||
box-shadow: 0 0 6px rgba(245, 166, 35, 0.3);
|
||||
}
|
||||
|
||||
.enrichment-chip.status-paused .enrichment-chip-dot {
|
||||
background: #f5a623;
|
||||
box-shadow: 0 0 6px rgba(245, 166, 35, 0.3);
|
||||
}
|
||||
|
||||
/* Status: Stopped */
|
||||
.enrichment-chip.status-stopped::before {
|
||||
background: #ff4757;
|
||||
box-shadow: 0 0 6px rgba(255, 71, 87, 0.3);
|
||||
}
|
||||
|
||||
.enrichment-chip.status-stopped .enrichment-chip-dot {
|
||||
background: #ff4757;
|
||||
box-shadow: 0 0 4px rgba(255, 71, 87, 0.3);
|
||||
}
|
||||
|
||||
/* Status: Not Configured */
|
||||
.enrichment-chip.status-not-configured {
|
||||
opacity: 0.5;
|
||||
border-style: dashed;
|
||||
}
|
||||
|
||||
.enrichment-chip.status-not-configured::before {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.enrichment-chip.status-not-configured .enrichment-chip-dot {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.enrichment-chip.status-not-configured[onclick]:hover {
|
||||
opacity: 0.8;
|
||||
border-color: rgba(var(--accent-rgb), 0.3);
|
||||
}
|
||||
|
||||
.enrichment-chip.status-not-configured[onclick]:hover::before {
|
||||
background: rgba(var(--accent-rgb), 0.4);
|
||||
}
|
||||
|
||||
.enrichment-chip-dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.enrichment-chip-name {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.enrichment-chip-status {
|
||||
font-size: 10px;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
white-space: nowrap;
|
||||
margin-left: auto;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.enrichment-chip.status-not-configured .enrichment-chip-status {
|
||||
color: rgba(var(--accent-rgb), 0.6);
|
||||
}
|
||||
|
||||
.enrichment-chip.status-paused .enrichment-chip-status {
|
||||
color: rgba(245, 166, 35, 0.7);
|
||||
}
|
||||
|
||||
.enrichment-chip.status-stopped .enrichment-chip-status {
|
||||
color: rgba(255, 71, 87, 0.7);
|
||||
}
|
||||
|
||||
|
||||
/* System Stats Grid */
|
||||
.stats-grid-dashboard {
|
||||
display: grid;
|
||||
|
|
@ -7978,6 +8146,23 @@ body.helper-mode-active #dashboard-activity-feed:hover {
|
|||
gap: 15px;
|
||||
}
|
||||
|
||||
.enrichment-status-grid {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.enrichment-chip {
|
||||
padding: 6px 10px 6px 8px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.enrichment-chip-name {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.enrichment-chip-status {
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.tool-card-controls {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
|
|
|
|||
Loading…
Reference in a new issue