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 = `
+
+
×
+
+
${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}
+
Deploy Pipeline
+
+ `;
+
+ 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.
+
+
Notification Type
+
+ Discord Webhook
+ Telegram
+ Pushbullet
+ Skip Notifications
+
+
+
+
+ Cancel
+ Deploy
+
+
+ `;
+
+ 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);