bug-report-js 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +144 -0
- package/background.js +279 -0
- package/content.js +905 -0
- package/icons/icon128.png +0 -0
- package/icons/icon16.png +0 -0
- package/icons/icon48.png +0 -0
- package/manifest.json +43 -0
- package/package.json +27 -0
- package/popup.css +289 -0
- package/popup.html +92 -0
- package/popup.js +126 -0
- package/report-template.js +623 -0
- package/sanitizer.js +262 -0
- package/website/README.md +282 -0
- package/website/bug-report.js +1089 -0
- package/website/docs.html +241 -0
- package/website/index.html +996 -0
|
@@ -0,0 +1,1089 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bug Report Widget — Website Edition (v2.0.0)
|
|
3
|
+
* Drop-in script for any website. No extension needed.
|
|
4
|
+
* Usage: <script src="bug-report.js"></script>
|
|
5
|
+
*
|
|
6
|
+
* v2.0.0 Changes:
|
|
7
|
+
* - Ist/Soll user prompts before report generation
|
|
8
|
+
* - Visual capture via layout2vector Canvas Writer with click-path annotations
|
|
9
|
+
* - HTML dashboard export instead of JSON
|
|
10
|
+
*/
|
|
11
|
+
(function (root, factory) {
|
|
12
|
+
if (typeof define === 'function' && define.amd) {
|
|
13
|
+
define([], factory);
|
|
14
|
+
} else if (typeof module === 'object' && module.exports) {
|
|
15
|
+
module.exports = factory();
|
|
16
|
+
} else {
|
|
17
|
+
root.BugReportWidget = factory();
|
|
18
|
+
}
|
|
19
|
+
}(typeof self !== 'undefined' ? self : this, function () {
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
const VERSION = '2.0.0';
|
|
23
|
+
// These are now defaults in config
|
|
24
|
+
|
|
25
|
+
let config = {
|
|
26
|
+
language: 'de',
|
|
27
|
+
icon: '🐛',
|
|
28
|
+
primaryColor: '#6366f1',
|
|
29
|
+
primaryColorHover: '#7577f5',
|
|
30
|
+
tooltipMessage: null,
|
|
31
|
+
limits: {
|
|
32
|
+
interactions: 50,
|
|
33
|
+
console: 100,
|
|
34
|
+
errors: 50,
|
|
35
|
+
network: 200
|
|
36
|
+
},
|
|
37
|
+
sanitization: {
|
|
38
|
+
safeParams: [
|
|
39
|
+
// Pagination & Navigation
|
|
40
|
+
'q','query','search','keyword','term','id','pid','post_id','item_id','article_id','slug','path','route',
|
|
41
|
+
'page','p','limit','offset','skip','take','cursor','start','end','per_page','size','index','first','last','next','prev','before','after',
|
|
42
|
+
// Sorting & Filtering
|
|
43
|
+
'sort','order','orderby','sortby','dir','direction','filter','max','min','category','tag','type','status','state','date','year','month','day',
|
|
44
|
+
// UI & Display
|
|
45
|
+
'view','mode','display','format','layout','theme','tab','panel','step','section','anchor','eventorigin',
|
|
46
|
+
// App State & Routing
|
|
47
|
+
'action','method','module','component','feature','flag','variant','experiment','version','v',
|
|
48
|
+
// Localization
|
|
49
|
+
'lang','locale','language','hl','gl','country','region','currency',
|
|
50
|
+
// Marketing & Analytics
|
|
51
|
+
'ref','source','utm_source','utm_medium','utm_campaign','utm_term','utm_content','gclid','fbclid','msclkid','mc_cid','mc_eid'
|
|
52
|
+
],
|
|
53
|
+
sensitiveParams: [
|
|
54
|
+
'token','access_token','refresh_token','id_token','auth_token',
|
|
55
|
+
'session','sessionid','session_id','sid','apikey','api_key','key','client_secret','secret',
|
|
56
|
+
'password','passwd','pwd','email','mail','e-mail',
|
|
57
|
+
'userid','user_id','uid','customerid','customer_id','orderid','order_id',
|
|
58
|
+
'ssn','credit_card','cc','cvv','auth','authorization','bearer',
|
|
59
|
+
'code','otp','verification','reset_token','nonce','csrf','xsrf'
|
|
60
|
+
],
|
|
61
|
+
patterns: [
|
|
62
|
+
{ name:'email', p:/[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}/g, r:'[REDACTED_EMAIL]' },
|
|
63
|
+
{ name:'phone', p:/(?<![a-zA-Z0-9])(?:\+?\d{1,4}[\s\-.]?)?\(?\d{2,4}\)?[\s\-.]?\d{3,4}[\s\-.]?\d{3,5}(?![a-zA-Z0-9])/g, r:'[REDACTED_PHONE]' },
|
|
64
|
+
{ name:'bearer_token', p:/Bearer\s+[A-Za-z0-9\-._~+/]+=*/gi, r:'Bearer [REDACTED_TOKEN]' },
|
|
65
|
+
{ name:'jwt', p:/eyJ[A-Za-z0-9\-_]+\.eyJ[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_.+/=]*/g, r:'[REDACTED_JWT]' },
|
|
66
|
+
{ name:'api_key', p:/(?:api[_\-]?key|apikey)\s*[:=]\s*["']?[A-Za-z0-9\-._~+/]{8,}["']?/gi, r:'[REDACTED_API_KEY]' },
|
|
67
|
+
{ name:'generic_secret', p:/(?:secret|private[_\-]?key|client[_\-]?secret)\s*[:=]\s*["']?[A-Za-z0-9\-._~+/]{8,}["']?/gi, r:'[REDACTED_SECRET]' },
|
|
68
|
+
{ name:'password_field', p:/(?:password|passwd|pwd)\s*[:=]\s*["']?[^\s"',}{]{1,}["']?/gi, r:'[REDACTED_PASSWORD]' },
|
|
69
|
+
{ name:'session_id', p:/(?:session[_\-]?id|sid|jsessionid|phpsessid)\s*[:=]\s*["']?[A-Za-z0-9\-._]{8,}["']?/gi, r:'[REDACTED_SESSION]' },
|
|
70
|
+
{ name:'cookie', p:/(?:cookie|set-cookie)\s*[:=]\s*["']?[^\n"']{8,}["']?/gi, r:'[REDACTED_COOKIE]' },
|
|
71
|
+
{ name:'authorization', p:/(?:authorization)\s*[:=]\s*["']?[^\n"']{8,}["']?/gi, r:'[REDACTED_AUTH]' }
|
|
72
|
+
]
|
|
73
|
+
},
|
|
74
|
+
translations: {
|
|
75
|
+
en: {
|
|
76
|
+
btnTitle: 'Create Bug Report',
|
|
77
|
+
modalTitle: 'Bug Report',
|
|
78
|
+
drawInstruction: 'Draw on the screenshot to highlight the problem.',
|
|
79
|
+
cancel: 'Cancel',
|
|
80
|
+
download: 'Download Report',
|
|
81
|
+
actualPrompt: 'What happened?',
|
|
82
|
+
actualDesc: 'Briefly describe the problem. (e.g., "Nothing happens when clicking save")',
|
|
83
|
+
actualPlaceholder: 'Actual state...',
|
|
84
|
+
expectedPrompt: 'What did you expect?',
|
|
85
|
+
expectedDesc: 'Describe the expected behavior. (e.g., "Data should be saved")',
|
|
86
|
+
expectedPlaceholder: 'Expected state...',
|
|
87
|
+
drawTitle: 'Mark Screenshot',
|
|
88
|
+
drawDesc: 'You can draw on the screenshot with your mouse to highlight the problem.',
|
|
89
|
+
actualLabel: 'Actual state',
|
|
90
|
+
expectedLabel: 'Expected state',
|
|
91
|
+
screenshotTitle: 'Annotated Screenshot (DOM Capture via layout2vector)',
|
|
92
|
+
screenshotNA: 'Screenshot not available',
|
|
93
|
+
interactions: 'User Interactions',
|
|
94
|
+
consoleLogs: 'Console Logs',
|
|
95
|
+
jsErrors: 'JavaScript Errors',
|
|
96
|
+
networkRequests: 'Network Requests',
|
|
97
|
+
sanitizationSummary: 'Sanitization Summary',
|
|
98
|
+
redactions: 'redactions',
|
|
99
|
+
noRedactions: 'No redactions were necessary.',
|
|
100
|
+
limitations: 'Capture Limitations',
|
|
101
|
+
reportTitle: 'Bug Report',
|
|
102
|
+
createdAt: 'Created on',
|
|
103
|
+
metaUrl: 'URL',
|
|
104
|
+
metaBrowser: 'Browser',
|
|
105
|
+
metaViewport: 'Viewport',
|
|
106
|
+
metaScreenResolution: 'Screen Resolution',
|
|
107
|
+
metaScrollPosition: 'Scroll Position',
|
|
108
|
+
metaZoomLevel: 'Zoom Level',
|
|
109
|
+
metaUserAgent: 'User Agent',
|
|
110
|
+
noInfo: 'No information provided',
|
|
111
|
+
unknown: 'Unknown',
|
|
112
|
+
generatedAt: 'Generated',
|
|
113
|
+
reportFooter: 'Bug Report Dashboard'
|
|
114
|
+
},
|
|
115
|
+
de: {
|
|
116
|
+
btnTitle: 'Bug Report erstellen',
|
|
117
|
+
modalTitle: 'Bug Report',
|
|
118
|
+
drawInstruction: 'Zeichne auf dem Screenshot, um das Problem zu markieren.',
|
|
119
|
+
cancel: 'Abbrechen',
|
|
120
|
+
download: 'Report herunterladen',
|
|
121
|
+
actualPrompt: 'Was ist passiert?',
|
|
122
|
+
actualDesc: 'Beschreibe kurz das Problem. (z.B. "Beim Klick auf Speichern passiert nichts")',
|
|
123
|
+
actualPlaceholder: 'Ist-Zustand...',
|
|
124
|
+
expectedPrompt: 'Was hättest du erwartet?',
|
|
125
|
+
expectedDesc: 'Beschreibe das gewünschte Verhalten. (z.B. "Die Daten sollten gespeichert werden")',
|
|
126
|
+
expectedPlaceholder: 'Soll-Zustand...',
|
|
127
|
+
drawTitle: 'Screenshot markieren',
|
|
128
|
+
drawDesc: 'Du kannst mit der Maus auf dem Screenshot zeichnen, um das Problem genauer zu zeigen.',
|
|
129
|
+
actualLabel: 'Ist-Zustand',
|
|
130
|
+
expectedLabel: 'Soll-Zustand',
|
|
131
|
+
screenshotTitle: 'Annotierter Screenshot (DOM Capture via layout2vector)',
|
|
132
|
+
screenshotNA: 'Screenshot nicht verfügbar',
|
|
133
|
+
interactions: 'User Interactions',
|
|
134
|
+
consoleLogs: 'Console Logs',
|
|
135
|
+
jsErrors: 'JavaScript Errors',
|
|
136
|
+
networkRequests: 'Network Requests',
|
|
137
|
+
sanitizationSummary: 'Sanitization Summary',
|
|
138
|
+
redactions: 'redactions',
|
|
139
|
+
noRedactions: 'Es waren keine Redactions notwendig.',
|
|
140
|
+
limitations: 'Capture Limitations',
|
|
141
|
+
reportTitle: 'Bug Report',
|
|
142
|
+
createdAt: 'Erstellt am',
|
|
143
|
+
metaUrl: 'URL',
|
|
144
|
+
metaBrowser: 'Browser',
|
|
145
|
+
metaViewport: 'Ansichtsfenster',
|
|
146
|
+
metaScreenResolution: 'Bildschirmauflösung',
|
|
147
|
+
metaScrollPosition: 'Scrollposition',
|
|
148
|
+
metaZoomLevel: 'Zoom-Stufe',
|
|
149
|
+
metaUserAgent: 'User Agent',
|
|
150
|
+
noInfo: 'Keine Angabe',
|
|
151
|
+
unknown: 'Unbekannt',
|
|
152
|
+
generatedAt: 'Generiert',
|
|
153
|
+
reportFooter: 'Bug Report Dashboard'
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
function t(key) {
|
|
159
|
+
const langDict = config.translations[config.language] || config.translations['en'];
|
|
160
|
+
return langDict[key] || key;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ═══════════════════════════════════════════════════════════════
|
|
164
|
+
// SANITIZER
|
|
165
|
+
// ═══════════════════════════════════════════════════════════════
|
|
166
|
+
|
|
167
|
+
let safeParamsSet = new Set(config.sanitization.safeParams);
|
|
168
|
+
let sensitiveParamsSet = new Set(config.sanitization.sensitiveParams);
|
|
169
|
+
|
|
170
|
+
function sanitizeUrl(u) {
|
|
171
|
+
if (!u || typeof u !== 'string') return u;
|
|
172
|
+
try {
|
|
173
|
+
const url = new URL(u);
|
|
174
|
+
const sp = new URLSearchParams();
|
|
175
|
+
for (const [k, v] of url.searchParams) {
|
|
176
|
+
const kl = k.toLowerCase();
|
|
177
|
+
if (sensitiveParamsSet.has(kl)) continue;
|
|
178
|
+
else if (safeParamsSet.has(kl)) sp.set(k, v);
|
|
179
|
+
else sp.set(k, '[PARAM_REMOVED]');
|
|
180
|
+
}
|
|
181
|
+
url.search = sp.toString();
|
|
182
|
+
if (url.hash && url.hash.length > 100) url.hash = '';
|
|
183
|
+
return url.toString();
|
|
184
|
+
} catch { return sanitizeText(u); }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function sanitizeText(t, rd) {
|
|
188
|
+
if (!t || typeof t !== 'string') return t;
|
|
189
|
+
let r = t;
|
|
190
|
+
for (const { name, p, r: rpl } of config.sanitization.patterns) {
|
|
191
|
+
p.lastIndex = 0;
|
|
192
|
+
const before = r;
|
|
193
|
+
r = r.replace(p, rpl);
|
|
194
|
+
if (rd && r !== before) { p.lastIndex = 0; rd[name] = (rd[name]||0) + (before.match(p)||[]).length; }
|
|
195
|
+
}
|
|
196
|
+
return r;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function sanitizeDeep(obj, rd) {
|
|
200
|
+
if (obj == null) return obj;
|
|
201
|
+
if (typeof obj === 'string') return sanitizeText(obj, rd);
|
|
202
|
+
if (Array.isArray(obj)) return obj.map(i => sanitizeDeep(i, rd));
|
|
203
|
+
if (typeof obj === 'object') {
|
|
204
|
+
const res = {};
|
|
205
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
206
|
+
const kl = k.toLowerCase();
|
|
207
|
+
if (kl === 'url' || kl.endsWith('url') || kl === 'href')
|
|
208
|
+
res[k] = typeof v === 'string' ? sanitizeUrl(v) : sanitizeDeep(v, rd);
|
|
209
|
+
else res[k] = sanitizeDeep(v, rd);
|
|
210
|
+
}
|
|
211
|
+
return res;
|
|
212
|
+
}
|
|
213
|
+
return obj;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ═══════════════════════════════════════════════════════════════
|
|
217
|
+
// BUFFERS
|
|
218
|
+
// ═══════════════════════════════════════════════════════════════
|
|
219
|
+
|
|
220
|
+
const interactions = [];
|
|
221
|
+
const consoleLogs = [];
|
|
222
|
+
const jsErrors = [];
|
|
223
|
+
const networkRequests = [];
|
|
224
|
+
|
|
225
|
+
function addInteraction(e) {
|
|
226
|
+
e._ts = Date.now();
|
|
227
|
+
interactions.push(e);
|
|
228
|
+
if (interactions.length > config.limits.interactions) interactions.shift();
|
|
229
|
+
}
|
|
230
|
+
function addConsole(e) { consoleLogs.push(e); if (consoleLogs.length > config.limits.console) consoleLogs.shift(); }
|
|
231
|
+
function addError(e) { jsErrors.push(e); if (jsErrors.length > config.limits.errors) jsErrors.shift(); }
|
|
232
|
+
function addNetwork(e) { networkRequests.push(e); if (networkRequests.length > config.limits.network) networkRequests.shift(); }
|
|
233
|
+
|
|
234
|
+
// ═══════════════════════════════════════════════════════════════
|
|
235
|
+
// HELPERS
|
|
236
|
+
// ═══════════════════════════════════════════════════════════════
|
|
237
|
+
|
|
238
|
+
function throttle(fn, d) {
|
|
239
|
+
let last = 0, tid = null;
|
|
240
|
+
return function (...a) {
|
|
241
|
+
const now = Date.now(), rem = d - (now - last);
|
|
242
|
+
if (rem <= 0) { last = now; fn.apply(this, a); }
|
|
243
|
+
else if (!tid) { tid = setTimeout(() => { last = Date.now(); tid = null; fn.apply(this, a); }, rem); }
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function buildSelector(el) {
|
|
248
|
+
if (!el || !el.tagName) return '';
|
|
249
|
+
const parts = []; let cur = el, depth = 0;
|
|
250
|
+
while (cur && cur !== document.body && depth < 4) {
|
|
251
|
+
const tag = cur.tagName.toLowerCase();
|
|
252
|
+
let sel = tag;
|
|
253
|
+
const cls = Array.from(cur.classList || [])
|
|
254
|
+
.filter(c => c.length < 40 && !/[0-9a-f]{8,}/i.test(c) && !/@/.test(c) && !/^user|^customer|^email/i.test(c))
|
|
255
|
+
.slice(0, 3);
|
|
256
|
+
if (cls.length) sel += '.' + cls.join('.');
|
|
257
|
+
if (cur.parentElement) {
|
|
258
|
+
const sibs = Array.from(cur.parentElement.children).filter(c => c.tagName === cur.tagName);
|
|
259
|
+
if (sibs.length > 1) sel += ':nth-of-type(' + (sibs.indexOf(cur) + 1) + ')';
|
|
260
|
+
}
|
|
261
|
+
parts.unshift(sel); cur = cur.parentElement; depth++;
|
|
262
|
+
}
|
|
263
|
+
return parts.join(' > ');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const LABEL_TAGS = new Set(['BUTTON','A','LABEL','H1','H2','H3','H4','H5','H6','SUMMARY','LEGEND','CAPTION','TH']);
|
|
267
|
+
function getLabel(el) {
|
|
268
|
+
if (!el || !LABEL_TAGS.has(el.tagName)) return undefined;
|
|
269
|
+
const t = (el.textContent || '').trim().substring(0, 50);
|
|
270
|
+
return t || undefined;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function baseEntry(type, el) {
|
|
274
|
+
return {
|
|
275
|
+
timestamp: new Date().toISOString(), eventType: type, url: location.href,
|
|
276
|
+
selector: buildSelector(el), tagName: el ? el.tagName : undefined,
|
|
277
|
+
ariaRole: el ? (el.getAttribute('role') || undefined) : undefined,
|
|
278
|
+
visibleLabel: getLabel(el),
|
|
279
|
+
scrollPosition: { x: Math.round(scrollX), y: Math.round(scrollY) },
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function parseBrowser() {
|
|
284
|
+
const ua = navigator.userAgent;
|
|
285
|
+
let n = 'Unknown', v = '';
|
|
286
|
+
if (ua.includes('Firefox/')) { n='Firefox'; v=ua.match(/Firefox\/([\d.]+)/)?.[1]||''; }
|
|
287
|
+
else if (ua.includes('Edg/')) { n='Edge'; v=ua.match(/Edg\/([\d.]+)/)?.[1]||''; }
|
|
288
|
+
else if (ua.includes('Chrome/') && !ua.includes('Edg/')) { n='Chrome'; v=ua.match(/Chrome\/([\d.]+)/)?.[1]||''; }
|
|
289
|
+
else if (ua.includes('Safari/') && !ua.includes('Chrome/')) { n='Safari'; v=ua.match(/Version\/([\d.]+)/)?.[1]||''; }
|
|
290
|
+
return { browserName: n, browserVersion: v };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function escapeHtml(str) {
|
|
294
|
+
if (!str || typeof str !== 'string') return str || '';
|
|
295
|
+
return str.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')
|
|
296
|
+
.replace(/"/g,'"').replace(/'/g,''');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ═══════════════════════════════════════════════════════════════
|
|
300
|
+
// INTERACTION TRACKING
|
|
301
|
+
// ═══════════════════════════════════════════════════════════════
|
|
302
|
+
|
|
303
|
+
document.addEventListener('click', e => {
|
|
304
|
+
const entry = baseEntry('click', e.target);
|
|
305
|
+
entry.viewportCoordinates = { x: Math.round(e.clientX), y: Math.round(e.clientY) };
|
|
306
|
+
addInteraction(entry);
|
|
307
|
+
}, true);
|
|
308
|
+
document.addEventListener('submit', e => addInteraction(baseEntry('submit', e.target)), true);
|
|
309
|
+
document.addEventListener('change', e => addInteraction(baseEntry('change', e.target)), true);
|
|
310
|
+
|
|
311
|
+
const ALLOWED_KEYS = new Set(['Enter','Escape','Tab','Backspace','Delete','ArrowUp','ArrowDown','ArrowLeft','ArrowRight','Home','End','PageUp','PageDown','F1','F2','F3','F4','F5','F6','F7','F8','F9','F10','F11','F12']);
|
|
312
|
+
document.addEventListener('keydown', e => {
|
|
313
|
+
if (!ALLOWED_KEYS.has(e.key)) return;
|
|
314
|
+
const entry = baseEntry('keyboard', e.target);
|
|
315
|
+
entry.key = e.key;
|
|
316
|
+
entry.modifiers = { ctrl: e.ctrlKey, alt: e.altKey, shift: e.shiftKey, meta: e.metaKey };
|
|
317
|
+
addInteraction(entry);
|
|
318
|
+
}, true);
|
|
319
|
+
|
|
320
|
+
window.addEventListener('scroll', throttle(() => {
|
|
321
|
+
addInteraction({ timestamp: new Date().toISOString(), eventType:'scroll', url: location.href,
|
|
322
|
+
scrollPosition: { x: Math.round(scrollX), y: Math.round(scrollY) } });
|
|
323
|
+
}, 500), { passive: true });
|
|
324
|
+
|
|
325
|
+
window.addEventListener('resize', throttle(() => {
|
|
326
|
+
addInteraction({ timestamp: new Date().toISOString(), eventType:'resize', url: location.href,
|
|
327
|
+
viewportSize: { width: innerWidth, height: innerHeight } });
|
|
328
|
+
}, 500));
|
|
329
|
+
|
|
330
|
+
let lastUrl = location.href;
|
|
331
|
+
const navObs = new MutationObserver(() => {
|
|
332
|
+
if (location.href !== lastUrl) { const old = lastUrl; lastUrl = location.href;
|
|
333
|
+
addInteraction({ timestamp: new Date().toISOString(), eventType:'navigation', url: lastUrl, previousUrl: old }); }
|
|
334
|
+
});
|
|
335
|
+
navObs.observe(document.documentElement, { childList: true, subtree: true });
|
|
336
|
+
window.addEventListener('popstate', () => {
|
|
337
|
+
if (location.href !== lastUrl) { const old = lastUrl; lastUrl = location.href;
|
|
338
|
+
addInteraction({ timestamp: new Date().toISOString(), eventType:'navigation', url: lastUrl, previousUrl: old }); }
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// ═══════════════════════════════════════════════════════════════
|
|
342
|
+
// CONSOLE & ERROR CAPTURE
|
|
343
|
+
// ═══════════════════════════════════════════════════════════════
|
|
344
|
+
|
|
345
|
+
const origConsole = {};
|
|
346
|
+
['log','warn','error','info','debug'].forEach(level => {
|
|
347
|
+
origConsole[level] = console[level];
|
|
348
|
+
console[level] = function (...args) {
|
|
349
|
+
try {
|
|
350
|
+
const msg = args.map(a => typeof a === 'string' ? a : (() => { try { return JSON.stringify(a); } catch { return String(a); } })()).join(' ').substring(0, 500);
|
|
351
|
+
addConsole({ timestamp: new Date().toISOString(), level, message: msg });
|
|
352
|
+
} catch {}
|
|
353
|
+
origConsole[level].apply(console, args);
|
|
354
|
+
};
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
window.addEventListener('error', e => {
|
|
358
|
+
try {
|
|
359
|
+
addError({ timestamp: new Date().toISOString(), message: (e.message||'').substring(0,500),
|
|
360
|
+
errorType: e.error ? e.error.constructor.name : 'Error',
|
|
361
|
+
stack: e.error?.stack?.substring(0,1000), sourceFile: e.filename, line: e.lineno, column: e.colno });
|
|
362
|
+
} catch {}
|
|
363
|
+
});
|
|
364
|
+
window.addEventListener('unhandledrejection', e => {
|
|
365
|
+
try {
|
|
366
|
+
const r = e.reason || {};
|
|
367
|
+
addError({ timestamp: new Date().toISOString(),
|
|
368
|
+
message: (typeof r === 'string' ? r : r.message || 'Unhandled Promise Rejection').substring(0,500),
|
|
369
|
+
errorType: r.constructor ? r.constructor.name : 'UnhandledRejection', stack: r.stack?.substring(0,1000) });
|
|
370
|
+
} catch {}
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// ═══════════════════════════════════════════════════════════════
|
|
374
|
+
// NETWORK CAPTURE (fetch + XHR monkey-patch)
|
|
375
|
+
// ═══════════════════════════════════════════════════════════════
|
|
376
|
+
|
|
377
|
+
const origFetch = window.fetch;
|
|
378
|
+
window.fetch = async function (...args) {
|
|
379
|
+
const url = typeof args[0] === 'string' ? args[0] : args[0]?.url || 'unknown';
|
|
380
|
+
const method = args[1]?.method || (args[0]?.method) || 'GET';
|
|
381
|
+
const start = Date.now();
|
|
382
|
+
try {
|
|
383
|
+
const res = await origFetch.apply(this, args);
|
|
384
|
+
addNetwork({ timestamp: new Date().toISOString(), method, url, type:'fetch',
|
|
385
|
+
statusCode: res.status, duration: Date.now()-start, error: false });
|
|
386
|
+
return res;
|
|
387
|
+
} catch (err) {
|
|
388
|
+
addNetwork({ timestamp: new Date().toISOString(), method, url, type:'fetch',
|
|
389
|
+
duration: Date.now()-start, error: true, errorDescription: err.message });
|
|
390
|
+
throw err;
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const origOpen = XMLHttpRequest.prototype.open;
|
|
395
|
+
const origSend = XMLHttpRequest.prototype.send;
|
|
396
|
+
XMLHttpRequest.prototype.open = function (method, url, ...rest) {
|
|
397
|
+
this._br = { method, url, start: 0 };
|
|
398
|
+
return origOpen.call(this, method, url, ...rest);
|
|
399
|
+
};
|
|
400
|
+
XMLHttpRequest.prototype.send = function (...args) {
|
|
401
|
+
if (this._br) {
|
|
402
|
+
this._br.start = Date.now();
|
|
403
|
+
this.addEventListener('loadend', () => {
|
|
404
|
+
addNetwork({ timestamp: new Date().toISOString(), method: this._br.method, url: this._br.url,
|
|
405
|
+
type:'xhr', statusCode: this.status || undefined, duration: Date.now()-this._br.start,
|
|
406
|
+
error: this.status === 0 || this.status >= 400 });
|
|
407
|
+
}, { once: true });
|
|
408
|
+
}
|
|
409
|
+
return origSend.apply(this, args);
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
// ═══════════════════════════════════════════════════════════════
|
|
413
|
+
// LAYOUT2VECTOR — Canvas-Based DOM Visual Capture
|
|
414
|
+
// ═══════════════════════════════════════════════════════════════
|
|
415
|
+
|
|
416
|
+
function drawRoundRect(ctx, x, y, w, h, r) {
|
|
417
|
+
ctx.beginPath();
|
|
418
|
+
ctx.moveTo(x + r, y);
|
|
419
|
+
ctx.lineTo(x + w - r, y);
|
|
420
|
+
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
|
|
421
|
+
ctx.lineTo(x + w, y + h - r);
|
|
422
|
+
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
|
|
423
|
+
ctx.lineTo(x + r, y + h);
|
|
424
|
+
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
|
|
425
|
+
ctx.lineTo(x, y + r);
|
|
426
|
+
ctx.quadraticCurveTo(x, y, x + r, y);
|
|
427
|
+
ctx.closePath();
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function isDirectTextNode(el) {
|
|
431
|
+
for (const node of el.childNodes) {
|
|
432
|
+
if (node.nodeType === Node.TEXT_NODE && node.textContent.trim().length > 0) return true;
|
|
433
|
+
}
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function getDirectText(el) {
|
|
438
|
+
let text = '';
|
|
439
|
+
for (const node of el.childNodes) {
|
|
440
|
+
if (node.nodeType === Node.TEXT_NODE) text += node.textContent;
|
|
441
|
+
}
|
|
442
|
+
return text.trim();
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Capture the visual state of the page via layout2vector Canvas Writer.
|
|
447
|
+
* Draws DOM geometry onto an HTML5 Canvas and annotates click coordinates.
|
|
448
|
+
* Returns a Base64 PNG data URL string.
|
|
449
|
+
*/
|
|
450
|
+
function captureVisualState() {
|
|
451
|
+
const vpW = innerWidth;
|
|
452
|
+
const vpH = innerHeight;
|
|
453
|
+
const dpr = devicePixelRatio || 1;
|
|
454
|
+
|
|
455
|
+
const canvas = document.createElement('canvas');
|
|
456
|
+
canvas.width = vpW * dpr;
|
|
457
|
+
canvas.height = vpH * dpr;
|
|
458
|
+
const ctx = canvas.getContext('2d');
|
|
459
|
+
ctx.scale(dpr, dpr);
|
|
460
|
+
|
|
461
|
+
// Page background
|
|
462
|
+
const bodyStyle = getComputedStyle(document.body);
|
|
463
|
+
const htmlStyle = getComputedStyle(document.documentElement);
|
|
464
|
+
const pageBg = bodyStyle.backgroundColor !== 'rgba(0, 0, 0, 0)'
|
|
465
|
+
? bodyStyle.backgroundColor
|
|
466
|
+
: (htmlStyle.backgroundColor !== 'rgba(0, 0, 0, 0)' ? htmlStyle.backgroundColor : '#ffffff');
|
|
467
|
+
ctx.fillStyle = pageBg;
|
|
468
|
+
ctx.fillRect(0, 0, vpW, vpH);
|
|
469
|
+
|
|
470
|
+
// Walk visible DOM elements
|
|
471
|
+
const elements = document.querySelectorAll('*');
|
|
472
|
+
for (const el of elements) {
|
|
473
|
+
const tag = el.tagName;
|
|
474
|
+
if (['SCRIPT','STYLE','META','LINK','HEAD','TITLE','NOSCRIPT','BR'].includes(tag)) continue;
|
|
475
|
+
// Skip our own widget elements
|
|
476
|
+
if (el.id === 'br-widget-btn' || el.id === 'br-widget-overlay' || el.closest('#br-widget-overlay')) continue;
|
|
477
|
+
|
|
478
|
+
try {
|
|
479
|
+
const rect = el.getBoundingClientRect();
|
|
480
|
+
if (rect.bottom < 0 || rect.top > vpH || rect.right < 0 || rect.left > vpW) continue;
|
|
481
|
+
if (rect.width === 0 || rect.height === 0) continue;
|
|
482
|
+
|
|
483
|
+
const style = getComputedStyle(el);
|
|
484
|
+
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') continue;
|
|
485
|
+
|
|
486
|
+
const x = rect.left, y = rect.top, w = rect.width, h = rect.height;
|
|
487
|
+
|
|
488
|
+
// Background
|
|
489
|
+
const bg = style.backgroundColor;
|
|
490
|
+
if (bg && bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent') {
|
|
491
|
+
ctx.fillStyle = bg;
|
|
492
|
+
const br = parseFloat(style.borderRadius) || 0;
|
|
493
|
+
if (br > 0) { drawRoundRect(ctx, x, y, w, h, Math.min(br, w/2, h/2)); ctx.fill(); }
|
|
494
|
+
else ctx.fillRect(x, y, w, h);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Border
|
|
498
|
+
const bw = parseFloat(style.borderTopWidth) || 0;
|
|
499
|
+
if (bw > 0) {
|
|
500
|
+
const bc = style.borderTopColor;
|
|
501
|
+
if (bc && bc !== 'rgba(0, 0, 0, 0)') {
|
|
502
|
+
ctx.strokeStyle = bc; ctx.lineWidth = bw;
|
|
503
|
+
const br = parseFloat(style.borderRadius) || 0;
|
|
504
|
+
if (br > 0) { drawRoundRect(ctx, x, y, w, h, Math.min(br, w/2, h/2)); ctx.stroke(); }
|
|
505
|
+
else ctx.strokeRect(x, y, w, h);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Text
|
|
510
|
+
if (isDirectTextNode(el)) {
|
|
511
|
+
const text = getDirectText(el).substring(0, 200);
|
|
512
|
+
if (text) {
|
|
513
|
+
const fontSize = parseFloat(style.fontSize) || 14;
|
|
514
|
+
ctx.font = `${style.fontWeight || 'normal'} ${fontSize}px ${style.fontFamily || 'sans-serif'}`;
|
|
515
|
+
ctx.fillStyle = style.color || '#000';
|
|
516
|
+
ctx.textBaseline = 'top';
|
|
517
|
+
ctx.save(); ctx.beginPath(); ctx.rect(x, y, w, h); ctx.clip();
|
|
518
|
+
const tx = x + (parseFloat(style.paddingLeft) || 0);
|
|
519
|
+
ctx.fillText(text, tx, y + (h - fontSize) / 2);
|
|
520
|
+
ctx.restore();
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Image / media placeholders
|
|
525
|
+
if (['IMG','SVG','VIDEO','CANVAS'].includes(tag)) {
|
|
526
|
+
ctx.fillStyle = 'rgba(99, 102, 241, 0.1)';
|
|
527
|
+
ctx.fillRect(x, y, w, h);
|
|
528
|
+
ctx.strokeStyle = 'rgba(99, 102, 241, 0.3)';
|
|
529
|
+
ctx.lineWidth = 1;
|
|
530
|
+
ctx.strokeRect(x, y, w, h);
|
|
531
|
+
ctx.fillStyle = 'rgba(99, 102, 241, 0.5)';
|
|
532
|
+
ctx.font = `${Math.min(w, h, 24) * 0.5}px sans-serif`;
|
|
533
|
+
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
|
534
|
+
ctx.fillText(tag === 'IMG' ? '🖼' : tag === 'SVG' ? '◇' : '▶', x + w/2, y + h/2);
|
|
535
|
+
ctx.textAlign = 'start';
|
|
536
|
+
}
|
|
537
|
+
} catch {}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return canvas.toDataURL('image/png');
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// ═══════════════════════════════════════════════════════════════
|
|
544
|
+
// HTML REPORT TEMPLATE (inline for widget — no external deps)
|
|
545
|
+
// ═══════════════════════════════════════════════════════════════
|
|
546
|
+
|
|
547
|
+
function syntaxHighlight(json) {
|
|
548
|
+
if (typeof json !== 'string') json = JSON.stringify(json, null, 2);
|
|
549
|
+
json = escapeHtml(json);
|
|
550
|
+
return json.replace(
|
|
551
|
+
/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,
|
|
552
|
+
function (match) {
|
|
553
|
+
let cls = 'json-number';
|
|
554
|
+
if (/^"/.test(match)) { cls = /:$/.test(match) ? 'json-key' : 'json-string'; }
|
|
555
|
+
else if (/true|false/.test(match)) cls = 'json-boolean';
|
|
556
|
+
else if (/null/.test(match)) cls = 'json-null';
|
|
557
|
+
return '<span class="' + cls + '">' + match + '</span>';
|
|
558
|
+
}
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function buildHtmlReport(data) {
|
|
563
|
+
const toolVersion = data.widgetVersion || data.extensionVersion || 'unknown';
|
|
564
|
+
const reportTime = data.reportTimestamp ? new Date(data.reportTimestamp).toLocaleString('de-DE', {
|
|
565
|
+
year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit',second:'2-digit'
|
|
566
|
+
}) : 'N/A';
|
|
567
|
+
const actual = data.userDescription?.actual || t('noInfo');
|
|
568
|
+
const expected = data.userDescription?.expected || t('noInfo');
|
|
569
|
+
const pm = data.pageMetadata || {};
|
|
570
|
+
|
|
571
|
+
return `<!DOCTYPE html>
|
|
572
|
+
<html lang="${config.language}">
|
|
573
|
+
<head>
|
|
574
|
+
<meta charset="UTF-8">
|
|
575
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
576
|
+
<title>${escapeHtml(t('reportTitle'))} — ${escapeHtml(reportTime)}</title>
|
|
577
|
+
<style>
|
|
578
|
+
:root{--bg-primary:#0f1117;--bg-secondary:#1a1d27;--bg-card:#222636;--bg-card-hover:#2a2f42;--border:#2d3348;--text-primary:#e8eaf0;--text-secondary:#9096a8;--text-muted:#636882;--accent:${config.primaryColor};--accent-glow:${config.primaryColor}25;--danger:#ef4444;--success:#22c55e;--warning:#f59e0b;--radius:12px;--radius-sm:8px;--font-mono:'SF Mono','Fira Code','Cascadia Code','Consolas',monospace;--font-sans:-apple-system,BlinkMacSystemFont,'Segoe UI','Inter',Roboto,sans-serif}
|
|
579
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
580
|
+
body{font-family:var(--font-sans);background:var(--bg-primary);color:var(--text-primary);line-height:1.6;-webkit-font-smoothing:antialiased}
|
|
581
|
+
.container{max-width:960px;margin:0 auto;padding:32px 24px 64px}
|
|
582
|
+
.report-header{display:flex;align-items:center;gap:12px;margin-bottom:8px}
|
|
583
|
+
.report-logo{font-size:32px}
|
|
584
|
+
.report-title{font-size:24px;font-weight:800;letter-spacing:-0.03em;flex:1}
|
|
585
|
+
.report-badge{font-size:11px;color:var(--text-muted);background:var(--bg-secondary);padding:4px 10px;border-radius:20px;border:1px solid var(--border);font-weight:500}
|
|
586
|
+
.report-sub{font-size:13px;color:var(--text-muted);margin-bottom:28px;padding-bottom:20px;border-bottom:1px solid var(--border)}
|
|
587
|
+
.section{margin-bottom:24px}
|
|
588
|
+
.section-header{display:flex;align-items:center;gap:8px;cursor:pointer;padding:14px 16px;background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius);transition:all .2s;user-select:none}
|
|
589
|
+
.section-header:hover{background:var(--bg-card);border-color:var(--accent)}
|
|
590
|
+
.section-icon{font-size:18px}
|
|
591
|
+
.section-title{font-size:14px;font-weight:700;flex:1}
|
|
592
|
+
.section-count{font-size:11px;background:var(--accent-glow);color:var(--accent);padding:2px 8px;border-radius:12px;font-weight:600}
|
|
593
|
+
.section-chevron{font-size:12px;color:var(--text-muted);transition:transform .2s}
|
|
594
|
+
.section-body{margin-top:8px;padding:16px;background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius);display:none;animation:slideDown .2s}
|
|
595
|
+
.section.open .section-body{display:block}
|
|
596
|
+
.section.open .section-chevron{transform:rotate(90deg)}
|
|
597
|
+
@keyframes slideDown{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}
|
|
598
|
+
.meta-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:10px;margin-bottom:24px}
|
|
599
|
+
.meta-card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-sm);padding:12px 14px}
|
|
600
|
+
.meta-label{font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:var(--text-muted);margin-bottom:4px;font-weight:600}
|
|
601
|
+
.meta-value{font-size:13px;font-weight:600;word-break:break-all}
|
|
602
|
+
.meta-value.mono{font-family:var(--font-mono);font-size:12px;font-weight:500}
|
|
603
|
+
.user-desc{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:24px}
|
|
604
|
+
@media(max-width:600px){.user-desc{grid-template-columns:1fr}}
|
|
605
|
+
.desc-card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:16px}
|
|
606
|
+
.desc-card.actual{border-left:3px solid var(--danger)}
|
|
607
|
+
.desc-card.expected{border-left:3px solid var(--success)}
|
|
608
|
+
.desc-label{font-size:11px;text-transform:uppercase;letter-spacing:.06em;font-weight:700;margin-bottom:8px}
|
|
609
|
+
.desc-card.actual .desc-label{color:var(--danger)}
|
|
610
|
+
.desc-card.expected .desc-label{color:var(--success)}
|
|
611
|
+
.desc-text{font-size:14px;line-height:1.6;color:var(--text-secondary);white-space:pre-wrap}
|
|
612
|
+
.screenshot-container{margin-bottom:24px;border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;background:var(--bg-secondary)}
|
|
613
|
+
.screenshot-label{font-size:11px;text-transform:uppercase;letter-spacing:.06em;font-weight:700;color:var(--text-muted);padding:12px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:6px}
|
|
614
|
+
.screenshot-container img{width:100%;display:block}
|
|
615
|
+
.json-block{background:var(--bg-primary);border:1px solid var(--border);border-radius:var(--radius-sm);padding:14px;overflow-x:auto;font-family:var(--font-mono);font-size:11.5px;line-height:1.7;white-space:pre-wrap;word-break:break-word;max-height:500px;overflow-y:auto}
|
|
616
|
+
.json-key{color:#93c5fd}.json-string{color:#86efac}.json-number{color:#fbbf24}.json-boolean{color:#c084fc}.json-null{color:#f87171}
|
|
617
|
+
.limitations-list{list-style:none;padding:0}
|
|
618
|
+
.limitations-list li{font-size:12.5px;color:var(--text-secondary);padding:6px 0;border-bottom:1px solid rgba(45,51,72,.5);display:flex;align-items:flex-start;gap:8px}
|
|
619
|
+
.limitations-list li:last-child{border-bottom:none}
|
|
620
|
+
.limitations-list li::before{content:'⚠️';font-size:12px;flex-shrink:0}
|
|
621
|
+
.summary-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:8px}
|
|
622
|
+
.summary-item{background:var(--bg-primary);border:1px solid var(--border);border-radius:var(--radius-sm);padding:10px 12px;text-align:center}
|
|
623
|
+
.summary-item-label{font-size:10px;text-transform:uppercase;letter-spacing:.04em;color:var(--text-muted);margin-bottom:4px}
|
|
624
|
+
.summary-item-value{font-size:18px;font-weight:700;color:var(--accent)}
|
|
625
|
+
.report-footer{text-align:center;font-size:11px;color:var(--text-muted);margin-top:40px;padding-top:20px;border-top:1px solid var(--border)}
|
|
626
|
+
::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}::-webkit-scrollbar-thumb:hover{background:var(--text-muted)}
|
|
627
|
+
</style>
|
|
628
|
+
</head>
|
|
629
|
+
<body>
|
|
630
|
+
<div class="container">
|
|
631
|
+
<div class="report-header">
|
|
632
|
+
<div class="report-logo">${config.icon}</div>
|
|
633
|
+
<div class="report-title">${escapeHtml(t('reportTitle'))}</div>
|
|
634
|
+
<div class="report-badge">Schema v${escapeHtml(data.schemaVersion||'2.0.0')}</div>
|
|
635
|
+
<div class="report-badge">Tool v${escapeHtml(toolVersion)}</div>
|
|
636
|
+
</div>
|
|
637
|
+
<div class="report-sub">${escapeHtml(t('createdAt'))} ${escapeHtml(reportTime)}</div>
|
|
638
|
+
|
|
639
|
+
<div class="user-desc">
|
|
640
|
+
<div class="desc-card actual"><div class="desc-label">${escapeHtml(t('actualLabel'))}</div><div class="desc-text">${escapeHtml(actual)}</div></div>
|
|
641
|
+
<div class="desc-card expected"><div class="desc-label">${escapeHtml(t('expectedLabel'))}</div><div class="desc-text">${escapeHtml(expected)}</div></div>
|
|
642
|
+
</div>
|
|
643
|
+
|
|
644
|
+
${data.screenshotBase64 ? `<div class="screenshot-container"><div class="screenshot-label">📸 ${escapeHtml(t('screenshotTitle'))}</div><img src="${data.screenshotBase64}" alt="Annotated screenshot"></div>` : `<div class="screenshot-container"><div class="screenshot-label">📸 ${escapeHtml(t('screenshotNA'))}</div></div>`}
|
|
645
|
+
|
|
646
|
+
<div class="meta-grid">
|
|
647
|
+
<div class="meta-card"><div class="meta-label">${escapeHtml(t('metaUrl'))}</div><div class="meta-value mono">${escapeHtml(pm.url||'N/A')}</div></div>
|
|
648
|
+
<div class="meta-card"><div class="meta-label">${escapeHtml(t('metaBrowser'))}</div><div class="meta-value">${escapeHtml((pm.browserName||t('unknown'))+' '+(pm.browserVersion||''))}</div></div>
|
|
649
|
+
<div class="meta-card"><div class="meta-label">${escapeHtml(t('metaViewport'))}</div><div class="meta-value">${pm.viewportSize?pm.viewportSize.width+' × '+pm.viewportSize.height:'N/A'}</div></div>
|
|
650
|
+
<div class="meta-card"><div class="meta-label">${escapeHtml(t('metaScreenResolution'))}</div><div class="meta-value">${pm.screenResolution?pm.screenResolution.width+' × '+pm.screenResolution.height:'N/A'}</div></div>
|
|
651
|
+
<div class="meta-card"><div class="meta-label">${escapeHtml(t('metaScrollPosition'))}</div><div class="meta-value">${pm.scrollPosition?'X: '+pm.scrollPosition.x+' Y: '+pm.scrollPosition.y:'N/A'}</div></div>
|
|
652
|
+
<div class="meta-card"><div class="meta-label">${escapeHtml(t('metaZoomLevel'))}</div><div class="meta-value">${pm.zoomLevel!=null?pm.zoomLevel:'N/A'}</div></div>
|
|
653
|
+
<div class="meta-card"><div class="meta-label">${escapeHtml(t('metaUserAgent'))}</div><div class="meta-value mono" style="font-size:10px">${escapeHtml(pm.userAgent||'N/A')}</div></div>
|
|
654
|
+
</div>
|
|
655
|
+
|
|
656
|
+
<div class="section open" id="sectionInteractions"><div class="section-header" onclick="this.parentElement.classList.toggle('open')"><span class="section-icon">👆</span><span class="section-title">${escapeHtml(t('interactions'))}</span><span class="section-count">${(data.interactions||[]).length}</span><span class="section-chevron">▶</span></div><div class="section-body"><div class="json-block">${syntaxHighlight(data.interactions||[])}</div></div></div>
|
|
657
|
+
|
|
658
|
+
<div class="section" id="sectionConsole"><div class="section-header" onclick="this.parentElement.classList.toggle('open')"><span class="section-icon">📋</span><span class="section-title">${escapeHtml(t('consoleLogs'))}</span><span class="section-count">${(data.consoleLogs||[]).length}</span><span class="section-chevron">▶</span></div><div class="section-body"><div class="json-block">${syntaxHighlight(data.consoleLogs||[])}</div></div></div>
|
|
659
|
+
|
|
660
|
+
<div class="section" id="sectionErrors"><div class="section-header" onclick="this.parentElement.classList.toggle('open')"><span class="section-icon">⚠️</span><span class="section-title">${escapeHtml(t('jsErrors'))}</span><span class="section-count">${(data.jsErrors||[]).length}</span><span class="section-chevron">▶</span></div><div class="section-body"><div class="json-block">${syntaxHighlight(data.jsErrors||[])}</div></div></div>
|
|
661
|
+
|
|
662
|
+
<div class="section" id="sectionNetwork"><div class="section-header" onclick="this.parentElement.classList.toggle('open')"><span class="section-icon">🌐</span><span class="section-title">${escapeHtml(t('networkRequests'))}</span><span class="section-count">${(data.networkRequests||[]).length}</span><span class="section-chevron">▶</span></div><div class="section-body"><div class="json-block">${syntaxHighlight(data.networkRequests||[])}</div></div></div>
|
|
663
|
+
|
|
664
|
+
<div class="section" id="sectionSanitization"><div class="section-header" onclick="this.parentElement.classList.toggle('open')"><span class="section-icon">🔒</span><span class="section-title">${escapeHtml(t('sanitizationSummary'))}</span><span class="section-count">${data.sanitizationSummary?.totalRedactions||0} ${escapeHtml(t('redactions'))}</span><span class="section-chevron">▶</span></div><div class="section-body">
|
|
665
|
+
${data.sanitizationSummary?.redactionsByType && Object.keys(data.sanitizationSummary.redactionsByType).length > 0 ? `<div class="summary-grid">${Object.entries(data.sanitizationSummary.redactionsByType).map(([t,c])=>`<div class="summary-item"><div class="summary-item-label">${escapeHtml(t)}</div><div class="summary-item-value">${c}</div></div>`).join('')}</div>` : `<p style="color:var(--text-muted);font-size:13px">${escapeHtml(t('noRedactions'))}</p>`}
|
|
666
|
+
</div></div>
|
|
667
|
+
|
|
668
|
+
<div class="section" id="sectionLimitations"><div class="section-header" onclick="this.parentElement.classList.toggle('open')"><span class="section-icon">ℹ️</span><span class="section-title">${escapeHtml(t('limitations'))}</span><span class="section-chevron">▶</span></div><div class="section-body"><ul class="limitations-list">${(data.captureLimitations||[]).map(l=>`<li>${escapeHtml(l)}</li>`).join('')}</ul></div></div>
|
|
669
|
+
|
|
670
|
+
<script type="application/json" id="bug-report-json">${escapeHtml(JSON.stringify(data,null,2))}</script>
|
|
671
|
+
|
|
672
|
+
<div class="report-footer">${escapeHtml(t('reportFooter'))} — Schema v${escapeHtml(data.schemaVersion||'2.0.0')} — Tool v${escapeHtml(toolVersion)}<br>${escapeHtml(t('generatedAt'))} ${escapeHtml(reportTime)}</div>
|
|
673
|
+
</div>
|
|
674
|
+
</body>
|
|
675
|
+
</html>`;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// ═══════════════════════════════════════════════════════════════
|
|
679
|
+
// REPORT GENERATION
|
|
680
|
+
// ═══════════════════════════════════════════════════════════════
|
|
681
|
+
|
|
682
|
+
const LIMITATIONS = [
|
|
683
|
+
'Only captures events after this script was loaded.',
|
|
684
|
+
'Cannot capture network requests made before script initialization.',
|
|
685
|
+
'Network capture is limited to fetch() and XMLHttpRequest (no image/script loads).',
|
|
686
|
+
'Form field values and typed characters are not captured.',
|
|
687
|
+
'Console logs and errors before script load are not included.',
|
|
688
|
+
'Visual capture renders a simplified DOM geometry, not a pixel-perfect screenshot.',
|
|
689
|
+
];
|
|
690
|
+
|
|
691
|
+
function generateReport(userDescription, screenshotBase64) {
|
|
692
|
+
const { browserName, browserVersion } = parseBrowser();
|
|
693
|
+
const raw = {
|
|
694
|
+
schemaVersion: '2.0.0', reportTimestamp: new Date().toISOString(), widgetVersion: VERSION,
|
|
695
|
+
pageMetadata: {
|
|
696
|
+
url: location.href, userAgent: navigator.userAgent,
|
|
697
|
+
viewportSize: { width: innerWidth, height: innerHeight },
|
|
698
|
+
screenResolution: { width: screen.width, height: screen.height },
|
|
699
|
+
scrollPosition: { x: Math.round(scrollX), y: Math.round(scrollY) },
|
|
700
|
+
zoomLevel: Math.round(devicePixelRatio * 100) / 100, browserName, browserVersion,
|
|
701
|
+
},
|
|
702
|
+
userDescription,
|
|
703
|
+
screenshotBase64: '__SCREENSHOT_PLACEHOLDER__',
|
|
704
|
+
interactions: interactions.map(({ _ts, ...r }) => r),
|
|
705
|
+
consoleLogs: [...consoleLogs], jsErrors: [...jsErrors], networkRequests: [...networkRequests],
|
|
706
|
+
};
|
|
707
|
+
const rd = {};
|
|
708
|
+
const report = sanitizeDeep(raw, rd);
|
|
709
|
+
report.screenshotBase64 = screenshotBase64;
|
|
710
|
+
report.sanitizationSummary = { totalRedactions: Object.values(rd).reduce((a,b)=>a+b,0), redactionsByType: rd };
|
|
711
|
+
report.captureLimitations = LIMITATIONS;
|
|
712
|
+
return report;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function showReportForm(dataUrl) {
|
|
716
|
+
return new Promise((resolve) => {
|
|
717
|
+
if (!dataUrl) {
|
|
718
|
+
resolve(null);
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const overlay = document.createElement('div');
|
|
723
|
+
overlay.style.position = 'fixed';
|
|
724
|
+
overlay.style.inset = '0';
|
|
725
|
+
overlay.style.zIndex = '2147483647';
|
|
726
|
+
overlay.style.background = 'rgba(15, 17, 23, 0.95)';
|
|
727
|
+
overlay.style.display = 'flex';
|
|
728
|
+
overlay.style.flexDirection = 'column';
|
|
729
|
+
overlay.style.fontFamily = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
|
|
730
|
+
|
|
731
|
+
const header = document.createElement('div');
|
|
732
|
+
header.style.padding = '16px 24px';
|
|
733
|
+
header.style.background = '#1a1d27';
|
|
734
|
+
header.style.color = '#e8eaf0';
|
|
735
|
+
header.style.display = 'flex';
|
|
736
|
+
header.style.justifyContent = 'space-between';
|
|
737
|
+
header.style.alignItems = 'center';
|
|
738
|
+
header.style.borderBottom = '1px solid #2d3348';
|
|
739
|
+
header.innerHTML = `
|
|
740
|
+
<div style="font-size: 16px; font-weight: 700; display: flex; align-items: center; gap: 10px;">
|
|
741
|
+
${config.icon} ${escapeHtml(t('btnTitle'))}
|
|
742
|
+
</div>
|
|
743
|
+
<div style="display: flex; gap: 12px;">
|
|
744
|
+
<button id="br-editor-cancel" style="padding: 10px 16px; background: #222636; border: 1px solid #2d3348; color: #e8eaf0; border-radius: 8px; cursor: pointer; font-size: 13px; font-weight: 600; transition: all 0.2s;">${escapeHtml(t('cancel'))}</button>
|
|
745
|
+
<button id="br-editor-save" disabled style="padding: 10px 20px; background: ${config.primaryColor}; border: none; color: #ffffff; border-radius: 8px; cursor: not-allowed; opacity: 0.5; font-size: 13px; font-weight: 700; box-shadow: 0 4px 12px ${config.primaryColor}4D; transition: all 0.2s;">${escapeHtml(t('download'))}</button>
|
|
746
|
+
</div>
|
|
747
|
+
`;
|
|
748
|
+
|
|
749
|
+
const scrollbarStyle = document.createElement('style');
|
|
750
|
+
scrollbarStyle.textContent = `
|
|
751
|
+
.br-modal-main { scrollbar-width: auto; scrollbar-color: ${config.primaryColor} #1a1d27; }
|
|
752
|
+
.br-modal-main::-webkit-scrollbar { width: 10px; }
|
|
753
|
+
.br-modal-main::-webkit-scrollbar-track { background: transparent; }
|
|
754
|
+
.br-modal-main::-webkit-scrollbar-thumb { background: ${config.primaryColor}; border-radius: 5px; border: 2px solid #1a1d27; }
|
|
755
|
+
.br-modal-main::-webkit-scrollbar-thumb:hover { background: ${config.primaryColorHover}; }
|
|
756
|
+
`;
|
|
757
|
+
overlay.appendChild(scrollbarStyle);
|
|
758
|
+
|
|
759
|
+
const main = document.createElement('div');
|
|
760
|
+
main.className = 'br-modal-main';
|
|
761
|
+
main.style.flex = '1';
|
|
762
|
+
main.style.display = 'flex';
|
|
763
|
+
main.style.overflowY = 'scroll';
|
|
764
|
+
main.style.flexWrap = 'wrap';
|
|
765
|
+
|
|
766
|
+
const canvasContainer = document.createElement('div');
|
|
767
|
+
canvasContainer.style.flex = '2 1 600px';
|
|
768
|
+
canvasContainer.style.overflow = 'auto';
|
|
769
|
+
canvasContainer.style.display = 'flex';
|
|
770
|
+
canvasContainer.style.alignItems = 'flex-start';
|
|
771
|
+
canvasContainer.style.justifyContent = 'center';
|
|
772
|
+
canvasContainer.style.padding = '24px';
|
|
773
|
+
canvasContainer.style.background = '#0f1117';
|
|
774
|
+
|
|
775
|
+
const canvas = document.createElement('canvas');
|
|
776
|
+
canvas.style.boxShadow = '0 10px 40px rgba(0,0,0,0.5)';
|
|
777
|
+
canvas.style.cursor = 'crosshair';
|
|
778
|
+
canvas.style.maxWidth = '100%';
|
|
779
|
+
canvas.style.height = 'auto';
|
|
780
|
+
canvas.style.background = '#ffffff';
|
|
781
|
+
canvas.style.borderRadius = '4px';
|
|
782
|
+
|
|
783
|
+
const formContainer = document.createElement('div');
|
|
784
|
+
formContainer.style.flex = '1 1 350px';
|
|
785
|
+
formContainer.style.background = '#1a1d27';
|
|
786
|
+
formContainer.style.borderLeft = '1px solid #2d3348';
|
|
787
|
+
formContainer.style.padding = '24px';
|
|
788
|
+
formContainer.style.display = 'flex';
|
|
789
|
+
formContainer.style.flexDirection = 'column';
|
|
790
|
+
formContainer.style.gap = '20px';
|
|
791
|
+
formContainer.style.overflowY = 'auto';
|
|
792
|
+
|
|
793
|
+
formContainer.innerHTML = `
|
|
794
|
+
<div>
|
|
795
|
+
<h3 style="margin: 0 0 8px 0; font-size: 14px; color: #e8eaf0;">${escapeHtml(t('actualPrompt'))} <span style="color:#ef4444">*</span></h3>
|
|
796
|
+
<p style="margin: 0 0 8px 0; font-size: 12px; color: #9096a8;">${escapeHtml(t('actualDesc'))}</p>
|
|
797
|
+
<textarea id="br-input-actual" placeholder="${escapeHtml(t('actualPlaceholder'))}" style="width: 100%; box-sizing: border-box; height: 100px; background: #0f1117; border: 1px solid #2d3348; border-radius: 8px; color: #e8eaf0; padding: 12px; font-family: inherit; font-size: 13px; resize: vertical;"></textarea>
|
|
798
|
+
</div>
|
|
799
|
+
<div>
|
|
800
|
+
<h3 style="margin: 0 0 8px 0; font-size: 14px; color: #e8eaf0;">${escapeHtml(t('expectedPrompt'))} <span style="color:#ef4444">*</span></h3>
|
|
801
|
+
<p style="margin: 0 0 8px 0; font-size: 12px; color: #9096a8;">${escapeHtml(t('expectedDesc'))}</p>
|
|
802
|
+
<textarea id="br-input-expected" placeholder="${escapeHtml(t('expectedPlaceholder'))}" style="width: 100%; box-sizing: border-box; height: 100px; background: #0f1117; border: 1px solid #2d3348; border-radius: 8px; color: #e8eaf0; padding: 12px; font-family: inherit; font-size: 13px; resize: vertical;"></textarea>
|
|
803
|
+
</div>
|
|
804
|
+
<div style="background: rgba(99, 102, 241, 0.1); border: 1px solid rgba(99, 102, 241, 0.2); border-radius: 8px; padding: 16px;">
|
|
805
|
+
<h3 style="margin: 0 0 8px 0; font-size: 13px; color: ${config.primaryColorHover}; display: flex; align-items: center; gap: 6px;">🖍 ${escapeHtml(t('drawTitle'))}</h3>
|
|
806
|
+
<p style="margin: 0; font-size: 12px; color: #9096a8; line-height: 1.5;">${escapeHtml(t('drawDesc'))}</p>
|
|
807
|
+
</div>
|
|
808
|
+
`;
|
|
809
|
+
|
|
810
|
+
const img = new Image();
|
|
811
|
+
img.onload = () => {
|
|
812
|
+
canvas.width = img.width;
|
|
813
|
+
canvas.height = img.height;
|
|
814
|
+
const ctx = canvas.getContext('2d');
|
|
815
|
+
ctx.drawImage(img, 0, 0);
|
|
816
|
+
|
|
817
|
+
let isDrawing = false;
|
|
818
|
+
|
|
819
|
+
function getPos(e) {
|
|
820
|
+
const rect = canvas.getBoundingClientRect();
|
|
821
|
+
const scaleX = canvas.width / rect.width;
|
|
822
|
+
const scaleY = canvas.height / rect.height;
|
|
823
|
+
const clientX = e.touches && e.touches.length > 0 ? e.touches[0].clientX : e.clientX;
|
|
824
|
+
const clientY = e.touches && e.touches.length > 0 ? e.touches[0].clientY : e.clientY;
|
|
825
|
+
return {
|
|
826
|
+
x: (clientX - rect.left) * scaleX,
|
|
827
|
+
y: (clientY - rect.top) * scaleY
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const startDrawing = (e) => {
|
|
832
|
+
if (e.type.startsWith('touch')) e.preventDefault();
|
|
833
|
+
isDrawing = true;
|
|
834
|
+
const pos = getPos(e);
|
|
835
|
+
ctx.beginPath();
|
|
836
|
+
ctx.moveTo(pos.x, pos.y);
|
|
837
|
+
ctx.strokeStyle = '#ef4444';
|
|
838
|
+
ctx.lineWidth = Math.max(4, img.width / 250);
|
|
839
|
+
ctx.lineCap = 'round';
|
|
840
|
+
ctx.lineJoin = 'round';
|
|
841
|
+
};
|
|
842
|
+
|
|
843
|
+
const draw = (e) => {
|
|
844
|
+
if (e.type.startsWith('touch')) e.preventDefault();
|
|
845
|
+
if (!isDrawing) return;
|
|
846
|
+
const pos = getPos(e);
|
|
847
|
+
ctx.lineTo(pos.x, pos.y);
|
|
848
|
+
ctx.stroke();
|
|
849
|
+
};
|
|
850
|
+
|
|
851
|
+
const stopDrawing = (e) => {
|
|
852
|
+
if (e && e.type && e.type.startsWith('touch') && e.cancelable) e.preventDefault();
|
|
853
|
+
isDrawing = false;
|
|
854
|
+
};
|
|
855
|
+
|
|
856
|
+
canvas.addEventListener('mousedown', startDrawing);
|
|
857
|
+
canvas.addEventListener('touchstart', startDrawing, { passive: false });
|
|
858
|
+
|
|
859
|
+
canvas.addEventListener('mousemove', draw);
|
|
860
|
+
canvas.addEventListener('touchmove', draw, { passive: false });
|
|
861
|
+
|
|
862
|
+
canvas.addEventListener('mouseup', stopDrawing);
|
|
863
|
+
canvas.addEventListener('mouseleave', stopDrawing);
|
|
864
|
+
canvas.addEventListener('touchend', stopDrawing, { passive: false });
|
|
865
|
+
canvas.addEventListener('touchcancel', stopDrawing, { passive: false });
|
|
866
|
+
};
|
|
867
|
+
img.src = dataUrl;
|
|
868
|
+
|
|
869
|
+
canvasContainer.appendChild(canvas);
|
|
870
|
+
main.appendChild(canvasContainer);
|
|
871
|
+
main.appendChild(formContainer);
|
|
872
|
+
overlay.appendChild(header);
|
|
873
|
+
overlay.appendChild(main);
|
|
874
|
+
document.body.appendChild(overlay);
|
|
875
|
+
|
|
876
|
+
const actualInput = document.getElementById('br-input-actual');
|
|
877
|
+
const expectedInput = document.getElementById('br-input-expected');
|
|
878
|
+
const saveBtn = document.getElementById('br-editor-save');
|
|
879
|
+
|
|
880
|
+
const validateInputs = () => {
|
|
881
|
+
if (actualInput.value.trim().length > 0 && expectedInput.value.trim().length > 0) {
|
|
882
|
+
saveBtn.disabled = false;
|
|
883
|
+
saveBtn.style.cursor = 'pointer';
|
|
884
|
+
saveBtn.style.opacity = '1';
|
|
885
|
+
} else {
|
|
886
|
+
saveBtn.disabled = true;
|
|
887
|
+
saveBtn.style.cursor = 'not-allowed';
|
|
888
|
+
saveBtn.style.opacity = '0.5';
|
|
889
|
+
}
|
|
890
|
+
};
|
|
891
|
+
|
|
892
|
+
actualInput.addEventListener('input', validateInputs);
|
|
893
|
+
expectedInput.addEventListener('input', validateInputs);
|
|
894
|
+
|
|
895
|
+
document.getElementById('br-editor-cancel').addEventListener('click', () => {
|
|
896
|
+
document.body.removeChild(overlay);
|
|
897
|
+
resolve(null);
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
saveBtn.addEventListener('click', () => {
|
|
901
|
+
const newDataUrl = canvas.toDataURL('image/png');
|
|
902
|
+
document.body.removeChild(overlay);
|
|
903
|
+
resolve({
|
|
904
|
+
screenshotBase64: newDataUrl,
|
|
905
|
+
actual: actualInput.value.trim(),
|
|
906
|
+
expected: expectedInput.value.trim()
|
|
907
|
+
});
|
|
908
|
+
});
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function downloadReport() {
|
|
913
|
+
// 1. Capture visual state synchronously
|
|
914
|
+
let rawScreenshot = null;
|
|
915
|
+
try {
|
|
916
|
+
rawScreenshot = captureVisualState();
|
|
917
|
+
} catch (e) {
|
|
918
|
+
console.error('Visual capture failed', e);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// 2. Async flow: show unified form, wait for user, then generate HTML
|
|
922
|
+
showReportForm(rawScreenshot).then((formData) => {
|
|
923
|
+
if (!formData) return; // User cancelled
|
|
924
|
+
|
|
925
|
+
const userDescription = {
|
|
926
|
+
actual: formData.actual,
|
|
927
|
+
expected: formData.expected
|
|
928
|
+
};
|
|
929
|
+
|
|
930
|
+
const report = generateReport(userDescription, formData.screenshotBase64);
|
|
931
|
+
const htmlStr = buildHtmlReport(report);
|
|
932
|
+
const blob = new Blob([htmlStr], { type: 'text/html' });
|
|
933
|
+
const a = document.createElement('a');
|
|
934
|
+
a.href = URL.createObjectURL(blob);
|
|
935
|
+
a.download = 'bug_report_' + Date.now() + '.html';
|
|
936
|
+
a.click();
|
|
937
|
+
URL.revokeObjectURL(a.href);
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// ═══════════════════════════════════════════════════════════════
|
|
942
|
+
// UI — Floating Button + Modal
|
|
943
|
+
// ═══════════════════════════════════════════════════════════════
|
|
944
|
+
|
|
945
|
+
function injectStyles() {
|
|
946
|
+
const style = document.createElement('style');
|
|
947
|
+
style.textContent = `
|
|
948
|
+
#br-widget-btn {
|
|
949
|
+
position:fixed; bottom:20px; right:20px; z-index:2147483647;
|
|
950
|
+
width:52px; height:52px; border-radius:50%; border:none; cursor:pointer;
|
|
951
|
+
background:linear-gradient(135deg,${config.primaryColor},${config.primaryColorHover}); color:#fff;
|
|
952
|
+
font-size:24px; display:flex; align-items:center; justify-content:center;
|
|
953
|
+
box-shadow:0 4px 20px ${config.primaryColor}66; transition:all .2s ease;
|
|
954
|
+
}
|
|
955
|
+
#br-widget-btn:hover { transform:scale(1.1); box-shadow:0 6px 28px ${config.primaryColor}8C; }
|
|
956
|
+
#br-widget-hint {
|
|
957
|
+
position:fixed; bottom:84px; right:20px; z-index:2147483640;
|
|
958
|
+
background:#222636; border:1px solid ${config.primaryColor};
|
|
959
|
+
border-radius:12px; padding:12px 16px; max-width:220px;
|
|
960
|
+
font-size:12px; color:#9096a8; line-height:1.5;
|
|
961
|
+
box-shadow:0 8px 32px rgba(0,0,0,0.4);
|
|
962
|
+
opacity:0; transform:translateY(10px); transition:all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
963
|
+
pointer-events:none;
|
|
964
|
+
}
|
|
965
|
+
#br-widget-hint.show { opacity:1; transform:translateY(0); }
|
|
966
|
+
#br-widget-hint::after {
|
|
967
|
+
content:''; position:absolute; bottom:-7px; right:20px;
|
|
968
|
+
width:12px; height:12px; background:#222636;
|
|
969
|
+
border-right:1px solid ${config.primaryColor};
|
|
970
|
+
border-bottom:1px solid ${config.primaryColor};
|
|
971
|
+
transform:rotate(45deg);
|
|
972
|
+
}
|
|
973
|
+
#br-widget-hint strong { color:${config.primaryColorHover}; }
|
|
974
|
+
.br-privacy { font-size:11px; color:#636882; background:rgba(34,197,94,0.06); border:1px solid rgba(34,197,94,0.15);
|
|
975
|
+
border-radius:6px; padding:8px 10px; margin-bottom:16px; line-height:1.5; }
|
|
976
|
+
.br-actions { display:flex; gap:8px; }
|
|
977
|
+
.br-btn { flex:1; padding:10px 14px; border:none; border-radius:10px; font-size:13px; font-weight:600;
|
|
978
|
+
cursor:pointer; transition:all .15s; font-family:inherit; display:flex; align-items:center; justify-content:center; gap:6px; }
|
|
979
|
+
.br-btn-primary { background:${config.primaryColor}; color:#fff; box-shadow:0 2px 8px ${config.primaryColor}33; }
|
|
980
|
+
.br-btn-primary:hover { background:${config.primaryColorHover}; box-shadow:0 4px 16px ${config.primaryColor}4D; transform:translateY(-1px); }
|
|
981
|
+
.br-btn-secondary { background:#222636; color:#9096a8; border:1px solid #2d3348; }
|
|
982
|
+
.br-btn-secondary:hover { background:#2a2f42; color:#e8eaf0; }
|
|
983
|
+
`;
|
|
984
|
+
document.head.appendChild(style);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
function createUI() {
|
|
988
|
+
injectStyles();
|
|
989
|
+
|
|
990
|
+
// Floating button
|
|
991
|
+
const btn = document.createElement('button');
|
|
992
|
+
btn.id = 'br-widget-btn';
|
|
993
|
+
btn.innerHTML = config.icon;
|
|
994
|
+
btn.title = t('btnTitle');
|
|
995
|
+
document.body.appendChild(btn);
|
|
996
|
+
|
|
997
|
+
btn.addEventListener('click', () => {
|
|
998
|
+
downloadReport();
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
// Optional native tooltip
|
|
1002
|
+
if (config.tooltipMessage) {
|
|
1003
|
+
const hint = document.createElement('div');
|
|
1004
|
+
hint.id = 'br-widget-hint';
|
|
1005
|
+
// Replace {icon} with the configured icon
|
|
1006
|
+
hint.innerHTML = config.tooltipMessage.replace(/{icon}/g, config.icon);
|
|
1007
|
+
document.body.appendChild(hint);
|
|
1008
|
+
|
|
1009
|
+
// Animate in
|
|
1010
|
+
setTimeout(() => hint.classList.add('show'), 500);
|
|
1011
|
+
|
|
1012
|
+
// Auto-hide after 8 seconds or when clicking the button
|
|
1013
|
+
let hidden = false;
|
|
1014
|
+
const hideHint = () => {
|
|
1015
|
+
if (hidden) return;
|
|
1016
|
+
hidden = true;
|
|
1017
|
+
hint.classList.remove('show');
|
|
1018
|
+
setTimeout(() => { if (hint.parentNode) hint.remove(); }, 400);
|
|
1019
|
+
};
|
|
1020
|
+
|
|
1021
|
+
setTimeout(hideHint, 8000);
|
|
1022
|
+
btn.addEventListener('click', hideHint);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1027
|
+
// INIT & EXPORT
|
|
1028
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1029
|
+
|
|
1030
|
+
let isInitialized = false;
|
|
1031
|
+
|
|
1032
|
+
function init(options = {}) {
|
|
1033
|
+
if (isInitialized) return;
|
|
1034
|
+
|
|
1035
|
+
// Auto-detect language from browser locale if not explicitly set
|
|
1036
|
+
if (!options.language && typeof navigator !== 'undefined' && navigator.language) {
|
|
1037
|
+
const lang = navigator.language.split('-')[0];
|
|
1038
|
+
if (config.translations[lang]) config.language = lang;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// Merge options
|
|
1042
|
+
if (options.language) config.language = options.language;
|
|
1043
|
+
if (options.icon) config.icon = options.icon;
|
|
1044
|
+
if (options.tooltipMessage) config.tooltipMessage = options.tooltipMessage;
|
|
1045
|
+
if (options.primaryColor) {
|
|
1046
|
+
config.primaryColor = options.primaryColor;
|
|
1047
|
+
// Simple heuristic for hover if not provided
|
|
1048
|
+
config.primaryColorHover = options.primaryColorHover || options.primaryColor;
|
|
1049
|
+
}
|
|
1050
|
+
if (options.limits) {
|
|
1051
|
+
Object.assign(config.limits, options.limits);
|
|
1052
|
+
}
|
|
1053
|
+
if (options.sanitization) {
|
|
1054
|
+
if (options.sanitization.safeParams) config.sanitization.safeParams = options.sanitization.safeParams;
|
|
1055
|
+
if (options.sanitization.sensitiveParams) config.sanitization.sensitiveParams = options.sanitization.sensitiveParams;
|
|
1056
|
+
if (options.sanitization.patterns) config.sanitization.patterns = options.sanitization.patterns;
|
|
1057
|
+
|
|
1058
|
+
// Update Sets
|
|
1059
|
+
safeParamsSet = new Set(config.sanitization.safeParams);
|
|
1060
|
+
sensitiveParamsSet = new Set(config.sanitization.sensitiveParams);
|
|
1061
|
+
}
|
|
1062
|
+
if (options.translations) {
|
|
1063
|
+
for (const [lang, dict] of Object.entries(options.translations)) {
|
|
1064
|
+
if (!config.translations[lang]) config.translations[lang] = {};
|
|
1065
|
+
Object.assign(config.translations[lang], dict);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
isInitialized = true;
|
|
1070
|
+
|
|
1071
|
+
if (document.readyState === 'loading') {
|
|
1072
|
+
document.addEventListener('DOMContentLoaded', createUI);
|
|
1073
|
+
} else {
|
|
1074
|
+
createUI();
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// Auto-init if no script tag attributes say otherwise, or just auto-init with defaults
|
|
1079
|
+
// for backwards compatibility if loaded directly via script tag.
|
|
1080
|
+
// We check if it's running in browser and not via import/require
|
|
1081
|
+
if (typeof window !== 'undefined' && typeof module === 'undefined' && typeof define === 'undefined') {
|
|
1082
|
+
// If it's just a raw script tag, we auto-initialize with defaults
|
|
1083
|
+
setTimeout(() => {
|
|
1084
|
+
if (!isInitialized) init();
|
|
1085
|
+
}, 0);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
return { init };
|
|
1089
|
+
}));
|