Revamp Automation Hub with one-click pipeline deployment

Add Pipelines tab with 7 pre-built automation groups that deploy
multiple linked automations in one click. Visual pipeline cards with
accent-colored gradients, connected flow nodes, and deploy buttons.

- Release Radar Sync, Discovery Weekly Sync, Playlist Auto-Sync:
  4-stage pipelines (refresh → discover → sync → download)
- New Music Pipeline: watchlist scan → download → cleanup → notify
- Nightly Operations: staggered time-based (1AM-4AM)
- Download Monitor: 3 notification automations for failures/quarantine/completion
- Library Guardian: quality scan → notify chain

Pipeline detail modal shows full automation breakdown with WHEN/DO/THEN
tags. Deploy prompts for notification config when pipeline includes
alert steps. All signal chains use unique prefixes to avoid collisions.
This commit is contained in:
Broque Thomas 2026-03-20 00:17:30 -07:00
parent 46f82027cb
commit e8c26fb015
2 changed files with 766 additions and 3 deletions

View file

@ -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 = `
<span class="section-chevron">&#9660;</span>
<span class="section-label">Automation Hub</span>
<span class="section-count">${AUTO_HUB_RECIPES.length} recipes</span>
<span class="section-count">${AUTO_HUB_GROUPS.length} pipelines · ${AUTO_HUB_RECIPES.length} recipes</span>
<span class="section-line"></span>
`;
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 ? '<span class="pipeline-connector"></span>' : '') +
`<div class="pipeline-node ${nodeClass}">
<span class="pipeline-node-icon">${step.icon}</span>
<span class="pipeline-node-label">${step.label}</span>
</div>`;
}).join('');
card.innerHTML = `
<div class="pipeline-card-top">
<span class="pipeline-card-icon">${group.icon}</span>
<div class="pipeline-card-title-row">
<div class="pipeline-card-name">${group.name}</div>
<span class="pipeline-card-badge">${group.badge}</span>
</div>
</div>
<div class="pipeline-card-desc">${group.desc}</div>
<div class="pipeline-flow">${stepsHtml}</div>
<div class="pipeline-card-footer">
<button class="pipeline-deploy-btn" onclick="event.stopPropagation(); deployHubGroup('${group.id}')">Deploy Pipeline</button>
</div>
`;
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 `
<div class="pipeline-detail-auto" style="--step-color: ${group.color}">
<div class="pipeline-detail-step-num">${i + 1}</div>
<div class="pipeline-detail-step-body">
<div class="pipeline-detail-step-name">${auto.name}</div>
<div class="pipeline-detail-step-flow">
<span class="pipeline-detail-tag when">WHEN</span>
<span class="pipeline-detail-tag-value">${_esc(triggerLabel)}</span>
<span class="pipeline-detail-tag do">DO</span>
<span class="pipeline-detail-tag-value">${_esc(actionLabel)}</span>
${thenLabels.length ? `<span class="pipeline-detail-tag then">THEN</span><span class="pipeline-detail-tag-value">${thenLabels.map(t => _esc(t)).join(', ')}</span>` : ''}
</div>
</div>
</div>`;
}).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 ? '<span class="pipeline-connector"></span>' : '') +
`<div class="pipeline-node ${nodeClass}">
<span class="pipeline-node-icon">${step.icon}</span>
<span class="pipeline-node-label">${step.label}</span>
</div>`;
}).join('');
const overlay = document.createElement('div');
overlay.className = 'pipeline-detail-overlay';
overlay.innerHTML = `
<div class="pipeline-detail-modal" style="--pipeline-color: ${group.color}">
<button class="pipeline-detail-close" onclick="this.closest('.pipeline-detail-overlay').remove()">&times;</button>
<div class="pipeline-detail-header">
<span class="pipeline-detail-icon">${group.icon}</span>
<div>
<div class="pipeline-detail-title">${group.name}</div>
<div class="pipeline-detail-desc">${group.desc}</div>
</div>
</div>
<div class="pipeline-detail-flow" style="--pipeline-color: ${group.color}">${flowHtml}</div>
<div class="pipeline-detail-section-title">How It Works</div>
<div class="pipeline-detail-section-desc">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'}.</div>
<div class="pipeline-detail-autos">${autoDetails}</div>
<button class="pipeline-deploy-btn" style="--pipeline-color: ${group.color}; margin-top: 8px;" onclick="this.closest('.pipeline-detail-overlay').remove(); deployHubGroup('${group.id}')">Deploy Pipeline</button>
</div>
`;
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 = `
<div style="background:var(--bg-secondary, #1e1e2e);border:1px solid rgba(255,255,255,0.1);border-radius:14px;padding:28px;max-width:420px;width:90%;color:var(--text-primary, #fff);font-family:inherit;">
<h3 style="margin:0 0 6px;font-size:1.1em;">Configure Notifications</h3>
<p style="margin:0 0 18px;font-size:0.85em;opacity:0.5;">${groupName} includes notification steps. Choose how to get notified.</p>
<div style="display:flex;flex-direction:column;gap:10px;margin-bottom:18px;">
<label style="font-size:0.85em;opacity:0.7;">Notification Type</label>
<select id="deploy-notify-type" style="padding:9px 12px;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.08);border-radius:8px;color:#fff;font-size:0.9em;">
<option value="discord_webhook">Discord Webhook</option>
<option value="telegram">Telegram</option>
<option value="pushbullet">Pushbullet</option>
<option value="none">Skip Notifications</option>
</select>
<div id="deploy-notify-fields"></div>
</div>
<div style="display:flex;gap:10px;justify-content:flex-end;">
<button id="deploy-notify-cancel" style="padding:8px 20px;border:1px solid rgba(255,255,255,0.1);border-radius:8px;background:transparent;color:rgba(255,255,255,0.7);cursor:pointer;font-size:0.88em;">Cancel</button>
<button id="deploy-notify-confirm" style="padding:8px 20px;border:none;border-radius:8px;background:var(--accent-color,#1db954);color:#fff;cursor:pointer;font-size:0.88em;font-weight:600;">Deploy</button>
</div>
</div>
`;
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 = '<input id="deploy-notify-url" type="text" placeholder="Discord Webhook URL" style="width:100%;padding:9px 12px;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.08);border-radius:8px;color:#fff;font-size:0.88em;margin-top:6px;box-sizing:border-box;">';
} else if (type === 'telegram') {
fieldsDiv.innerHTML = '<input id="deploy-notify-token" type="text" placeholder="Bot Token" style="width:100%;padding:9px 12px;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.08);border-radius:8px;color:#fff;font-size:0.88em;margin-top:6px;box-sizing:border-box;"><input id="deploy-notify-chat" type="text" placeholder="Chat ID" style="width:100%;padding:9px 12px;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.08);border-radius:8px;color:#fff;font-size:0.88em;margin-top:6px;box-sizing:border-box;">';
} else if (type === 'pushbullet') {
fieldsDiv.innerHTML = '<input id="deploy-notify-token" type="text" placeholder="Access Token" style="width:100%;padding:9px 12px;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.08);border-radius:8px;color:#fff;font-size:0.88em;margin-top:6px;box-sizing:border-box;">';
} 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');

View file

@ -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);