diff --git a/webui/static/script.js b/webui/static/script.js index c523ce85..969c9b28 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -56239,6 +56239,123 @@ const _autoIcons = { // --- Inspiration Templates --- // --- Automation Hub Data --- +// โ”€โ”€ Automation Hub: One-Click Pipeline Groups โ”€โ”€ +const AUTO_HUB_GROUPS = [ + { + id: 'release-radar', icon: '๐Ÿ“ก', name: 'Release Radar Sync', + desc: 'Auto-sync your Release Radar playlist every Friday. Refreshes from Spotify, discovers metadata, syncs to your media server, and downloads missing tracks.', + category: 'Sync', badge: '4 automations', color: '#1db954', + steps: [ + { label: 'Refresh', icon: '๐Ÿ”„', type: 'action' }, + { label: 'Discover', icon: '๐Ÿ”', type: 'action' }, + { label: 'Sync', icon: '๐Ÿ“‹', type: 'action' }, + { label: 'Download', icon: '๐Ÿ“ฅ', type: 'action' }, + ], + automations: [ + { name: 'Release Radar โ€” Refresh', trigger_type: 'weekly_time', trigger_config: { days: ['friday'], time: '18:00' }, action_type: 'refresh_mirrored', action_config: { all: true }, then_actions: [{ type: 'fire_signal', config: { signal_name: 'rr_refreshed' } }], group_name: 'Release Radar' }, + { name: 'Release Radar โ€” Discover', trigger_type: 'signal_received', trigger_config: { signal_name: 'rr_refreshed' }, action_type: 'discover_playlist', action_config: { all: true }, then_actions: [{ type: 'fire_signal', config: { signal_name: 'rr_discovered' } }], group_name: 'Release Radar' }, + { name: 'Release Radar โ€” Sync', trigger_type: 'signal_received', trigger_config: { signal_name: 'rr_discovered' }, action_type: 'sync_playlist', action_config: {}, then_actions: [{ type: 'fire_signal', config: { signal_name: 'rr_synced' } }], group_name: 'Release Radar' }, + { name: 'Release Radar โ€” Download', trigger_type: 'signal_received', trigger_config: { signal_name: 'rr_synced' }, action_type: 'process_wishlist', action_config: {}, then_actions: [], group_name: 'Release Radar', needs_notify: true }, + ] + }, + { + id: 'discovery-weekly', icon: '๐ŸŽต', name: 'Discovery Weekly Sync', + desc: 'Capture your Discover Weekly every Monday before Spotify replaces it. Refreshes, discovers, syncs, and downloads the full playlist.', + category: 'Sync', badge: '4 automations', color: '#6366f1', + steps: [ + { label: 'Refresh', icon: '๐Ÿ”„', type: 'action' }, + { label: 'Discover', icon: '๐Ÿ”', type: 'action' }, + { label: 'Sync', icon: '๐Ÿ“‹', type: 'action' }, + { label: 'Download', icon: '๐Ÿ“ฅ', type: 'action' }, + ], + automations: [ + { name: 'Discovery Weekly โ€” Refresh', trigger_type: 'weekly_time', trigger_config: { days: ['monday'], time: '06:00' }, action_type: 'refresh_mirrored', action_config: { all: true }, then_actions: [{ type: 'fire_signal', config: { signal_name: 'dw_refreshed' } }], group_name: 'Discovery Weekly' }, + { name: 'Discovery Weekly โ€” Discover', trigger_type: 'signal_received', trigger_config: { signal_name: 'dw_refreshed' }, action_type: 'discover_playlist', action_config: { all: true }, then_actions: [{ type: 'fire_signal', config: { signal_name: 'dw_discovered' } }], group_name: 'Discovery Weekly' }, + { name: 'Discovery Weekly โ€” Sync', trigger_type: 'signal_received', trigger_config: { signal_name: 'dw_discovered' }, action_type: 'sync_playlist', action_config: {}, then_actions: [{ type: 'fire_signal', config: { signal_name: 'dw_synced' } }], group_name: 'Discovery Weekly' }, + { name: 'Discovery Weekly โ€” Download', trigger_type: 'signal_received', trigger_config: { signal_name: 'dw_synced' }, action_type: 'process_wishlist', action_config: {}, then_actions: [], group_name: 'Discovery Weekly', needs_notify: true }, + ] + }, + { + id: 'playlist-auto-sync', icon: '๐Ÿ”„', name: 'Playlist Auto-Sync', + desc: 'Keep all mirrored playlists in sync. Refreshes every 6 hours, then discovers, syncs, and downloads any new tracks.', + category: 'Sync', badge: '4 automations', color: '#06b6d4', + steps: [ + { label: 'Refresh', icon: '๐Ÿ”„', type: 'action' }, + { label: 'Discover', icon: '๐Ÿ”', type: 'action' }, + { label: 'Sync', icon: '๐Ÿ“‹', type: 'action' }, + { label: 'Download', icon: '๐Ÿ“ฅ', type: 'action' }, + ], + automations: [ + { name: 'Playlist Sync โ€” Refresh', trigger_type: 'schedule', trigger_config: { interval: 6, unit: 'hours' }, action_type: 'refresh_mirrored', action_config: { all: true }, then_actions: [{ type: 'fire_signal', config: { signal_name: 'ps_refreshed' } }], group_name: 'Playlist Auto-Sync' }, + { name: 'Playlist Sync โ€” Discover', trigger_type: 'signal_received', trigger_config: { signal_name: 'ps_refreshed' }, action_type: 'discover_playlist', action_config: { all: true }, then_actions: [{ type: 'fire_signal', config: { signal_name: 'ps_discovered' } }], group_name: 'Playlist Auto-Sync' }, + { name: 'Playlist Sync โ€” Sync', trigger_type: 'signal_received', trigger_config: { signal_name: 'ps_discovered' }, action_type: 'sync_playlist', action_config: {}, then_actions: [{ type: 'fire_signal', config: { signal_name: 'ps_synced' } }], group_name: 'Playlist Auto-Sync' }, + { name: 'Playlist Sync โ€” Download', trigger_type: 'signal_received', trigger_config: { signal_name: 'ps_synced' }, action_type: 'process_wishlist', action_config: {}, then_actions: [], group_name: 'Playlist Auto-Sync', needs_notify: true }, + ] + }, + { + id: 'new-music-pipeline', icon: '๐Ÿš€', name: 'New Music Pipeline', + desc: 'Full hands-free new music workflow. Scans your watchlist for releases, downloads them, cleans up, and notifies you.', + category: 'Discovery', badge: '4 automations', color: '#f97316', + steps: [ + { label: 'Scan Artists', icon: '๐Ÿ”', type: 'action' }, + { label: 'Download', icon: '๐Ÿ“ฅ', type: 'action' }, + { label: 'Cleanup', icon: '๐Ÿงน', type: 'action' }, + { label: 'Notify', icon: '๐Ÿ””', type: 'notify' }, + ], + automations: [ + { name: 'New Music โ€” Scan Watchlist', trigger_type: 'schedule', trigger_config: { interval: 12, unit: 'hours' }, action_type: 'scan_watchlist', action_config: {}, then_actions: [{ type: 'fire_signal', config: { signal_name: 'nm_scanned' } }], group_name: 'New Music Pipeline' }, + { name: 'New Music โ€” Download', trigger_type: 'signal_received', trigger_config: { signal_name: 'nm_scanned' }, action_type: 'process_wishlist', action_config: {}, then_actions: [{ type: 'fire_signal', config: { signal_name: 'nm_downloaded' } }], group_name: 'New Music Pipeline' }, + { name: 'New Music โ€” Cleanup', trigger_type: 'signal_received', trigger_config: { signal_name: 'nm_downloaded' }, action_type: 'full_cleanup', action_config: {}, then_actions: [{ type: 'fire_signal', config: { signal_name: 'nm_cleaned' } }], group_name: 'New Music Pipeline' }, + { name: 'New Music โ€” Notify', trigger_type: 'signal_received', trigger_config: { signal_name: 'nm_cleaned' }, action_type: 'notify_only', action_config: {}, then_actions: [], group_name: 'New Music Pipeline', needs_notify: true }, + ] + }, + { + id: 'nightly-ops', icon: '๐ŸŒ™', name: 'Nightly Operations', + desc: 'Staggered overnight maintenance: scan, download, cleanup, and backup while you sleep.', + category: 'Maintenance', badge: '4 automations', color: '#8b5cf6', + steps: [ + { label: '1AM Scan', icon: '๐Ÿ”', type: 'action' }, + { label: '2AM Download', icon: '๐Ÿ“ฅ', type: 'action' }, + { label: '3AM Cleanup', icon: '๐Ÿงน', type: 'action' }, + { label: '4AM Backup', icon: '๐Ÿ’พ', type: 'action' }, + ], + automations: [ + { name: 'Nightly โ€” 1AM Scan', trigger_type: 'daily_time', trigger_config: { time: '01:00' }, action_type: 'scan_watchlist', action_config: {}, then_actions: [], group_name: 'Nightly Operations' }, + { name: 'Nightly โ€” 2AM Download', trigger_type: 'daily_time', trigger_config: { time: '02:00' }, action_type: 'process_wishlist', action_config: {}, then_actions: [], group_name: 'Nightly Operations' }, + { name: 'Nightly โ€” 3AM Cleanup', trigger_type: 'daily_time', trigger_config: { time: '03:00' }, action_type: 'full_cleanup', action_config: {}, then_actions: [], group_name: 'Nightly Operations' }, + { name: 'Nightly โ€” 4AM Backup', trigger_type: 'daily_time', trigger_config: { time: '04:00' }, action_type: 'backup_database', action_config: {}, then_actions: [], group_name: 'Nightly Operations' }, + ] + }, + { + id: 'download-monitor', icon: '๐Ÿ“Š', name: 'Download Monitor', + desc: 'Stay informed about your downloads. Get notified on failures, quarantined files, and completed batches.', + category: 'Alerts', badge: '3 automations', color: '#ef4444', + steps: [ + { label: 'Failures', icon: 'โŒ', type: 'notify' }, + { label: 'Quarantine', icon: 'โš ๏ธ', type: 'notify' }, + { label: 'Complete', icon: 'โœ…', type: 'notify' }, + ], + automations: [ + { name: 'Alert โ€” Download Failed', trigger_type: 'download_failed', trigger_config: {}, action_type: 'notify_only', action_config: {}, then_actions: [], group_name: 'Download Monitor', needs_notify: true }, + { name: 'Alert โ€” File Quarantined', trigger_type: 'download_quarantined', trigger_config: {}, action_type: 'notify_only', action_config: {}, then_actions: [], group_name: 'Download Monitor', needs_notify: true }, + { name: 'Alert โ€” Batch Complete', trigger_type: 'batch_complete', trigger_config: {}, action_type: 'notify_only', action_config: {}, then_actions: [], group_name: 'Download Monitor', needs_notify: true }, + ] + }, + { + id: 'library-guardian', icon: '๐Ÿ›ก๏ธ', name: 'Library Guardian', + desc: 'Protect your library quality. After scans, runs quality checks and notifies you of any issues found.', + category: 'Maintenance', badge: '2 automations', color: '#f59e0b', + steps: [ + { label: 'Quality Scan', icon: 'โœ…', type: 'action' }, + { label: 'Notify', icon: '๐Ÿ””', type: 'notify' }, + ], + automations: [ + { name: 'Guardian โ€” Quality Check', trigger_type: 'library_scan_completed', trigger_config: {}, action_type: 'start_quality_scan', action_config: {}, then_actions: [{ type: 'fire_signal', config: { signal_name: 'guardian_quality_done' } }], group_name: 'Library Guardian' }, + { name: 'Guardian โ€” Notify', trigger_type: 'signal_received', trigger_config: { signal_name: 'guardian_quality_done' }, action_type: 'notify_only', action_config: {}, then_actions: [], group_name: 'Library Guardian', needs_notify: true }, + ] + }, +]; + const AUTO_HUB_RECIPES = [ // Sync & Playlists { id: 'spotify-auto-sync', icon: '\uD83D\uDD01', name: 'Spotify Playlist Auto-Sync', desc: 'Refresh all mirrored playlists every 6 hours to keep them in sync with Spotify.', @@ -56541,7 +56658,7 @@ function _buildAutomationHub() { header.innerHTML = ` Automation Hub - ${AUTO_HUB_RECIPES.length} recipes + ${AUTO_HUB_GROUPS.length} pipelines ยท ${AUTO_HUB_RECIPES.length} recipes `; header.onclick = () => { @@ -56551,9 +56668,10 @@ function _buildAutomationHub() { const body = document.createElement('div'); body.className = 'automations-section-body'; - const activeTab = localStorage.getItem('auto_hub_tab') || 'recipes'; + const activeTab = localStorage.getItem('auto_hub_tab') || 'pipelines'; const tabs = [ - { id: 'recipes', label: 'Recipes' }, + { id: 'pipelines', label: 'Pipelines' }, + { id: 'recipes', label: 'Singles' }, { id: 'guides', label: 'Quick Start' }, { id: 'tips', label: 'Tips' }, { id: 'reference', label: 'Reference' }, @@ -56572,6 +56690,11 @@ function _buildAutomationHub() { body.appendChild(tabBar); // Build all tab contents + const pipelinesPane = _buildHubPipelines(); + pipelinesPane.id = 'auto-hub-pane-pipelines'; + pipelinesPane.className = 'auto-hub-tab-content' + (activeTab === 'pipelines' ? ' active' : ''); + body.appendChild(pipelinesPane); + const recipesPane = _buildHubRecipes(); recipesPane.id = 'auto-hub-pane-recipes'; recipesPane.className = 'auto-hub-tab-content' + (activeTab === 'recipes' ? ' active' : ''); @@ -56605,6 +56728,126 @@ function _switchHubTab(tabId, bodyEl) { localStorage.setItem('auto_hub_tab', tabId); } +function _buildHubPipelines() { + const pane = document.createElement('div'); + + const intro = document.createElement('div'); + intro.className = 'auto-hub-pipeline-intro'; + intro.innerHTML = 'One-click deployment โ€” each pipeline creates multiple linked automations that work together.'; + pane.appendChild(intro); + + const grid = document.createElement('div'); + grid.className = 'auto-hub-pipeline-grid'; + + AUTO_HUB_GROUPS.forEach(group => { + const card = document.createElement('div'); + card.className = 'auto-hub-pipeline-card'; + card.style.setProperty('--pipeline-color', group.color); + + // Pipeline flow visualization + const stepsHtml = group.steps.map((step, i) => { + const nodeClass = step.type === 'notify' ? 'pipeline-node-notify' : 'pipeline-node-action'; + return (i > 0 ? '' : '') + + `
+ ${step.icon} + ${step.label} +
`; + }).join(''); + + card.innerHTML = ` +
+ ${group.icon} +
+
${group.name}
+ ${group.badge} +
+
+
${group.desc}
+
${stepsHtml}
+ + `; + + card.addEventListener('click', (e) => { + if (e.target.closest('.pipeline-deploy-btn')) return; + showPipelineDetail(group.id); + }); + + grid.appendChild(card); + }); + + pane.appendChild(grid); + return pane; +} + +function showPipelineDetail(groupId) { + const group = AUTO_HUB_GROUPS.find(g => g.id === groupId); + if (!group) return; + + // Build automation detail list + const autoDetails = group.automations.map((auto, i) => { + const triggerLabel = _autoFormatTrigger(auto.trigger_type, auto.trigger_config); + const actionLabel = _autoFormatAction(auto.action_type); + const thenLabels = auto.then_actions.map(t => { + if (t.type === 'fire_signal') return `โšก Signal: ${t.config.signal_name}`; + return _autoFormatNotify(t.type); + }); + if (auto.needs_notify) thenLabels.push('๐Ÿ”” Your notification'); + + return ` +
+
${i + 1}
+
+
${auto.name}
+
+ WHEN + ${_esc(triggerLabel)} + DO + ${_esc(actionLabel)} + ${thenLabels.length ? `THEN${thenLabels.map(t => _esc(t)).join(', ')}` : ''} +
+
+
`; + }).join(''); + + // Build flow diagram + const flowHtml = group.steps.map((step, i) => { + const nodeClass = step.type === 'notify' ? 'pipeline-node-notify' : 'pipeline-node-action'; + return (i > 0 ? '' : '') + + `
+ ${step.icon} + ${step.label} +
`; + }).join(''); + + const overlay = document.createElement('div'); + overlay.className = 'pipeline-detail-overlay'; + overlay.innerHTML = ` +
+ +
+ ${group.icon} +
+
${group.name}
+
${group.desc}
+
+
+
${flowHtml}
+
How It Works
+
This pipeline deploys ${group.automations.length} automations${group.automations.some(a => a.then_actions.some(t => t.type === 'fire_signal')) ? ' linked by signals โ€” each step triggers the next automatically' : ' running on independent schedules'}.
+
${autoDetails}
+ +
+ `; + + overlay.addEventListener('click', (e) => { + if (e.target === overlay) overlay.remove(); + }); + + document.body.appendChild(overlay); +} + function _buildHubRecipes() { const pane = document.createElement('div'); const categories = ['All', 'Sync', 'Discovery', 'Maintenance', 'Alerts', 'Chains']; @@ -56832,6 +57075,142 @@ async function useHubRecipe(recipeId) { } } +async function deployHubGroup(groupId) { + const group = AUTO_HUB_GROUPS.find(g => g.id === groupId); + if (!group) return; + + // Check if any automations need notifications โ€” prompt for config + const needsNotify = group.automations.some(a => a.needs_notify); + let notifyConfig = null; + + if (needsNotify) { + notifyConfig = await _promptNotifyConfig(group.name); + if (notifyConfig === null) return; // User cancelled + if (notifyConfig === false) notifyConfig = null; // Skip notifications, still deploy + } + + // Deploy all automations in the group + let created = 0, failed = 0; + for (const auto of group.automations) { + try { + const payload = { + name: auto.name, + trigger_type: auto.trigger_type, + trigger_config: auto.trigger_config, + action_type: auto.action_type, + action_config: auto.action_config, + then_actions: [...auto.then_actions], + group_name: auto.group_name, + enabled: true, + }; + + // Inject notification config for automations that need it + if (auto.needs_notify && notifyConfig) { + payload.then_actions.push(notifyConfig); + } + + const response = await fetch('/api/automations', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + + if (response.ok) { + created++; + } else { + const err = await response.json(); + console.error(`Failed to create "${auto.name}":`, err); + failed++; + } + } catch (e) { + console.error(`Error creating "${auto.name}":`, e); + failed++; + } + } + + if (created > 0) { + showToast(`Deployed "${group.name}" โ€” ${created} automation${created > 1 ? 's' : ''} created${failed ? `, ${failed} failed` : ''}`, 'success'); + loadAutomationsPage(); + } else { + showToast(`Failed to deploy "${group.name}"`, 'error'); + } +} + +function _promptNotifyConfig(groupName) { + return new Promise(resolve => { + const overlay = document.createElement('div'); + overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:10000;display:flex;align-items:center;justify-content:center;'; + + overlay.innerHTML = ` +
+

Configure Notifications

+

${groupName} includes notification steps. Choose how to get notified.

+
+ + +
+
+
+ + +
+
+ `; + + document.body.appendChild(overlay); + + const typeSelect = overlay.querySelector('#deploy-notify-type'); + const fieldsDiv = overlay.querySelector('#deploy-notify-fields'); + + function updateFields() { + const type = typeSelect.value; + if (type === 'discord_webhook') { + fieldsDiv.innerHTML = ''; + } else if (type === 'telegram') { + fieldsDiv.innerHTML = ''; + } else if (type === 'pushbullet') { + fieldsDiv.innerHTML = ''; + } else { + fieldsDiv.innerHTML = ''; + } + } + typeSelect.addEventListener('change', updateFields); + updateFields(); + + overlay.querySelector('#deploy-notify-cancel').addEventListener('click', () => { + document.body.removeChild(overlay); + resolve(null); + }); + + overlay.querySelector('#deploy-notify-confirm').addEventListener('click', () => { + const type = typeSelect.value; + let config = {}; + if (type === 'discord_webhook') { + config = { webhook_url: (overlay.querySelector('#deploy-notify-url')?.value || '').trim() }; + } else if (type === 'telegram') { + config = { bot_token: (overlay.querySelector('#deploy-notify-token')?.value || '').trim(), chat_id: (overlay.querySelector('#deploy-notify-chat')?.value || '').trim() }; + } else if (type === 'pushbullet') { + config = { access_token: (overlay.querySelector('#deploy-notify-token')?.value || '').trim() }; + } else { + document.body.removeChild(overlay); + resolve(false); // Skip notifications but still deploy + return; + } + document.body.removeChild(overlay); + resolve({ type, config }); + }); + + overlay.addEventListener('click', (e) => { + if (e.target === overlay) { document.body.removeChild(overlay); resolve(null); } + }); + }); +} + // --- Filter Bar --- function _initAutoFilterBar(automations) { const bar = document.getElementById('auto-filter-bar'); diff --git a/webui/static/style.css b/webui/static/style.css index 71eb2b4e..d4431373 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -34693,6 +34693,390 @@ body.downloads-disabled [onclick*="DownloadMissing"]:not([onclick*="close"]) { grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 10px; } +/* โ”€โ”€ Pipeline Cards โ€” Premium Design โ”€โ”€ */ +.auto-hub-pipeline-intro { + font-size: 0.84em; + color: rgba(255,255,255,0.35); + padding: 0 0 20px; + line-height: 1.5; +} +.auto-hub-pipeline-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); + gap: 14px; +} +@media (max-width: 768px) { + .auto-hub-pipeline-grid { grid-template-columns: 1fr; } +} +.auto-hub-pipeline-card { + display: flex; + flex-direction: column; + background: linear-gradient( + 145deg, + color-mix(in srgb, var(--pipeline-color) 6%, rgba(20,20,28,0.95)), + rgba(14,14,20,0.98) + ); + border: 1px solid color-mix(in srgb, var(--pipeline-color) 15%, transparent); + border-radius: 16px; + padding: 22px; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + overflow: hidden; + cursor: default; +} +.auto-hub-pipeline-card::after { + content: ''; + position: absolute; + top: 0; right: 0; + width: 140px; height: 140px; + background: radial-gradient(circle at top right, + color-mix(in srgb, var(--pipeline-color) 8%, transparent), + transparent 70%); + pointer-events: none; +} +.auto-hub-pipeline-card:hover { + border-color: color-mix(in srgb, var(--pipeline-color) 30%, transparent); + transform: translateY(-2px); + box-shadow: 0 8px 30px color-mix(in srgb, var(--pipeline-color) 10%, transparent), + 0 2px 8px rgba(0,0,0,0.3); +} + +/* Top row: icon + title + badge */ +.pipeline-card-top { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 8px; + position: relative; + z-index: 1; +} +.pipeline-card-icon { + font-size: 1.3em; + flex-shrink: 0; + width: 38px; + height: 38px; + display: flex; + align-items: center; + justify-content: center; + background: color-mix(in srgb, var(--pipeline-color) 12%, transparent); + border-radius: 11px; + border: 1px solid color-mix(in srgb, var(--pipeline-color) 18%, transparent); +} +.pipeline-card-title-row { + flex: 1; + display: flex; + align-items: center; + gap: 10px; + min-width: 0; +} +.pipeline-card-name { + font-size: 0.98em; + font-weight: 600; + color: #fff; + letter-spacing: 0.01em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.pipeline-card-badge { + font-size: 0.65em; + font-weight: 600; + padding: 3px 9px; + border-radius: 20px; + background: color-mix(in srgb, var(--pipeline-color) 15%, transparent); + color: color-mix(in srgb, var(--pipeline-color) 80%, white); + white-space: nowrap; + flex-shrink: 0; + letter-spacing: 0.03em; + text-transform: uppercase; +} + +/* Description */ +.pipeline-card-desc { + font-size: 0.8em; + color: rgba(255,255,255,0.38); + line-height: 1.5; + margin-bottom: 14px; + position: relative; + z-index: 1; +} + +/* Pipeline flow โ€” connected circles */ +.pipeline-flow { + display: flex; + align-items: flex-start; + justify-content: center; + gap: 0; + padding: 10px 0; + flex: 1; + position: relative; + z-index: 1; +} +.pipeline-node { + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + flex: 1; + min-width: 0; +} +.pipeline-node-icon { + width: 38px; + height: 38px; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.05em; + border-radius: 50%; + background: rgba(255,255,255,0.04); + border: 1px solid rgba(255,255,255,0.07); + transition: all 0.25s; + flex-shrink: 0; +} +.pipeline-node-action .pipeline-node-icon { + background: color-mix(in srgb, var(--pipeline-color) 10%, transparent); + border-color: color-mix(in srgb, var(--pipeline-color) 20%, transparent); +} +.pipeline-node-notify .pipeline-node-icon { + background: rgba(251, 191, 36, 0.08); + border-color: rgba(251, 191, 36, 0.2); +} +.auto-hub-pipeline-card:hover .pipeline-node-icon { + transform: scale(1.06); + border-color: color-mix(in srgb, var(--pipeline-color) 35%, transparent); +} +.pipeline-node-label { + font-size: 0.66em; + color: rgba(255,255,255,0.38); + font-weight: 500; + text-align: center; + line-height: 1.3; + max-width: 80px; + word-wrap: break-word; +} +.pipeline-connector { + width: 100%; + max-width: 40px; + height: 1px; + background: linear-gradient(90deg, + color-mix(in srgb, var(--pipeline-color) 30%, transparent), + color-mix(in srgb, var(--pipeline-color) 12%, transparent)); + flex-shrink: 1; + margin-top: 19px; /* vertically center with circle icons */ + align-self: flex-start; +} + +/* Footer with deploy button */ +.pipeline-card-footer { + margin-top: 14px; + position: relative; + z-index: 1; +} +.pipeline-deploy-btn { + width: 100%; + padding: 10px; + border: none; + border-radius: 10px; + background: linear-gradient(135deg, + color-mix(in srgb, var(--pipeline-color) 85%, white), + var(--pipeline-color)); + color: #fff; + font-size: 0.84em; + font-weight: 600; + font-family: inherit; + cursor: pointer; + transition: all 0.25s; + letter-spacing: 0.02em; +} +.pipeline-deploy-btn:hover { + filter: brightness(1.12); + box-shadow: 0 4px 16px color-mix(in srgb, var(--pipeline-color) 30%, transparent); + transform: translateY(-1px); +} + +/* โ”€โ”€ Pipeline Detail Modal โ”€โ”€ */ +.pipeline-detail-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.75); + backdrop-filter: blur(8px); + z-index: 10000; + display: flex; + align-items: center; + justify-content: center; + padding: 20px; +} +.pipeline-detail-modal { + background: linear-gradient( + 160deg, + color-mix(in srgb, var(--pipeline-color) 5%, rgba(22, 22, 30, 0.98)), + rgba(14, 14, 20, 0.99) + ); + border: 1px solid color-mix(in srgb, var(--pipeline-color) 15%, transparent); + border-radius: 20px; + padding: 32px; + max-width: 580px; + width: 100%; + max-height: 85vh; + overflow-y: auto; + position: relative; + box-shadow: 0 24px 80px rgba(0, 0, 0, 0.5), + 0 0 40px color-mix(in srgb, var(--pipeline-color) 8%, transparent); +} +.pipeline-detail-modal::-webkit-scrollbar { width: 4px; } +.pipeline-detail-modal::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 2px; } + +.pipeline-detail-close { + position: absolute; + top: 16px; right: 18px; + background: none; + border: none; + color: rgba(255, 255, 255, 0.3); + font-size: 1.5em; + cursor: pointer; + width: 32px; height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 8px; + transition: all 0.2s; +} +.pipeline-detail-close:hover { + background: rgba(255, 255, 255, 0.06); + color: rgba(255, 255, 255, 0.7); +} +.pipeline-detail-header { + display: flex; + align-items: flex-start; + gap: 16px; + margin-bottom: 20px; + padding-right: 32px; +} +.pipeline-detail-icon { + font-size: 2em; + width: 52px; height: 52px; + display: flex; + align-items: center; + justify-content: center; + background: color-mix(in srgb, var(--pipeline-color) 12%, transparent); + border: 1px solid color-mix(in srgb, var(--pipeline-color) 20%, transparent); + border-radius: 14px; + flex-shrink: 0; +} +.pipeline-detail-title { + font-size: 1.2em; + font-weight: 700; + color: #fff; + margin-bottom: 6px; +} +.pipeline-detail-desc { + font-size: 0.85em; + color: rgba(255, 255, 255, 0.42); + line-height: 1.5; +} +.pipeline-detail-flow { + display: flex; + align-items: flex-start; + justify-content: center; + padding: 18px 8px; + background: rgba(255, 255, 255, 0.02); + border-radius: 12px; + margin-bottom: 22px; + border: 1px solid rgba(255, 255, 255, 0.04); +} +.pipeline-detail-section-title { + font-size: 0.72em; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: rgba(255, 255, 255, 0.35); + margin-bottom: 6px; +} +.pipeline-detail-section-desc { + font-size: 0.82em; + color: rgba(255, 255, 255, 0.38); + line-height: 1.5; + margin-bottom: 16px; +} +/* Automation step cards */ +.pipeline-detail-autos { + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: 18px; +} +.pipeline-detail-auto { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 14px 16px; + background: rgba(255, 255, 255, 0.025); + border: 1px solid rgba(255, 255, 255, 0.05); + border-radius: 12px; + transition: border-color 0.2s; +} +.pipeline-detail-auto:hover { + border-color: color-mix(in srgb, var(--step-color) 25%, transparent); +} +.pipeline-detail-step-num { + width: 26px; height: 26px; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.75em; + font-weight: 700; + color: color-mix(in srgb, var(--step-color) 80%, white); + background: color-mix(in srgb, var(--step-color) 15%, transparent); + border-radius: 50%; + flex-shrink: 0; + margin-top: 1px; +} +.pipeline-detail-step-body { + flex: 1; + min-width: 0; +} +.pipeline-detail-step-name { + font-size: 0.88em; + font-weight: 600; + color: rgba(255, 255, 255, 0.85); + margin-bottom: 6px; +} +.pipeline-detail-step-flow { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 5px; +} +.pipeline-detail-tag { + font-size: 0.62em; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; + padding: 2px 6px; + border-radius: 4px; +} +.pipeline-detail-tag.when { + background: rgba(99, 102, 241, 0.15); + color: rgba(129, 140, 248, 0.9); +} +.pipeline-detail-tag.do { + background: rgba(34, 197, 94, 0.15); + color: rgba(74, 222, 128, 0.9); +} +.pipeline-detail-tag.then { + background: rgba(251, 191, 36, 0.15); + color: rgba(253, 224, 71, 0.9); +} +.pipeline-detail-tag-value { + font-size: 0.78em; + color: rgba(255, 255, 255, 0.55); +} +@media (max-width: 600px) { + .pipeline-detail-modal { padding: 20px; border-radius: 14px; } + .pipeline-detail-header { flex-direction: column; gap: 12px; } + .pipeline-detail-flow { flex-wrap: wrap; } +} + .auto-hub-recipe-card { background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.06);