PageSourceSearch

https://factlens.pro/admin-inspector-v70.js

js factlens.pro collected 2026-10-02 02:06:06 UTC 8,694 bytes, 240 lines download raw bytes

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.