@dataengineeringformachinelearning/viking-ui 4.1.1 → 4.1.3
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/dist/LICENSE +201 -0
- package/dist/README.md +277 -0
- package/dist/deml-components.css +1145 -0
- package/dist/design-tokens.css +903 -0
- package/dist/fesm2022/dataengineeringformachinelearning-viking-ui.mjs +15120 -0
- package/dist/fesm2022/dataengineeringformachinelearning-viking-ui.mjs.map +1 -0
- package/dist/fonts/inter/InterVariable-Italic.woff2 +0 -0
- package/dist/fonts/inter/InterVariable.woff2 +0 -0
- package/dist/icons.js +306 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/package.json +119 -0
- package/dist/site-drakkar.js +229 -0
- package/dist/src/tokens/series-presets.ts +15 -0
- package/dist/src/tokens/tailwind.preset.js +209 -0
- package/dist/src/tokens/viking-tokens.json +320 -0
- package/dist/types/dataengineeringformachinelearning-viking-ui.d.ts +2859 -0
- package/dist/viking-components.css +2351 -0
- package/dist/viking-tokens.json +320 -0
- package/dist/viking-ui-elements.js +2425 -0
- package/dist/viking-ui.css +1 -0
- package/dist/viking.manifest.json +271 -0
- package/dist/web-components.js +5143 -0
- package/dist/widget.js +888 -0
- package/package.json +38 -11
package/dist/widget.js
ADDED
|
@@ -0,0 +1,888 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
if (customElements.get('platform-widget')) {
|
|
3
|
+
// If already registered, only run the auto-initialization for this script if not already done
|
|
4
|
+
const currentScript = findWidgetScript();
|
|
5
|
+
if (currentScript && currentScript.hasAttribute('data-page-id')) {
|
|
6
|
+
const pageId = currentScript.getAttribute('data-page-id');
|
|
7
|
+
const backendUrl = currentScript.getAttribute('data-backend-url');
|
|
8
|
+
const frontendUrl = currentScript.getAttribute('data-frontend-url');
|
|
9
|
+
|
|
10
|
+
// Check if we already inserted the widget next to this script tag
|
|
11
|
+
if (!currentScript.nextSibling || currentScript.nextSibling.nodeName !== 'PLATFORM-WIDGET') {
|
|
12
|
+
const widget = document.createElement('platform-widget');
|
|
13
|
+
widget.setAttribute('data-page-id', pageId);
|
|
14
|
+
if (backendUrl) {
|
|
15
|
+
widget.setAttribute('data-backend-url', backendUrl);
|
|
16
|
+
}
|
|
17
|
+
if (frontendUrl) {
|
|
18
|
+
widget.setAttribute('data-frontend-url', frontendUrl);
|
|
19
|
+
}
|
|
20
|
+
currentScript.parentNode.insertBefore(widget, currentScript.nextSibling);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Intercept console errors on host page safely to package as telemetry analytics
|
|
27
|
+
const recentErrors = [];
|
|
28
|
+
if (typeof window !== 'undefined') {
|
|
29
|
+
try {
|
|
30
|
+
const originalError = console.error;
|
|
31
|
+
console.error = (...args) => {
|
|
32
|
+
recentErrors.push(args.join(' '));
|
|
33
|
+
if (recentErrors.length > 5) recentErrors.shift();
|
|
34
|
+
originalError.apply(console, args);
|
|
35
|
+
};
|
|
36
|
+
} catch {}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Helper for idle callbacks to optimize performance
|
|
40
|
+
const runWhenIdle = callback => {
|
|
41
|
+
if (typeof window !== 'undefined') {
|
|
42
|
+
if ('requestIdleCallback' in window) {
|
|
43
|
+
window.requestIdleCallback(callback);
|
|
44
|
+
} else {
|
|
45
|
+
setTimeout(callback, 200);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const DEFAULT_STATUS_APP = 'https://deml.app';
|
|
51
|
+
|
|
52
|
+
const isMarketingHost = hostname =>
|
|
53
|
+
hostname === 'dataengineeringformachinelearning.com' ||
|
|
54
|
+
hostname === 'www.dataengineeringformachinelearning.com';
|
|
55
|
+
|
|
56
|
+
// Status pages are served by the Angular app (deml.app), never the marketing site.
|
|
57
|
+
const normalizeStatusAppHost = url => {
|
|
58
|
+
if (!url) return DEFAULT_STATUS_APP;
|
|
59
|
+
try {
|
|
60
|
+
const parsed = new URL(url.startsWith('http') ? url : `https://${url.replace(/^\/+/, '')}`);
|
|
61
|
+
if (isMarketingHost(parsed.hostname)) {
|
|
62
|
+
return DEFAULT_STATUS_APP;
|
|
63
|
+
}
|
|
64
|
+
return `${parsed.protocol}//${parsed.hostname}${parsed.port ? `:${parsed.port}` : ''}`;
|
|
65
|
+
} catch {
|
|
66
|
+
return String(url).replace(/\/$/, '');
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const statusPageUrl = (frontendHost, slug) =>
|
|
71
|
+
`${normalizeStatusAppHost(frontendHost)}/status/${slug}`;
|
|
72
|
+
|
|
73
|
+
const resolveFrontendHost = ({ explicit, backendUrl, scriptOrigin }) => {
|
|
74
|
+
if (explicit) {
|
|
75
|
+
return normalizeStatusAppHost(explicit);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (backendUrl) {
|
|
79
|
+
try {
|
|
80
|
+
const backend = new URL(backendUrl);
|
|
81
|
+
if (backend.hostname.startsWith('backend.')) {
|
|
82
|
+
return normalizeStatusAppHost(
|
|
83
|
+
`${backend.protocol}//${backend.hostname.slice('backend.'.length)}`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
if (backend.hostname === 'localhost' || backend.hostname === '127.0.0.1') {
|
|
87
|
+
const port = backend.port === '8000' ? '4200' : backend.port;
|
|
88
|
+
return normalizeStatusAppHost(
|
|
89
|
+
`${backend.protocol}//${backend.hostname}${port ? `:${port}` : ''}`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return normalizeStatusAppHost(
|
|
93
|
+
`${backend.protocol}//${backend.hostname}${backend.port ? `:${backend.port}` : ''}`,
|
|
94
|
+
);
|
|
95
|
+
} catch {}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
return normalizeStatusAppHost(scriptOrigin);
|
|
100
|
+
} catch {
|
|
101
|
+
return normalizeStatusAppHost(scriptOrigin || DEFAULT_STATUS_APP);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const fetchWithTimeout = async (resource, options = {}) => {
|
|
106
|
+
const { timeout = 5000 } = options;
|
|
107
|
+
const controller = new AbortController();
|
|
108
|
+
const id = setTimeout(() => controller.abort(), timeout);
|
|
109
|
+
try {
|
|
110
|
+
const response = await fetch(resource, { ...options, signal: controller.signal });
|
|
111
|
+
clearTimeout(id);
|
|
112
|
+
return response;
|
|
113
|
+
} catch (error) {
|
|
114
|
+
clearTimeout(id);
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
function findWidgetScript(pageId) {
|
|
120
|
+
const candidates = Array.from(document.querySelectorAll('script')).filter(script => {
|
|
121
|
+
const src = script.getAttribute('src') ?? '';
|
|
122
|
+
return script.hasAttribute('data-page-id') || /(?:^|\/)widget\.js(?:\?|$|#)/i.test(src);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
if (pageId) {
|
|
126
|
+
const matchingPage = candidates.find(
|
|
127
|
+
script => script.getAttribute('data-page-id') === pageId,
|
|
128
|
+
);
|
|
129
|
+
if (matchingPage) return matchingPage;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
document.currentScript ||
|
|
134
|
+
candidates.find(script => script.hasAttribute('data-page-id')) ||
|
|
135
|
+
candidates.find(script => script.src && /widget\.js/i.test(script.src)) ||
|
|
136
|
+
null
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const globalAgentData = {
|
|
141
|
+
clicks: 0,
|
|
142
|
+
xss_events: [],
|
|
143
|
+
dlp_events: [],
|
|
144
|
+
forms_protected: 0,
|
|
145
|
+
assets: [],
|
|
146
|
+
technologies: [],
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const initGlobalAgent = () => {
|
|
150
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') return;
|
|
151
|
+
|
|
152
|
+
// 1. Behavioral Analytics (Click tracking)
|
|
153
|
+
document.addEventListener(
|
|
154
|
+
'click',
|
|
155
|
+
() => {
|
|
156
|
+
globalAgentData.clicks++;
|
|
157
|
+
},
|
|
158
|
+
{ passive: true },
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
// 2. Asset Inventory (Wappalyzer style & third-party scripts)
|
|
162
|
+
runWhenIdle(() => {
|
|
163
|
+
try {
|
|
164
|
+
const now = new Date();
|
|
165
|
+
const lastScanStr = localStorage.getItem('deml_asset_scan_time');
|
|
166
|
+
const lastScanDate = lastScanStr ? new Date(parseInt(lastScanStr)) : null;
|
|
167
|
+
|
|
168
|
+
const isPast3AM = now.getHours() >= 3;
|
|
169
|
+
const isSameDay =
|
|
170
|
+
lastScanDate &&
|
|
171
|
+
lastScanDate.getDate() === now.getDate() &&
|
|
172
|
+
lastScanDate.getMonth() === now.getMonth() &&
|
|
173
|
+
lastScanDate.getFullYear() === now.getFullYear();
|
|
174
|
+
|
|
175
|
+
if (isPast3AM && !isSameDay) {
|
|
176
|
+
const techs = [];
|
|
177
|
+
if (window.React) techs.push('React');
|
|
178
|
+
if (window.angular) techs.push('Angular');
|
|
179
|
+
if (window.jQuery) techs.push('jQuery');
|
|
180
|
+
if (window.Vue) techs.push('Vue');
|
|
181
|
+
|
|
182
|
+
const generator = document.querySelector('meta[name="generator"]');
|
|
183
|
+
if (generator) techs.push(generator.content);
|
|
184
|
+
|
|
185
|
+
globalAgentData.technologies = techs;
|
|
186
|
+
|
|
187
|
+
if (window.performance && window.performance.getEntriesByType) {
|
|
188
|
+
const resources = window.performance.getEntriesByType('resource');
|
|
189
|
+
globalAgentData.assets = resources.map(r => r.name).slice(0, 50); // limit to save payload size
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
localStorage.setItem('deml_asset_scan_time', now.getTime().toString());
|
|
193
|
+
}
|
|
194
|
+
} catch {}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// 3. Global XSS Detection (Mutation Observer)
|
|
198
|
+
try {
|
|
199
|
+
const observer = new MutationObserver(mutations => {
|
|
200
|
+
for (const mutation of mutations) {
|
|
201
|
+
for (const node of mutation.addedNodes) {
|
|
202
|
+
if (node.tagName === 'SCRIPT') {
|
|
203
|
+
const src = node.src || 'inline';
|
|
204
|
+
if (!src.includes('deml.app')) {
|
|
205
|
+
globalAgentData.xss_events.push({
|
|
206
|
+
type: 'script_injected',
|
|
207
|
+
src: src.substring(0, 100),
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
} else if (node.tagName === 'IFRAME') {
|
|
211
|
+
globalAgentData.xss_events.push({
|
|
212
|
+
type: 'iframe_injected',
|
|
213
|
+
src: (node.src || 'unknown').substring(0, 100),
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
220
|
+
} catch {}
|
|
221
|
+
|
|
222
|
+
// CSP Violation Tracking
|
|
223
|
+
document.addEventListener('securitypolicyviolation', e => {
|
|
224
|
+
globalAgentData.xss_events.push({
|
|
225
|
+
type: 'csp_violation',
|
|
226
|
+
blockedURI: e.blockedURI,
|
|
227
|
+
violatedDirective: e.violatedDirective,
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// 4. Site-Wide DLP & Honeypots (Form Interception)
|
|
232
|
+
runWhenIdle(() => {
|
|
233
|
+
try {
|
|
234
|
+
const forms = document.querySelectorAll('form.deml-protected-form');
|
|
235
|
+
globalAgentData.forms_protected = forms.length;
|
|
236
|
+
|
|
237
|
+
forms.forEach(form => {
|
|
238
|
+
if (form.classList.contains('deml-ignore')) return;
|
|
239
|
+
|
|
240
|
+
const honeypot = document.createElement('input');
|
|
241
|
+
honeypot.type = 'text';
|
|
242
|
+
honeypot.name = 'deml_site_bot_check';
|
|
243
|
+
honeypot.classList.add('honeypot-field');
|
|
244
|
+
honeypot.tabIndex = -1;
|
|
245
|
+
honeypot.setAttribute('aria-hidden', 'true');
|
|
246
|
+
form.appendChild(honeypot);
|
|
247
|
+
|
|
248
|
+
form.addEventListener('submit', e => {
|
|
249
|
+
if (honeypot.value !== '') {
|
|
250
|
+
e.preventDefault(); // Trap bot
|
|
251
|
+
globalAgentData.dlp_events.push({ type: 'bot_trapped', formAction: form.action });
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const inputs = form.querySelectorAll('input[type="text"], textarea');
|
|
256
|
+
const dlpRegex = /(password|api[_-]?key|secret|sk-[a-zA-Z0-9]{20,})/i;
|
|
257
|
+
|
|
258
|
+
let foundSecret = false;
|
|
259
|
+
inputs.forEach(input => {
|
|
260
|
+
if (dlpRegex.test(input.value)) {
|
|
261
|
+
foundSecret = true;
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
if (foundSecret) {
|
|
266
|
+
globalAgentData.dlp_events.push({
|
|
267
|
+
type: 'secret_leak_attempt',
|
|
268
|
+
formAction: form.action,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
} catch {}
|
|
274
|
+
});
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
initGlobalAgent();
|
|
278
|
+
|
|
279
|
+
customElements.define(
|
|
280
|
+
'platform-widget',
|
|
281
|
+
class extends HTMLElement {
|
|
282
|
+
constructor() {
|
|
283
|
+
super();
|
|
284
|
+
this.attachShadow({ mode: 'open' });
|
|
285
|
+
this.clientIp = '127.0.0.1';
|
|
286
|
+
// Prevent unstyled flash on hard refresh before connectedCallback completes.
|
|
287
|
+
this.shadowRoot.innerHTML = '';
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async connectedCallback() {
|
|
291
|
+
const pageId = this.getAttribute('data-page-id');
|
|
292
|
+
if (!pageId) {
|
|
293
|
+
console.error('Widget missing data-page-id attribute');
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Resolve URLs: strictly prefer data-* attributes or the origin of the loaded script.
|
|
298
|
+
// No hardcoded production domains or fallbacks. Set data-backend-url / data-frontend-url on the embed script.
|
|
299
|
+
const currentScript = findWidgetScript(pageId);
|
|
300
|
+
const scriptOrigin =
|
|
301
|
+
currentScript && currentScript.getAttribute('src')
|
|
302
|
+
? new URL(currentScript.src).origin
|
|
303
|
+
: window.location.origin;
|
|
304
|
+
|
|
305
|
+
const backendUrl = this.getAttribute('data-backend-url') ?? '';
|
|
306
|
+
const frontendHost = resolveFrontendHost({
|
|
307
|
+
explicit: this.getAttribute('data-frontend-url'),
|
|
308
|
+
backendUrl,
|
|
309
|
+
scriptOrigin,
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// Resolve Client IP lazily during idle cycles
|
|
313
|
+
runWhenIdle(async () => {
|
|
314
|
+
try {
|
|
315
|
+
const ipRes = await fetchWithTimeout('https://api.ipify.org?format=json');
|
|
316
|
+
if (ipRes.ok) {
|
|
317
|
+
const ipData = await ipRes.json();
|
|
318
|
+
this.clientIp = ipData.ip || '127.0.0.1';
|
|
319
|
+
}
|
|
320
|
+
} catch {}
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
if (!window.__demlBehavioralBound) {
|
|
324
|
+
window.__demlBehavioralBound = true;
|
|
325
|
+
document.addEventListener('click', () => {
|
|
326
|
+
const count = Number(sessionStorage.getItem('deml_click_count') || 0) + 1;
|
|
327
|
+
sessionStorage.setItem('deml_click_count', String(count));
|
|
328
|
+
});
|
|
329
|
+
window.addEventListener(
|
|
330
|
+
'scroll',
|
|
331
|
+
() => {
|
|
332
|
+
const pct = Math.min(
|
|
333
|
+
100,
|
|
334
|
+
Math.round(
|
|
335
|
+
((window.scrollY + window.innerHeight) /
|
|
336
|
+
Math.max(document.documentElement.scrollHeight, 1)) *
|
|
337
|
+
100,
|
|
338
|
+
),
|
|
339
|
+
);
|
|
340
|
+
const prev = Number(sessionStorage.getItem('deml_scroll_pct') || 0);
|
|
341
|
+
if (pct > prev) sessionStorage.setItem('deml_scroll_pct', String(pct));
|
|
342
|
+
},
|
|
343
|
+
{ passive: true },
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Set up Shadow DOM structure including status indicators and vulnerability modal triggers
|
|
348
|
+
this.shadowRoot.innerHTML = `
|
|
349
|
+
<link rel="stylesheet" href="${scriptOrigin}/assets/viking-ui.css" />
|
|
350
|
+
<div class="widget-container">
|
|
351
|
+
<a class="widget-link" href="${statusPageUrl(frontendHost, pageId)}" target="_blank">
|
|
352
|
+
<span class="status-dot"></span>
|
|
353
|
+
<span class="status-text">Loading status...</span>
|
|
354
|
+
</a>
|
|
355
|
+
<span class="divider">|</span>
|
|
356
|
+
<button class="report-trigger" title="Report Security Vulnerability" aria-label="Report Security Vulnerability">
|
|
357
|
+
<svg class="report-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
358
|
+
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
|
|
359
|
+
<line x1="12" y1="8" x2="12" y2="12"/>
|
|
360
|
+
<line x1="12" y1="16" x2="12.01" y2="16"/>
|
|
361
|
+
</svg>
|
|
362
|
+
</button>
|
|
363
|
+
</div>
|
|
364
|
+
|
|
365
|
+
<div class="modal-overlay is-hidden" role="dialog" aria-modal="true" aria-labelledby="modal-title" aria-describedby="modal-desc">
|
|
366
|
+
<div class="modal-content">
|
|
367
|
+
<div class="modal-header">
|
|
368
|
+
<h3 id="modal-title">
|
|
369
|
+
<svg class="modal-title-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
370
|
+
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
|
|
371
|
+
</svg>
|
|
372
|
+
Report Security Threat
|
|
373
|
+
</h3>
|
|
374
|
+
<button class="close-btn" aria-label="Close dialog">×</button>
|
|
375
|
+
</div>
|
|
376
|
+
<div class="modal-body">
|
|
377
|
+
<p id="modal-desc" class="helper-text">Transmit vulnerabilities directly to triage. Technical telemetry will be attached automatically.</p>
|
|
378
|
+
|
|
379
|
+
<div class="security-banner">
|
|
380
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>
|
|
381
|
+
<span><strong>Security Reminder:</strong> You are submitting a report from <strong><span id="domain-name">${window.location.hostname}</span></strong>. We will NEVER ask for passwords, API keys, or MFA codes.</span>
|
|
382
|
+
</div>
|
|
383
|
+
|
|
384
|
+
<div class="form-field honeypot-field" aria-hidden="true">
|
|
385
|
+
<label for="vuln-bot-check">Please leave this field empty</label>
|
|
386
|
+
<input type="text" id="vuln-bot-check" class="input-bot" tabindex="-1" autocomplete="off" />
|
|
387
|
+
</div>
|
|
388
|
+
|
|
389
|
+
<div class="form-field">
|
|
390
|
+
<label for="vuln-title">Vulnerability Title</label>
|
|
391
|
+
<input type="text" id="vuln-title" class="input-title" placeholder="Summary of threat..." />
|
|
392
|
+
</div>
|
|
393
|
+
|
|
394
|
+
<div class="form-row">
|
|
395
|
+
<div class="form-field">
|
|
396
|
+
<label for="vuln-severity">Severity</label>
|
|
397
|
+
<select id="vuln-severity" class="input-severity">
|
|
398
|
+
<option value="Low">Low</option>
|
|
399
|
+
<option value="Medium" selected>Medium</option>
|
|
400
|
+
<option value="High">High</option>
|
|
401
|
+
<option value="Critical">Critical</option>
|
|
402
|
+
</select>
|
|
403
|
+
</div>
|
|
404
|
+
<div class="form-field">
|
|
405
|
+
<label for="vuln-cve">CVE ID (Optional)</label>
|
|
406
|
+
<input type="text" id="vuln-cve" class="input-cve" placeholder="E.g. CVE-2026-12345" />
|
|
407
|
+
</div>
|
|
408
|
+
</div>
|
|
409
|
+
|
|
410
|
+
<div class="form-field">
|
|
411
|
+
<label for="vuln-desc">Description & Repro Steps</label>
|
|
412
|
+
<textarea id="vuln-desc" class="input-desc" rows="4" placeholder="Detail how to reproduce the vulnerability..."></textarea>
|
|
413
|
+
</div>
|
|
414
|
+
|
|
415
|
+
<div class="status-msg is-hidden"></div>
|
|
416
|
+
</div>
|
|
417
|
+
<div class="modal-footer">
|
|
418
|
+
<button class="btn btn-cancel">Cancel</button>
|
|
419
|
+
<button class="btn btn-submit">Submit Report</button>
|
|
420
|
+
</div>
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
`;
|
|
424
|
+
|
|
425
|
+
this.classList.add('deml-ready');
|
|
426
|
+
|
|
427
|
+
const widgetLink = this.shadowRoot.querySelector('.widget-link');
|
|
428
|
+
const dot = this.shadowRoot.querySelector('.status-dot');
|
|
429
|
+
const text = this.shadowRoot.querySelector('.status-text');
|
|
430
|
+
|
|
431
|
+
// Modal triggers
|
|
432
|
+
const reportTrigger = this.shadowRoot.querySelector('.report-trigger');
|
|
433
|
+
const modalOverlay = this.shadowRoot.querySelector('.modal-overlay');
|
|
434
|
+
const closeBtn = this.shadowRoot.querySelector('.close-btn');
|
|
435
|
+
const btnCancel = this.shadowRoot.querySelector('.btn-cancel');
|
|
436
|
+
const btnSubmit = this.shadowRoot.querySelector('.btn-submit');
|
|
437
|
+
const statusMsg = this.shadowRoot.querySelector('.status-msg');
|
|
438
|
+
|
|
439
|
+
// Modal inputs
|
|
440
|
+
const inputTitle = this.shadowRoot.querySelector('.input-title');
|
|
441
|
+
const inputSeverity = this.shadowRoot.querySelector('.input-severity');
|
|
442
|
+
const inputCve = this.shadowRoot.querySelector('.input-cve');
|
|
443
|
+
const inputDesc = this.shadowRoot.querySelector('.input-desc');
|
|
444
|
+
const inputBot = this.shadowRoot.querySelector('.input-bot');
|
|
445
|
+
let dlpWarned = false;
|
|
446
|
+
|
|
447
|
+
const toggleModal = () => {
|
|
448
|
+
const visible = !modalOverlay.classList.contains('is-hidden');
|
|
449
|
+
if (visible) {
|
|
450
|
+
modalOverlay.classList.add('is-hidden');
|
|
451
|
+
reportTrigger.focus();
|
|
452
|
+
} else {
|
|
453
|
+
modalOverlay.classList.remove('is-hidden');
|
|
454
|
+
inputTitle.value = '';
|
|
455
|
+
inputDesc.value = '';
|
|
456
|
+
inputCve.value = '';
|
|
457
|
+
inputBot.value = '';
|
|
458
|
+
statusMsg.classList.add('is-hidden');
|
|
459
|
+
btnSubmit.disabled = false;
|
|
460
|
+
dlpWarned = false;
|
|
461
|
+
setTimeout(() => {
|
|
462
|
+
inputTitle.focus();
|
|
463
|
+
}, 50);
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
reportTrigger.addEventListener('click', toggleModal);
|
|
468
|
+
closeBtn.addEventListener('click', toggleModal);
|
|
469
|
+
btnCancel.addEventListener('click', toggleModal);
|
|
470
|
+
|
|
471
|
+
// Trap focus and close on Escape (Section 508 / WCAG Compliance)
|
|
472
|
+
this.shadowRoot.addEventListener('keydown', e => {
|
|
473
|
+
if (modalOverlay.classList.contains('is-hidden')) return;
|
|
474
|
+
|
|
475
|
+
if (e.key === 'Escape' || e.key === 'Esc') {
|
|
476
|
+
toggleModal();
|
|
477
|
+
e.preventDefault();
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (e.key === 'Tab') {
|
|
482
|
+
const focusables = [
|
|
483
|
+
closeBtn,
|
|
484
|
+
inputTitle,
|
|
485
|
+
inputSeverity,
|
|
486
|
+
inputCve,
|
|
487
|
+
inputDesc,
|
|
488
|
+
btnCancel,
|
|
489
|
+
btnSubmit,
|
|
490
|
+
];
|
|
491
|
+
const first = focusables[0];
|
|
492
|
+
const last = focusables[focusables.length - 1];
|
|
493
|
+
const activeEl = this.shadowRoot.activeElement;
|
|
494
|
+
|
|
495
|
+
if (e.shiftKey) {
|
|
496
|
+
if (activeEl === first) {
|
|
497
|
+
last.focus();
|
|
498
|
+
e.preventDefault();
|
|
499
|
+
}
|
|
500
|
+
} else {
|
|
501
|
+
if (activeEl === last) {
|
|
502
|
+
first.focus();
|
|
503
|
+
e.preventDefault();
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
const getBrowserThreatIndicators = () => {
|
|
510
|
+
const indicators = {
|
|
511
|
+
referrer: document.referrer || 'Direct',
|
|
512
|
+
language: navigator.language,
|
|
513
|
+
platform: navigator.platform,
|
|
514
|
+
cores: navigator.hardwareConcurrency || 'N/A',
|
|
515
|
+
memory_gb: navigator.deviceMemory || 'N/A',
|
|
516
|
+
webdriver: navigator.webdriver ? true : false,
|
|
517
|
+
plugins_count: navigator.plugins ? navigator.plugins.length : 0,
|
|
518
|
+
visibility_state: document.visibilityState || 'visible',
|
|
519
|
+
screen_color_depth: window.screen ? window.screen.colorDepth : 'N/A',
|
|
520
|
+
network_connection: {},
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
try {
|
|
524
|
+
if (navigator.connection) {
|
|
525
|
+
const conn = navigator.connection;
|
|
526
|
+
indicators.network_connection = {
|
|
527
|
+
effective_type: conn.effectiveType,
|
|
528
|
+
rtt_ms: conn.rtt,
|
|
529
|
+
downlink_mbps: conn.downlink,
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
} catch {}
|
|
533
|
+
return indicators;
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
btnSubmit.addEventListener('click', async () => {
|
|
537
|
+
const title = inputTitle.value.trim();
|
|
538
|
+
const description = inputDesc.value.trim();
|
|
539
|
+
if (!title || !description) return;
|
|
540
|
+
|
|
541
|
+
// Honeypot check
|
|
542
|
+
if (inputBot && inputBot.value !== '') {
|
|
543
|
+
// Fake success to fool bots
|
|
544
|
+
statusMsg.innerText = 'Threat reported successfully! Triage initiated.';
|
|
545
|
+
statusMsg.className = 'status-msg success';
|
|
546
|
+
statusMsg.classList.remove('is-hidden');
|
|
547
|
+
setTimeout(toggleModal, 2000);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
statusMsg.classList.add('is-hidden');
|
|
552
|
+
|
|
553
|
+
// Non-blocking DLP check
|
|
554
|
+
const dlpRegex = /(password|api[_-]?key|secret|sk-[a-zA-Z0-9]{20,})/i;
|
|
555
|
+
let flagged_dlp = false;
|
|
556
|
+
if (dlpRegex.test(description) || dlpRegex.test(title)) {
|
|
557
|
+
flagged_dlp = true;
|
|
558
|
+
if (!dlpWarned) {
|
|
559
|
+
statusMsg.innerText =
|
|
560
|
+
'Warning: Your report appears to contain sensitive credentials (e.g., password or API key). Please remove them. Click submit again to proceed anyway.';
|
|
561
|
+
statusMsg.className = 'status-msg warning';
|
|
562
|
+
statusMsg.classList.remove('is-hidden');
|
|
563
|
+
dlpWarned = true;
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
btnSubmit.disabled = true;
|
|
569
|
+
|
|
570
|
+
let pageLoadTime = 0;
|
|
571
|
+
let domInteractive = 0;
|
|
572
|
+
let dnsLookup = 0;
|
|
573
|
+
let fcpTime = 0;
|
|
574
|
+
let protocol = 'unknown';
|
|
575
|
+
try {
|
|
576
|
+
const [navigation] = window.performance.getEntriesByType('navigation');
|
|
577
|
+
if (navigation) {
|
|
578
|
+
pageLoadTime = Math.round(navigation.loadEventEnd - navigation.startTime);
|
|
579
|
+
domInteractive = Math.round(navigation.domInteractive - navigation.startTime);
|
|
580
|
+
dnsLookup = Math.round(navigation.domainLookupEnd - navigation.domainLookupStart);
|
|
581
|
+
protocol = navigation.nextHopProtocol || 'unknown';
|
|
582
|
+
}
|
|
583
|
+
const paints = window.performance.getEntriesByType('paint');
|
|
584
|
+
const fcp = paints.find(p => p.name === 'first-contentful-paint');
|
|
585
|
+
if (fcp) {
|
|
586
|
+
fcpTime = Math.round(fcp.startTime);
|
|
587
|
+
}
|
|
588
|
+
} catch {}
|
|
589
|
+
|
|
590
|
+
const payload = {
|
|
591
|
+
title,
|
|
592
|
+
description,
|
|
593
|
+
cve_id: inputCve.value.trim() || undefined,
|
|
594
|
+
customer_id: pageId, // Map page identifier as reporter reference
|
|
595
|
+
severity: inputSeverity.value,
|
|
596
|
+
telemetry_context: {
|
|
597
|
+
origin_url: window.location.href,
|
|
598
|
+
origin_hostname: window.location.hostname,
|
|
599
|
+
flagged_dlp: flagged_dlp,
|
|
600
|
+
userAgent: navigator.userAgent,
|
|
601
|
+
client_ip: this.clientIp,
|
|
602
|
+
reported_at: new Date().toISOString(),
|
|
603
|
+
screen_resolution: `${window.screen.width}x${window.screen.height}`,
|
|
604
|
+
viewport_size: `${window.innerWidth}x${window.innerHeight}`,
|
|
605
|
+
recent_console_errors: recentErrors,
|
|
606
|
+
performance_metrics: {
|
|
607
|
+
page_load_time_ms: pageLoadTime,
|
|
608
|
+
dom_interactive_time_ms: domInteractive,
|
|
609
|
+
dns_lookup_time_ms: dnsLookup,
|
|
610
|
+
first_contentful_paint_ms: fcpTime,
|
|
611
|
+
next_hop_protocol: protocol,
|
|
612
|
+
},
|
|
613
|
+
threat_indicators: getBrowserThreatIndicators(),
|
|
614
|
+
},
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
try {
|
|
618
|
+
const res = await fetchWithTimeout(`${backendUrl}/api/v1/agent/vulnerabilities`, {
|
|
619
|
+
method: 'POST',
|
|
620
|
+
headers: { 'Content-Type': 'application/json' },
|
|
621
|
+
body: JSON.stringify(payload),
|
|
622
|
+
timeout: 8000,
|
|
623
|
+
});
|
|
624
|
+
if (res.ok) {
|
|
625
|
+
statusMsg.innerText = 'Threat reported successfully! Triage initiated.';
|
|
626
|
+
statusMsg.className = 'status-msg success';
|
|
627
|
+
statusMsg.classList.remove('is-hidden');
|
|
628
|
+
setTimeout(toggleModal, 2000);
|
|
629
|
+
} else {
|
|
630
|
+
throw new Error('Server returned error status');
|
|
631
|
+
}
|
|
632
|
+
} catch {
|
|
633
|
+
statusMsg.innerText = 'Failed to report threat. Please try again.';
|
|
634
|
+
statusMsg.className = 'status-msg error';
|
|
635
|
+
statusMsg.classList.remove('is-hidden');
|
|
636
|
+
btnSubmit.disabled = false;
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
// Automatically report client performance analytics to feed the Threat Analysis (TA) model
|
|
641
|
+
const reportAnalyticsToTa = async () => {
|
|
642
|
+
let responseTimeMs = 250; // Fallback default
|
|
643
|
+
let fcpTime = 0;
|
|
644
|
+
let protocol = 'unknown';
|
|
645
|
+
const sessionStart = Number(sessionStorage.getItem('deml_session_start') || Date.now());
|
|
646
|
+
if (!sessionStorage.getItem('deml_session_start')) {
|
|
647
|
+
sessionStorage.setItem('deml_session_start', String(sessionStart));
|
|
648
|
+
}
|
|
649
|
+
const clickCount = Number(sessionStorage.getItem('deml_click_count') || 0);
|
|
650
|
+
const maxScrollPct = Number(sessionStorage.getItem('deml_scroll_pct') || 0);
|
|
651
|
+
const sessionDurationS = Math.max(1, Math.round((Date.now() - sessionStart) / 1000));
|
|
652
|
+
const clickEntropy = Math.min(1, clickCount / Math.max(sessionDurationS, 1));
|
|
653
|
+
const scrollDepthPct = Math.min(
|
|
654
|
+
100,
|
|
655
|
+
Math.round(
|
|
656
|
+
maxScrollPct ||
|
|
657
|
+
((window.scrollY + window.innerHeight) /
|
|
658
|
+
Math.max(document.documentElement.scrollHeight, 1)) *
|
|
659
|
+
100,
|
|
660
|
+
),
|
|
661
|
+
);
|
|
662
|
+
const behavioralEntropy = Math.min(
|
|
663
|
+
1,
|
|
664
|
+
(scrollDepthPct / 100) * 0.5 + clickEntropy * 0.3 + (sessionDurationS > 10 ? 0.2 : 0),
|
|
665
|
+
);
|
|
666
|
+
try {
|
|
667
|
+
const [navigation] = window.performance.getEntriesByType('navigation');
|
|
668
|
+
if (navigation && navigation.loadEventEnd > 0) {
|
|
669
|
+
responseTimeMs = Math.round(navigation.loadEventEnd - navigation.startTime);
|
|
670
|
+
protocol = navigation.nextHopProtocol || 'unknown';
|
|
671
|
+
} else if (window.performance.timing) {
|
|
672
|
+
const t = window.performance.timing;
|
|
673
|
+
if (t.loadEventEnd > 0 && t.navigationStart > 0) {
|
|
674
|
+
responseTimeMs = t.loadEventEnd - t.navigationStart;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
const paints = window.performance.getEntriesByType('paint');
|
|
678
|
+
const fcp = paints.find(p => p.name === 'first-contentful-paint');
|
|
679
|
+
if (fcp) {
|
|
680
|
+
fcpTime = Math.round(fcp.startTime);
|
|
681
|
+
}
|
|
682
|
+
} catch {}
|
|
683
|
+
|
|
684
|
+
const telemetryPayload = {
|
|
685
|
+
tenant_id: pageId,
|
|
686
|
+
url: window.location.href,
|
|
687
|
+
status_code: 200,
|
|
688
|
+
response_time_ms: responseTimeMs,
|
|
689
|
+
ip_address: this.clientIp,
|
|
690
|
+
is_active: true,
|
|
691
|
+
telemetry_context: {
|
|
692
|
+
performance_metrics: {
|
|
693
|
+
first_contentful_paint_ms: fcpTime,
|
|
694
|
+
next_hop_protocol: protocol,
|
|
695
|
+
},
|
|
696
|
+
behavioral: {
|
|
697
|
+
session_duration_s: sessionDurationS,
|
|
698
|
+
scroll_depth_pct: scrollDepthPct,
|
|
699
|
+
click_count: clickCount,
|
|
700
|
+
entropy: behavioralEntropy,
|
|
701
|
+
},
|
|
702
|
+
threat_indicators: getBrowserThreatIndicators(),
|
|
703
|
+
global_agent_data: globalAgentData,
|
|
704
|
+
},
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
try {
|
|
708
|
+
// Send legacy payload to backend for threat analysis
|
|
709
|
+
await fetchWithTimeout(`${backendUrl}/api/v1/telemetry/endpoints`, {
|
|
710
|
+
method: 'POST',
|
|
711
|
+
headers: { 'Content-Type': 'application/json' },
|
|
712
|
+
body: JSON.stringify(telemetryPayload),
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
// Dynamically load OpenTelemetry SDK via ESM to send traces to OTel Collector.
|
|
716
|
+
// Only runs when data-otel-url is explicitly set on the script tag — skipping it
|
|
717
|
+
// prevents spurious OPTIONS/POST requests to the page origin (e.g. 404 on telemetry.deml.app).
|
|
718
|
+
try {
|
|
719
|
+
const scriptTag = findWidgetScript(pageId);
|
|
720
|
+
const otelUrl = scriptTag?.getAttribute('data-otel-url') ?? '';
|
|
721
|
+
|
|
722
|
+
if (otelUrl) {
|
|
723
|
+
const { WebTracerProvider } =
|
|
724
|
+
await import('https://esm.sh/@opentelemetry/sdk-trace-web@1.24.1');
|
|
725
|
+
const { OTLPTraceExporter } =
|
|
726
|
+
await import('https://esm.sh/@opentelemetry/exporter-trace-otlp-http@0.51.1');
|
|
727
|
+
const { BatchSpanProcessor } =
|
|
728
|
+
await import('https://esm.sh/@opentelemetry/sdk-trace-base@1.24.1');
|
|
729
|
+
|
|
730
|
+
const provider = new WebTracerProvider();
|
|
731
|
+
|
|
732
|
+
const exporter = new OTLPTraceExporter({ url: otelUrl });
|
|
733
|
+
provider.addSpanProcessor(new BatchSpanProcessor(exporter));
|
|
734
|
+
provider.register();
|
|
735
|
+
|
|
736
|
+
const tracer = provider.getTracer('widget-telemetry');
|
|
737
|
+
const span = tracer.startSpan('page_load');
|
|
738
|
+
span.setAttribute('fcp_ms', fcpTime);
|
|
739
|
+
span.setAttribute('response_time_ms', responseTimeMs);
|
|
740
|
+
span.setAttribute('client_ip', this.clientIp);
|
|
741
|
+
span.end();
|
|
742
|
+
}
|
|
743
|
+
} catch (otelErr) {
|
|
744
|
+
console.warn('Failed to initialize OpenTelemetry', otelErr);
|
|
745
|
+
}
|
|
746
|
+
} catch (e) {
|
|
747
|
+
console.warn('Threat analysis telemetry reporting offline', e);
|
|
748
|
+
}
|
|
749
|
+
};
|
|
750
|
+
|
|
751
|
+
// Trigger analytics report once page finishes loading completely (idle-optimized)
|
|
752
|
+
if (document.readyState === 'complete') {
|
|
753
|
+
runWhenIdle(() => reportAnalyticsToTa());
|
|
754
|
+
} else {
|
|
755
|
+
window.addEventListener('load', () => {
|
|
756
|
+
runWhenIdle(() => reportAnalyticsToTa());
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// Fetch and cache status parameters defensively (5 minute TTL)
|
|
761
|
+
const fetchStatus = async () => {
|
|
762
|
+
const cacheKey = `deml_status_cache_v2_${pageId}`;
|
|
763
|
+
try {
|
|
764
|
+
const cached = sessionStorage.getItem(cacheKey);
|
|
765
|
+
if (cached) {
|
|
766
|
+
const { data, timestamp } = JSON.parse(cached);
|
|
767
|
+
if (Date.now() - timestamp < 300000) {
|
|
768
|
+
// 5-minute TTL
|
|
769
|
+
widgetLink.href = data.href;
|
|
770
|
+
dot.style.backgroundColor = data.color;
|
|
771
|
+
text.innerText = data.text;
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
} catch {}
|
|
776
|
+
|
|
777
|
+
try {
|
|
778
|
+
let page = null;
|
|
779
|
+
const slugApiUrl = `${backendUrl}/api/v1/system-status/status_pages/slug/${pageId}`;
|
|
780
|
+
try {
|
|
781
|
+
const res = await fetchWithTimeout(slugApiUrl);
|
|
782
|
+
if (res.ok) {
|
|
783
|
+
page = await res.json();
|
|
784
|
+
}
|
|
785
|
+
} catch (e) {
|
|
786
|
+
// Network error, ignore and try fallback
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
if (!page) {
|
|
790
|
+
// Fallback to fetching the list
|
|
791
|
+
const listApiUrl = `${backendUrl}/api/v1/system-status/status_pages`;
|
|
792
|
+
const res = await fetchWithTimeout(listApiUrl);
|
|
793
|
+
if (res.ok) {
|
|
794
|
+
const data = await res.json();
|
|
795
|
+
if (Array.isArray(data)) {
|
|
796
|
+
page = data.find(p => p.id === pageId || p.slug === pageId);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
if (page) {
|
|
802
|
+
const href = statusPageUrl(frontendHost, page.slug);
|
|
803
|
+
widgetLink.href = href;
|
|
804
|
+
|
|
805
|
+
let color = 'var(--color-success, var(--viking-green-500))';
|
|
806
|
+
let textContent = 'All Systems Operational';
|
|
807
|
+
|
|
808
|
+
try {
|
|
809
|
+
const [incidentsRes, servicesRes] = await Promise.all([
|
|
810
|
+
fetchWithTimeout(
|
|
811
|
+
`${backendUrl}/api/v1/system-status/status_pages/${page.id}/incidents`,
|
|
812
|
+
),
|
|
813
|
+
fetchWithTimeout(
|
|
814
|
+
`${backendUrl}/api/v1/system-status/status_pages/${page.id}/services`,
|
|
815
|
+
),
|
|
816
|
+
]);
|
|
817
|
+
|
|
818
|
+
if (incidentsRes.ok && servicesRes.ok) {
|
|
819
|
+
const incidents = await incidentsRes.json();
|
|
820
|
+
const services = await servicesRes.json();
|
|
821
|
+
|
|
822
|
+
if (Array.isArray(incidents) && Array.isArray(services)) {
|
|
823
|
+
const activeIncidents = incidents.filter(inc => inc.status !== 'Resolved');
|
|
824
|
+
const outages = services.filter(s => s.status === 'Outage');
|
|
825
|
+
const degraded = services.filter(s => s.status === 'Degraded');
|
|
826
|
+
|
|
827
|
+
if (activeIncidents.length > 0) {
|
|
828
|
+
color = 'var(--color-error, var(--viking-crimson-500))';
|
|
829
|
+
textContent = `Incident: ${activeIncidents[0].status}`;
|
|
830
|
+
} else if (outages.length > 0) {
|
|
831
|
+
color = 'var(--color-error, var(--viking-crimson-500))';
|
|
832
|
+
textContent = 'Service Outage';
|
|
833
|
+
} else if (degraded.length > 0) {
|
|
834
|
+
color = 'var(--color-warning, var(--viking-gold-500))';
|
|
835
|
+
textContent = 'Degraded Performance';
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
} catch (err) {
|
|
840
|
+
console.warn('Failed to fetch incidents or services for widget', err);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
dot.style.backgroundColor = color;
|
|
844
|
+
text.innerText = textContent;
|
|
845
|
+
|
|
846
|
+
try {
|
|
847
|
+
sessionStorage.setItem(
|
|
848
|
+
cacheKey,
|
|
849
|
+
JSON.stringify({
|
|
850
|
+
data: { href, color, text: textContent },
|
|
851
|
+
timestamp: Date.now(),
|
|
852
|
+
}),
|
|
853
|
+
);
|
|
854
|
+
} catch {}
|
|
855
|
+
} else {
|
|
856
|
+
dot.style.backgroundColor = 'var(--color-error, var(--viking-crimson-500))';
|
|
857
|
+
text.innerText = 'Status Page Not Found';
|
|
858
|
+
}
|
|
859
|
+
} catch (globalErr) {
|
|
860
|
+
dot.style.backgroundColor = 'var(--text-muted, var(--viking-metallic-300))';
|
|
861
|
+
text.innerText = 'Status Unknown';
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
|
|
865
|
+
fetchStatus();
|
|
866
|
+
}
|
|
867
|
+
},
|
|
868
|
+
);
|
|
869
|
+
|
|
870
|
+
// Auto-initialize legacy script-only installations
|
|
871
|
+
const currentScript = findWidgetScript();
|
|
872
|
+
if (currentScript && currentScript.hasAttribute('data-page-id')) {
|
|
873
|
+
const pageId = currentScript.getAttribute('data-page-id');
|
|
874
|
+
const backendUrl = currentScript.getAttribute('data-backend-url');
|
|
875
|
+
const frontendUrl = currentScript.getAttribute('data-frontend-url');
|
|
876
|
+
|
|
877
|
+
const widget = document.createElement('platform-widget');
|
|
878
|
+
widget.setAttribute('data-page-id', pageId);
|
|
879
|
+
if (backendUrl) {
|
|
880
|
+
widget.setAttribute('data-backend-url', backendUrl);
|
|
881
|
+
}
|
|
882
|
+
if (frontendUrl) {
|
|
883
|
+
widget.setAttribute('data-frontend-url', frontendUrl);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
currentScript.parentNode.insertBefore(widget, currentScript.nextSibling);
|
|
887
|
+
}
|
|
888
|
+
})();
|