// ==UserScript== // @name ConnectNGo Cabana Event Template Automation // @namespace connectngo-cabana-automation // @version 3.14 // @description Balanced layout, taller expanded mode to avoid scrolling, hidden scrollbars, stronger outer shadow. // @match https://us.connectngo.com/* // @match https://*.connectngo.com/* // @grant none // @run-at document-idle // ==/UserScript== (function () { 'use strict'; // ---------- CONFIG & DESIGN SYSTEM ---------- const ACTION_VALUE = 'event-creation-action-one-date'; const FILTER_WORD = 'cabana'; const STORAGE_KEY = 'cng_cabana_automation_state'; const THEMES = { dark: { bg: '#141416', cardBg: '#1c1c21', cardBorder: '#272730', textMain: '#f8fafc', textMuted: '#94a3b8', accent: '#6366f1', accentGradient: 'linear-gradient(135deg, #6366f1 0%, #a855f7 100%)', success: '#22c55e', danger: '#ef4444', warning: '#eab308', inputBg: '#141416', logBg: '#09090b', shadow: '0 30px 80px rgba(0,0,0,0.9), 0 12px 30px rgba(0,0,0,0.7), 0 0 0 1px rgba(0,0,0,0.4)' }, light: { bg: '#f1f5f9', cardBg: '#ffffff', cardBorder: '#e2e8f0', textMain: '#0f172a', textMuted: '#64748b', accent: '#4f46e5', accentGradient: 'linear-gradient(135deg, #4f46e5 0%, #9333ea 100%)', success: '#16a34a', danger: '#dc2626', warning: '#ca8a04', inputBg: '#f8fafc', logBg: '#f8fafc', shadow: '0 30px 70px rgba(15,23,42,0.35), 0 12px 28px rgba(15,23,42,0.25), 0 0 0 1px rgba(15,23,42,0.08)' } }; // ---------- STATE ---------- function loadState() { try { const raw = localStorage.getItem(STORAGE_KEY); return raw ? JSON.parse(raw) : null; } catch (e) { return null; } } function saveState(state) { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } function clearState() { localStorage.removeItem(STORAGE_KEY); } function getTodayAt(timeStr) { const d = new Date(); const yyyy = d.getFullYear(); const mm = String(d.getMonth() + 1).padStart(2, '0'); const dd = String(d.getDate()).padStart(2, '0'); return `${yyyy}-${mm}-${dd}T${timeStr}`; } function defaultState() { return { status: 'idle', step: null, scanned: [], pending: [], current: null, history: [], log: [], startDate: getTodayAt('10:00'), endDate: getTodayAt('18:00'), autoMode: false, theme: 'dark', expanded: false, panelPos: null }; } let state = loadState() || defaultState(); if (!state.history) state.history = []; if (state.autoMode === undefined) state.autoMode = false; if (!state.theme) state.theme = 'dark'; if (state.expanded === undefined) state.expanded = false; if (state.panelPos === undefined) state.panelPos = null; function log(msg) { const line = `[${new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit', second:'2-digit'})}] ${msg}`; state.log.unshift(line); state.log = state.log.slice(0, 40); saveState(state); renderLog(); console.log('[CabanaAutomation]', msg); } function formatDateTime(dtStr) { if (!dtStr) return ''; const d = new Date(dtStr); const pad = n => n.toString().padStart(2, '0'); return `${pad(d.getMonth() + 1)}/${pad(d.getDate())}/${d.getFullYear()} ${pad(d.getHours())}:${pad(d.getMinutes())}:00`; } // ---------- DOM HELPERS ---------- function waitFor(selectorFn, timeoutMs = 15000, intervalMs = 300) { return new Promise((resolve, reject) => { const start = Date.now(); const tick = () => { const el = selectorFn(); if (el) { return resolve(el); } if (Date.now() - start > timeoutMs) { return reject(new Error('Timed out waiting for element')); } setTimeout(tick, intervalMs); }; tick(); }); } function getRows() { return Array.from(document.querySelectorAll('tr[dusk$="-row"]')); } function rowName(row) { const span = row.querySelector('span.whitespace-no-wrap'); return span ? span.textContent.trim() : ''; } function rowViewHref(row) { const a = row.querySelector('a[dusk$="-view-button"]'); return a ? a.getAttribute('href') : null; } function extractIdFromUrl(url) { if (!url) return null; const m = url.match(/\/(\d+)(?:$|\?)/); return m ? m[1] : null; } function setNativeSelectValue(input, value) { input.value = value; input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); } function forceInputValue(el, value) { if (!el) return; try { el.focus(); const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set; nativeSetter.call(el, value); el.dispatchEvent(new Event('keydown', { bubbles: true })); el.dispatchEvent(new Event('keypress', { bubbles: true })); el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('keyup', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); el.blur(); } catch (err) { log(`Error forcing input value: ${err.message}`); } } function pageType() { const path = location.pathname; const params = new URLSearchParams(location.search); if (/^\/resources\/event-groups\/?$/.test(path)) { return 'group-list'; } if (/^\/resources\/event-groups\/\d+$/.test(path) && params.get('navigationTab') === 'event-templates') { return 'group-templates'; } if (/^\/resources\/event-groups\/\d+$/.test(path)) { return 'group-detail'; } if (/^\/resources\/event-templates\/\d+$/.test(path)) { return 'template-detail'; } return 'other'; } // ---------- AUTOMATION STEPS ---------- async function goToTemplatesTabForCurrent() { const url = state.current.groupUrl.split('?')[0] + '?navigationTab=event-templates'; log(`Opening Templates for "${state.current.name}"`); state.step = 'templates'; saveState(state); location.href = url; } async function handleGroupTemplatesPage() { try { await waitFor(() => getRows().length > 0, 15000); } catch (e) { log('ERROR: No templates appeared. Pausing.'); state.status = 'awaiting-manual'; saveState(state); renderPanel(); return; } const rows = getRows(); const targetName = state.current.name; let match = rows.find(r => rowName(r) === targetName) || rows.find(r => rowName(r).toLowerCase().includes(targetName.toLowerCase())); if (!match) { log(`ERROR: Could not find template row "${targetName}". Pausing.`); state.status = 'awaiting-manual'; saveState(state); renderPanel(); return; } const href = rowViewHref(match); state.step = 'action'; saveState(state); const viewButton = match.querySelector('a[dusk$="-view-button"]'); if (viewButton) viewButton.click(); else location.href = href; } async function handleTemplateDetailPage() { let select, button; try { select = await waitFor(() => document.querySelector('select[dusk="action-select"]'), 15000); button = await waitFor(() => document.querySelector('button[dusk="run-action-button"]'), 15000); } catch (e) { log('ERROR: Dropdown not found. Pausing.'); state.status = 'awaiting-manual'; saveState(state); renderPanel(); return; } setNativeSelectValue(select, ACTION_VALUE); await new Promise(r => setTimeout(r, 400)); state.step = 'modal_fill'; saveState(state); button.click(); } async function handleModalFill() { log('Waiting 1 second for modal animation...'); await new Promise(r => setTimeout(r, 1000)); try { const potentialInputs = Array.from(document.querySelectorAll('input[type="datetime-local"], input[type="text"], input[type="date"]')) .filter(el => el.closest('.modal, [role="dialog"], .fixed, [data-modal]')); const startInput = document.querySelector('input[name="start_date"], input[dusk="start-date"]') || potentialInputs[0]; const endInput = document.querySelector('input[name="end_date"], input[dusk="end-date"]') || potentialInputs[1]; const confirmBtn = document.querySelector('button[dusk="confirm-action-button"]'); const formattedStart = formatDateTime(state.startDate); const formattedEnd = formatDateTime(state.endDate); if (startInput && formattedStart) forceInputValue(startInput, formattedStart); if (endInput && formattedEnd) forceInputValue(endInput, formattedEnd); await new Promise(r => setTimeout(r, 600)); if (confirmBtn) { confirmBtn.click(); if (state.autoMode) { log('Dates injected. Auto-Advancing to next item in 2.5s...'); state.status = 'running'; saveState(state); renderPanel(); setTimeout(() => advanceToNext(false), 2500); } else { log('Dates injected. Paused for manual continuation.'); state.status = 'awaiting-manual'; saveState(state); renderPanel(); } } else { log('Could not find Confirm button. Pausing.'); state.status = 'awaiting-manual'; saveState(state); renderPanel(); } } catch (e) { log(`ERROR in Modal: ${e.message}. Pausing.`); state.status = 'awaiting-manual'; saveState(state); renderPanel(); } } function advanceToNext(wasSkipped = false) { state.history.push({ ...state.current, action: wasSkipped ? 'skipped' : 'done' }); log(wasSkipped ? `Skipped "${state.current.name}".` : `Completed "${state.current.name}".`); if (state.pending.length === 0) { log(`No more items in queue. Finished!`); state.current = null; state.status = 'finished'; saveState(state); renderPanel(); return; } const next = state.pending.shift(); state.current = next; state.status = 'running'; saveState(state); goToTemplatesTabForCurrent(); } function goPrevious() { if (!state.history || state.history.length === 0) { log('No previous history found.'); return; } const prev = state.history.pop(); if (state.current) { state.pending.unshift(state.current); } state.current = { name: prev.name, groupId: prev.groupId, groupUrl: prev.groupUrl }; state.status = 'running'; saveState(state); log(`Rewinding to previous item: "${state.current.name}"`); goToTemplatesTabForCurrent(); } // ---------- SCANNING ---------- function scanCurrentPage() { const rows = getRows(); let newItems = []; rows.forEach(row => { const name = rowName(row); if (!name.toLowerCase().includes(FILTER_WORD)) { return; } const href = rowViewHref(row); if (!href) { return; } const id = extractIdFromUrl(href); const alreadyProcessed = state.history.some(h => h.groupId === id); if (alreadyProcessed) return; newItems.push({ name, groupId: id, groupUrl: href, selected: true }); }); state.scanned = newItems; state.status = newItems.length > 0 ? 'reviewing' : 'idle'; if (newItems.length > 0) { log(`Found ${newItems.length} Cabana items.`); } else { log(`No new Cabana items found on this page.`); } saveState(state); renderPanel(); } // ---------- UI & DRAG LOGIC ---------- let panelEl = null; function ensureScrollbarStyles() { if (document.getElementById('cba-scrollbar-style')) return; const style = document.createElement('style'); style.id = 'cba-scrollbar-style'; style.textContent = ` #cba-panel * { scrollbar-width: none; -ms-overflow-style: none; } #cba-panel *::-webkit-scrollbar { width: 0; height: 0; display: none; } `; document.head.appendChild(style); } function renderLog() { if (!panelEl) { return; } const logEl = panelEl.querySelector('#cba-log'); if (logEl) logEl.innerHTML = state.log.map(l => `
${escapeHtml(l)}
`).join(''); } function escapeHtml(s) { return s.replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); } function ensurePanel() { ensureScrollbarStyles(); if (!document.getElementById('cba-panel')) { panelEl = document.createElement('div'); panelEl.id = 'cba-panel'; const pTop = state.panelPos?.top || 'auto'; const pLeft = state.panelPos?.left || 'auto'; const pBottom = state.panelPos?.top ? 'auto' : '24px'; const pRight = state.panelPos?.left ? 'auto' : '24px'; const width = state.expanded ? '800px' : '380px'; const height = state.expanded ? '780px' : '480px'; panelEl.style.cssText = ` position: fixed; top: ${pTop}; left: ${pLeft}; bottom: ${pBottom}; right: ${pRight}; width: ${width}; height: ${height}; max-height: 92vh; font: 13px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; border-radius: 20px; z-index: 9999999; display: flex; flex-direction: column; resize: none; overflow: hidden; transition: width 0.25s cubic-bezier(0.16, 1, 0.3, 1), height 0.25s cubic-bezier(0.16, 1, 0.3, 1), background 0.2s ease, color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease; `; document.body.appendChild(panelEl); } else { panelEl.style.width = state.expanded ? '800px' : '380px'; panelEl.style.height = state.expanded ? '780px' : '480px'; } return panelEl; } function btn(label, id, colorType) { const t = THEMES[state.theme]; const colors = { primary: `background:${t.accentGradient}; color:#fff;`, success: `background:${t.success}; color:#fff;`, danger: `background:${t.danger}; color:#fff;`, secondary: `background:${t.cardBorder}; color:${t.textMain};`, warning: `background:${t.warning}; color:#fff;` }; const style = colors[colorType] || colors.secondary; return ``; } function renderPanel() { const el = ensurePanel(); const t = THEMES[state.theme]; const type = pageType(); el.style.background = t.cardBg; el.style.color = t.textMain; el.style.border = `1px solid ${t.cardBorder}`; el.style.boxShadow = t.shadow; let body = ''; body += `
Cabana Automator
`; // NOTE: contentLayout is applied to the outer scroll container. Each logical // column (main content / side column) is rendered into its OWN wrapper div // below so that CSS Grid treats each column as a single grid item instead of // auto-placing every individual inner
into alternating grid cells. const contentLayout = state.expanded ? 'display: grid; grid-template-columns: 1.2fr 0.8fr; gap: 20px; align-items: start;' : 'display: flex; flex-direction: column;'; body += `
`; let mainContent = ''; let statusText = state.status.toUpperCase(); if (state.status === 'awaiting-manual') statusText = 'PAUSED'; mainContent += `
STATUS: ${statusText} ${state.step ? '(' + state.step + ')' : ''}
`; mainContent += `
Auto-Advance Mode:
`; if (state.status === 'idle') { if (type === 'group-list') { mainContent += `
Navigate to the Event Groups list, set per-page to 100, and click Scan.
`; mainContent += btn('Scan Page for Cabanas', 'cba-scan', 'primary'); } else { mainContent += `
Go to the Event Groups list page to scan.
`; } } else if (state.status === 'reviewing') { // SWAPPED: Now the Cabana selection menu takes up the primary prominent area on the left/main column mainContent += `
Select Cabanas to Process:
Select All Select None
`; // Full height prominent scrolling container for cabanas const listHeight = state.expanded ? '320px' : '220px'; mainContent += `
`; state.scanned.forEach((item, idx) => { const isChecked = item.selected !== false ? 'checked' : ''; mainContent += ``; }); mainContent += `
`; mainContent += `
Start:
End:
`; if (state.startDate && state.endDate) { mainContent += btn(`Start Selected Items`, 'cba-start-btn', 'success'); } else { mainContent += `
Please set both Start and End dates to continue.
`; } // Cancel is now a neat compact button instead of a giant blank box block mainContent += btn('Cancel', 'cba-clear', 'secondary'); } else if (state.status === 'running') { mainContent += `
`; mainContent += `
Currently Processing:
`; mainContent += `
${state.current ? escapeHtml(state.current.name) : '—'}
`; mainContent += `
Queue remaining: ${state.pending.length}
`; mainContent += `
`; mainContent += btn('Stop Automation', 'cba-stop', 'danger'); if (state.history.length > 0) mainContent += btn('← Previous', 'cba-prev', 'warning'); } else if (state.status === 'awaiting-manual') { mainContent += `
`; mainContent += `
Paused for Manual Review
`; mainContent += `
Please verify the popup, then click Continue.
`; mainContent += `
`; mainContent += btn('Continue → Next', 'cba-continue', 'success'); mainContent += btn('Skip This Item', 'cba-skip', 'secondary'); mainContent += `
`; if (state.history.length > 0) mainContent += btn('← Previous', 'cba-prev', 'warning'); mainContent += btn('Retry Step', 'cba-retry', 'secondary'); mainContent += btn('Stop Entirely', 'cba-stop', 'danger'); mainContent += `
`; } else if (state.status === 'finished') { let doneCount = state.history.filter(h => h.action === 'done').length; let skipCount = state.history.filter(h => h.action === 'skipped').length; mainContent += `
`; mainContent += `
Batch Complete!
`; mainContent += `
Processed: ${doneCount}
`; mainContent += `
Skipped: ${skipCount}
`; mainContent += `
`; mainContent += btn('Reset / Start Fresh', 'cba-clear', 'primary'); } // FIX: wrap mainContent in a single container div. Previously this was // concatenated directly into the grid container as many sibling top-level // divs, so CSS Grid auto-placement scattered them across BOTH columns // (col1/col2/col1/col2...) instead of keeping them together in column 1. // That's what caused the "Start Selected Items" button to jump into the // right column and left huge empty gaps in expanded mode. body += `
${mainContent}
`; // Side Column in Expanded View if (state.expanded) { let sideCol = `
`; sideCol += `
SYSTEM & CONTROLS
`; sideCol += `
`; sideCol += `
Batch Diagnostics
`; sideCol += `
Processed History: ${state.history.length} items
`; sideCol += `
Current Mode: ${state.theme}
`; sideCol += `
`; sideCol += `
ACTIVITY LOG
`; sideCol += `
`; sideCol += `
`; body += sideCol; } else { body += `
ACTIVITY LOG
`; body += `
`; } body += `
`; el.innerHTML = body; renderLog(); wireButtons(); } function wireButtons() { const dragHandle = document.getElementById('cba-drag-handle'); if (dragHandle) { let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; dragHandle.onmousedown = (e) => { if (e.target.closest('button')) return; e.preventDefault(); pos3 = e.clientX; pos4 = e.clientY; document.onmouseup = () => { document.onmouseup = null; document.onmousemove = null; dragHandle.style.cursor = 'grab'; state.panelPos = state.panelPos || {}; state.panelPos.top = panelEl.style.top; state.panelPos.left = panelEl.style.left; saveState(state); }; document.onmousemove = (e) => { e.preventDefault(); dragHandle.style.cursor = 'grabbing'; pos1 = pos3 - e.clientX; pos2 = pos4 - e.clientY; pos3 = e.clientX; pos4 = e.clientY; const rect = panelEl.getBoundingClientRect(); if (!panelEl.style.top || panelEl.style.top === 'auto') panelEl.style.top = rect.top + "px"; if (!panelEl.style.left || panelEl.style.left === 'auto') panelEl.style.left = rect.left + "px"; panelEl.style.bottom = "auto"; panelEl.style.right = "auto"; panelEl.style.top = (panelEl.offsetTop - pos2) + "px"; panelEl.style.left = (panelEl.offsetLeft - pos1) + "px"; }; }; } const toggleThemeBtn = document.getElementById('cba-toggle-theme'); if (toggleThemeBtn) toggleThemeBtn.onclick = () => { state.theme = state.theme === 'dark' ? 'light' : 'dark'; saveState(state); renderPanel(); }; const toggleExpandBtn = document.getElementById('cba-toggle-expand'); if (toggleExpandBtn) toggleExpandBtn.onclick = () => { state.expanded = !state.expanded; saveState(state); renderPanel(); }; const toggleAutoBtn = document.getElementById('cba-toggle-auto'); if (toggleAutoBtn) toggleAutoBtn.onclick = () => { if (state.status === 'reviewing' && state.scanned) { state.scanned.forEach((item, idx) => { const chk = document.getElementById(`chk-${idx}`); if (chk) item.selected = chk.checked; }); } state.autoMode = !state.autoMode; saveState(state); renderPanel(); }; const startInput = document.getElementById('cba-start'); if (startInput) startInput.onchange = (e) => { state.startDate = e.target.value; saveState(state); renderPanel(); }; const endInput = document.getElementById('cba-end'); if (endInput) endInput.onchange = (e) => { state.endDate = e.target.value; saveState(state); renderPanel(); }; const scanBtn = document.getElementById('cba-scan'); if (scanBtn) scanBtn.onclick = scanCurrentPage; const selAllBtn = document.getElementById('cba-sel-all'); if (selAllBtn) selAllBtn.onclick = (e) => { e.preventDefault(); state.scanned.forEach((item, idx) => { item.selected = true; const chk = document.getElementById(`chk-${idx}`); if (chk) chk.checked = true; }); saveState(state); }; const selNoneBtn = document.getElementById('cba-sel-none'); if (selNoneBtn) selNoneBtn.onclick = (e) => { e.preventDefault(); state.scanned.forEach((item, idx) => { item.selected = false; const chk = document.getElementById(`chk-${idx}`); if (chk) chk.checked = false; }); saveState(state); }; const startBtn = document.getElementById('cba-start-btn'); if (startBtn) startBtn.onclick = () => { const finalQueue = []; state.scanned.forEach((item, idx) => { const chk = document.getElementById(`chk-${idx}`); if (chk) item.selected = chk.checked; if (item.selected) finalQueue.push(item); }); if (finalQueue.length === 0) { log('No items selected.'); return; } state.pending = finalQueue; state.scanned = []; state.current = state.pending.shift(); state.status = 'running'; saveState(state); log(`Starting automated queue with ${state.pending.length + 1} items.`); goToTemplatesTabForCurrent(); }; const clearBtn = document.getElementById('cba-clear'); if (clearBtn) clearBtn.onclick = () => { state = defaultState(); clearState(); renderPanel(); }; const stopBtn = document.getElementById('cba-stop'); if (stopBtn) stopBtn.onclick = () => { state.status = 'idle'; state.current = null; state.scanned = []; state.pending = []; saveState(state); renderPanel(); }; const continueBtn = document.getElementById('cba-continue'); if (continueBtn) continueBtn.onclick = () => advanceToNext(false); const skipBtn = document.getElementById('cba-skip'); if (skipBtn) skipBtn.onclick = () => advanceToNext(true); const prevBtn = document.getElementById('cba-prev'); if (prevBtn) prevBtn.onclick = goPrevious; const retryBtn = document.getElementById('cba-retry'); if (retryBtn) retryBtn.onclick = () => { state.status = 'running'; saveState(state); runStepForCurrentPage(); }; } // ---------- MAIN DISPATCH ---------- async function runStepForCurrentPage() { if (state.status !== 'running') { return; } const type = pageType(); if (type === 'group-templates' && state.step === 'templates') { await handleGroupTemplatesPage(); } else if (type === 'template-detail' && state.step === 'action') { await handleTemplateDetailPage(); } else if (type === 'template-detail' && state.step === 'modal_fill') { await handleModalFill(); } } // ---------- INITIALIZATION & SPA OBSERVER ---------- let lastUrl = location.href; let lastStep = state.step; function init() { renderPanel(); runStepForCurrentPage(); setInterval(() => { if (!document.getElementById('cba-panel')) { renderPanel(); } if (location.href !== lastUrl || state.step !== lastStep) { lastUrl = location.href; lastStep = state.step; renderPanel(); runStepForCurrentPage(); } }, 500); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();