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:
parent
46f82027cb
commit
e8c26fb015
2 changed files with 766 additions and 3 deletions
|
|
@ -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">▼</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()">×</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');
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue