tacilytics 2.0.2 → 2.2.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/.env.example +12 -1
- package/README.md +96 -3
- package/auth.js +788 -0
- package/cli.js +36 -0
- package/importers.js +223 -0
- package/package.json +10 -2
- package/public/account.js +570 -0
- package/public/app.js +229 -41
- package/public/assets/icons.svg +6 -0
- package/public/i18n.js +242 -0
- package/public/index.html +194 -37
- package/public/style.css +140 -0
- package/public/tacilytics.js +41 -1
- package/public/theme.js +14 -0
- package/report.js +300 -0
- package/server.js +572 -110
- package/tz.js +96 -0
- package/webhook.js +109 -0
package/public/app.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
}
|
|
15
15
|
} catch (e) { /* corrupt value → keep both visible */ }
|
|
16
16
|
var lastStats = null;
|
|
17
|
+
function closeModal(ov, opt) { return window.TaciModal.close(ov, opt); } // one close path for all modals (account.js)
|
|
17
18
|
// dashboard segmentation filters (whitelist mirrors the server's FILTER_COLS + goal)
|
|
18
19
|
var FILTER_LABELS = { country: 'Country', source: 'Source', page: 'Page', browser: 'Browser', os: 'OS', device: 'Device', lang: 'Language', medium: 'Medium', campaign: 'Campaign', platform: 'Platform', version: 'App version', goal: 'Goal' };
|
|
19
20
|
|
|
@@ -34,7 +35,16 @@
|
|
|
34
35
|
// chart palette comes from CSS custom properties (single source of truth for the theme)
|
|
35
36
|
var CSSV = getComputedStyle(document.documentElement);
|
|
36
37
|
function cv(name, fb) { return (CSSV.getPropertyValue(name) || '').trim() || fb; }
|
|
37
|
-
var COL = {
|
|
38
|
+
var COL = {};
|
|
39
|
+
// chart colours follow the theme: re-read the CSS variables whenever it changes
|
|
40
|
+
function readColors() {
|
|
41
|
+
CSSV = getComputedStyle(document.documentElement);
|
|
42
|
+
COL.v = cv('--chart-visitors', '#5b8def'); COL.p = cv('--chart-pageviews', '#8b93a3'); COL.red = cv('--red', '#f87171');
|
|
43
|
+
COL.dim = cv('--text-dim', '#8b93a3'); COL.line = cv('--line-strong', '#323946'); COL.bg = cv('--bg', '#071114');
|
|
44
|
+
COL.grid = document.documentElement.getAttribute('data-theme') === 'light' ? 'rgba(10,40,38,.08)' : 'rgba(255,255,255,.05)';
|
|
45
|
+
}
|
|
46
|
+
readColors();
|
|
47
|
+
new MutationObserver(function () { readColors(); if (lastStats && lastStats.series) drawChart(lastStats.series); }).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
|
|
38
48
|
// Escape any value that originates from visitor beacons before it touches innerHTML.
|
|
39
49
|
function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; }); }
|
|
40
50
|
|
|
@@ -43,7 +53,8 @@
|
|
|
43
53
|
opts.headers = Object.assign({ 'Content-Type': 'application/json', 'x-session-token': TOKEN }, opts.headers || {});
|
|
44
54
|
return fetch(path, opts).then(function (r) {
|
|
45
55
|
if (r.status === 401) { if (!SHARE) showLogin(); throw new Error('auth'); }
|
|
46
|
-
|
|
56
|
+
// same rule as account.js: JSON errors come back as {error}, anything that is not JSON throws (Tyto)
|
|
57
|
+
return window.TaciReadJson(r);
|
|
47
58
|
});
|
|
48
59
|
}
|
|
49
60
|
|
|
@@ -56,10 +67,13 @@
|
|
|
56
67
|
var r = parseInt(h.slice(0, 2), 16), g = parseInt(h.slice(2, 4), 16), b = parseInt(h.slice(4, 6), 16);
|
|
57
68
|
return Math.max(r, g, b) < 90; // only near-black / deep navy marks; saturated reds stay
|
|
58
69
|
}
|
|
70
|
+
// lookup tables keyed by visitor data (source, country, platform …): own keys only, so a source
|
|
71
|
+
// called 'constructor' or '__proto__' stays plain text instead of hitting Object.prototype
|
|
72
|
+
function own(o, k) { return Object.prototype.hasOwnProperty.call(o, k) ? o[k] : undefined; }
|
|
59
73
|
function ico(id, cls) { return '<svg class="ico ' + (cls || '') + '" aria-hidden="true"><use href="' + SPRITE + '#' + id + '"></use></svg>'; }
|
|
60
74
|
function brand(name, fallbackUi) {
|
|
61
75
|
if (!name) return ico(fallbackUi || 'i-other', 'ui');
|
|
62
|
-
var c = BRAND_COLORS
|
|
76
|
+
var c = own(BRAND_COLORS, name);
|
|
63
77
|
if (name === 'samsung') c = '#6f8dff'; // Samsung navy is unreadable on dark — same hue, lighter
|
|
64
78
|
if (c && isDark(c)) c = '#e7f3f1'; // black logos (Apple, X, GitHub …) would vanish on the dark UI
|
|
65
79
|
return '<svg class="ico brand" aria-hidden="true"' + (c ? ' style="color:' + c + '"' : '') + '><use href="' + SPRITE + '#b-' + name + '"></use></svg>';
|
|
@@ -87,23 +101,16 @@
|
|
|
87
101
|
function flag(cc) { return flagIcon(cc); } // returns HTML now (img), never a text emoji
|
|
88
102
|
var NAMES = { DE: 'Germany', AT: 'Austria', CH: 'Switzerland', GB: 'United Kingdom', US: 'United States', FR: 'France', NL: 'Netherlands', ES: 'Spain', IT: 'Italy', SE: 'Sweden', NO: 'Norway', DK: 'Denmark', FI: 'Finland', PL: 'Poland', CZ: 'Czechia', BE: 'Belgium', IE: 'Ireland', PT: 'Portugal', CA: 'Canada', AU: 'Australia', BR: 'Brazil', IN: 'India', JP: 'Japan', CN: 'China', RU: 'Russia', TR: 'Turkey', MX: 'Mexico', HU: 'Hungary', RO: 'Romania', GR: 'Greece', UA: 'Ukraine', HR: 'Croatia', NZ: 'New Zealand', SG: 'Singapore', KR: 'South Korea', IL: 'Israel', AE: 'UAE', ZA: 'South Africa' };
|
|
89
103
|
|
|
90
|
-
// ── login ──
|
|
91
|
-
function showLogin() { $('login').classList.remove('hide'); $('app').classList.add('hide'); }
|
|
104
|
+
// ── login (the form itself lives in account.js: passphrase, accounts, TOTP, passkeys) ──
|
|
105
|
+
function showLogin() { clearInterval(window.__rt); [].forEach.call(document.querySelectorAll('.modal-overlay:not(#login):not(.hide)'), function (o) { closeModal(o, { force: true }); }); $('login').classList.remove('hide'); $('app').classList.add('hide'); var v = $('appVersion'); if (v) v.textContent = ''; if (window.TaciAccount) window.TaciAccount.prepareLogin(); }
|
|
92
106
|
function hideLogin() { $('login').classList.add('hide'); $('app').classList.remove('hide'); }
|
|
93
|
-
function
|
|
94
|
-
|
|
95
|
-
fetch('/auth/
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
boot();
|
|
101
|
-
})
|
|
102
|
-
.catch(function () { $('loginErr').textContent = 'Connection error'; });
|
|
103
|
-
}
|
|
104
|
-
$('loginBtn').onclick = login;
|
|
105
|
-
$('pw').addEventListener('keydown', function (e) { if (e.key === 'Enter') login(); });
|
|
106
|
-
$('logoutBtn').onclick = function () { localStorage.removeItem('nyx_token'); TOKEN = ''; showLogin(); };
|
|
107
|
+
function signedIn(token) { TOKEN = token; localStorage.setItem('nyx_token', TOKEN); $('loginErr').textContent = ''; boot(); }
|
|
108
|
+
$('logoutBtn').onclick = function () {
|
|
109
|
+
fetch('/auth/logout', { method: 'POST', headers: { 'x-session-token': TOKEN } }).catch(function () {});
|
|
110
|
+
localStorage.removeItem('nyx_token'); TOKEN = ''; showLogin();
|
|
111
|
+
};
|
|
112
|
+
// hooks for account.js
|
|
113
|
+
window.Taci = { api: api, esc: esc, ico: ico, signedIn: signedIn, sites: function () { return state.sites; }, reloadSites: function () { return loadSites().then(load); } };
|
|
107
114
|
|
|
108
115
|
// ── sites ──
|
|
109
116
|
function loadSites() {
|
|
@@ -114,15 +121,48 @@
|
|
|
114
121
|
if (!sites.length) { sel.innerHTML = '<option>no sites yet</option>'; return; }
|
|
115
122
|
if (!state.site || !sites.some(function (s) { return s.domain === state.site; })) state.site = sites[0].domain;
|
|
116
123
|
sel.value = state.site;
|
|
124
|
+
showTz();
|
|
117
125
|
});
|
|
118
126
|
}
|
|
119
|
-
$('siteSelect').onchange = function () { state.site = this.value; localStorage.setItem('nyx_site', state.site); load(); if (typeof loadFunnels === 'function') loadFunnels(); };
|
|
127
|
+
$('siteSelect').onchange = function () { state.site = this.value; showTz(); localStorage.setItem('nyx_site', state.site); load(); if (typeof loadFunnels === 'function') loadFunnels(); };
|
|
128
|
+
|
|
129
|
+
// ── time zones: the browser's zone is the default for a new site; the list comes from Intl ──
|
|
130
|
+
var BROWSER_TZ = (function () { try { return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'; } catch (e) { return 'UTC'; } })();
|
|
131
|
+
var TZ_LIST = (function () {
|
|
132
|
+
var l = []; try { l = Intl.supportedValuesOf('timeZone'); } catch (e) { /* old browser: the two below */ }
|
|
133
|
+
if (l.indexOf('UTC') < 0) l = ['UTC'].concat(l);
|
|
134
|
+
if (l.indexOf(BROWSER_TZ) < 0) l.push(BROWSER_TZ);
|
|
135
|
+
return l;
|
|
136
|
+
})();
|
|
137
|
+
function fillTz(sel, value) {
|
|
138
|
+
var list = TZ_LIST.indexOf(value) < 0 ? TZ_LIST.concat([value]) : TZ_LIST;
|
|
139
|
+
sel.innerHTML = list.map(function (z) { return '<option value="' + esc(z) + '">' + esc(z.replace(/_/g, ' ')) + '</option>'; }).join('');
|
|
140
|
+
sel.value = value;
|
|
141
|
+
}
|
|
142
|
+
// today in the site's time zone (en-CA formats as YYYY-MM-DD)
|
|
143
|
+
function localToday() { var s = currentSite(); try { return new Intl.DateTimeFormat('en-CA', { timeZone: (s && s.timezone) || 'UTC', year: 'numeric', month: '2-digit', day: '2-digit' }).format(new Date()); } catch (e) { return new Date().toISOString().slice(0, 10); } }
|
|
144
|
+
function currentSite() { return (state.sites || []).filter(function (s) { return s.domain === state.site; })[0]; }
|
|
145
|
+
function showTz() { var s = currentSite(); $('tzName').textContent = s ? String(s.timezone || 'UTC').replace(/_/g, ' ') : 'UTC'; $('tzBtn').classList.toggle('hide', !s); }
|
|
146
|
+
$('tzBtn').onclick = function () {
|
|
147
|
+
var s = currentSite(); if (!s) return;
|
|
148
|
+
$('tzSite').textContent = s.domain; $('tzErr').textContent = ''; fillTz($('tzSelect'), s.timezone || 'UTC');
|
|
149
|
+
$('tzModal').classList.remove('hide'); $('tzSelect').focus();
|
|
150
|
+
};
|
|
151
|
+
$('cancelTz').onclick = function () { closeModal($('tzModal'), { force: true }); };
|
|
152
|
+
$('saveTz').onclick = function () {
|
|
153
|
+
var s = currentSite(); if (!s) return;
|
|
154
|
+
api('/api/sites/' + s.id, { method: 'PATCH', body: JSON.stringify({ timezone: $('tzSelect').value }) }).then(function (r) {
|
|
155
|
+
if (r.error) { $('tzErr').textContent = r.error; return; }
|
|
156
|
+
closeModal($('tzModal'), { force: true });
|
|
157
|
+
loadSites().then(load);
|
|
158
|
+
});
|
|
159
|
+
};
|
|
120
160
|
|
|
121
|
-
$('addSiteBtn').onclick = function () { $('addModal').classList.remove('hide'); $('newDomain').value = ''; $('addErr').textContent = ''; $('newDomain').focus(); };
|
|
122
|
-
$('cancelAdd').onclick = function () { $('addModal')
|
|
161
|
+
$('addSiteBtn').onclick = function () { $('addModal').classList.remove('hide'); $('newDomain').value = ''; fillTz($('newTz'), BROWSER_TZ); $('addErr').textContent = ''; $('newDomain').focus(); };
|
|
162
|
+
$('cancelAdd').onclick = function () { closeModal($('addModal'), { force: true }); };
|
|
123
163
|
$('createSiteBtn').onclick = function () {
|
|
124
164
|
var d = $('newDomain').value.trim();
|
|
125
|
-
api('/api/sites', { method: 'POST', body: JSON.stringify({ domain: d }) }).then(function (s) {
|
|
165
|
+
api('/api/sites', { method: 'POST', body: JSON.stringify({ domain: d, timezone: $('newTz').value }) }).then(function (s) {
|
|
126
166
|
if (s.error) { $('addErr').textContent = s.error; return; }
|
|
127
167
|
$('addModal').classList.add('hide');
|
|
128
168
|
state.site = s.domain; localStorage.setItem('nyx_site', state.site);
|
|
@@ -155,7 +195,7 @@
|
|
|
155
195
|
return {
|
|
156
196
|
s1: 'Paste this once into the <code><head></code> of your site:',
|
|
157
197
|
c1: snippetFor(domain), s2: '', c2: '',
|
|
158
|
-
hint: 'Track goals & properties from your code:<br><code>tacilytics(\'Signup\')</code> · <code>tacilytics(\'Signup\', { plan: \'pro\' })</code><br><span style="opacity:.7">Privacy flags on the tag: <code>data-honor-dnt</code> · <code>data-track-outbound</code></span>'
|
|
198
|
+
hint: 'Track goals & properties from your code:<br><code>tacilytics(\'Signup\')</code> · <code>tacilytics(\'Signup\', { plan: \'pro\' })</code><br><span style="opacity:.7">Privacy flags on the tag: <code>data-honor-dnt</code> · <code>data-track-outbound</code> · <code>data-track-downloads</code></span>'
|
|
159
199
|
};
|
|
160
200
|
}
|
|
161
201
|
function renderSnippet() {
|
|
@@ -200,14 +240,15 @@
|
|
|
200
240
|
};
|
|
201
241
|
|
|
202
242
|
$('snippetBtn').onclick = function () { if (state.site) showSnippet(state.site); };
|
|
203
|
-
$('closeSnippet').onclick = function () { $('snippetModal')
|
|
243
|
+
$('closeSnippet').onclick = function () { closeModal($('snippetModal')); };
|
|
204
244
|
$('copySnippet').onclick = function () { copyFrom('copySnippet', 'snippetCode'); };
|
|
205
245
|
$('copySnippet2').onclick = function () { copyFrom('copySnippet2', 'snippetCode2', 'Copy step 2'); };
|
|
206
246
|
|
|
207
247
|
// ── live visitors (last 30 min) ──
|
|
208
248
|
var liveTimer = null;
|
|
209
249
|
function openLive() { if (!state.site || state.site === 'no sites yet') return; $('liveModal').classList.remove('hide'); fetchLive(); clearInterval(liveTimer); liveTimer = setInterval(fetchLive, 8000); }
|
|
210
|
-
function closeLive() { $('liveModal')
|
|
250
|
+
function closeLive() { closeModal($('liveModal')); }
|
|
251
|
+
$('liveModal').addEventListener('taci:close', function () { clearInterval(liveTimer); liveTimer = null; }); // every exit stops polling
|
|
211
252
|
function fetchLive() {
|
|
212
253
|
if (document.hidden) return;
|
|
213
254
|
api('/api/realtime?site=' + encodeURIComponent(state.site)).then(function (r) {
|
|
@@ -330,8 +371,8 @@
|
|
|
330
371
|
el.classList.remove('hide');
|
|
331
372
|
var chips = keys.map(function (k) {
|
|
332
373
|
var v = state.filters[k];
|
|
333
|
-
var disp = k === 'country' ? (flagIcon(v) + ' ' + esc(NAMES
|
|
334
|
-
return '<span class="chip"><span class="k">' + esc(FILTER_LABELS
|
|
374
|
+
var disp = k === 'country' ? (flagIcon(v) + ' ' + esc(own(NAMES, v) || v)) : k === 'platform' ? (platformIcon(v) + ' ' + esc(platformLabel(v))) : esc(v);
|
|
375
|
+
return '<span class="chip"><span class="k">' + esc(own(FILTER_LABELS, k) || k) + '</span>' + disp +
|
|
335
376
|
'<button data-rm="' + esc(k) + '" title="Remove" aria-label="Remove filter">' + ico('i-x', 'ui') + '</button></span>';
|
|
336
377
|
}).join('');
|
|
337
378
|
el.innerHTML = '<span class="flabel">Filtered by</span>' + chips + '<span class="chip clear" id="clearFilters">Clear all</span>';
|
|
@@ -367,6 +408,23 @@
|
|
|
367
408
|
'<path d="' + line + '" fill="none" stroke="' + color + '" stroke-width="1.6" vector-effect="non-scaling-stroke" opacity=".9"/></svg>';
|
|
368
409
|
card.insertAdjacentHTML('beforeend', svg);
|
|
369
410
|
}
|
|
411
|
+
// ── chart annotations: a thin coral line + diamond per annotated day (daily views only) ──
|
|
412
|
+
function annotationsFor(d) {
|
|
413
|
+
var list = (lastStats && lastStats.annotations) || [];
|
|
414
|
+
return d && d.day ? list.filter(function (a) { return a.day === d.day; }) : [];
|
|
415
|
+
}
|
|
416
|
+
function annotationMarks(series, x, H) {
|
|
417
|
+
var list = (lastStats && lastStats.annotations) || [];
|
|
418
|
+
if (!list.length) return '';
|
|
419
|
+
var out = '';
|
|
420
|
+
series.forEach(function (d, i) {
|
|
421
|
+
if (!annotationsFor(d).length) return;
|
|
422
|
+
var cx = x(i).toFixed(1);
|
|
423
|
+
out += '<line x1="' + cx + '" x2="' + cx + '" y1="14" y2="' + H + '" stroke="var(--accent-2, #ff7a5c)" stroke-width="1.2" stroke-dasharray="3 4" opacity=".75" vector-effect="non-scaling-stroke"/>' +
|
|
424
|
+
'<path d="M' + cx + ' 3 l5 6 l-5 6 l-5 -6 z" fill="var(--accent-2, #ff7a5c)"/>';
|
|
425
|
+
});
|
|
426
|
+
return out;
|
|
427
|
+
}
|
|
370
428
|
function drawChart(series) {
|
|
371
429
|
var svg = $('chart'); var W = 800, H = 220, pad = 8;
|
|
372
430
|
var showV = state.show.visitors, showP = state.show.pageviews;
|
|
@@ -388,25 +446,146 @@
|
|
|
388
446
|
'<defs><linearGradient id="ag" x1="0" y1="0" x2="0" y2="1">' +
|
|
389
447
|
'<stop offset="0%" stop-color="' + COL.v + '" stop-opacity="0.32"/><stop offset="100%" stop-color="' + COL.v + '" stop-opacity="0"/>' +
|
|
390
448
|
'</linearGradient></defs>' +
|
|
391
|
-
[0.25, 0.5, 0.75].map(function (f) { var gy = (pad + f * (H - 2 * pad)).toFixed(1); return '<line x1="0" x2="' + W + '" y1="' + gy + '" y2="' + gy + '" stroke="
|
|
449
|
+
[0.25, 0.5, 0.75].map(function (f) { var gy = (pad + f * (H - 2 * pad)).toFixed(1); return '<line x1="0" x2="' + W + '" y1="' + gy + '" y2="' + gy + '" stroke="' + COL.grid + '" stroke-dasharray="2 6" vector-effect="non-scaling-stroke"/>'; }).join('') +
|
|
392
450
|
(showV ? '<path d="' + areaP + '" fill="url(#ag)"/>' : '') +
|
|
393
451
|
(showP ? '<path d="' + path('pageviews') + '" fill="none" stroke="' + COL.p + '" stroke-width="1.6" stroke-dasharray="4 4" opacity="0.85"/>' : '') +
|
|
394
452
|
(showV ? '<path d="' + path('visitors') + '" fill="none" stroke="' + COL.v + '" stroke-width="2.4" stroke-linecap="round"/>' : '') +
|
|
395
|
-
(showV && n <= 40 ? series.map(function (d, i) { return '<circle cx="' + x(i).toFixed(1) + '" cy="' + y(d.visitors).toFixed(1) + '" r="2.4" fill="' + COL.v + '" stroke="
|
|
453
|
+
(showV && n <= 40 ? series.map(function (d, i) { return '<circle cx="' + x(i).toFixed(1) + '" cy="' + y(d.visitors).toFixed(1) + '" r="2.4" fill="' + COL.v + '" stroke="' + COL.bg + '" stroke-width="1.5"/>'; }).join('') : '') +
|
|
454
|
+
annotationMarks(series, x, H) +
|
|
396
455
|
hovers;
|
|
397
456
|
var tip = $('tip');
|
|
398
457
|
Array.prototype.forEach.call(svg.querySelectorAll('rect'), function (rect) {
|
|
399
458
|
rect.addEventListener('mousemove', function (e) {
|
|
400
459
|
var d = series[+rect.dataset.i];
|
|
460
|
+
var notes = annotationsFor(d);
|
|
401
461
|
tip.innerHTML = '<strong>' + esc(d.label) + '</strong>' +
|
|
402
462
|
(showV ? '<br>' + d.visitors + ' visitors' : '') +
|
|
403
|
-
(showP ? '<br>' + d.pageviews + ' views' : '')
|
|
463
|
+
(showP ? '<br>' + d.pageviews + ' views' : '') +
|
|
464
|
+
notes.map(function (a) { return '<br><span class="tip-note" data-no-i18n>◆ ' + esc(a.text) + '</span>'; }).join('');
|
|
404
465
|
tip.style.left = (e.clientX + 14) + 'px'; tip.style.top = (e.clientY - 10) + 'px'; tip.style.opacity = 1;
|
|
405
466
|
});
|
|
406
467
|
rect.addEventListener('mouseleave', function () { tip.style.opacity = 0; });
|
|
407
468
|
});
|
|
408
469
|
}
|
|
409
470
|
|
|
471
|
+
// ── import history (Plausible / GA4) ──
|
|
472
|
+
var impSrc = 'plausible';
|
|
473
|
+
var IMP_HOWTO = {
|
|
474
|
+
plausible: 'In Plausible: <strong>Site settings → Imports & Exports → Export data</strong>. Upload the ZIP you get (or the CSV files in it).',
|
|
475
|
+
ga: 'In Google Analytics 4: open a report (e.g. <strong>Pages and screens</strong>, <strong>Traffic acquisition</strong>, <strong>Demographic details</strong>), add <strong>Date</strong> as a secondary dimension, then <strong>Share → Download file → CSV</strong>. One file per report; pick several at once.',
|
|
476
|
+
};
|
|
477
|
+
function openImport() {
|
|
478
|
+
$('importModal').classList.remove('hide'); $('impErr').textContent = ''; $('impStatus').textContent = ''; $('impSite').textContent = state.site;
|
|
479
|
+
setImpSrc(impSrc); loadImports();
|
|
480
|
+
}
|
|
481
|
+
function setImpSrc(src) {
|
|
482
|
+
impSrc = src;
|
|
483
|
+
Array.prototype.forEach.call($('impTabs').children, function (b) { b.classList.toggle('active', b.dataset.src === src); });
|
|
484
|
+
$('impHowto').innerHTML = IMP_HOWTO[src];
|
|
485
|
+
}
|
|
486
|
+
function loadImports() {
|
|
487
|
+
api('/api/imports?site=' + encodeURIComponent(state.site)).then(function (r) {
|
|
488
|
+
if (r.error) return;
|
|
489
|
+
$('impList').innerHTML = r.imports.length ? '<div class="set-card"><h3>Imported</h3>' + r.imports.map(function (i) {
|
|
490
|
+
return '<div class="set-row"><span><strong>' + (i.source === 'ga' ? 'Google Analytics' : 'Plausible') + '</strong> ' + esc(i.first_day || '?') + ' – ' + esc(i.last_day || '?') +
|
|
491
|
+
' <span class="muted small">· ' + Number(i.pageviews).toLocaleString() + ' views' + (i.created_by ? ' · by ' + esc(i.created_by) : '') + '</span></span>' +
|
|
492
|
+
'<button class="btn ghost danger mini" data-delimp="' + i.id + '">Remove</button></div>';
|
|
493
|
+
}).join('') + (r.countsUntil ? '<p class="muted small" style="margin:8px 0 0">Own data starts ' + esc(r.countsUntil) + '; imported days from then on are ignored.</p>' : '') + '</div>' : '';
|
|
494
|
+
$('impList').querySelectorAll('[data-delimp]').forEach(function (b) {
|
|
495
|
+
b.onclick = function () { if (confirm('Remove this import? The imported numbers disappear from all charts.')) api('/api/imports/' + b.dataset.delimp, { method: 'DELETE' }).then(function () { loadImports(); load(); }); };
|
|
496
|
+
});
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
$('importBtn').onclick = openImport;
|
|
500
|
+
$('closeImport').onclick = function () { closeModal($('importModal')); };
|
|
501
|
+
$('impTabs').onclick = function (e) { var b = e.target.closest('button'); if (b) setImpSrc(b.dataset.src); };
|
|
502
|
+
$('impGo').onclick = function () {
|
|
503
|
+
var files = Array.prototype.slice.call($('impFile').files || []);
|
|
504
|
+
$('impErr').textContent = '';
|
|
505
|
+
if (!files.length) { $('impErr').textContent = 'Choose a file first'; return; }
|
|
506
|
+
var done = 0, views = 0;
|
|
507
|
+
$('impStatus').textContent = 'Uploading…';
|
|
508
|
+
// one request per file: each file becomes its own import (and can be removed on its own)
|
|
509
|
+
files.reduce(function (p, f) {
|
|
510
|
+
return p.then(function () {
|
|
511
|
+
return fetch('/api/imports?site=' + encodeURIComponent(state.site) + '&source=' + impSrc + '&name=' + encodeURIComponent(f.name), {
|
|
512
|
+
method: 'POST', headers: { 'x-session-token': localStorage.getItem('nyx_token') || '', 'Content-Type': /\.zip$/i.test(f.name) ? 'application/zip' : 'text/csv' }, body: f,
|
|
513
|
+
}).then(function (r) { return r.json().then(function (j) { if (!r.ok) throw new Error(f.name + ': ' + (j.error || r.status)); done++; views += j.pageviews || 0; if (j.note) $('impStatus').textContent = j.note; }); });
|
|
514
|
+
});
|
|
515
|
+
}, Promise.resolve()).then(function () {
|
|
516
|
+
$('impStatus').textContent = done + ' file' + (done > 1 ? 's' : '') + ' imported (' + views.toLocaleString() + ' views). ' + ($('impStatus').textContent.indexOf('Days from') === 0 ? $('impStatus').textContent : '');
|
|
517
|
+
$('impFile').value = ''; loadImports(); load();
|
|
518
|
+
}).catch(function (e) { $('impErr').textContent = e.message; $('impStatus').textContent = ''; loadImports(); load(); });
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
// ── alerts ──
|
|
522
|
+
var KIND_LABEL = { spike: 'Traffic spike', drop: 'Traffic drop', goal: 'Goal' };
|
|
523
|
+
function openAlerts() {
|
|
524
|
+
// a fresh form only when the modal opens — openAlerts also re-renders the list while it is open
|
|
525
|
+
if ($('alertsModal').classList.contains('hide')) { $('alKind').value = 'spike'; $('alGoalRow').classList.add('hide'); $('alGoal').value = ''; $('alThreshold').value = '10'; $('alWindow').value = '24'; $('alWebhook').value = ''; }
|
|
526
|
+
$('alertsModal').classList.remove('hide'); $('alertsErr').textContent = ''; $('alertsSite').textContent = state.site;
|
|
527
|
+
var goals = ((lastStats && lastStats.goals) || []).map(function (g) { return g.name; });
|
|
528
|
+
$('alGoalList').innerHTML = goals.map(function (g) { return '<option value="' + esc(g) + '">'; }).join('');
|
|
529
|
+
api('/api/alerts?site=' + encodeURIComponent(state.site)).then(function (r) {
|
|
530
|
+
if (r.error) { $('alertsErr').textContent = r.error; return; }
|
|
531
|
+
$('alHint').textContent = r.fallbackWebhook ? 'Leave the webhook empty to use the server default (DIGEST_WEBHOOK_URL).' : 'Discord: channel settings → Integrations → Webhooks → Copy URL.';
|
|
532
|
+
$('alWebhook').placeholder = r.fallbackWebhook ? 'Webhook URL (optional — server default)' : 'Webhook URL (https://…)';
|
|
533
|
+
$('alertsList').innerHTML = r.alerts.length ? '<div class="set-card">' + r.alerts.map(function (a) {
|
|
534
|
+
var what = a.kind === 'goal' ? a.threshold + '× <span data-no-i18n>' + esc(a.goal) + '</span> in ' + (a.window_hours === 1 ? '1 h' : a.window_hours === 24 ? '24 h' : '7 days') : KIND_LABEL[a.kind];
|
|
535
|
+
return '<div class="set-row"><span>' + ico(a.kind === 'goal' ? 'i-goal' : a.kind === 'drop' ? 'i-down' : 'i-up', 'ui') + '<strong>' + what + '</strong>' +
|
|
536
|
+
' <span class="muted small">→ ' + (a.webhook ? '<span data-no-i18n>' + esc(a.webhook) + '</span>' : 'server default') + (a.last_fired ? ' · last sent ' + new Date(a.last_fired).toLocaleString() : '') + '</span></span>' +
|
|
537
|
+
'<label class="small muted" style="display:inline-flex;gap:5px;align-items:center"><input type="checkbox" data-al-on="' + a.id + '"' + (a.enabled ? ' checked' : '') + ' style="width:auto;margin:0"> on</label>' +
|
|
538
|
+
'<button class="btn ghost mini" data-al-test="' + a.id + '">Test</button>' +
|
|
539
|
+
'<button class="btn ghost danger mini" data-al-del="' + a.id + '">Remove</button></div>';
|
|
540
|
+
}).join('') + '</div>' : '<p class="muted small">No alerts for this site yet.</p>';
|
|
541
|
+
var L = $('alertsList');
|
|
542
|
+
L.querySelectorAll('[data-al-on]').forEach(function (c) { c.onchange = function () { api('/api/alerts/' + c.dataset.alOn, { method: 'PATCH', body: JSON.stringify({ enabled: c.checked }) }); }; });
|
|
543
|
+
L.querySelectorAll('[data-al-del]').forEach(function (b) { b.onclick = function () { api('/api/alerts/' + b.dataset.alDel, { method: 'DELETE' }).then(openAlerts); }; });
|
|
544
|
+
L.querySelectorAll('[data-al-test]').forEach(function (b) {
|
|
545
|
+
b.onclick = function () {
|
|
546
|
+
b.textContent = 'Sending…';
|
|
547
|
+
api('/api/alerts/' + b.dataset.alTest + '/test', { method: 'POST' }).then(function (r) { b.textContent = r.error ? 'Failed' : 'Sent ✓'; if (r.error) $('alertsErr').textContent = r.error; });
|
|
548
|
+
};
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
$('alertsBtn').onclick = openAlerts;
|
|
553
|
+
$('closeAlerts').onclick = function () { closeModal($('alertsModal'), { force: true }); };
|
|
554
|
+
$('alKind').onchange = function () { $('alGoalRow').classList.toggle('hide', this.value !== 'goal'); };
|
|
555
|
+
$('alAdd').onclick = function () {
|
|
556
|
+
$('alertsErr').textContent = '';
|
|
557
|
+
var body = { site: state.site, kind: $('alKind').value, webhook: $('alWebhook').value.trim() || undefined };
|
|
558
|
+
if (body.kind === 'goal') { body.goal = $('alGoal').value.trim(); body.threshold = Number($('alThreshold').value); body.window_hours = Number($('alWindow').value); }
|
|
559
|
+
api('/api/alerts', { method: 'POST', body: JSON.stringify(body) }).then(function (r) {
|
|
560
|
+
if (r.error) { $('alertsErr').textContent = r.error; return; }
|
|
561
|
+
$('alWebhook').value = ''; window.TaciModal.snapshot($('alertsModal')); openAlerts();
|
|
562
|
+
});
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
// ── add / list annotations ──
|
|
566
|
+
function openAnnotations() {
|
|
567
|
+
$('annModal').classList.remove('hide'); $('annErr').textContent = '';
|
|
568
|
+
$('annDay').value = localToday(); $('annText').value = ''; $('annText').focus(); window.TaciModal.snapshot($('annModal')); // also when re-opened after a save
|
|
569
|
+
api('/api/annotations?site=' + encodeURIComponent(state.site)).then(function (list) {
|
|
570
|
+
$('annList').innerHTML = (list || []).length ? list.map(function (a) {
|
|
571
|
+
return '<div class="set-row"><span><strong>' + esc(a.day) + '</strong> <span data-no-i18n>' + esc(a.text) + '</span>' + (a.created_by ? ' <span class="muted small">· ' + esc(a.created_by) + '</span>' : '') + '</span>' +
|
|
572
|
+
'<button class="btn ghost danger mini" data-delann="' + a.id + '">Remove</button></div>';
|
|
573
|
+
}).join('') : '<p class="muted small">No notes yet.</p>';
|
|
574
|
+
$('annList').querySelectorAll('[data-delann]').forEach(function (b) {
|
|
575
|
+
b.onclick = function () { api('/api/annotations/' + b.dataset.delann, { method: 'DELETE' }).then(function () { openAnnotations(); load(); }); };
|
|
576
|
+
});
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
$('annBtn').onclick = openAnnotations;
|
|
580
|
+
$('closeAnn').onclick = function () { closeModal($('annModal'), { force: true }); };
|
|
581
|
+
$('saveAnn').onclick = function () {
|
|
582
|
+
api('/api/annotations', { method: 'POST', body: JSON.stringify({ site: state.site, day: $('annDay').value, text: $('annText').value }) }).then(function (r) {
|
|
583
|
+
if (r.error) { $('annErr').textContent = r.error; return; }
|
|
584
|
+
openAnnotations(); load();
|
|
585
|
+
});
|
|
586
|
+
};
|
|
587
|
+
$('annText').addEventListener('keydown', function (e) { if (e.key === 'Enter') $('saveAnn').click(); });
|
|
588
|
+
|
|
410
589
|
// ── tab-aware panels (Pages: Top/Entry/Exit · Sources: Source/Medium/Campaign) ──
|
|
411
590
|
function renderPagesPanel() {
|
|
412
591
|
var s = lastStats; if (!s) return;
|
|
@@ -420,8 +599,8 @@
|
|
|
420
599
|
'android-app://com.google.android.googlequicksearchbox': 'Google App (Android)', 't.co': 'X / Twitter',
|
|
421
600
|
};
|
|
422
601
|
var PLATFORM_NAMES = { web: 'Web', android: 'Android app', ios: 'iOS app', macos: 'macOS app', windows: 'Windows app', linux: 'Linux app' };
|
|
423
|
-
function platformIcon(n) { return n === 'web' ? ico('i-web', 'ui') : n === 'windows' ? ico('i-desktop', 'ui') : brand(PLATFORM_ICON
|
|
424
|
-
function platformLabel(n) { return PLATFORM_NAMES
|
|
602
|
+
function platformIcon(n) { return n === 'web' ? ico('i-web', 'ui') : n === 'windows' ? ico('i-desktop', 'ui') : brand(own(PLATFORM_ICON, n), 'i-mobile'); }
|
|
603
|
+
function platformLabel(n) { return own(PLATFORM_NAMES, n) || n; }
|
|
425
604
|
state.pfTab = 'platforms';
|
|
426
605
|
function renderPlatformsPanel() {
|
|
427
606
|
var s = lastStats; if (!s) return;
|
|
@@ -433,7 +612,7 @@
|
|
|
433
612
|
if (state.pfTab === 'appVersions') bars('p-platforms', s.appVersions || [], { filterKey: 'version', icon: function () { return ico('i-layers', 'ui'); }, label: function (r) { return 'v' + r.name; } });
|
|
434
613
|
else bars('p-platforms', s.platforms || [], { filterKey: 'platform', icon: function (r) { return platformIcon(r.name); }, label: function (r) { return platformLabel(r.name); } });
|
|
435
614
|
}
|
|
436
|
-
function sourceLabel(n) { if (n === 'Direct') return 'Direct / none'; var nice = SOURCE_NAMES
|
|
615
|
+
function sourceLabel(n) { if (n === 'Direct') return 'Direct / none'; var nice = own(SOURCE_NAMES, n); return nice ? nice + ' · ' + n : n; }
|
|
437
616
|
function renderSourcesPanel() {
|
|
438
617
|
var s = lastStats; if (!s) return;
|
|
439
618
|
var tab = state.srcTab, fk = tab === 'mediums' ? 'medium' : tab === 'campaigns' ? 'campaign' : 'source';
|
|
@@ -545,6 +724,10 @@
|
|
|
545
724
|
api('/api/stats?' + statsQuery()).then(function (s) {
|
|
546
725
|
if (s.error) return;
|
|
547
726
|
lastStats = s;
|
|
727
|
+
var imn = $('importedNote');
|
|
728
|
+
if (s.imported && s.imported.included) { imn.textContent = 'Includes imported history' + (s.imported.until ? ' (until ' + s.imported.until + ')' : '') + '.'; imn.title = 'Visitors on imported days are summed per day, not unique across days.'; imn.classList.remove('hide'); }
|
|
729
|
+
else if (s.imported && s.imported.excludedByFilter) { imn.textContent = 'Imported history is hidden while a filter is active.'; imn.classList.remove('hide'); }
|
|
730
|
+
else imn.classList.add('hide');
|
|
548
731
|
$('s-visitors').textContent = fmt(s.totals.visitors);
|
|
549
732
|
$('s-pageviews').textContent = fmt(s.totals.pageviews);
|
|
550
733
|
$('s-bounce').textContent = s.totals.bounce + '%';
|
|
@@ -558,9 +741,9 @@
|
|
|
558
741
|
sparkline('s-pageviews', (s.series || []).map(function (d) { return d.pageviews; }), COL.p);
|
|
559
742
|
renderPagesPanel();
|
|
560
743
|
renderSourcesPanel();
|
|
561
|
-
bars('p-countries', s.countries, { filterKey: 'country', icon: function (r) { return flagIcon(r.name); }, label: function (r) { return NAMES
|
|
562
|
-
bars('p-browsers', s.browsers, { filterKey: 'browser', icon: function (r) { return r.name === 'App' ? ico('i-mobile', 'ui') : r.name === 'Edge' ? ico('i-browser', 'ui') : brand(BROWSER_ICON
|
|
563
|
-
bars('p-os', s.os, { filterKey: 'os', icon: function (r) { return r.name === 'Windows' ? ico('i-desktop', 'ui') : brand(OS_ICON
|
|
744
|
+
bars('p-countries', s.countries, { filterKey: 'country', icon: function (r) { return flagIcon(r.name); }, label: function (r) { return own(NAMES, r.name) || r.name; } });
|
|
745
|
+
bars('p-browsers', s.browsers, { filterKey: 'browser', icon: function (r) { return r.name === 'App' ? ico('i-mobile', 'ui') : r.name === 'Edge' ? ico('i-browser', 'ui') : brand(own(BROWSER_ICON, r.name), 'i-browser'); } });
|
|
746
|
+
bars('p-os', s.os, { filterKey: 'os', icon: function (r) { return r.name === 'Windows' ? ico('i-desktop', 'ui') : brand(own(OS_ICON, r.name), 'i-os'); } });
|
|
564
747
|
bars('p-devices', s.devices, { filterKey: 'device', icon: function (r) { return ico(r.name === 'Desktop' ? 'i-desktop' : r.name === 'Tablet' ? 'i-tablet' : 'i-mobile', 'ui'); } });
|
|
565
748
|
renderPlatformsPanel();
|
|
566
749
|
bars('p-languages', s.languages, { filterKey: 'lang' });
|
|
@@ -696,7 +879,7 @@
|
|
|
696
879
|
};
|
|
697
880
|
$('copyShare').onclick = function () { navigator.clipboard.writeText($('shareUrl').textContent); $('copyShare').textContent = 'Copied'; setTimeout(function () { $('copyShare').textContent = 'Copy link'; }, 1500); };
|
|
698
881
|
$('revokeShare').onclick = function () { if (shareToken) api('/api/shares/' + shareToken, { method: 'DELETE' }).then(function () { $('shareModal').classList.add('hide'); }); };
|
|
699
|
-
$('closeShare').onclick = function () { $('shareModal')
|
|
882
|
+
$('closeShare').onclick = function () { closeModal($('shareModal')); };
|
|
700
883
|
|
|
701
884
|
// ── funnels ──
|
|
702
885
|
function fmtPct(p) { return (p == null ? '—' : p + '%'); }
|
|
@@ -744,7 +927,7 @@
|
|
|
744
927
|
$('funnelModal').classList.remove('hide'); $('funnelName').focus();
|
|
745
928
|
};
|
|
746
929
|
$('addStepBtn').onclick = function () { if ($('funnelSteps').children.length < 8) $('funnelSteps').appendChild(stepRow()); };
|
|
747
|
-
$('cancelFunnel').onclick = function () { $('funnelModal')
|
|
930
|
+
$('cancelFunnel').onclick = function () { closeModal($('funnelModal'), { force: true }); };
|
|
748
931
|
$('createFunnelBtn').onclick = function () {
|
|
749
932
|
var steps = Array.prototype.map.call($('funnelSteps').children, function (row) {
|
|
750
933
|
return { type: row.querySelector('.step-type').value, value: row.querySelector('.step-value').value.trim() };
|
|
@@ -758,6 +941,7 @@
|
|
|
758
941
|
|
|
759
942
|
function boot() {
|
|
760
943
|
hideLogin();
|
|
944
|
+
if (window.TaciAccount) window.TaciAccount.afterBoot();
|
|
761
945
|
syncLegend();
|
|
762
946
|
loadSites().then(function () { load(); loadFunnels(); });
|
|
763
947
|
// live dashboard: refresh every 15s while the tab is visible
|
|
@@ -776,7 +960,7 @@
|
|
|
776
960
|
state.site = info.domain;
|
|
777
961
|
var sel = $('siteSelect');
|
|
778
962
|
sel.innerHTML = '<option>' + esc(info.domain) + '</option>'; sel.disabled = true;
|
|
779
|
-
['addSiteBtn', 'logoutBtn', 'shareBtn', 'snippetBtn', 'addFunnelBtn'].forEach(function (id) { var el = $(id); if (el) el.classList.add('hide'); });
|
|
963
|
+
['addSiteBtn', 'tzBtn', 'logoutBtn', 'settingsBtn', 'shareBtn', 'snippetBtn', 'addFunnelBtn', 'annBtn', 'alertsBtn', 'importBtn'].forEach(function (id) { var el = $(id); if (el) el.classList.add('hide'); });
|
|
780
964
|
syncLegend();
|
|
781
965
|
load();
|
|
782
966
|
clearInterval(window.__rt);
|
|
@@ -784,5 +968,9 @@
|
|
|
784
968
|
});
|
|
785
969
|
}
|
|
786
970
|
|
|
787
|
-
|
|
971
|
+
var INVITE = (location.pathname.match(/^\/invite\/([a-f0-9]{32,64})$/) || [])[1] || '';
|
|
972
|
+
if (SHARE) bootShare();
|
|
973
|
+
else if (INVITE) { $('login').classList.add('hide'); window.TaciAccount.showInvite(INVITE); }
|
|
974
|
+
else if (/^\/reset\/[a-f0-9]{32,64}$/.test(location.pathname)) { $('login').classList.add('hide'); window.TaciAccount.showReset(location.pathname.split('/').pop()); }
|
|
975
|
+
else if (TOKEN) boot(); else showLogin();
|
|
788
976
|
})();
|
package/public/assets/icons.svg
CHANGED
|
@@ -197,4 +197,10 @@
|
|
|
197
197
|
<symbol id="b-bluesky" viewBox="0 0 24 24"><path fill="currentColor" d="M5.202 2.857C7.954 4.922 10.913 9.11 12 11.358c1.087-2.247 4.046-6.436 6.798-8.501C20.783 1.366 24 .213 24 3.883c0 .732-.42 6.156-.667 7.037-.856 3.061-3.978 3.842-6.755 3.37 4.854.826 6.089 3.562 3.422 6.299-5.065 5.196-7.28-1.304-7.847-2.97-.104-.305-.152-.448-.153-.327 0-.121-.05.022-.153.327-.568 1.666-2.782 8.166-7.847 2.97-2.667-2.737-1.432-5.473 3.422-6.3-2.777.473-5.899-.308-6.755-3.369C.42 10.04 0 4.615 0 3.883c0-3.67 3.217-2.517 5.202-1.026"/></symbol>
|
|
198
198
|
<symbol id="b-mastodon" viewBox="0 0 24 24"><path fill="currentColor" d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z"/></symbol>
|
|
199
199
|
<symbol id="b-signal" viewBox="0 0 24 24"><path fill="currentColor" d="M12 0q-.934 0-1.83.139l.17 1.111a11 11 0 0 1 3.32 0l.172-1.111A12 12 0 0 0 12 0M9.152.34A12 12 0 0 0 5.77 1.742l.584.961a10.8 10.8 0 0 1 3.066-1.27zm5.696 0-.268 1.094a10.8 10.8 0 0 1 3.066 1.27l.584-.962A12 12 0 0 0 14.848.34M12 2.25a9.75 9.75 0 0 0-8.539 14.459c.074.134.1.292.064.441l-1.013 4.338 4.338-1.013a.62.62 0 0 1 .441.064A9.7 9.7 0 0 0 12 21.75c5.385 0 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25m-7.092.068a12 12 0 0 0-2.59 2.59l.909.664a11 11 0 0 1 2.345-2.345zm14.184 0-.664.909a11 11 0 0 1 2.345 2.345l.909-.664a12 12 0 0 0-2.59-2.59M1.742 5.77A12 12 0 0 0 .34 9.152l1.094.268a10.8 10.8 0 0 1 1.269-3.066zm20.516 0-.961.584a10.8 10.8 0 0 1 1.27 3.066l1.093-.268a12 12 0 0 0-1.402-3.383M.138 10.168A12 12 0 0 0 0 12q0 .934.139 1.83l1.111-.17A11 11 0 0 1 1.125 12q0-.848.125-1.66zm23.723.002-1.111.17q.125.812.125 1.66c0 .848-.042 1.12-.125 1.66l1.111.172a12.1 12.1 0 0 0 0-3.662M1.434 14.58l-1.094.268a12 12 0 0 0 .96 2.591l-.265 1.14 1.096.255.36-1.539-.188-.365a10.8 10.8 0 0 1-.87-2.35m21.133 0a10.8 10.8 0 0 1-1.27 3.067l.962.584a12 12 0 0 0 1.402-3.383zm-1.793 3.848a11 11 0 0 1-2.345 2.345l.664.909a12 12 0 0 0 2.59-2.59zm-19.959 1.1L.357 21.48a1.8 1.8 0 0 0 2.162 2.161l1.954-.455-.256-1.095-1.953.455a.675.675 0 0 1-.81-.81l.454-1.954zm16.832 1.769a10.8 10.8 0 0 1-3.066 1.27l.268 1.093a12 12 0 0 0 3.382-1.402zm-10.94.213-1.54.36.256 1.095 1.139-.266c.814.415 1.683.74 2.591.961l.268-1.094a10.8 10.8 0 0 1-2.35-.869zm3.634 1.24-.172 1.111a12.1 12.1 0 0 0 3.662 0l-.17-1.111q-.812.125-1.66.125a11 11 0 0 1-1.66-.125"/></symbol>
|
|
200
|
+
<symbol id="i-key" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z" /><circle cx="16.5" cy="7.5" r=".5" fill="currentColor" /></symbol>
|
|
201
|
+
<symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /><circle cx="12" cy="12" r="3" /></symbol>
|
|
202
|
+
<symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" /><path d="m9 12 2 2 4-4" /></symbol>
|
|
203
|
+
<symbol id="i-user-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" /><circle cx="9" cy="7" r="4" /><line x1="19" x2="19" y1="8" y2="14" /><line x1="22" x2="16" y1="11" y2="11" /></symbol>
|
|
204
|
+
<symbol id="i-smartphone" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="20" x="5" y="2" rx="2" ry="2" /><path d="M12 18h.01" /></symbol>
|
|
205
|
+
<symbol id="i-bell" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M10.268 21a2 2 0 0 0 3.464 0" /><path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" /></symbol>
|
|
200
206
|
</svg>
|