tacilytics 2.1.0 → 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/.env.example +5 -0
- package/README.md +70 -3
- package/auth.js +203 -10
- package/importers.js +223 -0
- package/package.json +6 -1
- package/public/account.js +252 -16
- package/public/app.js +216 -25
- package/public/assets/icons.svg +1 -0
- package/public/i18n.js +242 -0
- package/public/index.html +120 -6
- package/public/style.css +102 -0
- package/public/tacilytics.js +41 -1
- package/public/theme.js +14 -0
- package/report.js +300 -0
- package/server.js +592 -69
- 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>';
|
|
@@ -88,7 +102,7 @@
|
|
|
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
104
|
// ── login (the form itself lives in account.js: passphrase, accounts, TOTP, passkeys) ──
|
|
91
|
-
function showLogin() { clearInterval(window.__rt); $('login').classList.remove('hide'); $('app').classList.add('hide'); if (window.TaciAccount) window.TaciAccount.prepareLogin(); }
|
|
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
107
|
function signedIn(token) { TOKEN = token; localStorage.setItem('nyx_token', TOKEN); $('loginErr').textContent = ''; boot(); }
|
|
94
108
|
$('logoutBtn').onclick = function () {
|
|
@@ -107,15 +121,48 @@
|
|
|
107
121
|
if (!sites.length) { sel.innerHTML = '<option>no sites yet</option>'; return; }
|
|
108
122
|
if (!state.site || !sites.some(function (s) { return s.domain === state.site; })) state.site = sites[0].domain;
|
|
109
123
|
sel.value = state.site;
|
|
124
|
+
showTz();
|
|
110
125
|
});
|
|
111
126
|
}
|
|
112
|
-
$('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(); };
|
|
113
128
|
|
|
114
|
-
|
|
115
|
-
|
|
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
|
+
};
|
|
160
|
+
|
|
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 }); };
|
|
116
163
|
$('createSiteBtn').onclick = function () {
|
|
117
164
|
var d = $('newDomain').value.trim();
|
|
118
|
-
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) {
|
|
119
166
|
if (s.error) { $('addErr').textContent = s.error; return; }
|
|
120
167
|
$('addModal').classList.add('hide');
|
|
121
168
|
state.site = s.domain; localStorage.setItem('nyx_site', state.site);
|
|
@@ -148,7 +195,7 @@
|
|
|
148
195
|
return {
|
|
149
196
|
s1: 'Paste this once into the <code><head></code> of your site:',
|
|
150
197
|
c1: snippetFor(domain), s2: '', c2: '',
|
|
151
|
-
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>'
|
|
152
199
|
};
|
|
153
200
|
}
|
|
154
201
|
function renderSnippet() {
|
|
@@ -193,14 +240,15 @@
|
|
|
193
240
|
};
|
|
194
241
|
|
|
195
242
|
$('snippetBtn').onclick = function () { if (state.site) showSnippet(state.site); };
|
|
196
|
-
$('closeSnippet').onclick = function () { $('snippetModal')
|
|
243
|
+
$('closeSnippet').onclick = function () { closeModal($('snippetModal')); };
|
|
197
244
|
$('copySnippet').onclick = function () { copyFrom('copySnippet', 'snippetCode'); };
|
|
198
245
|
$('copySnippet2').onclick = function () { copyFrom('copySnippet2', 'snippetCode2', 'Copy step 2'); };
|
|
199
246
|
|
|
200
247
|
// ── live visitors (last 30 min) ──
|
|
201
248
|
var liveTimer = null;
|
|
202
249
|
function openLive() { if (!state.site || state.site === 'no sites yet') return; $('liveModal').classList.remove('hide'); fetchLive(); clearInterval(liveTimer); liveTimer = setInterval(fetchLive, 8000); }
|
|
203
|
-
function closeLive() { $('liveModal')
|
|
250
|
+
function closeLive() { closeModal($('liveModal')); }
|
|
251
|
+
$('liveModal').addEventListener('taci:close', function () { clearInterval(liveTimer); liveTimer = null; }); // every exit stops polling
|
|
204
252
|
function fetchLive() {
|
|
205
253
|
if (document.hidden) return;
|
|
206
254
|
api('/api/realtime?site=' + encodeURIComponent(state.site)).then(function (r) {
|
|
@@ -323,8 +371,8 @@
|
|
|
323
371
|
el.classList.remove('hide');
|
|
324
372
|
var chips = keys.map(function (k) {
|
|
325
373
|
var v = state.filters[k];
|
|
326
|
-
var disp = k === 'country' ? (flagIcon(v) + ' ' + esc(NAMES
|
|
327
|
-
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 +
|
|
328
376
|
'<button data-rm="' + esc(k) + '" title="Remove" aria-label="Remove filter">' + ico('i-x', 'ui') + '</button></span>';
|
|
329
377
|
}).join('');
|
|
330
378
|
el.innerHTML = '<span class="flabel">Filtered by</span>' + chips + '<span class="chip clear" id="clearFilters">Clear all</span>';
|
|
@@ -360,6 +408,23 @@
|
|
|
360
408
|
'<path d="' + line + '" fill="none" stroke="' + color + '" stroke-width="1.6" vector-effect="non-scaling-stroke" opacity=".9"/></svg>';
|
|
361
409
|
card.insertAdjacentHTML('beforeend', svg);
|
|
362
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
|
+
}
|
|
363
428
|
function drawChart(series) {
|
|
364
429
|
var svg = $('chart'); var W = 800, H = 220, pad = 8;
|
|
365
430
|
var showV = state.show.visitors, showP = state.show.pageviews;
|
|
@@ -381,25 +446,146 @@
|
|
|
381
446
|
'<defs><linearGradient id="ag" x1="0" y1="0" x2="0" y2="1">' +
|
|
382
447
|
'<stop offset="0%" stop-color="' + COL.v + '" stop-opacity="0.32"/><stop offset="100%" stop-color="' + COL.v + '" stop-opacity="0"/>' +
|
|
383
448
|
'</linearGradient></defs>' +
|
|
384
|
-
[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('') +
|
|
385
450
|
(showV ? '<path d="' + areaP + '" fill="url(#ag)"/>' : '') +
|
|
386
451
|
(showP ? '<path d="' + path('pageviews') + '" fill="none" stroke="' + COL.p + '" stroke-width="1.6" stroke-dasharray="4 4" opacity="0.85"/>' : '') +
|
|
387
452
|
(showV ? '<path d="' + path('visitors') + '" fill="none" stroke="' + COL.v + '" stroke-width="2.4" stroke-linecap="round"/>' : '') +
|
|
388
|
-
(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) +
|
|
389
455
|
hovers;
|
|
390
456
|
var tip = $('tip');
|
|
391
457
|
Array.prototype.forEach.call(svg.querySelectorAll('rect'), function (rect) {
|
|
392
458
|
rect.addEventListener('mousemove', function (e) {
|
|
393
459
|
var d = series[+rect.dataset.i];
|
|
460
|
+
var notes = annotationsFor(d);
|
|
394
461
|
tip.innerHTML = '<strong>' + esc(d.label) + '</strong>' +
|
|
395
462
|
(showV ? '<br>' + d.visitors + ' visitors' : '') +
|
|
396
|
-
(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('');
|
|
397
465
|
tip.style.left = (e.clientX + 14) + 'px'; tip.style.top = (e.clientY - 10) + 'px'; tip.style.opacity = 1;
|
|
398
466
|
});
|
|
399
467
|
rect.addEventListener('mouseleave', function () { tip.style.opacity = 0; });
|
|
400
468
|
});
|
|
401
469
|
}
|
|
402
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
|
+
|
|
403
589
|
// ── tab-aware panels (Pages: Top/Entry/Exit · Sources: Source/Medium/Campaign) ──
|
|
404
590
|
function renderPagesPanel() {
|
|
405
591
|
var s = lastStats; if (!s) return;
|
|
@@ -413,8 +599,8 @@
|
|
|
413
599
|
'android-app://com.google.android.googlequicksearchbox': 'Google App (Android)', 't.co': 'X / Twitter',
|
|
414
600
|
};
|
|
415
601
|
var PLATFORM_NAMES = { web: 'Web', android: 'Android app', ios: 'iOS app', macos: 'macOS app', windows: 'Windows app', linux: 'Linux app' };
|
|
416
|
-
function platformIcon(n) { return n === 'web' ? ico('i-web', 'ui') : n === 'windows' ? ico('i-desktop', 'ui') : brand(PLATFORM_ICON
|
|
417
|
-
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; }
|
|
418
604
|
state.pfTab = 'platforms';
|
|
419
605
|
function renderPlatformsPanel() {
|
|
420
606
|
var s = lastStats; if (!s) return;
|
|
@@ -426,7 +612,7 @@
|
|
|
426
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; } });
|
|
427
613
|
else bars('p-platforms', s.platforms || [], { filterKey: 'platform', icon: function (r) { return platformIcon(r.name); }, label: function (r) { return platformLabel(r.name); } });
|
|
428
614
|
}
|
|
429
|
-
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; }
|
|
430
616
|
function renderSourcesPanel() {
|
|
431
617
|
var s = lastStats; if (!s) return;
|
|
432
618
|
var tab = state.srcTab, fk = tab === 'mediums' ? 'medium' : tab === 'campaigns' ? 'campaign' : 'source';
|
|
@@ -538,6 +724,10 @@
|
|
|
538
724
|
api('/api/stats?' + statsQuery()).then(function (s) {
|
|
539
725
|
if (s.error) return;
|
|
540
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');
|
|
541
731
|
$('s-visitors').textContent = fmt(s.totals.visitors);
|
|
542
732
|
$('s-pageviews').textContent = fmt(s.totals.pageviews);
|
|
543
733
|
$('s-bounce').textContent = s.totals.bounce + '%';
|
|
@@ -551,9 +741,9 @@
|
|
|
551
741
|
sparkline('s-pageviews', (s.series || []).map(function (d) { return d.pageviews; }), COL.p);
|
|
552
742
|
renderPagesPanel();
|
|
553
743
|
renderSourcesPanel();
|
|
554
|
-
bars('p-countries', s.countries, { filterKey: 'country', icon: function (r) { return flagIcon(r.name); }, label: function (r) { return NAMES
|
|
555
|
-
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
|
|
556
|
-
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'); } });
|
|
557
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'); } });
|
|
558
748
|
renderPlatformsPanel();
|
|
559
749
|
bars('p-languages', s.languages, { filterKey: 'lang' });
|
|
@@ -689,7 +879,7 @@
|
|
|
689
879
|
};
|
|
690
880
|
$('copyShare').onclick = function () { navigator.clipboard.writeText($('shareUrl').textContent); $('copyShare').textContent = 'Copied'; setTimeout(function () { $('copyShare').textContent = 'Copy link'; }, 1500); };
|
|
691
881
|
$('revokeShare').onclick = function () { if (shareToken) api('/api/shares/' + shareToken, { method: 'DELETE' }).then(function () { $('shareModal').classList.add('hide'); }); };
|
|
692
|
-
$('closeShare').onclick = function () { $('shareModal')
|
|
882
|
+
$('closeShare').onclick = function () { closeModal($('shareModal')); };
|
|
693
883
|
|
|
694
884
|
// ── funnels ──
|
|
695
885
|
function fmtPct(p) { return (p == null ? '—' : p + '%'); }
|
|
@@ -737,7 +927,7 @@
|
|
|
737
927
|
$('funnelModal').classList.remove('hide'); $('funnelName').focus();
|
|
738
928
|
};
|
|
739
929
|
$('addStepBtn').onclick = function () { if ($('funnelSteps').children.length < 8) $('funnelSteps').appendChild(stepRow()); };
|
|
740
|
-
$('cancelFunnel').onclick = function () { $('funnelModal')
|
|
930
|
+
$('cancelFunnel').onclick = function () { closeModal($('funnelModal'), { force: true }); };
|
|
741
931
|
$('createFunnelBtn').onclick = function () {
|
|
742
932
|
var steps = Array.prototype.map.call($('funnelSteps').children, function (row) {
|
|
743
933
|
return { type: row.querySelector('.step-type').value, value: row.querySelector('.step-value').value.trim() };
|
|
@@ -770,7 +960,7 @@
|
|
|
770
960
|
state.site = info.domain;
|
|
771
961
|
var sel = $('siteSelect');
|
|
772
962
|
sel.innerHTML = '<option>' + esc(info.domain) + '</option>'; sel.disabled = true;
|
|
773
|
-
['addSiteBtn', 'logoutBtn', 'settingsBtn', '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'); });
|
|
774
964
|
syncLegend();
|
|
775
965
|
load();
|
|
776
966
|
clearInterval(window.__rt);
|
|
@@ -781,5 +971,6 @@
|
|
|
781
971
|
var INVITE = (location.pathname.match(/^\/invite\/([a-f0-9]{32,64})$/) || [])[1] || '';
|
|
782
972
|
if (SHARE) bootShare();
|
|
783
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()); }
|
|
784
975
|
else if (TOKEN) boot(); else showLogin();
|
|
785
976
|
})();
|
package/public/assets/icons.svg
CHANGED
|
@@ -202,4 +202,5 @@
|
|
|
202
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
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
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>
|
|
205
206
|
</svg>
|