(function () { 'use strict'; const ENDPOINT = '/visitor_log.php'; const VISITOR_KEY = 'kvhs_visitor_id_v1'; const SESSION_KEY = 'kvhs_visitor_session_v1'; const SESSION_TTL_MS = 30 * 60 * 1000; let pageStartMs = Date.now(); let maxScrollPercent = 0; let lastScrollReportPercent = 0; let visibleSectionsSeen = {}; let audioHooked = false; function now() { return Date.now(); } function makeId(prefix) { if (window.crypto && typeof window.crypto.randomUUID === 'function') { return prefix + '_' + window.crypto.randomUUID(); } return prefix + '_' + now().toString(36) + '_' + Math.random().toString(36).slice(2); } function safeGet(key) { try { return localStorage.getItem(key) || ''; } catch (err) { return ''; } } function safeSet(key, value) { try { localStorage.setItem(key, value); } catch (err) {} } function visitorId() { let id = safeGet(VISITOR_KEY); if (!id) { id = makeId('kvhs_visitor'); safeSet(VISITOR_KEY, id); } return id; } function sessionId() { let row = null; try { row = JSON.parse(safeGet(SESSION_KEY) || 'null'); } catch (err) { row = null; } if (!row || !row.id || !row.updated_at || (now() - Number(row.updated_at)) > SESSION_TTL_MS) { row = { id: makeId('kvhs_session'), created_at: now(), updated_at: now() }; } else { row.updated_at = now(); } safeSet(SESSION_KEY, JSON.stringify(row)); return row.id; } function connectionLabel() { const c = navigator.connection || navigator.mozConnection || navigator.webkitConnection; if (!c) return ''; return [ c.effectiveType || '', c.downlink ? String(c.downlink) + 'mbps' : '', c.saveData ? 'save-data' : '' ].filter(Boolean).join(' / '); } function pagePath() { return location.pathname + location.search; } function pageAgeSeconds() { return Math.max(0, Math.round((Date.now() - pageStartMs) / 1000)); } function scrollPercent() { const doc = document.documentElement || document.body; const body = document.body; const scrollTop = window.scrollY || doc.scrollTop || body.scrollTop || 0; const scrollHeight = Math.max( doc.scrollHeight || 0, body.scrollHeight || 0, doc.offsetHeight || 0, body.offsetHeight || 0 ); const viewportHeight = window.innerHeight || doc.clientHeight || 0; const possible = Math.max(1, scrollHeight - viewportHeight); return Math.max(0, Math.min(100, Math.round((scrollTop / possible) * 100))); } function updateMaxScroll() { maxScrollPercent = Math.max(maxScrollPercent, scrollPercent()); return maxScrollPercent; } function screenText() { return window.screen ? String(screen.width || '') + 'x' + String(screen.height || '') : ''; } function viewportText() { return String(window.innerWidth || '') + 'x' + String(window.innerHeight || ''); } function loadTiming() { let out = { load_ms: null, dom_ready_ms: null, first_byte_ms: null, transfer_ms: null }; try { const nav = performance.getEntriesByType && performance.getEntriesByType('navigation')[0]; if (nav) { out.load_ms = nav.loadEventEnd ? Math.round(nav.loadEventEnd) : null; out.dom_ready_ms = nav.domContentLoadedEventEnd ? Math.round(nav.domContentLoadedEventEnd) : null; out.first_byte_ms = nav.responseStart ? Math.round(nav.responseStart) : null; out.transfer_ms = nav.responseEnd && nav.responseStart ? Math.round(nav.responseEnd - nav.responseStart) : null; } } catch (err) {} return out; } function basePayload(eventName, extra) { const timing = loadTiming(); return Object.assign({ event: eventName, visitor_id: visitorId(), session_id: sessionId(), page: location.href, page_path: pagePath(), page_title: document.title || '', referrer: document.referrer || '', user_agent: navigator.userAgent || '', language: navigator.language || '', languages: Array.isArray(navigator.languages) ? navigator.languages.join(', ') : '', timezone: (Intl.DateTimeFormat && Intl.DateTimeFormat().resolvedOptions().timeZone) || '', screen: screenText(), viewport: viewportText(), pixel_ratio: String(window.devicePixelRatio || ''), color_scheme: window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light', touch: ('ontouchstart' in window) || (navigator.maxTouchPoints > 0), connection: connectionLabel(), /* MAX MODE fields */ page_age_seconds: pageAgeSeconds(), max_scroll_percent: updateMaxScroll(), visible_sections: Object.keys(visibleSectionsSeen).join(', '), /* old timing field kept for compatibility */ load_ms: timing.load_ms, /* expanded timing fields */ dom_ready_ms: timing.dom_ready_ms, first_byte_ms: timing.first_byte_ms, transfer_ms: timing.transfer_ms }, extra || {}); } function send(eventName, extra) { const payload = JSON.stringify(basePayload(eventName, extra)); if (navigator.sendBeacon) { const blob = new Blob([payload], { type: 'application/json' }); if (navigator.sendBeacon(ENDPOINT, blob)) { return; } } fetch(ENDPOINT, { method: 'POST', cache: 'no-store', keepalive: true, headers: { 'Content-Type': 'application/json' }, body: payload }).catch(function () {}); } function trimmedText(el) { if (!el) return ''; return String(el.innerText || el.textContent || el.getAttribute('aria-label') || el.title || '') .replace(/\s+/g, ' ') .trim() .slice(0, 180); } function classifyClick(target, href) { const hay = [ href || '', target && target.id || '', target && target.className || '', trimmedText(target) ].join(' ').toLowerCase(); if ( hay.includes('cast1.sql2.smrn.com') || hay.includes('listen live') || hay.includes('listen now') || hay.includes('stream') ) { return 'listen_click'; } if (hay.includes('notes')) return 'notes_click'; if (hay.includes('listening-room') || hay.includes('listening room')) return 'listening_room_click'; if (hay.includes('calendar')) return 'calendar_click'; if (hay.includes('music_history')) return 'music_history_click'; if (hay.includes('heart') || hay.includes('♥') || hay.includes('♡')) return 'heart_click'; if (hay.includes('about this song') || hay.includes('current transmission')) return 'about_song_click'; return 'click'; } function isOutboundHref(href) { if (!href) return false; try { const url = new URL(href, location.href); return url.hostname && url.hostname !== location.hostname; } catch (err) { return false; } } function sectionNameFromElement(el) { if (!el) return ''; const id = String(el.id || '').toLowerCase(); const cls = String(el.className || '').toLowerCase(); const txt = trimmedText(el).toLowerCase(); const hay = [id, cls, txt].join(' '); if (hay.includes('now playing') || hay.includes('now-playing') || hay.includes('album')) return 'now_playing'; if (hay.includes('about this song') || hay.includes('current transmission')) return 'about_song'; if (hay.includes('calendar')) return 'calendar'; if (hay.includes('listener notes') || hay.includes('notes')) return 'listener_notes'; if (hay.includes('listening room')) return 'listening_room'; if (hay.includes('recent transmissions') || hay.includes('earlier transmissions')) return 'transmission_log'; if (hay.includes('listen live') || hay.includes('kvhs-player')) return 'listen_bar'; return ''; } function scanVisibleSections() { const selectors = [ '#song-info-box', '#home-calendar-mini', '#home-notes', '#kvhs-player', '.now-playing-card', '.left-card', '.side-card', '.log-housing', '.listening-room-card', '.bottom-bar', '.live-player-nav' ]; const seenNow = []; selectors.forEach(function (selector) { let nodes = []; try { nodes = Array.prototype.slice.call(document.querySelectorAll(selector)); } catch (err) { nodes = []; } nodes.forEach(function (el) { const rect = el.getBoundingClientRect(); if ( rect.width > 0 && rect.height > 0 && rect.bottom > 0 && rect.top < (window.innerHeight || document.documentElement.clientHeight || 0) ) { const name = sectionNameFromElement(el); if (name && !visibleSectionsSeen[name]) { visibleSectionsSeen[name] = true; seenNow.push(name); } } }); }); if (seenNow.length) { send('section_view', { section_names: seenNow.join(', '), section_count: seenNow.length }); } } function hookAudioPlayer() { if (audioHooked) return; const audio = document.querySelector('audio, #kvhs-player'); if (!audio) return; audioHooked = true; ['play', 'pause', 'ended', 'waiting', 'playing', 'error', 'stalled'].forEach(function (name) { audio.addEventListener(name, function () { let currentTime = null; let duration = null; let paused = null; let src = ''; try { currentTime = Math.round(audio.currentTime || 0); duration = isFinite(audio.duration) ? Math.round(audio.duration || 0) : null; paused = !!audio.paused; src = audio.currentSrc || audio.src || ''; } catch (err) {} send('audio_' + name, { audio_current_time: currentTime, audio_duration: duration, audio_paused: paused, audio_src: src }); }); }); } function setupScrollReporting() { let timer = null; window.addEventListener('scroll', function () { updateMaxScroll(); if (timer) return; timer = setTimeout(function () { timer = null; const current = updateMaxScroll(); if (current >= lastScrollReportPercent + 25 || current >= 95) { lastScrollReportPercent = current; send('scroll_depth', { scroll_percent: current }); } scanVisibleSections(); }, 700); }, { passive: true }); } document.addEventListener('click', function (event) { const target = event.target && event.target.closest ? event.target.closest('a, button, summary, input, select, textarea, [role="button"]') : null; if (!target) return; const href = target.href || target.getAttribute('href') || ''; const eventName = isOutboundHref(href) ? 'outbound_click' : classifyClick(target, href); send(eventName, { event_target: String(target.tagName || '').toLowerCase() + (target.id ? '#' + target.id : ''), event_text: trimmedText(target), event_href: href, outbound_host: isOutboundHref(href) ? (new URL(href, location.href)).hostname : '' }); }, true); window.addEventListener('error', function (event) { send('js_error', { error_message: String(event.message || '').slice(0, 300), error_source: String(event.filename || '').slice(0, 240), error_line: event.lineno || null, error_column: event.colno || null }); }); window.addEventListener('unhandledrejection', function (event) { let reason = ''; try { reason = event.reason && event.reason.message ? event.reason.message : String(event.reason || ''); } catch (err) { reason = 'Unhandled promise rejection'; } send('js_error', { error_message: reason.slice(0, 300), error_source: 'unhandledrejection', error_line: null, error_column: null }); }); window.addEventListener('load', function () { hookAudioPlayer(); setupScrollReporting(); scanVisibleSections(); send('page_view'); setTimeout(function () { hookAudioPlayer(); scanVisibleSections(); send('page_ready'); }, 1200); setTimeout(function () { scanVisibleSections(); }, 3500); }); document.addEventListener('visibilitychange', function () { const eventName = document.visibilityState === 'hidden' ? 'page_hidden' : 'page_visible'; send(eventName, { page_age_seconds: pageAgeSeconds(), max_scroll_percent: updateMaxScroll() }); }); window.addEventListener('beforeunload', function () { send('page_exit', { page_age_seconds: pageAgeSeconds(), max_scroll_percent: updateMaxScroll() }); }); setInterval(function () { if (document.visibilityState !== 'hidden') { hookAudioPlayer(); scanVisibleSections(); send('heartbeat', { page_age_seconds: pageAgeSeconds(), max_scroll_percent: updateMaxScroll() }); } }, 60000); window.KVHSVisitorPing = { send: send }; })();