// ==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 `${label} `;
}
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
${state.expanded ? '🔍 Normal' : '📖 Expanded'}
${state.theme === 'dark' ? '☀️ Light' : '🌙 Dark'}
⠿
`;
// 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:
${state.autoMode ? 'ON' : 'OFF'}
`;
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:
`;
// 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 += `
${escapeHtml(item.name)}
`;
});
mainContent += `
`;
mainContent += `
`;
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();
}
})();