// ==UserScript== // @name Browser Auto Refresh (Floating Window) // @namespace Charlie Chimp // @version 2.3.2 // @author BetterCallSaul // @description Universal auto-refresh script with draggable floating window, customizable intervals, and smart positioning. Works on any website with domain-based configuration. // @match *://*/* // @icon https://raw.githubusercontent.com/BetterCallSaulAtlas/browser-auto-refresh/main/screenshots/icon-128x128.png // @updateURL https://raw.githubusercontent.com/BetterCallSaulAtlas/browser-auto-refresh/main/jamf_auto_refresh.js // @downloadURL https://raw.githubusercontent.com/BetterCallSaulAtlas/browser-auto-refresh/main/jamf_auto_refresh.js // @supportURL https://github.com/BetterCallSaulAtlas/browser-auto-refresh/issues // @homepageURL https://github.com/BetterCallSaulAtlas/browser-auto-refresh // @grant none // @run-at document-end // ==/UserScript== (function () { 'use strict'; // Generate unique script ID for this session const SCRIPT_SESSION_KEY = '__jamfAutoRefreshLoaded__'; const WIDGET_ID = 'cc-auto-refresh-nav'; // Check if script already loaded in this session (survives SPA navigation) if (window[SCRIPT_SESSION_KEY]) { console.log('[Jamf Auto-Refresh] Already loaded in this window session, aborting'); return; } // Check if widget already exists in DOM if (document.getElementById(WIDGET_ID)) { console.log('[Jamf Auto-Refresh] Widget already exists in DOM, aborting'); return; } // Mark as loaded immediately to prevent race conditions window[SCRIPT_SESSION_KEY] = true; // ============================================================================ // USER CONFIGURATION // ============================================================================ // Default configuration if nothing exists in localStorage. // You can still edit this array, or use the visual Settings UI. // // Simple format (backward compatible): // '*.jamfcloud.com' // // Advanced format (new in v2.1.0): // { // domain: '*.jamfcloud.com', // interval: 60000, // Custom refresh interval (ms), null = use global // paths: { // include: ['*'], // Glob or regex patterns // exclude: [] // Exclude patterns (takes priority) // }, // enabled: true // } // // Path pattern examples: // - '*' or ['*'] All paths (default) // - '/computers.html' Exact path // - '/computers*' Starts with /computers // - '*/devices/*' Contains /devices/ anywhere // - 'regex:^/computers/.*\\.html$' Regex pattern const DEFAULT_DOMAIN_CONFIG = [ '*.jamfcloud.com', 'pke.atlassian.com' ]; // ============================================================================ // END USER CONFIGURATION // ============================================================================ // Storage keys const STORAGE_KEY_DOMAINS = 'cc_auto_refresh_domains:' + location.host; // Normalize config entry to advanced format function normalizeConfigEntry(entry) { if (typeof entry === 'string') { // Simple string format - convert to object return { domain: entry, interval: null, paths: { include: ['*'], exclude: [] }, enabled: true }; } // Already an object - ensure it has all properties return { domain: entry.domain || '', interval: entry.interval || null, paths: entry.paths || { include: ['*'], exclude: [] }, enabled: entry.enabled !== false }; } // Load domain configuration from localStorage or use defaults function loadDomainConfig() { const stored = localStorage.getItem(STORAGE_KEY_DOMAINS); if (stored) { try { const parsed = JSON.parse(stored); // Only use stored config if it's a non-empty array if (Array.isArray(parsed) && parsed.length > 0) { return parsed.map(normalizeConfigEntry); } // Empty array in storage - fall through to defaults } catch (e) { console.warn('[Jamf Auto-Refresh] Failed to parse stored domains, using defaults'); } } // No valid stored config - use defaults console.log('[Jamf Auto-Refresh] Using DEFAULT_DOMAIN_CONFIG:', DEFAULT_DOMAIN_CONFIG); return DEFAULT_DOMAIN_CONFIG.map(normalizeConfigEntry); } // Save domain configuration to localStorage function saveDomainConfig(configs) { localStorage.setItem(STORAGE_KEY_DOMAINS, JSON.stringify(configs)); } // Legacy support - load as simple domain array for backward compat with v2.0.0 code function loadEnabledDomains() { return loadDomainConfig().map(config => config.domain); } // Legacy support - save as config objects function saveEnabledDomains(domains) { const configs = domains.map(domain => typeof domain === 'string' ? normalizeConfigEntry(domain) : domain ); saveDomainConfig(configs); } // Check if a hostname matches a domain pattern function matchesDomainPattern(hostname, pattern) { const cleanPattern = pattern.replace(/\*/g, ''); if (pattern.startsWith('*')) { return hostname.includes(cleanPattern) || hostname.endsWith(cleanPattern); } return hostname === cleanPattern || hostname.endsWith('.' + cleanPattern); } // Check if a path matches a pattern (glob or regex) function matchesPathPattern(currentPath, pattern) { // Special case: single * means match all paths if (pattern === '*') { return true; } // Regex pattern (starts with "regex:") if (pattern.startsWith('regex:')) { try { const regexStr = pattern.substring(6); const regex = new RegExp(regexStr); return regex.test(currentPath); } catch (e) { console.warn('[Jamf Auto-Refresh] Invalid regex pattern:', pattern, e); return false; } } // Glob pattern // Convert glob to regex: * = [^/]*, ** = .*, ? = . const regexPattern = pattern .replace(/\*\*/g, '') // Temporarily replace ** .replace(/\*/g, '[^/]*') // * matches anything except / .replace(//g, '.*') // ** matches anything including / .replace(/\?/g, '.') // ? matches single char .replace(/\./g, '\\.'); // Escape literal dots try { const regex = new RegExp(`^${regexPattern}$`); return regex.test(currentPath); } catch (e) { console.warn('[Jamf Auto-Refresh] Invalid glob pattern:', pattern, e); return false; } } // Check if current path matches the path configuration function matchesPathConfig(currentPath, pathConfig) { if (!pathConfig) { return true; // No path config = match all } const include = pathConfig.include || ['*']; const exclude = pathConfig.exclude || []; // Check exclude patterns first (they take priority) for (const pattern of exclude) { if (matchesPathPattern(currentPath, pattern)) { return false; } } // Check include patterns for (const pattern of include) { if (matchesPathPattern(currentPath, pattern)) { return true; } } return false; } // Find the first matching config for current URL function findMatchingConfig(hostname, pathname, configs) { for (const config of configs) { if (!config.enabled) continue; // Check domain if (!matchesDomainPattern(hostname, config.domain)) continue; // Check paths if (!matchesPathConfig(pathname, config.paths)) continue; // Match found return config; } return null; } // Check if current URL matches any configuration const currentHostname = window.location.hostname; const currentPath = window.location.pathname; let domainConfigs = loadDomainConfig(); let enabledDomains = domainConfigs.map(c => c.domain); // For legacy compatibility const matchedConfig = findMatchingConfig(currentHostname, currentPath, domainConfigs); // Exit early if no matching configuration if (!matchedConfig) { console.log('[Jamf Auto-Refresh] No matching configuration for:', currentHostname + currentPath); console.log('[Jamf Auto-Refresh] Available configs:', domainConfigs.length); return; } console.log('[Jamf Auto-Refresh] Matched configuration:', { domain: matchedConfig.domain, interval: matchedConfig.interval ? `${matchedConfig.interval}ms` : 'global', paths: matchedConfig.paths }); // Widget ID (already checked at top of script) const instanceId = 'cc-auto-refresh-nav'; // Remove any orphaned modals from previous navigation const orphanedModal = document.getElementById('cc-auto-refresh-modal'); if (orphanedModal) { console.log('[Jamf Auto-Refresh] Removing orphaned modal from previous navigation'); orphanedModal.remove(); } const REFRESH_INTERVAL_MS = 1 * 60 * 1000; // default 1 minute const DELAY_WHILE_TYPING_MS = 10 * 1000; // Delay if user is typing when refresh would occur const STORAGE_KEY_ENABLED = 'cc_auto_refresh_enabled:' + location.host; const STORAGE_KEY_POS = 'cc_auto_refresh_pos:' + location.host; const STORAGE_KEY_INTERVAL = 'cc_auto_refresh_interval_ms:' + location.host; const STORAGE_KEY_COUNT = 'cc_auto_refresh_count:' + location.host; const STORAGE_KEY_LAST_REFRESH = 'cc_auto_refresh_last:' + location.host; const STORAGE_KEY_SESSION_START = 'cc_auto_refresh_session_start:' + location.host; const STORAGE_KEY_MINI_MODE = 'cc_auto_refresh_mini_mode:' + location.host; const MIN_REFRESH_MS = 5 * 1000; // 5 seconds minimum for safety const MAX_REFRESH_MS = 12 * 60 * 60 * 1000; // 12 hours max const INTERVAL_OPTIONS = [ { label: '15 sec', value: 15 * 1000 }, { label: '30 sec', value: 30 * 1000 }, { label: '1 min', value: 1 * 60 * 1000 }, { label: '2 min', value: 2 * 60 * 1000 }, { label: '3 min', value: 3 * 60 * 1000 }, { label: '5 min', value: 5 * 60 * 1000 }, { label: '10 min', value: 10 * 60 * 1000 }, { label: '15 min', value: 15 * 60 * 1000 }, { label: '30 min', value: 30 * 60 * 1000 }, ]; // Load refresh interval - use matched config's interval or global default let refreshIntervalMs = (() => { // Check if matched config has custom interval if (matchedConfig.interval && Number.isFinite(matchedConfig.interval)) { console.log('[Jamf Auto-Refresh] Using domain-specific interval:', matchedConfig.interval); return Math.max(MIN_REFRESH_MS, Math.min(MAX_REFRESH_MS, matchedConfig.interval)); } // Fall back to global interval const raw = parseInt(localStorage.getItem(STORAGE_KEY_INTERVAL) || '', 10); const v = Number.isFinite(raw) ? raw : REFRESH_INTERVAL_MS; return Math.max(MIN_REFRESH_MS, Math.min(MAX_REFRESH_MS, v)); })(); let enabled = (() => { const raw = localStorage.getItem(STORAGE_KEY_ENABLED); return raw === null ? true : raw === 'true'; })(); let nextRefreshAt = enabled ? Date.now() + refreshIntervalMs : null; let refreshContainer, statusEl, tickTimer; let statusMessage = null; // Mini mode state let isMiniMode = (() => { const raw = localStorage.getItem(STORAGE_KEY_MINI_MODE); return raw === 'true'; })(); // UI elements references for mini mode let uiElements = {}; // Global modal reference to prevent duplicates let currentModal = null; // Load session data from localStorage let sessionRefreshCount = (() => { const raw = parseInt(localStorage.getItem(STORAGE_KEY_COUNT) || '0', 10); return Number.isFinite(raw) ? raw : 0; })(); let lastRefreshTime = (() => { const raw = parseInt(localStorage.getItem(STORAGE_KEY_LAST_REFRESH) || '', 10); return Number.isFinite(raw) ? raw : null; })(); let sessionStartTime = (() => { const raw = parseInt(localStorage.getItem(STORAGE_KEY_SESSION_START) || '', 10); if (Number.isFinite(raw) && raw > 0) { return raw; } // First time - initialize session start time const now = Date.now(); localStorage.setItem(STORAGE_KEY_SESSION_START, String(now)); return now; })(); let isDragging = false; let dragOffsetX = 0; let dragOffsetY = 0; let hasDragged = false; function formatTime(ms) { if (ms < 0) ms = 0; const totalSec = Math.floor(ms / 1000); const m = Math.floor(totalSec / 60); const s = totalSec % 60; const mm = String(m); const ss = String(s).padStart(2, '0'); return `${mm}:${ss}`; } function formatDuration(ms) { const totalSec = Math.round(ms / 1000); if (totalSec < 60) return `${totalSec} sec`; const minutes = totalSec / 60; if (Number.isInteger(minutes)) { return `${minutes} min`; } const whole = Math.floor(minutes); const remainderSec = totalSec - whole * 60; return `${whole} min ${remainderSec} sec`; } function formatTimeAgo(ms) { const seconds = Math.floor(ms / 1000); if (seconds < 60) return `${seconds} sec ago`; const minutes = Math.floor(seconds / 60); if (minutes < 60) return `${minutes} min ago`; const hours = Math.floor(minutes / 60); const remainingMins = minutes % 60; if (hours < 24) { return remainingMins > 0 ? `${hours}h ${remainingMins}m ago` : `${hours}h ago`; } const days = Math.floor(hours / 24); return `${days}d ago`; } function formatSessionDuration(ms) { const seconds = Math.floor(ms / 1000); if (seconds < 60) return `${seconds}s`; const minutes = Math.floor(seconds / 60); if (minutes < 60) { const remainingSec = seconds % 60; return remainingSec > 0 ? `${minutes}m ${remainingSec}s` : `${minutes}m`; } const hours = Math.floor(minutes / 60); const remainingMins = minutes % 60; if (hours < 24) { return remainingMins > 0 ? `${hours}h ${remainingMins}m` : `${hours}h`; } const days = Math.floor(hours / 24); const remainingHours = hours % 24; return remainingHours > 0 ? `${days}d ${remainingHours}h` : `${days}d`; } function isUserTyping() { const a = document.activeElement; if (!a) return false; const tag = (a.tagName || '').toLowerCase(); const isFormField = ['input', 'textarea', 'select'].includes(tag); return a.isContentEditable || isFormField; } function scheduleNext(ms = refreshIntervalMs) { nextRefreshAt = Date.now() + ms; } function updateUI() { if (!refreshContainer || !statusEl) return; if (statusMessage) { statusEl.textContent = statusMessage; if (uiElements.miniModeTimer && isMiniMode) { uiElements.miniModeTimer.textContent = statusMessage; } return; } // Update countdown content if (enabled) { const remaining = Math.max(0, nextRefreshAt ? nextRefreshAt - Date.now() : 0); statusEl.textContent = `Next refresh: ${formatTime(remaining)}`; // Update mini mode timer if in mini mode if (uiElements.miniModeTimer && isMiniMode) { const pauseIcon = enabled ? '⏸' : '▶'; uiElements.miniModeTimer.textContent = `${pauseIcon} ${formatTime(remaining)}`; } } else { statusEl.textContent = 'Auto-refresh is OFF'; // Update mini mode timer if in mini mode if (uiElements.miniModeTimer && isMiniMode) { uiElements.miniModeTimer.textContent = '⏸ OFF'; } } } function loadPosition() { const saved = localStorage.getItem(STORAGE_KEY_POS); let position = { bottom: '20px', left: '20px' }; if (saved) { try { position = JSON.parse(saved); } catch (e) { // Ignore parse errors, use default } } // Validate position against current viewport if widget exists // This handles cases where browser was resized between sessions if (refreshContainer) { position = constrainToViewport(position.bottom, position.left, { maintainRelativePosition: true }); } return position; } function savePosition(bottom, left) { localStorage.setItem(STORAGE_KEY_POS, JSON.stringify({ bottom, left })); } function constrainToViewport(bottom, left, options = {}) { const { maintainRelativePosition = true, duringDrag = false } = options; const widget = refreshContainer; if (!widget) return { bottom, left }; const rect = widget.getBoundingClientRect(); const widgetWidth = rect.width; const widgetHeight = rect.height; const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; // Minimum visible padding (pixels that must remain visible) const MIN_VISIBLE = 20; // Parse bottom and left values (handle "20px" string format) const bottomPx = parseInt(String(bottom).replace('px', ''), 10) || 0; const leftPx = parseInt(String(left).replace('px', ''), 10) || 0; // Calculate constraint boundaries // Bottom edge: widget bottom must be at least MIN_VISIBLE from viewport bottom const minBottom = MIN_VISIBLE; // Top edge: widget top (bottom + height) must be at least MIN_VISIBLE from viewport top const maxBottom = viewportHeight - widgetHeight - MIN_VISIBLE; // Left edge: widget left must be at least MIN_VISIBLE from viewport left const minLeft = MIN_VISIBLE; // Right edge: widget right (left + width) must be at least MIN_VISIBLE from viewport right const maxLeft = viewportWidth - widgetWidth - MIN_VISIBLE; // During drag, just apply simple constraints if (duringDrag) { const constrainedBottom = Math.max(minBottom, Math.min(maxBottom, bottomPx)); const constrainedLeft = Math.max(minLeft, Math.min(maxLeft, leftPx)); return { bottom: `${Math.round(constrainedBottom)}px`, left: `${Math.round(constrainedLeft)}px` }; } // Check if position was previously at a constraint edge // Allow 5px tolerance for edge detection const EDGE_TOLERANCE = 5; const wasAtLeftEdge = leftPx <= minLeft + EDGE_TOLERANCE; const wasAtRightEdge = leftPx >= maxLeft - EDGE_TOLERANCE; const wasAtTopEdge = bottomPx >= maxBottom - EDGE_TOLERANCE; const wasAtBottomEdge = bottomPx <= minBottom + EDGE_TOLERANCE; // If maintaining relative position and widget isn't stuck at an edge if (maintainRelativePosition && !wasAtLeftEdge && !wasAtRightEdge && !wasAtTopEdge && !wasAtBottomEdge) { // Calculate current position as percentages of viewport const bottomPercent = bottomPx / viewportHeight; const leftPercent = leftPx / viewportWidth; // Apply percentages to current viewport size let newBottom = bottomPercent * viewportHeight; let newLeft = leftPercent * viewportWidth; // Apply constraints to ensure ENTIRE widget is visible newBottom = Math.max(minBottom, Math.min(maxBottom, newBottom)); newLeft = Math.max(minLeft, Math.min(maxLeft, newLeft)); return { bottom: `${Math.round(newBottom)}px`, left: `${Math.round(newLeft)}px` }; } // Widget was at edge or simple constraint requested - just re-constrain const constrainedBottom = Math.max(minBottom, Math.min(maxBottom, bottomPx)); const constrainedLeft = Math.max(minLeft, Math.min(maxLeft, leftPx)); return { bottom: `${Math.round(constrainedBottom)}px`, left: `${Math.round(constrainedLeft)}px` }; } function startDragging(e) { isDragging = true; hasDragged = false; const rect = refreshContainer.getBoundingClientRect(); dragOffsetX = e.clientX - rect.left; dragOffsetY = e.clientY - rect.top; refreshContainer.style.cursor = 'grabbing'; e.preventDefault(); } function stopDragging() { if (isDragging) { isDragging = false; refreshContainer.style.cursor = 'grab'; // Save position const bottom = refreshContainer.style.bottom; const left = refreshContainer.style.left; savePosition(bottom, left); } } function drag(e) { if (!isDragging) return; hasDragged = true; const x = e.clientX - dragOffsetX; const y = e.clientY - dragOffsetY; // Convert to bottom/left positioning const bottom = window.innerHeight - y - refreshContainer.offsetHeight; const left = x; // Apply viewport constraints during drag const constrained = constrainToViewport(bottom, left, { duringDrag: true }); refreshContainer.style.bottom = constrained.bottom; refreshContainer.style.left = constrained.left; } function openDomainManager() { // Close existing modal if one is open if (currentModal && document.body.contains(currentModal)) { console.log('[Jamf Auto-Refresh] Closing existing modal before opening new one'); document.body.removeChild(currentModal); currentModal = null; } // Create modal overlay const overlay = document.createElement('div'); overlay.id = 'cc-auto-refresh-modal'; overlay.style.position = 'fixed'; overlay.style.top = '0'; overlay.style.left = '0'; overlay.style.width = '100%'; overlay.style.height = '100%'; overlay.style.background = 'rgba(0,0,0,0.7)'; overlay.style.zIndex = '999999'; overlay.style.display = 'flex'; overlay.style.alignItems = 'center'; overlay.style.justifyContent = 'center'; overlay.style.backdropFilter = 'blur(4px)'; // Store reference to current modal currentModal = overlay; // Create modal const modal = document.createElement('div'); modal.style.background = 'linear-gradient(135deg, #1e293b 0%, #0f172a 100%)'; modal.style.border = '1px solid rgba(255,255,255,0.2)'; modal.style.borderRadius = '16px'; modal.style.boxShadow = '0 20px 60px rgba(0,0,0,0.5)'; modal.style.padding = '24px'; modal.style.width = '90%'; modal.style.maxWidth = '500px'; modal.style.maxHeight = '80vh'; modal.style.overflow = 'auto'; modal.style.fontFamily = 'system-ui,-apple-system,Segoe UI,Roboto,sans-serif'; modal.style.color = '#f8fafc'; // Modal header const modalHeader = document.createElement('div'); modalHeader.style.display = 'flex'; modalHeader.style.justifyContent = 'space-between'; modalHeader.style.alignItems = 'center'; modalHeader.style.marginBottom = '20px'; modalHeader.style.paddingBottom = '16px'; modalHeader.style.borderBottom = '1px solid rgba(255,255,255,0.1)'; const modalTitle = document.createElement('h2'); modalTitle.textContent = '⚙️ Domain Settings'; modalTitle.style.margin = '0'; modalTitle.style.fontSize = '20px'; modalTitle.style.fontWeight = '600'; modalTitle.style.color = '#22c55e'; const closeBtn = document.createElement('button'); closeBtn.textContent = '✕'; closeBtn.style.background = 'transparent'; closeBtn.style.border = 'none'; closeBtn.style.color = 'rgba(255,255,255,0.6)'; closeBtn.style.fontSize = '24px'; closeBtn.style.cursor = 'pointer'; closeBtn.style.padding = '0'; closeBtn.style.width = '32px'; closeBtn.style.height = '32px'; closeBtn.style.borderRadius = '6px'; closeBtn.style.transition = 'all 0.2s ease'; closeBtn.addEventListener('mouseenter', () => { closeBtn.style.background = 'rgba(255,255,255,0.1)'; closeBtn.style.color = '#fff'; }); closeBtn.addEventListener('mouseleave', () => { closeBtn.style.background = 'transparent'; closeBtn.style.color = 'rgba(255,255,255,0.6)'; }); closeBtn.addEventListener('click', () => { document.body.removeChild(overlay); currentModal = null; }); modalHeader.appendChild(modalTitle); modalHeader.appendChild(closeBtn); // Current domain indicator const currentDomainInfo = document.createElement('div'); currentDomainInfo.style.padding = '12px'; currentDomainInfo.style.background = 'rgba(34,197,94,0.1)'; currentDomainInfo.style.border = '1px solid rgba(34,197,94,0.3)'; currentDomainInfo.style.borderRadius = '8px'; currentDomainInfo.style.marginBottom = '20px'; currentDomainInfo.style.fontSize = '13px'; currentDomainInfo.innerHTML = `Current domain: ${currentHostname}`; // Domain list label const listLabel = document.createElement('div'); listLabel.textContent = 'Enabled Domains:'; listLabel.style.fontSize = '14px'; listLabel.style.fontWeight = '600'; listLabel.style.marginBottom = '12px'; listLabel.style.color = '#cbd5e1'; // Domain list container const domainList = document.createElement('div'); domainList.style.marginBottom = '16px'; function buildAdvancedSettings(container, config, configIndex, allConfigs, rerenderCallback) { container.innerHTML = ''; // Custom Interval Section const intervalSection = document.createElement('div'); intervalSection.style.marginBottom = '12px'; const intervalLabel = document.createElement('div'); intervalLabel.textContent = '⏱️ Custom Interval:'; intervalLabel.style.fontSize = '12px'; intervalLabel.style.fontWeight = '600'; intervalLabel.style.marginBottom = '6px'; intervalLabel.style.color = '#cbd5e1'; const intervalSelect = document.createElement('select'); intervalSelect.style.width = '100%'; intervalSelect.style.padding = '6px 8px'; intervalSelect.style.border = '1px solid rgba(255,255,255,0.2)'; intervalSelect.style.borderRadius = '6px'; intervalSelect.style.background = '#334155'; intervalSelect.style.color = '#f8fafc'; intervalSelect.style.fontSize = '12px'; intervalSelect.style.cursor = 'pointer'; // Add "Use Global" option const globalOption = document.createElement('option'); globalOption.value = 'null'; globalOption.text = 'Use Global Default'; globalOption.selected = !config.interval; intervalSelect.add(globalOption); // Add interval options INTERVAL_OPTIONS.forEach(opt => { const optionEl = document.createElement('option'); optionEl.value = String(opt.value); optionEl.text = opt.label; if (config.interval === opt.value) { optionEl.selected = true; } intervalSelect.add(optionEl); }); intervalSelect.addEventListener('change', () => { const val = intervalSelect.value; config.interval = val === 'null' ? null : parseInt(val, 10); allConfigs[configIndex] = config; saveDomainConfig(allConfigs); domainConfigs = allConfigs; rerenderCallback(); }); intervalSection.appendChild(intervalLabel); intervalSection.appendChild(intervalSelect); // Path Patterns Section const pathSection = document.createElement('div'); pathSection.style.marginBottom = '12px'; const pathLabel = document.createElement('div'); pathLabel.textContent = '📍 Path Patterns:'; pathLabel.style.fontSize = '12px'; pathLabel.style.fontWeight = '600'; pathLabel.style.marginBottom = '6px'; pathLabel.style.color = '#cbd5e1'; // Include patterns const includeLabel = document.createElement('div'); includeLabel.textContent = 'Include (matches these):'; includeLabel.style.fontSize = '11px'; includeLabel.style.marginBottom = '4px'; includeLabel.style.color = 'rgba(255,255,255,0.7)'; const includeList = document.createElement('div'); includeList.style.marginBottom = '8px'; const renderIncludeList = () => { includeList.innerHTML = ''; config.paths.include.forEach((pattern, i) => { const patternRow = document.createElement('div'); patternRow.style.display = 'flex'; patternRow.style.gap = '4px'; patternRow.style.marginBottom = '4px'; patternRow.style.alignItems = 'center'; const patternText = document.createElement('span'); patternText.textContent = pattern; patternText.style.flex = '1'; patternText.style.fontSize = '11px'; patternText.style.fontFamily = 'monospace'; patternText.style.padding = '4px 6px'; patternText.style.background = 'rgba(34,197,94,0.1)'; patternText.style.border = '1px solid rgba(34,197,94,0.3)'; patternText.style.borderRadius = '4px'; patternText.style.color = '#86efac'; const removeBtn = document.createElement('button'); removeBtn.textContent = '✕'; removeBtn.style.background = 'transparent'; removeBtn.style.border = 'none'; removeBtn.style.color = '#ef4444'; removeBtn.style.cursor = 'pointer'; removeBtn.style.padding = '2px 6px'; removeBtn.style.fontSize = '14px'; removeBtn.addEventListener('click', () => { if (config.paths.include.length > 1) { config.paths.include.splice(i, 1); allConfigs[configIndex] = config; saveDomainConfig(allConfigs); domainConfigs = allConfigs; renderIncludeList(); rerenderCallback(); } }); patternRow.appendChild(patternText); patternRow.appendChild(removeBtn); includeList.appendChild(patternRow); }); }; renderIncludeList(); const includeInput = document.createElement('input'); includeInput.type = 'text'; includeInput.placeholder = 'e.g., /computers* or regex:^/devices/.*'; includeInput.style.width = '100%'; includeInput.style.padding = '6px 8px'; includeInput.style.border = '1px solid rgba(255,255,255,0.2)'; includeInput.style.borderRadius = '4px'; includeInput.style.background = '#334155'; includeInput.style.color = '#f8fafc'; includeInput.style.fontSize = '11px'; includeInput.style.fontFamily = 'monospace'; includeInput.style.marginBottom = '4px'; includeInput.style.boxSizing = 'border-box'; const includeAddBtn = document.createElement('button'); includeAddBtn.textContent = '+ Add Include'; includeAddBtn.style.padding = '4px 8px'; includeAddBtn.style.border = 'none'; includeAddBtn.style.borderRadius = '4px'; includeAddBtn.style.background = '#22c55e'; includeAddBtn.style.color = 'white'; includeAddBtn.style.cursor = 'pointer'; includeAddBtn.style.fontSize = '11px'; includeAddBtn.style.fontWeight = '600'; includeAddBtn.addEventListener('click', () => { const pattern = includeInput.value.trim(); if (pattern) { config.paths.include.push(pattern); allConfigs[configIndex] = config; saveDomainConfig(allConfigs); domainConfigs = allConfigs; includeInput.value = ''; renderIncludeList(); rerenderCallback(); } }); includeInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') includeAddBtn.click(); }); // Exclude patterns const excludeLabel = document.createElement('div'); excludeLabel.textContent = 'Exclude (blocks these):'; excludeLabel.style.fontSize = '11px'; excludeLabel.style.marginBottom = '4px'; excludeLabel.style.marginTop = '12px'; excludeLabel.style.color = 'rgba(255,255,255,0.7)'; const excludeList = document.createElement('div'); excludeList.style.marginBottom = '8px'; const renderExcludeList = () => { excludeList.innerHTML = ''; if (config.paths.exclude.length === 0) { const emptyMsg = document.createElement('div'); emptyMsg.textContent = 'No exclude patterns'; emptyMsg.style.fontSize = '11px'; emptyMsg.style.color = 'rgba(255,255,255,0.4)'; emptyMsg.style.fontStyle = 'italic'; emptyMsg.style.padding = '4px'; excludeList.appendChild(emptyMsg); return; } config.paths.exclude.forEach((pattern, i) => { const patternRow = document.createElement('div'); patternRow.style.display = 'flex'; patternRow.style.gap = '4px'; patternRow.style.marginBottom = '4px'; patternRow.style.alignItems = 'center'; const patternText = document.createElement('span'); patternText.textContent = pattern; patternText.style.flex = '1'; patternText.style.fontSize = '11px'; patternText.style.fontFamily = 'monospace'; patternText.style.padding = '4px 6px'; patternText.style.background = 'rgba(239,68,68,0.1)'; patternText.style.border = '1px solid rgba(239,68,68,0.3)'; patternText.style.borderRadius = '4px'; patternText.style.color = '#fca5a5'; const removeBtn = document.createElement('button'); removeBtn.textContent = '✕'; removeBtn.style.background = 'transparent'; removeBtn.style.border = 'none'; removeBtn.style.color = '#ef4444'; removeBtn.style.cursor = 'pointer'; removeBtn.style.padding = '2px 6px'; removeBtn.style.fontSize = '14px'; removeBtn.addEventListener('click', () => { config.paths.exclude.splice(i, 1); allConfigs[configIndex] = config; saveDomainConfig(allConfigs); domainConfigs = allConfigs; renderExcludeList(); rerenderCallback(); }); patternRow.appendChild(patternText); patternRow.appendChild(removeBtn); excludeList.appendChild(patternRow); }); }; renderExcludeList(); const excludeInput = document.createElement('input'); excludeInput.type = 'text'; excludeInput.placeholder = 'e.g., /settings/* or regex:^/admin/.*'; excludeInput.style.width = '100%'; excludeInput.style.padding = '6px 8px'; excludeInput.style.border = '1px solid rgba(255,255,255,0.2)'; excludeInput.style.borderRadius = '4px'; excludeInput.style.background = '#334155'; excludeInput.style.color = '#f8fafc'; excludeInput.style.fontSize = '11px'; excludeInput.style.fontFamily = 'monospace'; excludeInput.style.marginBottom = '4px'; excludeInput.style.boxSizing = 'border-box'; const excludeAddBtn = document.createElement('button'); excludeAddBtn.textContent = '+ Add Exclude'; excludeAddBtn.style.padding = '4px 8px'; excludeAddBtn.style.border = 'none'; excludeAddBtn.style.borderRadius = '4px'; excludeAddBtn.style.background = '#ef4444'; excludeAddBtn.style.color = 'white'; excludeAddBtn.style.cursor = 'pointer'; excludeAddBtn.style.fontSize = '11px'; excludeAddBtn.style.fontWeight = '600'; excludeAddBtn.addEventListener('click', () => { const pattern = excludeInput.value.trim(); if (pattern) { config.paths.exclude.push(pattern); allConfigs[configIndex] = config; saveDomainConfig(allConfigs); domainConfigs = allConfigs; excludeInput.value = ''; renderExcludeList(); rerenderCallback(); } }); excludeInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') excludeAddBtn.click(); }); // Pattern help const patternHelp = document.createElement('div'); patternHelp.style.fontSize = '10px'; patternHelp.style.color = 'rgba(255,255,255,0.5)'; patternHelp.style.marginTop = '8px'; patternHelp.style.padding = '6px'; patternHelp.style.background = 'rgba(255,255,255,0.03)'; patternHelp.style.borderRadius = '4px'; patternHelp.innerHTML = ` Pattern types:
• Glob: /computers*, */devices/*
• Regex: regex:^/computers/.*\\.html$ `; // Assemble path section pathSection.appendChild(pathLabel); pathSection.appendChild(includeLabel); pathSection.appendChild(includeList); pathSection.appendChild(includeInput); pathSection.appendChild(includeAddBtn); pathSection.appendChild(excludeLabel); pathSection.appendChild(excludeList); pathSection.appendChild(excludeInput); pathSection.appendChild(excludeAddBtn); pathSection.appendChild(patternHelp); // Assemble container container.appendChild(intervalSection); container.appendChild(pathSection); } function renderDomainList() { domainList.innerHTML = ''; const configs = loadDomainConfig(); if (configs.length === 0) { const emptyMsg = document.createElement('div'); emptyMsg.textContent = 'No domains configured. Add one below.'; emptyMsg.style.padding = '12px'; emptyMsg.style.color = 'rgba(255,255,255,0.5)'; emptyMsg.style.fontStyle = 'italic'; emptyMsg.style.fontSize = '13px'; domainList.appendChild(emptyMsg); return; } configs.forEach((config, index) => { const configCard = document.createElement('div'); configCard.style.marginBottom = '12px'; configCard.style.border = '1px solid rgba(255,255,255,0.1)'; configCard.style.borderRadius = '8px'; configCard.style.background = 'rgba(255,255,255,0.05)'; configCard.style.overflow = 'hidden'; // Main domain row const domainRow = document.createElement('div'); domainRow.style.display = 'flex'; domainRow.style.alignItems = 'center'; domainRow.style.justifyContent = 'space-between'; domainRow.style.padding = '12px'; domainRow.style.cursor = 'pointer'; domainRow.style.transition = 'background 0.2s ease'; domainRow.addEventListener('mouseenter', () => { domainRow.style.background = 'rgba(255,255,255,0.05)'; }); domainRow.addEventListener('mouseleave', () => { domainRow.style.background = 'transparent'; }); const domainInfo = document.createElement('div'); domainInfo.style.flex = '1'; const domainText = document.createElement('div'); domainText.textContent = config.domain; domainText.style.fontFamily = 'monospace'; domainText.style.fontSize = '14px'; domainText.style.fontWeight = '600'; domainText.style.color = matchesDomainPattern(currentHostname, config.domain) ? '#22c55e' : '#f8fafc'; domainText.style.marginBottom = '4px'; const domainMeta = document.createElement('div'); domainMeta.style.fontSize = '11px'; domainMeta.style.color = 'rgba(255,255,255,0.5)'; const intervalText = config.interval ? formatDuration(config.interval) : 'Global'; const pathText = (config.paths.include.length === 1 && config.paths.include[0] === '*') ? 'All paths' : `${config.paths.include.length} path(s)`; domainMeta.textContent = `⏱️ ${intervalText} • 📍 ${pathText}`; domainInfo.appendChild(domainText); domainInfo.appendChild(domainMeta); const actionButtons = document.createElement('div'); actionButtons.style.display = 'flex'; actionButtons.style.gap = '4px'; const expandBtn = document.createElement('button'); expandBtn.textContent = '▼'; expandBtn.style.background = 'transparent'; expandBtn.style.border = 'none'; expandBtn.style.color = 'rgba(255,255,255,0.6)'; expandBtn.style.cursor = 'pointer'; expandBtn.style.padding = '4px 8px'; expandBtn.style.borderRadius = '4px'; expandBtn.style.fontSize = '12px'; expandBtn.style.transition = 'all 0.2s ease'; const deleteBtn = document.createElement('button'); deleteBtn.textContent = '🗑️'; deleteBtn.style.background = 'transparent'; deleteBtn.style.border = 'none'; deleteBtn.style.color = '#ef4444'; deleteBtn.style.cursor = 'pointer'; deleteBtn.style.padding = '4px 8px'; deleteBtn.style.borderRadius = '4px'; deleteBtn.style.fontSize = '16px'; deleteBtn.style.transition = 'all 0.2s ease'; deleteBtn.addEventListener('mouseenter', () => { deleteBtn.style.background = 'rgba(239,68,68,0.2)'; }); deleteBtn.addEventListener('mouseleave', () => { deleteBtn.style.background = 'transparent'; }); deleteBtn.addEventListener('click', (e) => { e.stopPropagation(); const updatedConfigs = configs.filter((_, i) => i !== index); saveDomainConfig(updatedConfigs); domainConfigs = updatedConfigs; enabledDomains = updatedConfigs.map(c => c.domain); renderDomainList(); }); actionButtons.appendChild(expandBtn); actionButtons.appendChild(deleteBtn); domainRow.appendChild(domainInfo); domainRow.appendChild(actionButtons); // Advanced settings panel (initially hidden) const advancedPanel = document.createElement('div'); advancedPanel.style.display = 'none'; advancedPanel.style.padding = '12px'; advancedPanel.style.borderTop = '1px solid rgba(255,255,255,0.1)'; advancedPanel.style.background = 'rgba(0,0,0,0.2)'; // Build advanced settings UI buildAdvancedSettings(advancedPanel, config, index, configs, renderDomainList); // Toggle expand/collapse let isExpanded = false; const toggleExpand = () => { isExpanded = !isExpanded; advancedPanel.style.display = isExpanded ? 'block' : 'none'; expandBtn.textContent = isExpanded ? '▲' : '▼'; expandBtn.style.background = isExpanded ? 'rgba(255,255,255,0.1)' : 'transparent'; }; expandBtn.addEventListener('click', (e) => { e.stopPropagation(); toggleExpand(); }); domainRow.addEventListener('click', toggleExpand); configCard.appendChild(domainRow); configCard.appendChild(advancedPanel); domainList.appendChild(configCard); }); } renderDomainList(); // Add domain section const addSection = document.createElement('div'); addSection.style.marginTop = '20px'; addSection.style.padding = '16px'; addSection.style.background = 'rgba(0,0,0,0.2)'; addSection.style.borderRadius = '8px'; addSection.style.border = '1px solid rgba(255,255,255,0.1)'; const addLabel = document.createElement('div'); addLabel.textContent = 'Add Domain:'; addLabel.style.fontSize = '14px'; addLabel.style.fontWeight = '600'; addLabel.style.marginBottom = '8px'; addLabel.style.color = '#cbd5e1'; const addInput = document.createElement('input'); addInput.type = 'text'; addInput.placeholder = 'e.g., *.jamfcloud.com or yourcompany.com'; addInput.style.width = '100%'; addInput.style.padding = '10px'; addInput.style.border = '1px solid rgba(255,255,255,0.2)'; addInput.style.borderRadius = '6px'; addInput.style.background = '#334155'; addInput.style.color = '#f8fafc'; addInput.style.fontSize = '13px'; addInput.style.fontFamily = 'monospace'; addInput.style.marginBottom = '8px'; addInput.style.boxSizing = 'border-box'; const addHint = document.createElement('div'); addHint.style.fontSize = '11px'; addHint.style.color = 'rgba(255,255,255,0.5)'; addHint.style.marginBottom = '12px'; addHint.innerHTML = 'Use * for wildcards. Examples: *.jamfcloud.com, jamf.company.com'; const addBtnRow = document.createElement('div'); addBtnRow.style.display = 'flex'; addBtnRow.style.gap = '8px'; const addBtn = document.createElement('button'); addBtn.textContent = '✚ Add Domain'; addBtn.style.flex = '1'; addBtn.style.padding = '10px'; addBtn.style.border = 'none'; addBtn.style.borderRadius = '6px'; addBtn.style.background = '#22c55e'; addBtn.style.color = 'white'; addBtn.style.cursor = 'pointer'; addBtn.style.fontWeight = '600'; addBtn.style.fontSize = '13px'; addBtn.style.transition = 'all 0.2s ease'; addBtn.addEventListener('mouseenter', () => { addBtn.style.background = '#16a34a'; }); addBtn.addEventListener('mouseleave', () => { addBtn.style.background = '#22c55e'; }); const testBtn = document.createElement('button'); testBtn.textContent = '🧪 Test'; testBtn.style.padding = '10px 16px'; testBtn.style.border = 'none'; testBtn.style.borderRadius = '6px'; testBtn.style.background = '#3b82f6'; testBtn.style.color = 'white'; testBtn.style.cursor = 'pointer'; testBtn.style.fontWeight = '600'; testBtn.style.fontSize = '13px'; testBtn.style.transition = 'all 0.2s ease'; testBtn.addEventListener('mouseenter', () => { testBtn.style.background = '#2563eb'; }); testBtn.addEventListener('mouseleave', () => { testBtn.style.background = '#3b82f6'; }); const feedbackMsg = document.createElement('div'); feedbackMsg.style.marginTop = '8px'; feedbackMsg.style.fontSize = '12px'; feedbackMsg.style.padding = '8px'; feedbackMsg.style.borderRadius = '6px'; feedbackMsg.style.display = 'none'; addBtn.addEventListener('click', () => { const domain = addInput.value.trim(); if (!domain) { feedbackMsg.textContent = '⚠️ Please enter a domain pattern'; feedbackMsg.style.background = 'rgba(239,68,68,0.2)'; feedbackMsg.style.color = '#fca5a5'; feedbackMsg.style.display = 'block'; return; } const domains = loadEnabledDomains(); if (domains.includes(domain)) { feedbackMsg.textContent = '⚠️ This domain is already in the list'; feedbackMsg.style.background = 'rgba(251,146,60,0.2)'; feedbackMsg.style.color = '#fdba74'; feedbackMsg.style.display = 'block'; return; } domains.push(domain); saveEnabledDomains(domains); enabledDomains = domains; addInput.value = ''; feedbackMsg.textContent = '✅ Domain added successfully!'; feedbackMsg.style.background = 'rgba(34,197,94,0.2)'; feedbackMsg.style.color = '#86efac'; feedbackMsg.style.display = 'block'; renderDomainList(); setTimeout(() => { feedbackMsg.style.display = 'none'; }, 3000); }); testBtn.addEventListener('click', () => { const domain = addInput.value.trim(); if (!domain) { feedbackMsg.textContent = '⚠️ Please enter a domain pattern to test'; feedbackMsg.style.background = 'rgba(239,68,68,0.2)'; feedbackMsg.style.color = '#fca5a5'; feedbackMsg.style.display = 'block'; return; } const matches = matchesDomainPattern(currentHostname, domain); if (matches) { feedbackMsg.textContent = `✅ Pattern "${domain}" matches current domain "${currentHostname}"`; feedbackMsg.style.background = 'rgba(34,197,94,0.2)'; feedbackMsg.style.color = '#86efac'; } else { feedbackMsg.textContent = `❌ Pattern "${domain}" does NOT match current domain "${currentHostname}"`; feedbackMsg.style.background = 'rgba(239,68,68,0.2)'; feedbackMsg.style.color = '#fca5a5'; } feedbackMsg.style.display = 'block'; }); // Allow Enter key to add domain addInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { addBtn.click(); } }); addBtnRow.appendChild(addBtn); addBtnRow.appendChild(testBtn); addSection.appendChild(addLabel); addSection.appendChild(addInput); addSection.appendChild(addHint); addSection.appendChild(addBtnRow); addSection.appendChild(feedbackMsg); // Assemble modal modal.appendChild(modalHeader); modal.appendChild(currentDomainInfo); modal.appendChild(listLabel); modal.appendChild(domainList); modal.appendChild(addSection); overlay.appendChild(modal); document.body.appendChild(overlay); // Close on overlay click overlay.addEventListener('click', (e) => { if (e.target === overlay) { document.body.removeChild(overlay); currentModal = null; } }); // Focus input setTimeout(() => addInput.focus(), 100); } // Toggle between mini and full mode // @param {boolean} [forceMode] - Optional: true for mini mode, false for full mode, undefined to toggle function toggleMiniMode(forceMode) { if (!refreshContainer) return; // If forceMode is provided, use it; otherwise toggle if (forceMode !== undefined) { isMiniMode = forceMode; } else { isMiniMode = !isMiniMode; } localStorage.setItem(STORAGE_KEY_MINI_MODE, String(isMiniMode)); if (isMiniMode) { // Enter mini mode refreshContainer.style.width = 'auto'; refreshContainer.style.minWidth = '120px'; refreshContainer.style.padding = '8px 12px'; refreshContainer.style.borderRadius = '20px'; refreshContainer.style.cursor = 'grab'; refreshContainer.setAttribute('aria-label', 'Auto refresh widget (collapsed)'); // Hide full mode elements if (uiElements.statusRow) uiElements.statusRow.style.display = 'none'; if (uiElements.refreshNowBtn) uiElements.refreshNowBtn.style.display = 'none'; if (uiElements.toggleBtn) uiElements.toggleBtn.style.display = 'none'; if (uiElements.intervalRow) uiElements.intervalRow.style.display = 'none'; if (uiElements.settingsBtn) uiElements.settingsBtn.style.display = 'none'; // Update header for mini mode if (uiElements.header) { uiElements.header.style.marginBottom = '0'; uiElements.header.style.paddingBottom = '0'; uiElements.header.style.borderBottom = 'none'; } // Hide title text, keep only icon if (uiElements.title) { uiElements.title.textContent = '🔄'; uiElements.title.style.fontSize = '18px'; } // Change minimize button to expand if (uiElements.minimizeBtn) { uiElements.minimizeBtn.innerHTML = '+'; uiElements.minimizeBtn.title = 'Expand widget'; } // Create or show mini mode timer if (!uiElements.miniModeTimer) { uiElements.miniModeTimer = document.createElement('div'); uiElements.miniModeTimer.style.fontSize = '14px'; uiElements.miniModeTimer.style.fontWeight = '500'; uiElements.miniModeTimer.style.display = 'inline-flex'; uiElements.miniModeTimer.style.alignItems = 'center'; uiElements.miniModeTimer.style.gap = '6px'; uiElements.miniModeTimer.style.marginLeft = '8px'; uiElements.miniModeTimer.style.marginRight = '12px'; uiElements.header.insertBefore(uiElements.miniModeTimer, uiElements.header.lastChild); } uiElements.miniModeTimer.style.display = 'inline-flex'; // Make entire widget clickable to expand refreshContainer.onclick = (e) => { // Don't toggle if user just finished dragging if (hasDragged) { hasDragged = false; return; } if (e.target !== uiElements.minimizeBtn && !e.target.closest('button')) { toggleMiniMode(); } }; updateUI(); } else { // Enter full mode refreshContainer.style.width = '300px'; refreshContainer.style.minWidth = 'auto'; refreshContainer.style.padding = '16px'; refreshContainer.style.borderRadius = '12px'; refreshContainer.style.cursor = 'grab'; refreshContainer.setAttribute('aria-label', 'Auto refresh widget (expanded)'); // Show full mode elements if (uiElements.statusRow) uiElements.statusRow.style.display = 'block'; if (uiElements.refreshNowBtn) uiElements.refreshNowBtn.style.display = 'block'; if (uiElements.toggleBtn) uiElements.toggleBtn.style.display = 'block'; if (uiElements.intervalRow) uiElements.intervalRow.style.display = 'flex'; if (uiElements.settingsBtn) uiElements.settingsBtn.style.display = 'block'; // Restore header if (uiElements.header) { uiElements.header.style.marginBottom = '12px'; uiElements.header.style.paddingBottom = '12px'; uiElements.header.style.borderBottom = '1px solid rgba(255,255,255,0.1)'; } // Restore title if (uiElements.title) { uiElements.title.textContent = '🔄 Auto Refresh'; uiElements.title.style.fontSize = '16px'; } // Change expand button back to minimize if (uiElements.minimizeBtn) { uiElements.minimizeBtn.innerHTML = '−'; uiElements.minimizeBtn.title = 'Minimize widget'; } // Hide mini mode timer if (uiElements.miniModeTimer) { uiElements.miniModeTimer.style.display = 'none'; } // Remove widget click handler refreshContainer.onclick = null; updateUI(); } // Ensure widget stays in viewport after resize const currentBottom = refreshContainer.style.bottom; const currentLeft = refreshContainer.style.left; const constrained = constrainToViewport(currentBottom, currentLeft, { duringDrag: false }); refreshContainer.style.bottom = constrained.bottom; refreshContainer.style.left = constrained.left; } function createUI() { const position = loadPosition(); // Create floating window container refreshContainer = document.createElement('div'); refreshContainer.id = instanceId; refreshContainer.style.position = 'fixed'; refreshContainer.style.bottom = position.bottom; refreshContainer.style.left = position.left; refreshContainer.style.width = '300px'; refreshContainer.style.background = 'linear-gradient(135deg, #1e293b 0%, #0f172a 100%)'; refreshContainer.style.border = '1px solid rgba(255,255,255,0.2)'; refreshContainer.style.borderRadius = '12px'; refreshContainer.style.boxShadow = '0 8px 32px rgba(0,0,0,0.4)'; refreshContainer.style.padding = '16px'; refreshContainer.style.fontFamily = 'system-ui,-apple-system,Segoe UI,Roboto,sans-serif'; refreshContainer.style.fontSize = '14px'; refreshContainer.style.color = '#f8fafc'; refreshContainer.style.zIndex = '99999'; refreshContainer.style.cursor = 'grab'; refreshContainer.style.userSelect = 'none'; // Header with title and drag handle const header = document.createElement('div'); header.style.display = 'flex'; header.style.alignItems = 'center'; header.style.justifyContent = 'space-between'; header.style.marginBottom = '12px'; header.style.paddingBottom = '12px'; header.style.borderBottom = '1px solid rgba(255,255,255,0.1)'; const title = document.createElement('div'); title.style.fontWeight = '600'; title.style.fontSize = '16px'; title.style.color = '#22c55e'; title.textContent = '🔄 Auto Refresh'; const headerButtons = document.createElement('div'); headerButtons.style.display = 'flex'; headerButtons.style.alignItems = 'center'; headerButtons.style.gap = '8px'; const minimizeBtn = document.createElement('button'); minimizeBtn.innerHTML = '−'; minimizeBtn.title = 'Minimize widget'; minimizeBtn.style.background = 'rgba(255,255,255,0.1)'; minimizeBtn.style.border = 'none'; minimizeBtn.style.borderRadius = '4px'; minimizeBtn.style.color = '#f8fafc'; minimizeBtn.style.cursor = 'pointer'; minimizeBtn.style.fontSize = '18px'; minimizeBtn.style.width = '24px'; minimizeBtn.style.height = '24px'; minimizeBtn.style.padding = '0'; minimizeBtn.style.display = 'flex'; minimizeBtn.style.alignItems = 'center'; minimizeBtn.style.justifyContent = 'center'; minimizeBtn.style.transition = 'all 0.2s ease'; minimizeBtn.addEventListener('mouseenter', () => { minimizeBtn.style.background = 'rgba(255,255,255,0.2)'; }); minimizeBtn.addEventListener('mouseleave', () => { minimizeBtn.style.background = 'rgba(255,255,255,0.1)'; }); minimizeBtn.addEventListener('click', (e) => { e.stopPropagation(); toggleMiniMode(); }); const dragHandle = document.createElement('div'); dragHandle.style.color = 'rgba(255,255,255,0.4)'; dragHandle.style.fontSize = '12px'; dragHandle.style.cursor = 'grab'; dragHandle.textContent = '⋮⋮'; headerButtons.appendChild(minimizeBtn); headerButtons.appendChild(dragHandle); header.appendChild(title); header.appendChild(headerButtons); // Status display const statusRow = document.createElement('div'); statusRow.style.marginBottom = '12px'; statusRow.style.padding = '12px'; statusRow.style.background = 'rgba(0,0,0,0.3)'; statusRow.style.borderRadius = '8px'; statusRow.style.border = '1px solid rgba(255,255,255,0.1)'; statusEl = document.createElement('div'); statusEl.style.fontSize = '13px'; statusEl.style.fontWeight = '500'; statusEl.style.marginBottom = '8px'; statusEl.style.color = '#22c55e'; statusRow.appendChild(statusEl); // Session counter const sessionCounterEl = document.createElement('div'); sessionCounterEl.style.fontSize = '11px'; sessionCounterEl.style.opacity = '0.7'; sessionCounterEl.style.marginTop = '6px'; sessionCounterEl.textContent = `Refreshed ${sessionRefreshCount} times this session`; statusRow.appendChild(sessionCounterEl); // Last refresh timestamp const lastRefreshEl = document.createElement('div'); lastRefreshEl.style.fontSize = '11px'; lastRefreshEl.style.opacity = '0.7'; lastRefreshEl.style.marginTop = '2px'; lastRefreshEl.textContent = lastRefreshTime ? `Last: ${formatTimeAgo(Date.now() - lastRefreshTime)}` : 'No refresh yet'; statusRow.appendChild(lastRefreshEl); // Session duration const sessionDurationEl = document.createElement('div'); sessionDurationEl.style.fontSize = '11px'; sessionDurationEl.style.opacity = '0.7'; sessionDurationEl.style.marginTop = '2px'; sessionDurationEl.textContent = `Session: ${formatSessionDuration(Date.now() - sessionStartTime)}`; statusRow.appendChild(sessionDurationEl); // Manual refresh button const refreshNowBtn = document.createElement('button'); refreshNowBtn.textContent = '🔄 Refresh Now'; refreshNowBtn.style.width = '100%'; refreshNowBtn.style.padding = '10px'; refreshNowBtn.style.marginBottom = '8px'; refreshNowBtn.style.border = 'none'; refreshNowBtn.style.borderRadius = '8px'; refreshNowBtn.style.background = '#3b82f6'; refreshNowBtn.style.color = 'white'; refreshNowBtn.style.cursor = 'pointer'; refreshNowBtn.style.fontWeight = '600'; refreshNowBtn.style.fontSize = '14px'; refreshNowBtn.style.transition = 'all 0.2s ease'; refreshNowBtn.addEventListener('mouseenter', () => { refreshNowBtn.style.background = '#2563eb'; refreshNowBtn.style.transform = 'translateY(-1px)'; }); refreshNowBtn.addEventListener('mouseleave', () => { refreshNowBtn.style.background = '#3b82f6'; refreshNowBtn.style.transform = 'translateY(0)'; }); refreshNowBtn.addEventListener('click', (e) => { e.stopPropagation(); sessionRefreshCount++; lastRefreshTime = Date.now(); localStorage.setItem(STORAGE_KEY_COUNT, String(sessionRefreshCount)); localStorage.setItem(STORAGE_KEY_LAST_REFRESH, String(lastRefreshTime)); window.location.reload(); }); // Toggle button const toggleBtn = document.createElement('button'); toggleBtn.textContent = enabled ? '⏸ Disable Auto-refresh' : '▶ Enable Auto-refresh'; toggleBtn.style.width = '100%'; toggleBtn.style.padding = '10px'; toggleBtn.style.marginBottom = '12px'; toggleBtn.style.border = 'none'; toggleBtn.style.borderRadius = '8px'; toggleBtn.style.background = enabled ? '#ef4444' : '#22c55e'; toggleBtn.style.color = 'white'; toggleBtn.style.cursor = 'pointer'; toggleBtn.style.fontWeight = '600'; toggleBtn.style.fontSize = '14px'; toggleBtn.style.transition = 'all 0.2s ease'; toggleBtn.addEventListener('mouseenter', () => { toggleBtn.style.transform = 'translateY(-1px)'; toggleBtn.style.boxShadow = '0 4px 12px rgba(0,0,0,0.3)'; }); toggleBtn.addEventListener('mouseleave', () => { toggleBtn.style.transform = 'translateY(0)'; toggleBtn.style.boxShadow = 'none'; }); toggleBtn.addEventListener('click', (e) => { e.stopPropagation(); enabled = !enabled; localStorage.setItem(STORAGE_KEY_ENABLED, String(enabled)); if (enabled) scheduleNext(); else nextRefreshAt = null; toggleBtn.textContent = enabled ? '⏸ Disable Auto-refresh' : '▶ Enable Auto-refresh'; toggleBtn.style.background = enabled ? '#ef4444' : '#22c55e'; updateUI(); }); // Interval selector const intervalRow = document.createElement('div'); intervalRow.style.display = 'flex'; intervalRow.style.alignItems = 'center'; intervalRow.style.gap = '8px'; intervalRow.style.marginBottom = '8px'; const intervalLabel = document.createElement('label'); intervalLabel.textContent = 'Interval:'; intervalLabel.style.fontSize = '13px'; intervalLabel.style.fontWeight = '500'; intervalLabel.style.minWidth = '60px'; const intervalSelect = document.createElement('select'); intervalSelect.style.flex = '1'; intervalSelect.style.padding = '6px 8px'; intervalSelect.style.border = '1px solid rgba(255,255,255,0.2)'; intervalSelect.style.borderRadius = '6px'; intervalSelect.style.background = '#334155'; intervalSelect.style.color = '#f8fafc'; intervalSelect.style.fontSize = '13px'; intervalSelect.style.cursor = 'pointer'; const populateOptions = (selectedValue) => { intervalSelect.innerHTML = ''; let hasMatch = false; INTERVAL_OPTIONS.forEach(opt => { const optionEl = document.createElement('option'); optionEl.text = opt.label; optionEl.value = String(opt.value); if (opt.value === selectedValue) { optionEl.selected = true; hasMatch = true; } intervalSelect.add(optionEl); }); if (!hasMatch && selectedValue) { const customOption = document.createElement('option'); customOption.text = `Custom (${formatDuration(selectedValue)})`; customOption.value = String(selectedValue); customOption.selected = true; intervalSelect.add(customOption); } }; populateOptions(refreshIntervalMs); intervalSelect.addEventListener('change', (e) => { e.stopPropagation(); const val = parseInt(intervalSelect.value, 10); if (Number.isFinite(val)) { refreshIntervalMs = Math.max(MIN_REFRESH_MS, Math.min(MAX_REFRESH_MS, val)); localStorage.setItem(STORAGE_KEY_INTERVAL, String(refreshIntervalMs)); populateOptions(refreshIntervalMs); if (enabled) scheduleNext(refreshIntervalMs); updateUI(); } }); intervalRow.appendChild(intervalLabel); intervalRow.appendChild(intervalSelect); // Settings button const settingsBtn = document.createElement('button'); settingsBtn.textContent = '⚙️ Domain Settings'; settingsBtn.style.width = '100%'; settingsBtn.style.padding = '10px'; settingsBtn.style.marginTop = '8px'; settingsBtn.style.border = 'none'; settingsBtn.style.borderRadius = '8px'; settingsBtn.style.background = '#64748b'; settingsBtn.style.color = 'white'; settingsBtn.style.cursor = 'pointer'; settingsBtn.style.fontWeight = '600'; settingsBtn.style.fontSize = '14px'; settingsBtn.style.transition = 'all 0.2s ease'; settingsBtn.addEventListener('mouseenter', () => { settingsBtn.style.background = '#475569'; settingsBtn.style.transform = 'translateY(-1px)'; }); settingsBtn.addEventListener('mouseleave', () => { settingsBtn.style.background = '#64748b'; settingsBtn.style.transform = 'translateY(0)'; }); settingsBtn.addEventListener('click', (e) => { e.stopPropagation(); openDomainManager(); }); // Assemble the UI refreshContainer.appendChild(header); refreshContainer.appendChild(statusRow); refreshContainer.appendChild(refreshNowBtn); refreshContainer.appendChild(toggleBtn); refreshContainer.appendChild(intervalRow); refreshContainer.appendChild(settingsBtn); // Store references for updates window.__ccRefreshSessionCounter = sessionCounterEl; window.__ccRefreshLastTimestamp = lastRefreshEl; window.__ccRefreshSessionDuration = sessionDurationEl; // Store UI element references for mini mode uiElements = { header, title, minimizeBtn, statusRow, refreshNowBtn, toggleBtn, intervalRow, settingsBtn, miniModeTimer: null }; // Add drag functionality header.addEventListener('mousedown', startDragging); // Store drag functions on window to prevent duplicates if (!window.__jamfAutoRefreshDragListeners) { window.__jamfAutoRefreshDragListeners = true; document.addEventListener('mousemove', drag); document.addEventListener('mouseup', stopDragging); } // Prevent text selection while dragging refreshContainer.addEventListener('dragstart', (e) => e.preventDefault()); // Handle window resize - reposition widget if it goes off-screen if (!window.__jamfAutoRefreshResizeListener) { window.__jamfAutoRefreshResizeListener = true; let resizeTimeout; window.addEventListener('resize', () => { // Debounce resize events clearTimeout(resizeTimeout); resizeTimeout = setTimeout(() => { if (!refreshContainer) return; // Get current position const currentBottom = refreshContainer.style.bottom; const currentLeft = refreshContainer.style.left; // Validate against new viewport size (maintain relative position) const constrained = constrainToViewport(currentBottom, currentLeft, { maintainRelativePosition: true }); // Update if position changed if (constrained.bottom !== currentBottom || constrained.left !== currentLeft) { refreshContainer.style.bottom = constrained.bottom; refreshContainer.style.left = constrained.left; // Save new position savePosition(constrained.bottom, constrained.left); console.log('[Jamf Auto-Refresh] Widget repositioned after resize:', constrained); } }, 250); // 250ms debounce }); } // Add to page document.body.appendChild(refreshContainer); // Restore mini mode state if it was previously minimized if (isMiniMode) { // Apply mini mode without animation on initial load refreshContainer.style.transition = 'none'; toggleMiniMode(true); // Force mini mode instead of toggling // Re-enable transitions after a frame requestAnimationFrame(() => { refreshContainer.style.transition = 'all 0.2s ease'; }); } else { // Add transition for future toggles refreshContainer.style.transition = 'all 0.2s ease'; } console.log('[Jamf Auto-Refresh] Mini mode restored:', isMiniMode); // Final validation after widget is in DOM and has dimensions // This ensures the position is correct based on actual rendered size setTimeout(() => { if (!refreshContainer) return; const finalConstrained = constrainToViewport( refreshContainer.style.bottom, refreshContainer.style.left, { maintainRelativePosition: false } // Don't scale on initial load, just constrain ); // Only update if position actually changed if (finalConstrained.bottom !== refreshContainer.style.bottom || finalConstrained.left !== refreshContainer.style.left) { refreshContainer.style.bottom = finalConstrained.bottom; refreshContainer.style.left = finalConstrained.left; savePosition(finalConstrained.bottom, finalConstrained.left); console.log('[Jamf Auto-Refresh] Widget position adjusted after render:', finalConstrained); } }, 0); console.log('[Jamf Auto-Refresh] Floating window created'); } function tick() { if (!refreshContainer) { clearInterval(tickTimer); return; } if (!enabled) { statusMessage = null; updateUI(); return; } if (!nextRefreshAt) { scheduleNext(); } const now = Date.now(); const remaining = nextRefreshAt - now; // Update last refresh timestamp display if it exists if (window.__ccRefreshLastTimestamp && lastRefreshTime) { window.__ccRefreshLastTimestamp.textContent = `Last: ${formatTimeAgo(now - lastRefreshTime)}`; } // Update session duration display if it exists if (window.__ccRefreshSessionDuration) { window.__ccRefreshSessionDuration.textContent = `Session: ${formatSessionDuration(now - sessionStartTime)}`; } if (remaining <= 0) { if (isUserTyping()) { // Delay refresh slightly to avoid interrupting text entry scheduleNext(DELAY_WHILE_TYPING_MS); statusMessage = 'Refresh delayed while typing…'; } else { statusMessage = null; updateUI(); sessionRefreshCount++; lastRefreshTime = Date.now(); // Save to localStorage before refresh localStorage.setItem(STORAGE_KEY_COUNT, String(sessionRefreshCount)); localStorage.setItem(STORAGE_KEY_LAST_REFRESH, String(lastRefreshTime)); // Update session counter and last refresh if elements exist if (window.__ccRefreshSessionCounter) { window.__ccRefreshSessionCounter.textContent = `Refreshed ${sessionRefreshCount} times this session`; } if (window.__ccRefreshLastTimestamp) { window.__ccRefreshLastTimestamp.textContent = `Last: just now`; } window.location.reload(); return; // In case reload is blocked for some reason } } else { statusMessage = null; } updateUI(); } function init() { createUI(); updateUI(); tickTimer = setInterval(tick, 1000); // Handle AngularJS navigation and SPA changes function handleUrlChange() { // Reset the timer on SPA navigation for clarity if (enabled) scheduleNext(); updateUI(); } // Only override history methods if not already done if (!window.__jamfAutoRefreshHistoryPatched) { window.__jamfAutoRefreshHistoryPatched = true; const originalPushState = history.pushState; const originalReplaceState = history.replaceState; history.pushState = function () { const ret = originalPushState.apply(this, arguments); handleUrlChange(); return ret; }; history.replaceState = function () { const ret = originalReplaceState.apply(this, arguments); handleUrlChange(); return ret; }; // Add popstate listener only once window.addEventListener('popstate', handleUrlChange); } // Watch for Angular route changes if available if (window.angular) { try { const rootScope = window.angular.element(document).scope().$root; if (rootScope) { rootScope.$on('$routeChangeSuccess', handleUrlChange); rootScope.$on('$stateChangeSuccess', handleUrlChange); } } catch (e) { // Ignore Angular integration errors } } // Run tick immediately to set initial countdown tick(); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init, { once: true }); } else { init(); } })();