1(() => { 2 'use strict'; 3 if (window.__factlensAdminInspectorV70) return; 4 window.__factlensAdminInspectorV70 = true; 5 6 const MAX_URLS = 120; 7 let enhanceQueued = false; 8 9 function safeHttpUrl(value) { 10 try { 11 const url = new URL(String(value || '').trim()); 12 return ['http:', 'https:'].includes(url.protocol) ? url.href : ''; 13 } catch { 14 return ''; 15 } 16 } 17 18 function cleanLabel(value) { 19 return String(value || '') 20 .replace(/\[(\d+)\]/g, ' $1 ') 21 .replace(/[._-]+/g, ' ') 22 .replace(/\s+/g, ' ') 23 .trim() 24 .slice(0, 90) || 'Retained URL'; 25 } 26 27 function urlCandidates(value) { 28 if (typeof value !== 'string') return []; 29 const exact = safeHttpUrl(value); 30 if (exact) return [exact]; 31 return [...value.matchAll(/https?:\/\/[^\s<>"'`]+/gi)] 32 .map((match) => safeHttpUrl(match[0].replace(/[),.;!?]+$/g, ''))) 33 .filter(Boolean); 34 } 35 36 function collectRetainedUrls(root) { 37 const out = []; 38 const seenUrls = new Set(); 39 const seenObjects = new WeakSet(); 40 41 const add = (url, path) => { 42 if (!url || seenUrls.has(url) || out.length >= MAX_URLS) return; 43 seenUrls.add(url); 44 out.push({ 45 url, 46 label: cleanLabel(path.join(' ⺠')), 47 priority: path.at(-1) === 'page_url' ? 0 : /(?:page|video|youtube|post|media).*url/i.test(path.at(-1) || '') ? 1 : 2, 48 }); 49 }; 50 51 const visit = (value, path = [], depth = 0) => { 52 if (value == null || depth > 14 || out.length >= MAX_URLS) return; 53 if (typeof value === 'string') { 54 for (const url of urlCandidates(value)) add(url, path); 55 return; 56 } 57 if (typeof value !== 'object') return; 58 if (seenObjects.has(value)) return; 59 seenObjects.add(value); 60 if (Array.isArray(value)) { 61 value.forEach((item, index) => visit(item, [...path, `[${index}]`], depth + 1)); 62 return; 63 } 64 for (const [key, item] of Object.entries(value)) visit(item, [...path, key], depth + 1); 65 }; 66 67 visit(root); 68 return out.sort((left, right) => left.priority - right.priority || left.label.localeCompare(right.label)); 69 } 70 71 function validVerdictColor(value) { 72 const color = String(value || '').trim(); 73 return /^#[0-9a-f]{6}$/i.test(color) ? color : ''; 74 } 75 76 function claimKey(value) { 77 return String(value || '').toLowerCase().replace(/[^\p{L}\p{N}]+/gu, ' ').trim().slice(0, 500); 78 } 79 80 function collectVerdictColors(root) { 81 const colors = new Map(); 82 const seenObjects = new WeakSet(); 83 84 const visit = (value, depth = 0) => { 85 if (!value || depth > 14 || typeof value !== 'object') return; 86 if (seenObjects.has(value)) return; 87 seenObjects.add(value); 88 if (Array.isArray(value)) { 89 value.forEach((item) => visit(item, depth + 1)); 90 return; 91 } 92 93 const claim = claimKey(value.claim || value.claim_text || ''); 94 const explicit = validVerdictColor(value.verdictColor) 95 || validVerdictColor(value.verdict_color) 96 || validVerdictColor(value.color); 97 if (claim && explicit && !colors.has(claim)) colors.set(claim, explicit); 98 Object.values(value).forEach((item) => visit(item, depth + 1)); 99 }; 100 101 visit(root); 102 return colors; 103 } 104 105 function retainedDetail(modal) { 106 const details = [...modal.querySelectorAll('details')]; 107 const raw = details.find((item) => item.querySelector('summary')?.textContent?.includes('Complete raw session-detail JSON')); 108 const text = raw?.querySelector('pre')?.textContent || ''; 109 if (!text.trim()) return null; 110 try { return JSON.parse(text); } catch { return null; } 111 } 112 113 function urlSection(modal) { 114 return [...modal.querySelectorAll('.v50-section')] 115 .find((section) => section.querySelector('.v50-section-title')?.textContent?.includes('Source / page / media URLs')) || null; 116 } 117 118 function makeUrlCard(item) { 119 const card = document.createElement('div'); 120 card.className = 'v78-url-card v70-url-card'; 121 122 const label = document.createElement('span'); 123 label.textContent = item.label; 124 const value = document.createElement('strong'); 125 value.textContent = item.url; 126 127 const actions = document.createElement('div'); 128 actions.className = 'v70-url-actions'; 129 const open = document.createElement('a'); 130 open.href = item.url; 131 open.target = '_blank'; 132 open.rel = 'noopener noreferrer'; 133 open.textContent = 'Open'; 134 const copy = document.createElement('button'); 135 copy.type = 'button'; 136 copy.dataset.v70CopyUrl = item.url; 137 copy.setAttribute('data-v70-copy-url', item.url); 138 copy.textContent = 'Copy'; 139 actions.append(open, copy); 140 card.append(label, value, actions); 141 return card; 142 } 143 144 function renderRetainedUrls(modal, detail) { 145 const section = urlSection(modal); 146 if (!section) return; 147 const urls = collectRetainedUrls(detail); 148 const previous = [...section.children].filter((child) => !child.classList.contains('v50-section-title'));
149 previous.forEach((child) => child.remove()); 150 if (!urls.length) { 151 const empty = document.createElement('div'); 152 empty.className = 'v50-empty'; 153 empty.textContent = 'No HTTP/HTTPS page, video, post, media, evidence, or source URL was retained for this session.'; 154 section.append(empty); 155 return; 156 } 157 const grid = document.createElement('div'); 158 grid.className = 'v78-url-grid'; 159 urls.forEach((item) => grid.append(makeUrlCard(item))); 160 section.append(grid); 161 } 162 163 function applyVerdictColors(modal, detail) { 164 const colors = collectVerdictColors(detail); 165 if (!colors.size) return; 166 modal.querySelectorAll('.v78-claim').forEach((card) => { 167 const key = claimKey(card.querySelector('h3')?.textContent || ''); 168 const color = colors.get(key); 169 if (!color) return; 170 card.style.setProperty('--v78-verdict', color); 171 card.dataset.v70VerdictColor = color; 172 card.setAttribute('data-v70-verdict-color', color); 173 }); 174 } 175 176 function addStyles() { 177 if (document.getElementById('factlens-admin-inspector-v70-style')) return; 178 const style = document.createElement('style'); 179 style.id = 'factlens-admin-inspector-v70-style'; 180 style.textContent = ` 181 .v70-url-card{color:inherit;text-decoration:none}.v70-url-actions{display:flex;gap:6px;margin-top:5px}.v70-url-actions a,.v70-url-actions button{appearance:none;border:1px solid var(--border);border-radius:7px;background:transparent;color:inherit;padding:5px 8px;font:700 .62rem/1 inherit;cursor:pointer;text-decoration:none}.v70-url-actions a:hover,.v70-url-actions button:hover{border-color:var(--border-2)} 182 .v78-claim[data-v70-verdict-color] .v78-claim-head strong{color:var(--v78-verdict)} 183 `; 184 document.head.append(style); 185 } 186 187 async function copyUrl(button) { 188 const url = safeHttpUrl(button?.dataset?.v70CopyUrl || button?.getAttribute('data-v70-copy-url')); 189 if (!url) return; 190 const original = button.textContent; 191 try { 192 await navigator.clipboard.writeText(url); 193 button.textContent = 'Copied'; 194 } catch { 195 const input = document.createElement('textarea'); 196 input.value = url; 197 input.setAttribute('readonly', ''); 198 input.style.cssText = 'position:fixed;opacity:0;pointer-events:none'; 199 document.body.append(input); 200 input.select(); 201 try { document.execCommand('copy'); button.textContent = 'Copied'; } catch { button.textContent = 'Copy failed'; } 202 input.remove(); 203 } 204 setTimeout(() => { if (button.isConnected) button.textContent = original; }, 1200); 205 } 206 207 function enhanceModal(modal) { 208 if (!modal || !modal.querySelector('.v78-admin-modal')) return; 209 const detail = retainedDetail(modal); 210 if (!detail) return; 211 const signature = String(detail?.session?.id || detail?.session_id || modal.querySelector('header p')?.textContent || 'session'); 212 if (modal.dataset.v70Enhanced === signature) return; 213 addStyles(); 214 renderRetainedUrls(modal, detail); 215 applyVerdictColors(modal, detail); 216 modal.dataset.v70Enhanced = signature; 217 } 218 219 function enhanceCurrentModal() { 220 enhanceQueued = false; 221 enhanceModal(document.getElementById('factlens-v78-admin-modal')); 222 } 223 224 function queueEnhance() { 225 if (enhanceQueued) return; 226 enhanceQueued = true; 227 queueMicrotask(enhanceCurrentModal); 228 } 229 230 document.addEventListener('click', (event) => { 231 const copy = event.target?.closest?.('[data-v70-copy-url]'); 232 if (copy) { 233 event.preventDefault(); 234 void copyUrl(copy); 235 } 236 }); 237 238 new MutationObserver(queueEnhance).observe(document.documentElement, { childList: true, subtree: true }); 239 queueEnhance(); 240})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.