Replace Inspiration templates with tabbed Automation Hub
Swap the flat 8-card Inspiration section for a rich 4-tab Automation Hub (Recipes, Quick Start, Tips, Reference) with 20 categorized recipe cards, 5 step-by-step guides, 8 power-user tips, and full trigger/action/then reference tables. Includes category + difficulty filters, chain flow visuals, accordion expand/collapse, and responsive grid layouts.
This commit is contained in:
parent
6b5e37aded
commit
9a6ee0e229
2 changed files with 890 additions and 53 deletions
|
|
@ -55765,25 +55765,199 @@ const _autoIcons = {
|
|||
};
|
||||
|
||||
// --- Inspiration Templates ---
|
||||
const AUTO_TEMPLATES = [
|
||||
{ icon: '\uD83D\uDD01', name: 'Playlist Auto-Sync Pipeline', desc: 'Refresh a mirrored playlist, discover new tracks, and sync to your server automatically.',
|
||||
category: 'Sync', when: { type: 'schedule', config: { interval: 6, unit: 'hours' } }, do: { type: 'refresh_mirrored', config: {} }, then: [] },
|
||||
{ icon: '\uD83D\uDD14', name: 'New Release Monitor', desc: 'Scan your watchlist for new releases every 12 hours.',
|
||||
category: 'Monitor', when: { type: 'schedule', config: { interval: 12, unit: 'hours' } }, do: { type: 'scan_watchlist', config: {} }, then: [] },
|
||||
{ icon: '\uD83C\uDF19', name: 'Nightly Wishlist Processor', desc: 'Process your wishlist at 3 AM every night while you sleep.',
|
||||
category: 'Sync', when: { type: 'daily_time', config: { time: '03:00' } }, do: { type: 'process_wishlist', config: {} }, then: [] },
|
||||
{ icon: '\uD83D\uDD0D', name: 'Discovery Pipeline', desc: 'Auto-discover tracks when a new playlist is mirrored.',
|
||||
category: 'Sync', when: { type: 'mirrored_playlist_created', config: {} }, do: { type: 'discover_playlist', config: {} }, then: [] },
|
||||
{ icon: '\uD83D\uDCBE', name: 'Weekly Library Backup', desc: 'Back up your database every Sunday at 4 AM.',
|
||||
category: 'Maintenance', when: { type: 'weekly_time', config: { days: ['sunday'], time: '04:00' } }, do: { type: 'backup_database', config: {} }, then: [] },
|
||||
{ icon: '\uD83E\uDDF9', name: 'Post-Batch Cleanup', desc: 'Run a full cleanup after any batch download completes.',
|
||||
category: 'Maintenance', when: { type: 'batch_complete', config: {} }, do: { type: 'full_cleanup', config: {} }, then: [] },
|
||||
{ icon: '\u274C', name: 'Download Failure Alert', desc: 'Get notified via Discord when a download fails.',
|
||||
category: 'Monitor', when: { type: 'download_failed', config: {} }, do: { type: 'notify_only', config: {} }, then: [{ type: 'discord_webhook', config: {} }] },
|
||||
{ icon: '\uD83E\uDDF9', name: 'Full Library Maintenance', desc: 'Run full cleanup every Saturday at 5 AM — dedup, quarantine, wishlist tidy.',
|
||||
category: 'Maintenance', when: { type: 'weekly_time', config: { days: ['saturday'], time: '05:00' } }, do: { type: 'full_cleanup', config: {} }, then: [] },
|
||||
// --- Automation Hub Data ---
|
||||
|
||||
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.',
|
||||
category: 'Sync', difficulty: 'beginner', when: { type: 'schedule', config: { interval: 6, unit: 'hours' } }, do: { type: 'refresh_mirrored', config: {} }, then: [] },
|
||||
{ id: 'release-radar-pipeline', icon: '\uD83D\uDCE1', name: 'Release Radar Pipeline', desc: 'Every Friday, refresh mirrored playlists, discover new tracks, then sync. Chain 3 automations for a full pipeline.',
|
||||
category: 'Sync', difficulty: 'intermediate', when: { type: 'weekly_time', config: { days: ['friday'], time: '18:00' } }, do: { type: 'refresh_mirrored', config: {} }, then: [],
|
||||
chain: ['Refresh Mirrored', 'Discover Playlist', 'Sync Playlist'], note: 'Create 3 separate automations and chain them with signals for the full pipeline.' },
|
||||
{ id: 'discover-weekly-grab', icon: '\uD83C\uDFB5', name: 'Discover Weekly Grab', desc: 'Every Monday, refresh your mirrored Discover Weekly to capture the new playlist before Spotify replaces it.',
|
||||
category: 'Sync', difficulty: 'beginner', when: { type: 'weekly_time', config: { days: ['monday'], time: '08:00' } }, do: { type: 'refresh_mirrored', config: {} }, then: [] },
|
||||
{ id: 'playlist-change-watcher', icon: '\uD83D\uDD14', name: 'Playlist Change Watcher', desc: 'Get a Discord notification whenever any tracked playlist changes.',
|
||||
category: 'Sync', difficulty: 'beginner', when: { type: 'playlist_changed', config: {} }, do: { type: 'notify_only', config: {} }, then: [{ type: 'discord_webhook', config: {} }] },
|
||||
{ id: 'new-mirror-discovery', icon: '\uD83D\uDD0D', name: 'New Mirror Auto-Discovery', desc: 'Automatically discover tracks when you mirror a new playlist.',
|
||||
category: 'Sync', difficulty: 'beginner', when: { type: 'mirrored_playlist_created', config: {} }, do: { type: 'discover_playlist', config: {} }, then: [] },
|
||||
// New Music Discovery
|
||||
{ id: 'complete-new-release', icon: '\uD83D\uDE80', name: 'Complete New Release Pipeline', desc: 'Full hands-free chain: scan watchlist \u2192 process wishlist \u2192 quality scan \u2192 notify. Requires 3 automations linked by signals.',
|
||||
category: 'Discovery', difficulty: 'advanced', when: { type: 'schedule', config: { interval: 12, unit: 'hours' } }, do: { type: 'scan_watchlist', config: {} }, then: [{ type: 'fire_signal', config: { signal_name: 'watchlist_done' } }],
|
||||
chain: ['Scan Watchlist', '\u26A1 watchlist_done', 'Process Wishlist', '\u26A1 wishlist_done', 'Quality Scan', 'Discord'],
|
||||
note: 'Create 3 automations: (1) Schedule\u2192Scan Watchlist\u2192fire watchlist_done, (2) Signal watchlist_done\u2192Process Wishlist\u2192fire wishlist_done, (3) Signal wishlist_done\u2192Quality Scan\u2192Discord.' },
|
||||
{ id: 'new-release-monitor', icon: '\uD83D\uDD14', name: 'New Release Monitor', desc: 'Scan your watchlist for new releases every 12 hours.',
|
||||
category: 'Discovery', difficulty: 'beginner', when: { type: 'schedule', config: { interval: 12, unit: 'hours' } }, do: { type: 'scan_watchlist', config: {} }, then: [] },
|
||||
{ id: 'artist-watch-alert', icon: '\uD83C\uDFA4', name: 'Artist Watch Alert', desc: 'Get a Telegram notification when you add a new artist to your watchlist.',
|
||||
category: 'Discovery', difficulty: 'beginner', when: { type: 'watchlist_artist_added', config: {} }, do: { type: 'notify_only', config: {} }, then: [{ type: 'telegram', config: {} }] },
|
||||
{ id: 'discovery-pool-refresh', icon: '\uD83C\uDF10', name: 'Discovery Pool Refresh', desc: 'Refresh the discovery pool every night at 2 AM with fresh recommendations.',
|
||||
category: 'Discovery', difficulty: 'beginner', when: { type: 'daily_time', config: { time: '02:00' } }, do: { type: 'update_discovery_pool', config: {} }, then: [] },
|
||||
{ id: 'nightly-wishlist', icon: '\uD83C\uDF19', name: 'Nightly Wishlist Processor', desc: 'Process your wishlist at 3 AM every night while you sleep.',
|
||||
category: 'Discovery', difficulty: 'beginner', when: { type: 'daily_time', config: { time: '03:00' } }, do: { type: 'process_wishlist', config: {} }, then: [] },
|
||||
// Library Maintenance
|
||||
{ id: 'full-library-maintenance', icon: '\uD83E\uDDF9', name: 'Full Library Maintenance', desc: 'Run full cleanup every Saturday at 5 AM \u2014 dedup, quarantine, wishlist tidy.',
|
||||
category: 'Maintenance', difficulty: 'intermediate', when: { type: 'weekly_time', config: { days: ['saturday'], time: '05:00' } }, do: { type: 'full_cleanup', config: {} }, then: [] },
|
||||
{ id: 'post-batch-cleanup', icon: '\uD83E\uDDF9', name: 'Post-Batch Cleanup', desc: 'Run a full cleanup after any batch download completes.',
|
||||
category: 'Maintenance', difficulty: 'beginner', when: { type: 'batch_complete', config: {} }, do: { type: 'full_cleanup', config: {} }, then: [] },
|
||||
{ id: 'weekly-db-backup', icon: '\uD83D\uDCBE', name: 'Weekly Database Backup', desc: 'Back up your database every Sunday at 4 AM.',
|
||||
category: 'Maintenance', difficulty: 'beginner', when: { type: 'weekly_time', config: { days: ['sunday'], time: '04:00' } }, do: { type: 'backup_database', config: {} }, then: [] },
|
||||
{ id: 'quality-assurance', icon: '\u2705', name: 'Quality Assurance Pipeline', desc: 'After a library scan completes, run a quality scan and fire a signal when done.',
|
||||
category: 'Maintenance', difficulty: 'intermediate', when: { type: 'library_scan_completed', config: {} }, do: { type: 'start_quality_scan', config: {} }, then: [{ type: 'fire_signal', config: { signal_name: 'quality_done' } }] },
|
||||
{ id: 'import-cleanup', icon: '\uD83D\uDCE5', name: 'Import Cleanup', desc: 'Automatically scan the library after an import completes to keep things tidy.',
|
||||
category: 'Maintenance', difficulty: 'intermediate', when: { type: 'import_completed', config: {} }, do: { type: 'scan_library', config: {} }, then: [] },
|
||||
// Notifications & Alerts
|
||||
{ id: 'download-failure-alert', icon: '\u274C', name: 'Download Failure Alert', desc: 'Get notified via Discord when a download fails.',
|
||||
category: 'Alerts', difficulty: 'beginner', when: { type: 'download_failed', config: {} }, do: { type: 'notify_only', config: {} }, then: [{ type: 'discord_webhook', config: {} }] },
|
||||
{ id: 'quarantine-alert', icon: '\u26A0\uFE0F', name: 'Quarantine Alert', desc: 'Get a Pushbullet alert when a file is quarantined.',
|
||||
category: 'Alerts', difficulty: 'beginner', when: { type: 'download_quarantined', config: {} }, do: { type: 'notify_only', config: {} }, then: [{ type: 'pushbullet', config: {} }] },
|
||||
{ id: 'batch-complete-notify', icon: '\uD83C\uDFC1', name: 'Batch Complete Notification', desc: 'Get a Telegram message when a batch download finishes.',
|
||||
category: 'Alerts', difficulty: 'beginner', when: { type: 'batch_complete', config: {} }, do: { type: 'notify_only', config: {} }, then: [{ type: 'telegram', config: {} }] },
|
||||
// Power User Chains
|
||||
{ id: 'full-hands-free', icon: '\uD83E\uDD16', name: 'Full Hands-Free Pipeline', desc: 'The ultimate automation chain: scan \u2192 process \u2192 download \u2192 clean \u2192 notify. Requires 5 automations linked by signals.',
|
||||
category: 'Chains', difficulty: 'advanced', when: { type: 'schedule', config: { interval: 12, unit: 'hours' } }, do: { type: 'scan_watchlist', config: {} }, then: [{ type: 'fire_signal', config: { signal_name: 'scan_done' } }],
|
||||
chain: ['Scan Watchlist', '\u26A1 scan_done', 'Process Wishlist', '\u26A1 process_done', 'Full Cleanup', '\u26A1 cleanup_done', 'Quality Scan', 'Discord'],
|
||||
note: 'Build 4-5 automations, each firing a signal for the next step. Start small and add stages.' },
|
||||
{ id: 'staggered-nightly', icon: '\uD83C\uDF03', name: 'Staggered Nightly Pipeline', desc: 'Spread tasks across the night: 1 AM scan, 2 AM process, 3 AM cleanup, 4 AM backup.',
|
||||
category: 'Chains', difficulty: 'intermediate', when: { type: 'daily_time', config: { time: '01:00' } }, do: { type: 'scan_watchlist', config: {} }, then: [],
|
||||
chain: ['1:00 Scan', '2:00 Process', '3:00 Cleanup', '4:00 Backup'],
|
||||
note: 'Create 4 daily_time automations at staggered hours. No signals needed \u2014 just timing.' },
|
||||
];
|
||||
|
||||
const AUTO_HUB_GUIDES = [
|
||||
{ id: 'auto-sync-playlists', icon: '\uD83D\uDD01', title: 'Auto-Sync Your Spotify Playlists', subtitle: 'Mirror a Spotify playlist and schedule automatic refreshes.', difficulty: 'beginner',
|
||||
steps: [
|
||||
'Go to the <strong>Playlists</strong> page and find a Spotify playlist you want to track.',
|
||||
'Click <strong>Mirror Playlist</strong> to create a local copy.',
|
||||
'Go to <strong>Automations</strong> and click <strong>New Automation</strong>.',
|
||||
'Set WHEN to <strong>Schedule \u2192 Every 6 hours</strong>.',
|
||||
'Set DO to <strong>Refresh Mirrored Playlists</strong>.',
|
||||
'Save and enable \u2014 your playlist will now stay in sync automatically.'
|
||||
], relatedRecipes: ['spotify-auto-sync', 'discover-weekly-grab'] },
|
||||
{ id: 'discord-download-alerts', icon: '\uD83D\uDCE2', title: 'Get Discord Alerts for Downloads', subtitle: 'Set up Discord webhook notifications for download events.', difficulty: 'beginner',
|
||||
steps: [
|
||||
'In Discord, go to your channel\'s settings \u2192 <strong>Integrations \u2192 Webhooks</strong>.',
|
||||
'Create a webhook and copy the URL.',
|
||||
'In SoulSync, go to <strong>Settings \u2192 Notifications</strong> and paste the Discord webhook URL.',
|
||||
'Go to <strong>Automations \u2192 New Automation</strong>.',
|
||||
'Set WHEN to <strong>Download Failed</strong> (or any event), DO to <strong>Notify Only</strong>, THEN to <strong>Discord</strong>.'
|
||||
], relatedRecipes: ['download-failure-alert', 'batch-complete-notify'] },
|
||||
{ id: 'hands-free-pipeline', icon: '\uD83E\uDD16', title: 'Build a Hands-Free Library Pipeline', subtitle: 'Chain watchlist scanning, wishlist processing, and cleanup with signals.', difficulty: 'intermediate',
|
||||
steps: [
|
||||
'Create Automation 1: <strong>Schedule (12h) \u2192 Scan Watchlist</strong>, THEN fire signal <code>scan_done</code>.',
|
||||
'Create Automation 2: <strong>Signal scan_done \u2192 Process Wishlist</strong>, THEN fire signal <code>process_done</code>.',
|
||||
'Create Automation 3: <strong>Signal process_done \u2192 Full Cleanup</strong>.',
|
||||
'Enable all three automations.',
|
||||
'Test by manually running Automation 1 \u2014 watch the chain execute.',
|
||||
'Add a THEN notification (Discord/Telegram) to the last automation for completion alerts.',
|
||||
'Adjust the schedule interval based on how often you want new music checked.'
|
||||
], relatedRecipes: ['complete-new-release', 'full-hands-free'] },
|
||||
{ id: 'signal-chains', icon: '\u26A1', title: 'Set Up Signal Chains', subtitle: 'Use fire_signal and signal_received to link automations together.', difficulty: 'advanced',
|
||||
steps: [
|
||||
'Understand the concept: <strong>fire_signal</strong> is a THEN action that emits a named signal. <strong>signal_received</strong> is a WHEN trigger that listens for it.',
|
||||
'In your first automation, add a THEN action \u2192 <strong>Fire Signal</strong> and name it (e.g., <code>step1_done</code>).',
|
||||
'Create a second automation with WHEN \u2192 <strong>Signal Received</strong> \u2192 signal name <code>step1_done</code>.',
|
||||
'The second automation will fire automatically when the first one completes.',
|
||||
'Chain up to 5 levels deep (safety limit). SoulSync detects cycles automatically.',
|
||||
'Use descriptive signal names like <code>watchlist_scanned</code> or <code>cleanup_finished</code>.'
|
||||
], relatedRecipes: ['quality-assurance', 'complete-new-release'] },
|
||||
{ id: 'nightly-maintenance', icon: '\uD83C\uDF19', title: 'Schedule Nightly Maintenance', subtitle: 'Set up backup, cleanup, and quality scans to run overnight.', difficulty: 'intermediate',
|
||||
steps: [
|
||||
'Create a <strong>Daily Time (04:00) \u2192 Backup Database</strong> automation.',
|
||||
'Create a <strong>Weekly Time (Saturday, 05:00) \u2192 Full Cleanup</strong> automation.',
|
||||
'Create a <strong>Daily Time (02:00) \u2192 Update Discovery Pool</strong> automation.',
|
||||
'Stagger times by at least 1 hour to avoid resource contention.',
|
||||
'Add Discord/Telegram notifications to any you want alerts for.'
|
||||
], relatedRecipes: ['weekly-db-backup', 'full-library-maintenance', 'staggered-nightly'] },
|
||||
];
|
||||
|
||||
const AUTO_HUB_TIPS = [
|
||||
{ icon: '\u26A1', title: 'Signal Chaining 101', body: '<strong>fire_signal</strong> (a THEN action) emits a named event. <strong>signal_received</strong> (a WHEN trigger) listens for it. This lets you chain automations: when one finishes, the next starts automatically.', tag: 'Signals' },
|
||||
{ icon: '\u23F0', title: 'Stagger Your Schedules', body: 'If you have multiple timed automations, space them at least 1 hour apart. Running scan, process, and cleanup at the same time creates resource contention and can slow everything down.', tag: 'Performance' },
|
||||
{ icon: '\uD83C\uDFAF', title: 'Use Conditions to Filter', body: 'Add conditions to event triggers to only fire on specific artists, formats, or quality levels. For example, trigger only when a downloaded track\'s artist matches "Radiohead".', tag: 'Filtering' },
|
||||
{ icon: '\uD83D\uDCC1', title: 'Group Related Automations', body: 'Use the Group dropdown when creating automations to organize them. Groups like "Nightly", "Notifications", or "Pipeline" make it easy to find and manage related automations.', tag: 'Organization' },
|
||||
{ icon: '\uD83D\uDD04', title: 'Avoid Chain Loops', body: 'SoulSync has built-in cycle detection, but it\'s good practice to design signal names carefully. If A fires signal X and B listens for X and fires Y, make sure nothing fires X again downstream.', tag: 'Safety' },
|
||||
{ icon: '\uD83D\uDCDA', title: 'Stack THEN Actions', body: 'Each automation supports up to 3 THEN actions. Combine notification channels (Discord + Telegram) with a fire_signal to both notify yourself and trigger the next automation.', tag: 'Power' },
|
||||
{ icon: '\u2699\uFE0F', title: 'System vs Custom', body: 'System automations handle core tasks like Spotify enrichment and are managed automatically. Create custom automations to extend their behavior \u2014 trigger on their completion events.', tag: 'Basics' },
|
||||
{ icon: '\uD83E\uDDEA', title: 'Test with Notify Only', body: 'Set DO to <strong>Notify Only</strong> when testing a new trigger. You\'ll see when it fires without any side effects. Once you\'re confident in the timing, switch to the real action.', tag: 'Testing' },
|
||||
];
|
||||
|
||||
const AUTO_HUB_REFERENCE = {
|
||||
triggers: [
|
||||
{ group: 'Time-Based', items: [
|
||||
{ type: 'schedule', label: 'Schedule', desc: 'Repeating interval (e.g., every 6 hours)' },
|
||||
{ type: 'daily_time', label: 'Daily Time', desc: 'Every day at a specific time (e.g., 03:00)' },
|
||||
{ type: 'weekly_time', label: 'Weekly Time', desc: 'Specific days + time (e.g., Saturday at 05:00)' },
|
||||
]},
|
||||
{ group: 'Download Events', items: [
|
||||
{ type: 'track_downloaded', label: 'Track Downloaded', desc: 'Fires when a single track download completes' },
|
||||
{ type: 'batch_complete', label: 'Batch Complete', desc: 'Fires when a batch download job finishes' },
|
||||
{ type: 'download_failed', label: 'Download Failed', desc: 'Fires when a download fails or errors out' },
|
||||
{ type: 'download_quarantined', label: 'File Quarantined', desc: 'Fires when a downloaded file is quarantined for quality issues' },
|
||||
]},
|
||||
{ group: 'Watchlist & Wishlist', items: [
|
||||
{ type: 'watchlist_new_release', label: 'New Release Found', desc: 'Fires when a watched artist has a new release' },
|
||||
{ type: 'watchlist_scan_completed', label: 'Watchlist Scan Done', desc: 'Fires after a full watchlist scan completes' },
|
||||
{ type: 'watchlist_artist_added', label: 'Artist Watched', desc: 'Fires when a new artist is added to the watchlist' },
|
||||
{ type: 'watchlist_artist_removed', label: 'Artist Unwatched', desc: 'Fires when an artist is removed from the watchlist' },
|
||||
{ type: 'wishlist_item_added', label: 'Wishlist Item Added', desc: 'Fires when a new item is added to the wishlist' },
|
||||
{ type: 'wishlist_processing_completed', label: 'Wishlist Processed', desc: 'Fires after the wishlist processor completes a run' },
|
||||
]},
|
||||
{ group: 'Playlists', items: [
|
||||
{ type: 'playlist_synced', label: 'Playlist Synced', desc: 'Fires when a playlist sync operation completes' },
|
||||
{ type: 'playlist_changed', label: 'Playlist Changed', desc: 'Fires when a tracked playlist has changes detected' },
|
||||
{ type: 'mirrored_playlist_created', label: 'Playlist Mirrored', desc: 'Fires when a new mirrored playlist is created' },
|
||||
{ type: 'discovery_completed', label: 'Discovery Complete', desc: 'Fires when playlist discovery finishes' },
|
||||
]},
|
||||
{ group: 'Library & System', items: [
|
||||
{ type: 'app_started', label: 'App Started', desc: 'Fires once when SoulSync starts up' },
|
||||
{ type: 'import_completed', label: 'Import Complete', desc: 'Fires when a library import operation finishes' },
|
||||
{ type: 'library_scan_completed', label: 'Library Scan Done', desc: 'Fires after a full library scan completes' },
|
||||
{ type: 'quality_scan_completed', label: 'Quality Scan Done', desc: 'Fires when a quality scan finishes' },
|
||||
{ type: 'duplicate_scan_completed', label: 'Duplicate Scan Done', desc: 'Fires when the duplicate scanner finishes' },
|
||||
{ type: 'database_update_completed', label: 'Database Updated', desc: 'Fires after a database update operation' },
|
||||
]},
|
||||
{ group: 'Signals', items: [
|
||||
{ type: 'signal_received', label: 'Signal Received', desc: 'Fires when a named signal is emitted by another automation\'s fire_signal THEN action' },
|
||||
]},
|
||||
],
|
||||
actions: [
|
||||
{ group: 'Downloads & Sync', items: [
|
||||
{ type: 'process_wishlist', label: 'Process Wishlist', desc: 'Download all pending wishlist items' },
|
||||
{ type: 'refresh_mirrored', label: 'Refresh Mirrored', desc: 'Refresh all mirrored playlists from their sources' },
|
||||
{ type: 'sync_playlist', label: 'Sync Playlist', desc: 'Sync a specific playlist to your library' },
|
||||
{ type: 'discover_playlist', label: 'Discover Playlist', desc: 'Run track discovery on mirrored playlists' },
|
||||
{ type: 'scan_watchlist', label: 'Scan Watchlist', desc: 'Check watched artists for new releases' },
|
||||
{ type: 'update_discovery_pool', label: 'Update Discovery', desc: 'Refresh the discovery pool with new recommendations' },
|
||||
]},
|
||||
{ group: 'Library Tools', items: [
|
||||
{ type: 'scan_library', label: 'Scan Library', desc: 'Full scan of local music library files' },
|
||||
{ type: 'start_quality_scan', label: 'Quality Scan', desc: 'Check library tracks for quality issues' },
|
||||
{ type: 'start_database_update', label: 'Update Database', desc: 'Run a database update/maintenance operation' },
|
||||
{ type: 'backup_database', label: 'Backup Database', desc: 'Create a backup of the music database' },
|
||||
]},
|
||||
{ group: 'Cleanup', items: [
|
||||
{ type: 'full_cleanup', label: 'Full Cleanup', desc: 'Run all cleanup tasks: dedup, quarantine, wishlist tidy' },
|
||||
{ type: 'run_duplicate_cleaner', label: 'Duplicate Cleaner', desc: 'Find and handle duplicate tracks' },
|
||||
{ type: 'clear_quarantine', label: 'Clear Quarantine', desc: 'Remove all quarantined files' },
|
||||
{ type: 'cleanup_wishlist', label: 'Clean Wishlist', desc: 'Remove completed/invalid wishlist items' },
|
||||
{ type: 'clean_search_history', label: 'Clean Search History', desc: 'Clear old search history entries' },
|
||||
{ type: 'clean_completed_downloads', label: 'Clean Downloads', desc: 'Remove completed download records' },
|
||||
]},
|
||||
{ group: 'Other', items: [
|
||||
{ type: 'notify_only', label: 'Notify Only', desc: 'No action \u2014 just trigger THEN notifications. Great for testing.' },
|
||||
]},
|
||||
],
|
||||
thenActions: [
|
||||
{ group: 'Notifications', items: [
|
||||
{ type: 'discord_webhook', label: 'Discord Webhook', desc: 'Send a message to a Discord channel via webhook' },
|
||||
{ type: 'telegram', label: 'Telegram', desc: 'Send a message to a Telegram chat via bot' },
|
||||
{ type: 'pushbullet', label: 'Pushbullet', desc: 'Send a push notification via Pushbullet' },
|
||||
]},
|
||||
{ group: 'Chaining', items: [
|
||||
{ type: 'fire_signal', label: 'Fire Signal', desc: 'Emit a named signal that other automations can listen for with signal_received' },
|
||||
]},
|
||||
],
|
||||
};
|
||||
|
||||
// --- Load & Render List ---
|
||||
|
||||
function _buildAutomationSection(id, label, automations, useGrid) {
|
||||
|
|
@ -55839,8 +56013,8 @@ async function loadAutomations() {
|
|||
list.appendChild(_buildAutomationSection('auto-section-system', 'System', systemAutos, true));
|
||||
}
|
||||
|
||||
// Inspiration / Templates section
|
||||
list.appendChild(_buildTemplatesSection());
|
||||
// Automation Hub section
|
||||
list.appendChild(_buildAutomationHub());
|
||||
|
||||
// User automations — split by group
|
||||
const groups = [...new Set(userAutos.filter(a => a.group_name).map(a => a.group_name))].sort();
|
||||
|
|
@ -55882,59 +56056,297 @@ async function loadAutomations() {
|
|||
}
|
||||
}
|
||||
|
||||
// --- Templates Section ---
|
||||
function _buildTemplatesSection() {
|
||||
// --- Automation Hub ---
|
||||
|
||||
function _buildAutomationHub() {
|
||||
const section = document.createElement('div');
|
||||
section.className = 'automations-section';
|
||||
section.id = 'auto-section-templates';
|
||||
const collapsed = localStorage.getItem('auto_section_auto-section-templates') === '1';
|
||||
section.id = 'auto-section-hub';
|
||||
const collapsed = localStorage.getItem('auto_section_auto-section-hub') === '1';
|
||||
if (collapsed) section.classList.add('collapsed');
|
||||
const header = document.createElement('div');
|
||||
header.className = 'automations-section-header';
|
||||
header.innerHTML = `
|
||||
<span class="section-chevron">▼</span>
|
||||
<span class="section-label">Inspiration</span>
|
||||
<span class="section-count">${AUTO_TEMPLATES.length}</span>
|
||||
<span class="section-label">Automation Hub</span>
|
||||
<span class="section-count">${AUTO_HUB_RECIPES.length} recipes</span>
|
||||
<span class="section-line"></span>
|
||||
`;
|
||||
header.onclick = () => {
|
||||
section.classList.toggle('collapsed');
|
||||
localStorage.setItem('auto_section_auto-section-templates', section.classList.contains('collapsed') ? '1' : '0');
|
||||
localStorage.setItem('auto_section_auto-section-hub', section.classList.contains('collapsed') ? '1' : '0');
|
||||
};
|
||||
const body = document.createElement('div');
|
||||
body.className = 'automations-section-body';
|
||||
const grid = document.createElement('div');
|
||||
grid.className = 'automations-grid';
|
||||
AUTO_TEMPLATES.forEach((t, i) => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'auto-template-card';
|
||||
const trigLabel = _autoFormatTrigger(t.when.type, t.when.config);
|
||||
const actLabel = _autoFormatAction(t.do.type);
|
||||
card.innerHTML = `
|
||||
<div class="auto-template-icon">${t.icon}</div>
|
||||
<div class="auto-template-info">
|
||||
<div class="auto-template-name">${_esc(t.name)}</div>
|
||||
<div class="auto-template-desc">${_esc(t.desc)}</div>
|
||||
<div class="auto-template-flow">
|
||||
<span class="flow-trigger">${_esc(trigLabel)}</span>
|
||||
<span class="flow-arrow">→</span>
|
||||
<span class="flow-action">${_esc(actLabel)}</span>
|
||||
${t.then.length ? t.then.map(th => `<span class="flow-arrow">→</span><span class="flow-notify">${_esc(_autoFormatNotify(th.type))}</span>`).join('') : ''}
|
||||
</div>
|
||||
</div>
|
||||
<button class="auto-template-use" onclick="event.stopPropagation(); useTemplate(${i})">Use</button>
|
||||
`;
|
||||
card.onclick = () => useTemplate(i);
|
||||
grid.appendChild(card);
|
||||
|
||||
const activeTab = localStorage.getItem('auto_hub_tab') || 'recipes';
|
||||
const tabs = [
|
||||
{ id: 'recipes', label: 'Recipes' },
|
||||
{ id: 'guides', label: 'Quick Start' },
|
||||
{ id: 'tips', label: 'Tips' },
|
||||
{ id: 'reference', label: 'Reference' },
|
||||
];
|
||||
|
||||
const tabBar = document.createElement('div');
|
||||
tabBar.className = 'auto-hub-tabs';
|
||||
tabs.forEach(t => {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'auto-hub-tab' + (t.id === activeTab ? ' active' : '');
|
||||
btn.textContent = t.label;
|
||||
btn.dataset.tab = t.id;
|
||||
btn.onclick = (e) => { e.stopPropagation(); _switchHubTab(t.id, body); };
|
||||
tabBar.appendChild(btn);
|
||||
});
|
||||
body.appendChild(grid);
|
||||
body.appendChild(tabBar);
|
||||
|
||||
// Build all tab contents
|
||||
const recipesPane = _buildHubRecipes();
|
||||
recipesPane.id = 'auto-hub-pane-recipes';
|
||||
recipesPane.className = 'auto-hub-tab-content' + (activeTab === 'recipes' ? ' active' : '');
|
||||
body.appendChild(recipesPane);
|
||||
|
||||
const guidesPane = _buildHubGuides();
|
||||
guidesPane.id = 'auto-hub-pane-guides';
|
||||
guidesPane.className = 'auto-hub-tab-content' + (activeTab === 'guides' ? ' active' : '');
|
||||
body.appendChild(guidesPane);
|
||||
|
||||
const tipsPane = _buildHubTips();
|
||||
tipsPane.id = 'auto-hub-pane-tips';
|
||||
tipsPane.className = 'auto-hub-tab-content' + (activeTab === 'tips' ? ' active' : '');
|
||||
body.appendChild(tipsPane);
|
||||
|
||||
const refPane = _buildHubReference();
|
||||
refPane.id = 'auto-hub-pane-reference';
|
||||
refPane.className = 'auto-hub-tab-content' + (activeTab === 'reference' ? ' active' : '');
|
||||
body.appendChild(refPane);
|
||||
|
||||
section.appendChild(header);
|
||||
section.appendChild(body);
|
||||
return section;
|
||||
}
|
||||
|
||||
async function useTemplate(index) {
|
||||
const t = AUTO_TEMPLATES[index];
|
||||
function _switchHubTab(tabId, bodyEl) {
|
||||
const container = bodyEl || document.querySelector('#auto-section-hub .automations-section-body');
|
||||
if (!container) return;
|
||||
container.querySelectorAll('.auto-hub-tab').forEach(t => t.classList.toggle('active', t.dataset.tab === tabId));
|
||||
container.querySelectorAll('.auto-hub-tab-content').forEach(p => p.classList.toggle('active', p.id === 'auto-hub-pane-' + tabId));
|
||||
localStorage.setItem('auto_hub_tab', tabId);
|
||||
}
|
||||
|
||||
function _buildHubRecipes() {
|
||||
const pane = document.createElement('div');
|
||||
const categories = ['All', 'Sync', 'Discovery', 'Maintenance', 'Alerts', 'Chains'];
|
||||
const difficulties = ['All', 'Beginner', 'Intermediate', 'Advanced'];
|
||||
|
||||
let activeCat = 'All', activeDiff = 'All';
|
||||
|
||||
// Category filters
|
||||
const catFilters = document.createElement('div');
|
||||
catFilters.className = 'auto-hub-filters';
|
||||
categories.forEach(c => {
|
||||
const pill = document.createElement('button');
|
||||
pill.className = 'auto-hub-filter-pill' + (c === 'All' ? ' active' : '');
|
||||
pill.textContent = c;
|
||||
pill.dataset.filter = c;
|
||||
pill.dataset.filterType = 'category';
|
||||
pill.onclick = () => {
|
||||
activeCat = c;
|
||||
catFilters.querySelectorAll('.auto-hub-filter-pill').forEach(p => p.classList.toggle('active', p.dataset.filter === c));
|
||||
filterRecipes();
|
||||
};
|
||||
catFilters.appendChild(pill);
|
||||
});
|
||||
pane.appendChild(catFilters);
|
||||
|
||||
// Difficulty filters
|
||||
const diffFilters = document.createElement('div');
|
||||
diffFilters.className = 'auto-hub-filters';
|
||||
difficulties.forEach(d => {
|
||||
const pill = document.createElement('button');
|
||||
pill.className = 'auto-hub-filter-pill' + (d === 'All' ? ' active' : '');
|
||||
pill.textContent = d;
|
||||
pill.dataset.filter = d;
|
||||
pill.dataset.filterType = 'difficulty';
|
||||
pill.onclick = () => {
|
||||
activeDiff = d;
|
||||
diffFilters.querySelectorAll('.auto-hub-filter-pill').forEach(p => p.classList.toggle('active', p.dataset.filter === d));
|
||||
filterRecipes();
|
||||
};
|
||||
diffFilters.appendChild(pill);
|
||||
});
|
||||
pane.appendChild(diffFilters);
|
||||
|
||||
const grid = document.createElement('div');
|
||||
grid.className = 'auto-hub-recipes-grid';
|
||||
|
||||
AUTO_HUB_RECIPES.forEach(r => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'auto-hub-recipe-card';
|
||||
card.dataset.category = r.category;
|
||||
card.dataset.difficulty = r.difficulty;
|
||||
|
||||
const trigLabel = _autoFormatTrigger(r.when.type, r.when.config);
|
||||
const actLabel = _autoFormatAction(r.do.type);
|
||||
|
||||
let chainHTML = '';
|
||||
if (r.chain) {
|
||||
chainHTML = '<div class="auto-hub-recipe-chain">' + r.chain.map((step, i) => {
|
||||
let cls = 'flow-action';
|
||||
if (i === 0) cls = 'flow-trigger';
|
||||
else if (step.startsWith('\u26A1')) cls = 'flow-notify';
|
||||
return (i > 0 ? '<span class="flow-arrow">→</span>' : '') +
|
||||
`<span class="${cls}">${_esc(step)}</span>`;
|
||||
}).join('') + '</div>';
|
||||
} else {
|
||||
chainHTML = `<div class="auto-hub-recipe-chain">
|
||||
<span class="flow-trigger">${_esc(trigLabel)}</span>
|
||||
<span class="flow-arrow">→</span>
|
||||
<span class="flow-action">${_esc(actLabel)}</span>
|
||||
${r.then.length ? r.then.map(th => `<span class="flow-arrow">→</span><span class="flow-notify">${_esc(_autoFormatNotify(th.type))}</span>`).join('') : ''}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="auto-hub-recipe-header">
|
||||
<div class="auto-hub-recipe-icon">${r.icon}</div>
|
||||
<div class="auto-hub-recipe-name">${_esc(r.name)}</div>
|
||||
<span class="auto-hub-badge ${r.difficulty}">${_esc(r.difficulty)}</span>
|
||||
</div>
|
||||
<div class="auto-hub-recipe-desc">${_esc(r.desc)}</div>
|
||||
${chainHTML}
|
||||
${r.note ? `<div class="auto-hub-recipe-note">${_esc(r.note)}</div>` : ''}
|
||||
<button class="auto-hub-recipe-use" onclick="event.stopPropagation(); useHubRecipe('${r.id}')">Use This</button>
|
||||
`;
|
||||
card.onclick = () => useHubRecipe(r.id);
|
||||
grid.appendChild(card);
|
||||
});
|
||||
pane.appendChild(grid);
|
||||
|
||||
function filterRecipes() {
|
||||
grid.querySelectorAll('.auto-hub-recipe-card').forEach(card => {
|
||||
const catMatch = activeCat === 'All' || card.dataset.category === activeCat;
|
||||
const diffMatch = activeDiff === 'All' || card.dataset.difficulty === activeDiff.toLowerCase();
|
||||
card.style.display = (catMatch && diffMatch) ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
return pane;
|
||||
}
|
||||
|
||||
function _buildHubGuides() {
|
||||
const pane = document.createElement('div');
|
||||
|
||||
const callout = document.createElement('div');
|
||||
callout.className = 'auto-hub-callout';
|
||||
callout.innerHTML = '<span class="auto-hub-callout-icon">\uD83D\uDCA1</span><span>Click any guide to expand step-by-step instructions. Related recipes let you jump straight to a pre-filled template.</span>';
|
||||
pane.appendChild(callout);
|
||||
|
||||
AUTO_HUB_GUIDES.forEach(g => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'auto-hub-guide-card';
|
||||
|
||||
const headerEl = document.createElement('div');
|
||||
headerEl.className = 'auto-hub-guide-header';
|
||||
headerEl.innerHTML = `
|
||||
<span class="auto-hub-guide-icon">${g.icon}</span>
|
||||
<span class="auto-hub-guide-title">${_esc(g.title)}</span>
|
||||
<span class="auto-hub-badge ${g.difficulty}">${_esc(g.difficulty)}</span>
|
||||
<span class="auto-hub-guide-chevron">▼</span>
|
||||
`;
|
||||
headerEl.onclick = () => card.classList.toggle('expanded');
|
||||
card.appendChild(headerEl);
|
||||
|
||||
const bodyEl = document.createElement('div');
|
||||
bodyEl.className = 'auto-hub-guide-body';
|
||||
bodyEl.innerHTML = `
|
||||
<div class="auto-hub-guide-subtitle">${_esc(g.subtitle)}</div>
|
||||
<ol class="auto-hub-steps">${g.steps.map(s => `<li>${s}</li>`).join('')}</ol>
|
||||
${g.relatedRecipes.length ? `
|
||||
<div class="auto-hub-guide-related">
|
||||
<span class="auto-hub-guide-related-label">Related:</span>
|
||||
${g.relatedRecipes.map(rId => {
|
||||
const recipe = AUTO_HUB_RECIPES.find(r => r.id === rId);
|
||||
return recipe ? `<button class="auto-hub-guide-related-link" onclick="event.stopPropagation(); useHubRecipe('${rId}')">${recipe.icon} ${_esc(recipe.name)}</button>` : '';
|
||||
}).join('')}
|
||||
</div>
|
||||
` : ''}
|
||||
`;
|
||||
card.appendChild(bodyEl);
|
||||
pane.appendChild(card);
|
||||
});
|
||||
|
||||
return pane;
|
||||
}
|
||||
|
||||
function _buildHubTips() {
|
||||
const pane = document.createElement('div');
|
||||
|
||||
const callout = document.createElement('div');
|
||||
callout.className = 'auto-hub-callout';
|
||||
callout.innerHTML = '<span class="auto-hub-callout-icon">\u2728</span><span>Power-user tips to get the most out of your automations.</span>';
|
||||
pane.appendChild(callout);
|
||||
|
||||
const grid = document.createElement('div');
|
||||
grid.className = 'auto-hub-tips-grid';
|
||||
AUTO_HUB_TIPS.forEach(t => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'auto-hub-tip-card';
|
||||
card.innerHTML = `
|
||||
<div class="auto-hub-tip-header">
|
||||
<span class="auto-hub-tip-icon">${t.icon}</span>
|
||||
<span class="auto-hub-tip-title">${_esc(t.title)}</span>
|
||||
<span class="auto-hub-tip-tag">${_esc(t.tag)}</span>
|
||||
</div>
|
||||
<div class="auto-hub-tip-body">${t.body}</div>
|
||||
`;
|
||||
grid.appendChild(card);
|
||||
});
|
||||
pane.appendChild(grid);
|
||||
return pane;
|
||||
}
|
||||
|
||||
function _buildHubReference() {
|
||||
const pane = document.createElement('div');
|
||||
const sections = [
|
||||
{ label: 'Triggers (WHEN)', data: AUTO_HUB_REFERENCE.triggers },
|
||||
{ label: 'Actions (DO)', data: AUTO_HUB_REFERENCE.actions },
|
||||
{ label: 'Then Actions (THEN)', data: AUTO_HUB_REFERENCE.thenActions },
|
||||
];
|
||||
|
||||
sections.forEach(sec => {
|
||||
const totalItems = sec.data.reduce((n, g) => n + g.items.length, 0);
|
||||
const group = document.createElement('div');
|
||||
group.className = 'auto-hub-ref-group';
|
||||
|
||||
const header = document.createElement('div');
|
||||
header.className = 'auto-hub-ref-group-header';
|
||||
header.innerHTML = `
|
||||
<span class="auto-hub-ref-group-label">${_esc(sec.label)}</span>
|
||||
<span class="auto-hub-ref-group-count">${totalItems}</span>
|
||||
<span class="auto-hub-ref-chevron">▼</span>
|
||||
`;
|
||||
header.onclick = () => group.classList.toggle('expanded');
|
||||
group.appendChild(header);
|
||||
|
||||
const body = document.createElement('div');
|
||||
body.className = 'auto-hub-ref-body';
|
||||
sec.data.forEach(sub => {
|
||||
body.innerHTML += `<div class="auto-hub-ref-subheader">${_esc(sub.group)}</div>`;
|
||||
let tableHTML = '<table class="auto-hub-table"><thead><tr><th>Type</th><th>Description</th></tr></thead><tbody>';
|
||||
sub.items.forEach(item => {
|
||||
tableHTML += `<tr><td>${_esc(item.label)}</td><td>${_esc(item.desc)}</td></tr>`;
|
||||
});
|
||||
tableHTML += '</tbody></table>';
|
||||
body.innerHTML += tableHTML;
|
||||
});
|
||||
group.appendChild(body);
|
||||
pane.appendChild(group);
|
||||
});
|
||||
return pane;
|
||||
}
|
||||
|
||||
async function useHubRecipe(recipeId) {
|
||||
const t = AUTO_HUB_RECIPES.find(r => r.id === recipeId);
|
||||
if (!t) return;
|
||||
await showAutomationBuilder();
|
||||
document.getElementById('builder-name').value = t.name;
|
||||
|
|
@ -55943,6 +56355,9 @@ async function useTemplate(index) {
|
|||
_autoBuilder.then = t.then.map(th => ({ type: th.type, config: JSON.parse(JSON.stringify(th.config)) }));
|
||||
_renderBuilderSidebar();
|
||||
_renderBuilderCanvas();
|
||||
if (t.note) {
|
||||
showToast(t.note, 'info');
|
||||
}
|
||||
}
|
||||
|
||||
// --- Filter Bar ---
|
||||
|
|
|
|||
|
|
@ -34306,7 +34306,7 @@ body.downloads-disabled [onclick*="DownloadMissing"]:not([onclick*="close"]) {
|
|||
.automations-section-body {
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease, opacity 0.25s ease;
|
||||
max-height: 2000px;
|
||||
max-height: 8000px;
|
||||
opacity: 1;
|
||||
}
|
||||
.automations-section.collapsed .automations-section-body {
|
||||
|
|
@ -34581,6 +34581,428 @@ body.downloads-disabled [onclick*="DownloadMissing"]:not([onclick*="close"]) {
|
|||
}
|
||||
.auto-template-use:hover { background: rgba(var(--accent-rgb),0.3); transform: translateY(-1px); }
|
||||
|
||||
/* --- Automation Hub --- */
|
||||
.auto-hub-tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.06);
|
||||
padding: 0 4px 0 0;
|
||||
margin-bottom: 16px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.auto-hub-tab {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: rgba(255,255,255,0.45);
|
||||
font-size: 12.5px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
.auto-hub-tab:hover {
|
||||
color: rgba(255,255,255,0.7);
|
||||
background: rgba(255,255,255,0.03);
|
||||
}
|
||||
.auto-hub-tab.active {
|
||||
color: rgb(var(--accent-light-rgb));
|
||||
border-bottom-color: rgb(var(--accent-light-rgb));
|
||||
}
|
||||
.auto-hub-tab-content { display: none; }
|
||||
.auto-hub-tab-content.active { display: block; }
|
||||
|
||||
/* Hub Filters */
|
||||
.auto-hub-filters {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.auto-hub-filter-pill {
|
||||
padding: 4px 12px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(255,255,255,0.08);
|
||||
background: rgba(255,255,255,0.03);
|
||||
color: rgba(255,255,255,0.45);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.auto-hub-filter-pill:hover {
|
||||
background: rgba(255,255,255,0.06);
|
||||
color: rgba(255,255,255,0.65);
|
||||
}
|
||||
.auto-hub-filter-pill.active {
|
||||
background: rgba(var(--accent-rgb),0.15);
|
||||
border-color: rgba(var(--accent-rgb),0.3);
|
||||
color: rgb(var(--accent-light-rgb));
|
||||
}
|
||||
|
||||
/* Recipe Cards Grid */
|
||||
.auto-hub-recipes-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
.auto-hub-recipe-card {
|
||||
background: rgba(255,255,255,0.03);
|
||||
border: 1px solid rgba(255,255,255,0.06);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
.auto-hub-recipe-card:hover {
|
||||
background: rgba(var(--accent-rgb),0.06);
|
||||
border-color: rgba(var(--accent-rgb),0.2);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.auto-hub-recipe-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.auto-hub-recipe-icon {
|
||||
font-size: 20px;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
background: rgba(255,255,255,0.04);
|
||||
border-radius: 9px;
|
||||
}
|
||||
.auto-hub-recipe-name {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
.auto-hub-badge {
|
||||
padding: 2px 9px;
|
||||
border-radius: 10px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
.auto-hub-badge.beginner {
|
||||
background: rgba(52, 211, 153, 0.12);
|
||||
color: #6ee7b7;
|
||||
}
|
||||
.auto-hub-badge.intermediate {
|
||||
background: rgba(251, 191, 36, 0.12);
|
||||
color: #fbbf24;
|
||||
}
|
||||
.auto-hub-badge.advanced {
|
||||
background: rgba(192, 90, 242, 0.12);
|
||||
color: #c084fc;
|
||||
}
|
||||
.auto-hub-recipe-desc {
|
||||
font-size: 11.5px;
|
||||
line-height: 1.45;
|
||||
color: rgba(255,255,255,0.42);
|
||||
}
|
||||
.auto-hub-recipe-chain {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
.auto-hub-recipe-note {
|
||||
font-size: 10.5px;
|
||||
color: rgba(var(--accent-light-rgb), 0.7);
|
||||
background: rgba(var(--accent-rgb), 0.06);
|
||||
border-radius: 6px;
|
||||
padding: 5px 10px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.auto-hub-recipe-use {
|
||||
align-self: flex-end;
|
||||
padding: 5px 14px;
|
||||
border-radius: 14px;
|
||||
border: none;
|
||||
background: rgba(var(--accent-rgb),0.15);
|
||||
color: rgb(var(--accent-light-rgb));
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.auto-hub-recipe-use:hover {
|
||||
background: rgba(var(--accent-rgb),0.3);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Guide Cards */
|
||||
.auto-hub-guide-card {
|
||||
background: rgba(255,255,255,0.03);
|
||||
border: 1px solid rgba(255,255,255,0.06);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.auto-hub-guide-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.auto-hub-guide-icon {
|
||||
font-size: 22px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.auto-hub-guide-title {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
.auto-hub-guide-subtitle {
|
||||
font-size: 12px;
|
||||
color: rgba(255,255,255,0.4);
|
||||
margin-bottom: 12px;
|
||||
padding-left: 34px;
|
||||
}
|
||||
.auto-hub-steps {
|
||||
counter-reset: hub-step;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 12px 0;
|
||||
}
|
||||
.auto-hub-steps li {
|
||||
counter-increment: hub-step;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 9px 0;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.04);
|
||||
font-size: 12.5px;
|
||||
line-height: 1.55;
|
||||
color: rgba(255,255,255,0.65);
|
||||
}
|
||||
.auto-hub-steps li:last-child { border-bottom: none; }
|
||||
.auto-hub-steps li::before {
|
||||
content: counter(hub-step);
|
||||
min-width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background: rgba(var(--accent-rgb),0.15);
|
||||
color: rgb(var(--accent-light-rgb));
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.auto-hub-guide-related {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid rgba(255,255,255,0.04);
|
||||
}
|
||||
.auto-hub-guide-related-label {
|
||||
font-size: 10.5px;
|
||||
color: rgba(255,255,255,0.3);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
align-self: center;
|
||||
}
|
||||
.auto-hub-guide-related-link {
|
||||
padding: 3px 10px;
|
||||
border-radius: 10px;
|
||||
background: rgba(var(--accent-rgb),0.1);
|
||||
color: rgb(var(--accent-light-rgb));
|
||||
font-size: 10.5px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
border: none;
|
||||
}
|
||||
.auto-hub-guide-related-link:hover {
|
||||
background: rgba(var(--accent-rgb),0.2);
|
||||
}
|
||||
.auto-hub-guide-body { display: none; }
|
||||
.auto-hub-guide-card.expanded .auto-hub-guide-body { display: block; }
|
||||
.auto-hub-guide-chevron {
|
||||
color: rgba(255,255,255,0.3);
|
||||
font-size: 11px;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
.auto-hub-guide-card.expanded .auto-hub-guide-chevron {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* Tips Grid */
|
||||
.auto-hub-tips-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
.auto-hub-tip-card {
|
||||
background: rgba(255,255,255,0.03);
|
||||
border: 1px solid rgba(255,255,255,0.06);
|
||||
border-left: 3px solid rgba(var(--accent-rgb),0.4);
|
||||
border-radius: 10px;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
.auto-hub-tip-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.auto-hub-tip-icon { font-size: 16px; flex-shrink: 0; }
|
||||
.auto-hub-tip-title {
|
||||
font-size: 12.5px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
flex: 1;
|
||||
}
|
||||
.auto-hub-tip-tag {
|
||||
padding: 2px 8px;
|
||||
border-radius: 8px;
|
||||
background: rgba(255,255,255,0.05);
|
||||
color: rgba(255,255,255,0.35);
|
||||
font-size: 9.5px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
.auto-hub-tip-body {
|
||||
font-size: 11.5px;
|
||||
line-height: 1.5;
|
||||
color: rgba(255,255,255,0.45);
|
||||
}
|
||||
|
||||
/* Reference Accordion */
|
||||
.auto-hub-ref-group {
|
||||
border: 1px solid rgba(255,255,255,0.06);
|
||||
border-radius: 10px;
|
||||
margin-bottom: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.auto-hub-ref-group-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 16px;
|
||||
background: rgba(255,255,255,0.03);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
.auto-hub-ref-group-header:hover {
|
||||
background: rgba(255,255,255,0.05);
|
||||
}
|
||||
.auto-hub-ref-group-label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: rgba(255,255,255,0.8);
|
||||
flex: 1;
|
||||
}
|
||||
.auto-hub-ref-group-count {
|
||||
font-size: 10px;
|
||||
color: rgba(255,255,255,0.25);
|
||||
padding: 1px 7px;
|
||||
border-radius: 8px;
|
||||
background: rgba(255,255,255,0.04);
|
||||
}
|
||||
.auto-hub-ref-chevron {
|
||||
color: rgba(255,255,255,0.3);
|
||||
font-size: 11px;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
.auto-hub-ref-group.expanded .auto-hub-ref-chevron {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.auto-hub-ref-body {
|
||||
display: none;
|
||||
padding: 0 16px 12px;
|
||||
}
|
||||
.auto-hub-ref-group.expanded .auto-hub-ref-body {
|
||||
display: block;
|
||||
}
|
||||
.auto-hub-ref-subheader {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: rgba(255,255,255,0.35);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.4px;
|
||||
padding: 10px 0 6px;
|
||||
}
|
||||
.auto-hub-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.auto-hub-table th {
|
||||
text-align: left;
|
||||
padding: 7px 10px;
|
||||
font-weight: 600;
|
||||
color: rgba(255,255,255,0.5);
|
||||
border-bottom: 1px solid rgba(255,255,255,0.08);
|
||||
background: rgba(255,255,255,0.02);
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.4px;
|
||||
}
|
||||
.auto-hub-table td {
|
||||
padding: 7px 10px;
|
||||
color: rgba(255,255,255,0.55);
|
||||
border-bottom: 1px solid rgba(255,255,255,0.03);
|
||||
line-height: 1.4;
|
||||
}
|
||||
.auto-hub-table td:first-child {
|
||||
color: rgba(255,255,255,0.7);
|
||||
font-weight: 500;
|
||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||
font-size: 11px;
|
||||
}
|
||||
.auto-hub-table tr:hover td {
|
||||
background: rgba(255,255,255,0.02);
|
||||
}
|
||||
|
||||
/* Hub Callout */
|
||||
.auto-hub-callout {
|
||||
padding: 12px 16px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 14px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
background: rgba(var(--accent-rgb),0.06);
|
||||
border: 1px solid rgba(var(--accent-rgb),0.12);
|
||||
color: rgba(255,255,255,0.65);
|
||||
}
|
||||
.auto-hub-callout-icon {
|
||||
font-size: 15px;
|
||||
flex-shrink: 0;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 800px) {
|
||||
.auto-hub-recipes-grid { grid-template-columns: 1fr; }
|
||||
.auto-hub-tips-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
/* --- Group Dropdown Popover --- */
|
||||
.auto-group-dropdown {
|
||||
z-index: 9999;
|
||||
|
|
|
|||
Loading…
Reference in a new issue