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/i18n.js
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/* Tacilytics — dashboard translations.
|
|
2
|
+
The UI is written in English. When German is active, every text node and every
|
|
3
|
+
placeholder / title / aria-label that appears in the DOM is looked up in a dictionary
|
|
4
|
+
(exact match, then a few patterns for strings with numbers or names in them).
|
|
5
|
+
A MutationObserver covers everything rendered later, so new code needs no t() calls.
|
|
6
|
+
Unknown strings stay English; with ?i18n=debug window.__taciMissing collects them (for translators).
|
|
7
|
+
Language: Settings → Security → Language (auto = browser language), per browser. */
|
|
8
|
+
(function () {
|
|
9
|
+
var DE = {
|
|
10
|
+
// header / toolbar
|
|
11
|
+
'Settings': 'Einstellungen', 'Sign out': 'Abmelden', 'Add site': 'Seite hinzufügen', 'online now': 'gerade online', 'online': 'online',
|
|
12
|
+
'View live visitors': 'Live-Besucher ansehen', 'Account, security and team': 'Konto, Sicherheit und Team',
|
|
13
|
+
"Cookieless analytics · we count, we don't tell": 'Analyse ohne Cookies · wir zählen, wir verraten nichts',
|
|
14
|
+
'Today': 'Heute', '7 days': '7 Tage', '30 days': '30 Tage', '90 days': '90 Tage', '12 mo': '12 Mon.',
|
|
15
|
+
'From': 'Von', 'To': 'Bis', 'Apply': 'Anwenden', 'CSV': 'CSV', 'Share': 'Teilen', 'Embed code': 'Einbettungscode',
|
|
16
|
+
'Alerts': 'Alarme', 'Import': 'Import', 'Create a public read-only link': 'Öffentlichen Nur-Lesen-Link erstellen',
|
|
17
|
+
'Get a message on traffic spikes, drops or reached goals': 'Nachricht bei Traffic-Spitzen, Einbrüchen oder erreichten Zielen',
|
|
18
|
+
'Bring your history from Google Analytics or Plausible': 'Alte Daten aus Google Analytics oder Plausible übernehmen',
|
|
19
|
+
'Filtered by': 'Gefiltert nach', 'Clear all': 'Alle entfernen',
|
|
20
|
+
// stat cards / chart
|
|
21
|
+
'Unique visitors': 'Besucher', 'Visitors': 'Besucher', 'Pageviews': 'Seitenaufrufe', 'Bounce rate': 'Absprungrate',
|
|
22
|
+
'Views / visitor': 'Aufrufe / Besucher', 'Avg. visit time': 'Ø Besuchsdauer', 'Conversion rate': 'Conversion-Rate',
|
|
23
|
+
'Revenue': 'Umsatz', 'Orders': 'Bestellungen', 'Avg. order value': 'Ø Bestellwert', 'Notes': 'Notizen',
|
|
24
|
+
'Mark a day in the chart, e.g. a newsletter or a release': 'Einen Tag im Diagramm markieren, z. B. Newsletter oder Release',
|
|
25
|
+
'Includes imported history.': 'Enthält importierte Daten.', 'Imported history is hidden while a filter is active.': 'Importierte Daten sind ausgeblendet, solange ein Filter aktiv ist.',
|
|
26
|
+
'vs. previous period': 'ggü. Vorzeitraum', 'vs. last year': 'ggü. Vorjahr',
|
|
27
|
+
// panels
|
|
28
|
+
'Pages': 'Seiten', 'Top': 'Top', 'Entry': 'Einstieg', 'Exit': 'Ausstieg', 'Sources': 'Quellen', 'Source': 'Quelle', 'Medium': 'Medium', 'Campaign': 'Kampagne',
|
|
29
|
+
'Locations': 'Länder', 'Browsers': 'Browser', 'OS': 'Betriebssysteme', 'Devices': 'Geräte', 'Languages': 'Sprachen',
|
|
30
|
+
'Platforms': 'Plattformen', 'Platform': 'Plattform', 'App version': 'App-Version', 'web vs. apps': 'Web vs. Apps',
|
|
31
|
+
'Goals & conversions': 'Ziele & Conversions', 'Goal': 'Ziel', 'Funnels': 'Funnels', '+ New funnel': '+ Neuer Funnel',
|
|
32
|
+
'Visitor journeys': 'Besucherwege', 'Scroll depth': 'Scrolltiefe', 'avg · share reaching 75 %': 'Ø · Anteil, der 75 % erreicht',
|
|
33
|
+
'Site search': 'Seitensuche', 'No results': 'Ohne Treffer', 'Broken pages (404)': 'Kaputte Seiten (404)',
|
|
34
|
+
'Revenue by source': 'Umsatz nach Quelle', 'Revenue by platform': 'Umsatz nach Plattform', 'first source of the buying visit': 'erste Quelle des Kaufbesuchs',
|
|
35
|
+
'No data yet': 'Noch keine Daten', 'Not enough data yet': 'Noch nicht genug Daten', 'Add a site to begin': 'Füge eine Seite hinzu, um zu starten',
|
|
36
|
+
'No funnels yet — define one to see where visitors drop off.': 'Noch keine Funnels. Lege einen an, um zu sehen, wo Besucher abspringen.',
|
|
37
|
+
'Direct': 'Direkt', 'Other': 'Andere', 'Left the site': 'Seite verlassen', 'Desktop': 'Desktop', 'Mobile': 'Mobil', 'Tablet': 'Tablet',
|
|
38
|
+
'visitors': 'Besucher', 'views': 'Aufrufe', 'Page': 'Seite',
|
|
39
|
+
// live
|
|
40
|
+
'Live': 'Live', 'Recent activity': 'Letzte Aktivität', 'Visitors over the last 30 minutes · updates every few seconds': 'Besucher der letzten 30 Minuten · aktualisiert sich alle paar Sekunden',
|
|
41
|
+
'No visitors in the last 30 minutes': 'Keine Besucher in den letzten 30 Minuten',
|
|
42
|
+
// common buttons
|
|
43
|
+
'Close': 'Schließen', 'Signing in with your password also asks for a 6-digit code from your authenticator app.': 'Bei der Anmeldung mit Passwort wird zusätzlich ein 6-stelliger Code aus deiner Authenticator-App abgefragt.', 'Cancel': 'Abbrechen', 'Create': 'Erstellen', 'Add': 'Hinzufügen', 'Save': 'Speichern', 'Remove': 'Entfernen',
|
|
44
|
+
'Revoke': 'Widerrufen', 'Copy': 'Kopieren', 'Copy link': 'Link kopieren', 'Copied ✓': 'Kopiert ✓', 'Copied': 'Kopiert', 'Saved ✓': 'Gespeichert ✓',
|
|
45
|
+
'Sending…': 'Wird gesendet …', 'Sent ✓': 'Gesendet ✓', 'Failed': 'Fehlgeschlagen', 'Test': 'Test', 'Loading…': 'Lädt …', 'Older': 'Ältere',
|
|
46
|
+
'Uploading…': 'Wird hochgeladen …', 'Download': 'Herunterladen', 'Downloaded': 'Heruntergeladen', 'Changed ✓': 'Geändert ✓', 'on': 'an',
|
|
47
|
+
// add site / snippet / share / funnel
|
|
48
|
+
'Add a site': 'Seite hinzufügen', 'Enter the domain you want to track.': 'Gib die Domain ein, die du messen willst.', 'example.com': 'beispiel.de',
|
|
49
|
+
'Add tracking': 'Tracking einbauen', 'for': 'für', '. Same dashboard for website and apps.': '. Ein Dashboard für Website und Apps.',
|
|
50
|
+
'Website': 'Website', 'Flutter app': 'Flutter-App', 'Other apps': 'Andere Apps', 'Copy step 2': 'Schritt 2 kopieren',
|
|
51
|
+
'Share dashboard': 'Dashboard teilen', 'Anyone with this link can view stats for': 'Jeder mit diesem Link sieht die Zahlen von',
|
|
52
|
+
'— read-only, no login.': '. Nur lesen, ohne Anmeldung.', 'Revoke this link': 'Diesen Link widerrufen',
|
|
53
|
+
'New funnel': 'Neuer Funnel', '2–8 ordered steps. A visitor advances only after completing the previous step.': '2–8 Schritte in fester Reihenfolge. Ein Besucher kommt erst weiter, wenn er den vorigen Schritt erledigt hat.',
|
|
54
|
+
'Funnel name, e.g. Signup flow': 'Name, z. B. Registrierung', '+ Add step': '+ Schritt hinzufügen',
|
|
55
|
+
'Link expired': 'Link abgelaufen', 'This share link was revoked or never existed.': 'Dieser Link wurde widerrufen oder hat nie existiert.',
|
|
56
|
+
// notes / alerts / import
|
|
57
|
+
'Notes in the chart': 'Notizen im Diagramm', 'e.g. Newsletter sent': 'z. B. Newsletter verschickt', 'No notes yet.': 'Noch keine Notizen.',
|
|
58
|
+
'Mark a day — a newsletter, a release, a sale — so a spike explains itself later. Everyone with access to this site sees them.': 'Markiere einen Tag, z. B. Newsletter, Release oder Sale, damit eine Spitze später erklärt ist. Alle mit Zugriff auf die Seite sehen die Notizen.',
|
|
59
|
+
'Alerts for': 'Alarme für', 'New alert': 'Neuer Alarm', 'Add alert': 'Alarm hinzufügen', 'No alerts for this site yet.': 'Für diese Seite gibt es noch keine Alarme.',
|
|
60
|
+
'Tacilytics sends a message to a webhook (Discord, Slack, or anything that takes JSON). Checked every 10 minutes.': 'Tacilytics schickt eine Nachricht an einen Webhook (Discord, Slack oder alles, was JSON annimmt). Geprüft wird alle 10 Minuten.',
|
|
61
|
+
'Traffic spike — about 3× the usual for this hour': 'Traffic-Spitze: etwa 3× so viel wie sonst zu dieser Uhrzeit',
|
|
62
|
+
'Traffic drop — under 20 % of usual (tracker broken?)': 'Traffic-Einbruch: unter 20 % von sonst (Tracker kaputt?)',
|
|
63
|
+
'Goal reached — e.g. 10× Purchase': 'Ziel erreicht, z. B. 10× Purchase', 'Event name, e.g. Purchase': 'Event-Name, z. B. Purchase', 'How many': 'Wie oft',
|
|
64
|
+
'in 1 hour': 'in 1 Stunde', 'in 24 hours': 'in 24 Stunden', 'in 7 days': 'in 7 Tagen', 'Traffic spike': 'Traffic-Spitze', 'Traffic drop': 'Traffic-Einbruch',
|
|
65
|
+
'Webhook URL (https://…)': 'Webhook-URL (https://…)', 'Webhook URL (optional — server default)': 'Webhook-URL (optional, sonst Server-Standard)',
|
|
66
|
+
'Leave the webhook empty to use the server default (DIGEST_WEBHOOK_URL).': 'Webhook leer lassen, um den Server-Standard zu nutzen (DIGEST_WEBHOOK_URL).',
|
|
67
|
+
'Discord: channel settings → Integrations → Webhooks → Copy URL.': 'Discord: Kanaleinstellungen → Integrationen → Webhooks → URL kopieren.',
|
|
68
|
+
'Import history into': 'Daten importieren in', 'Imported': 'Importiert', 'Plausible': 'Plausible', 'Google Analytics 4': 'Google Analytics 4',
|
|
69
|
+
'Bring your old numbers along. They show up in the chart and in the panels for the days': 'Nimm deine alten Zahlen mit. Sie erscheinen im Diagramm und in den Listen für die Tage,',
|
|
70
|
+
'before': 'bevor', 'Tacilytics started counting — never twice.': 'Tacilytics selbst gezählt hat, nie doppelt.',
|
|
71
|
+
'Choose a file first': 'Bitte zuerst eine Datei wählen',
|
|
72
|
+
// sign in / invite / reset
|
|
73
|
+
'Cookieless analytics. Sign in with your passphrase.': 'Analyse ohne Cookies. Melde dich mit deiner Passphrase an.',
|
|
74
|
+
'Cookieless analytics. Sign in to your account.': 'Analyse ohne Cookies. Melde dich mit deinem Konto an.',
|
|
75
|
+
'Passphrase': 'Passphrase', 'Password': 'Passwort', 'Username': 'Benutzername', 'Sign in': 'Anmelden', 'Sign in with a passkey': 'Mit Passkey anmelden',
|
|
76
|
+
'6-digit code': '6-stelliger Code', 'Verify': 'Bestätigen', 'Lost your phone? Enter one of your recovery codes instead.': 'Handy verloren? Gib stattdessen einen deiner Wiederherstellungscodes ein.',
|
|
77
|
+
'Wrong password': 'Falsches Passwort', 'Wrong username or password': 'Falscher Benutzername oder falsches Passwort', 'Wrong code': 'Falscher Code',
|
|
78
|
+
'Connection error': 'Verbindungsfehler', 'Too many attempts — wait 15 minutes': 'Zu viele Versuche, bitte 15 Minuten warten',
|
|
79
|
+
'Sign-in expired — enter your password again': 'Anmeldung abgelaufen, bitte Passwort erneut eingeben', 'Passkey not accepted': 'Passkey nicht akzeptiert',
|
|
80
|
+
'Join Tacilytics': 'Tacilytics beitreten', 'Choose a username and a password.': 'Wähle Benutzername und Passwort.', 'Create account': 'Konto erstellen',
|
|
81
|
+
'Password (at least 10 characters)': 'Passwort (mindestens 10 Zeichen)', 'New password': 'Neues Passwort',
|
|
82
|
+
'New password (at least 10 characters)': 'Neues Passwort (mindestens 10 Zeichen)', 'Choose a new password.': 'Wähle ein neues Passwort.', 'Save password': 'Passwort speichern',
|
|
83
|
+
'Password saved. Sign in with your new password.': 'Passwort gespeichert. Melde dich mit dem neuen Passwort an.',
|
|
84
|
+
'This invite link is invalid, used or expired': 'Dieser Einladungslink ist ungültig, schon benutzt oder abgelaufen',
|
|
85
|
+
'This reset link is invalid, used or expired': 'Dieser Link ist ungültig, schon benutzt oder abgelaufen',
|
|
86
|
+
// settings
|
|
87
|
+
'Security': 'Sicherheit', 'Team': 'Team', 'Activity': 'Aktivität',
|
|
88
|
+
'Passkeys': 'Passkeys', 'optional': 'optional', 'No passkey yet.': 'Noch kein Passkey.', 'Add passkey': 'Passkey hinzufügen', 'Name, e.g. MacBook': 'Name, z. B. MacBook',
|
|
89
|
+
'Sign in with Face ID, Touch ID, Windows Hello or a security key. No password, no code — and it cannot be phished.': 'Anmelden mit Face ID, Touch ID, Windows Hello oder Sicherheitsschlüssel. Kein Passwort, kein Code, und Phishing geht nicht.',
|
|
90
|
+
'This browser does not support passkeys.': 'Dieser Browser unterstützt keine Passkeys.',
|
|
91
|
+
'Two-factor code (OTP)': 'Zwei-Faktor-Code (OTP)', 'Set up': 'Einrichten', 'Turn on': 'Einschalten', 'Turn off': 'Ausschalten', 'New recovery codes': 'Neue Wiederherstellungscodes',
|
|
92
|
+
'Use any authenticator app (1Password, Google Authenticator, Authy, Bitwarden …).': 'Geht mit jeder Authenticator-App (1Password, Google Authenticator, Authy, Bitwarden …).',
|
|
93
|
+
'Scan the code, or enter this key by hand:': 'Scanne den Code oder gib den Schlüssel von Hand ein:', 'Your password': 'Dein Passwort',
|
|
94
|
+
'Save your recovery codes': 'Wiederherstellungscodes sichern', 'I saved them': 'Gesichert',
|
|
95
|
+
'Each code works once, instead of a 6-digit code. Keep them somewhere safe — they are shown only now.': 'Jeder Code funktioniert einmal, anstelle eines 6-stelligen Codes. Bewahre sie sicher auf, sie werden nur jetzt angezeigt.',
|
|
96
|
+
"Don’t count my own visits": 'Eigene Besuche nicht zählen', 'Ignore this browser': 'Diesen Browser ignorieren', 'Count again': 'Wieder zählen',
|
|
97
|
+
'Open your site once with one of these links, in every browser and on every phone you use. That browser is then ignored on that site (stored in the browser, nothing on the server). The second link undoes it.': 'Öffne deine Seite einmal mit einem dieser Links, in jedem Browser und auf jedem Handy, das du nutzt. Dieser Browser wird dann auf der Seite nicht mehr gezählt (gespeichert im Browser, nichts auf dem Server). Der zweite Link macht es rückgängig.',
|
|
98
|
+
'Weekly report': 'Wochenbericht', 'e-mail': 'E-Mail', 'every Monday': 'jeden Montag', 'Send one now': 'Jetzt einen senden',
|
|
99
|
+
'Every Monday morning, for each of your sites: key figures, a day-by-day chart, highlights, top pages, sources, countries, devices, goals, shop numbers and your notes — compared with the week before.': 'Jeden Montagmorgen für jede deiner Seiten: Kennzahlen, Tagesdiagramm, Auffälliges, Top-Seiten, Quellen, Länder, Geräte, Ziele, Shop-Zahlen und deine Notizen, verglichen mit der Vorwoche.',
|
|
100
|
+
'Language of the e-mail': 'Sprache der Mail',
|
|
101
|
+
'E-mail is not set up on this server yet (SMTP_URL and REPORT_FROM in .env).': 'Auf diesem Server ist noch kein Mailversand eingerichtet (SMTP_URL und REPORT_FROM in der .env).',
|
|
102
|
+
'API keys': 'API-Keys', 'No keys yet.': 'Noch keine Keys.', 'Create key': 'Key erstellen', 'read-only': 'nur lesen', 'read + write': 'lesen + schreiben', 'never used': 'nie benutzt',
|
|
103
|
+
'Name, e.g. reporting script': 'Name, z. B. Reporting-Skript', 'Copy key': 'Key kopieren', 'Copy it now — it is shown only once.': 'Jetzt kopieren, er wird nur einmal angezeigt.',
|
|
104
|
+
'Current password': 'Aktuelles Passwort', 'New password (10+)': 'Neues Passwort (10+)', 'Change': 'Ändern',
|
|
105
|
+
'Changing it signs you out on all other devices.': 'Danach bist du auf allen anderen Geräten abgemeldet.',
|
|
106
|
+
'Language': 'Sprache', 'Theme': 'Darstellung', 'System': 'System', 'Light': 'Hell', 'Dark': 'Dunkel', 'Automatic': 'Automatisch',
|
|
107
|
+
'Appearance': 'Darstellung', 'Language and appearance are saved in this browser.': 'Sprache und Darstellung werden in diesem Browser gespeichert.',
|
|
108
|
+
'Create your account': 'Konto erstellen', 'Create owner account': 'Owner-Konto erstellen',
|
|
109
|
+
'Members': 'Mitglieder', 'Invite someone': 'Jemanden einladen', 'Create link': 'Link erstellen', 'Open invites': 'Offene Einladungen',
|
|
110
|
+
'Creates a link that works once, for 7 days. Send it yourself — Tacilytics sends no e-mail.': 'Erstellt einen Link, der einmal und 7 Tage lang funktioniert. Verschick ihn selbst, Tacilytics verschickt keine Mails.',
|
|
111
|
+
'Owner': 'Owner', 'Admin': 'Admin', 'Viewer': 'Viewer', 'Sites': 'Seiten', 'Password link': 'Passwort-Link', 'Reset 2FA': '2FA zurücksetzen',
|
|
112
|
+
'One-time link to set a new password (24 h)': 'Einmaliger Link für ein neues Passwort (24 h)', '(you)': '(du)', 'all sites': 'alle Seiten', '2FA on': '2FA an', 'no 2FA': 'ohne 2FA',
|
|
113
|
+
'Nothing yet.': 'Noch nichts.', 'someone': 'jemand',
|
|
114
|
+
'Remove this passkey?': 'Diesen Passkey entfernen?', 'Remove this member? They are signed out immediately.': 'Mitglied entfernen? Die Person wird sofort abgemeldet.',
|
|
115
|
+
'Turn off two-factor codes?': 'Zwei-Faktor-Codes ausschalten?', 'Turn off 2FA for this member? Use this when they lost their phone.': '2FA für dieses Mitglied ausschalten? Das ist für den Fall, dass das Handy weg ist.',
|
|
116
|
+
'Revoke this key? Scripts using it stop working immediately.': 'Key widerrufen? Skripte, die ihn nutzen, funktionieren sofort nicht mehr.',
|
|
117
|
+
'Remove this import? The imported numbers disappear from all charts.': 'Import entfernen? Die importierten Zahlen verschwinden aus allen Diagrammen.',
|
|
118
|
+
// activity labels
|
|
119
|
+
'added site': 'hat Seite hinzugefügt', 'removed site': 'hat Seite entfernt', 'created share link': 'hat Share-Link erstellt', 'revoked share link': 'hat Share-Link widerrufen',
|
|
120
|
+
'added note': 'hat Notiz hinzugefügt', 'removed note': 'hat Notiz entfernt', 'added funnel': 'hat Funnel angelegt', 'removed funnel': 'hat Funnel entfernt',
|
|
121
|
+
'created the first account': 'hat das erste Konto erstellt', 'created invite': 'hat eingeladen', 'revoked invite': 'hat Einladung widerrufen',
|
|
122
|
+
'joined via invite': 'ist über Einladung beigetreten', 'changed role': 'hat Rolle geändert', 'removed member': 'hat Mitglied entfernt',
|
|
123
|
+
'reset 2FA of': 'hat 2FA zurückgesetzt von', 'created password link for': 'hat Passwort-Link erstellt für', 'changed own password': 'hat eigenes Passwort geändert',
|
|
124
|
+
'set a new password via link': 'hat per Link ein neues Passwort gesetzt', 'turned 2FA on': 'hat 2FA eingeschaltet', 'turned 2FA off': 'hat 2FA ausgeschaltet',
|
|
125
|
+
'made new recovery codes': 'hat neue Wiederherstellungscodes erzeugt', 'added passkey': 'hat Passkey hinzugefügt', 'removed passkey': 'hat Passkey entfernt',
|
|
126
|
+
'created API key': 'hat API-Key erstellt', 'revoked API key': 'hat API-Key widerrufen', 'turned the weekly report on': 'hat den Wochenbericht eingeschaltet',
|
|
127
|
+
'turned the weekly report off': 'hat den Wochenbericht ausgeschaltet', 'added alert': 'hat Alarm hinzugefügt', 'removed alert': 'hat Alarm entfernt',
|
|
128
|
+
'turned alert on': 'hat Alarm eingeschaltet', 'turned alert off': 'hat Alarm ausgeschaltet', 'imported history': 'hat Daten importiert', 'removed import': 'hat Import entfernt',
|
|
129
|
+
'reset the login (password, 2FA, sessions) of': 'hat die Anmeldung zurückgesetzt (Passwort, 2FA, Sitzungen) von',
|
|
130
|
+
'Visitors on imported days are summed per day, not unique across days.': 'Besucher an importierten Tagen werden pro Tag summiert, nicht über Tage hinweg eindeutig gezählt.',
|
|
131
|
+
'Tacilytics · open source ·': 'Tacilytics · Open Source ·',
|
|
132
|
+
'· open source ·': '· Open Source ·',
|
|
133
|
+
'Discard your changes?': 'Änderungen verwerfen?',
|
|
134
|
+
'Time zone': 'Zeitzone',
|
|
135
|
+
'Time zone (days start at midnight here)': 'Zeitzone (hier beginnt der Tag um Mitternacht)',
|
|
136
|
+
'Time zone of this site — days start at midnight here': 'Zeitzone dieser Website, hier beginnt der Tag um Mitternacht',
|
|
137
|
+
'Days, hours, the weekly report and alerts follow this time zone for': 'Tage, Stunden, Wochenbericht und Alarme richten sich nach dieser Zeitzone für',
|
|
138
|
+
'Stored data does not change — switching back and forth is safe.': 'Gespeicherte Daten ändern sich nicht, Hin- und Herwechseln ist gefahrlos.',
|
|
139
|
+
'Unique visitors per day are an approximation outside UTC: the anonymous visitor ID changes at midnight UTC.': 'Eindeutige Besucher pro Tag sind außerhalb von UTC eine Näherung: Die anonyme Besucher-ID wechselt um Mitternacht UTC.',
|
|
140
|
+
'Unknown time zone': 'Unbekannte Zeitzone',
|
|
141
|
+
'Direct / none': 'Direkt / keine', 'Android app': 'Android-App', 'iOS app': 'iOS-App', 'Web': 'Web',
|
|
142
|
+
'For scripts, agents and the CLI (': 'Für Skripte, Agents und die CLI (',
|
|
143
|
+
'). A key acts as you, sees only your sites, and cannot touch the team or your security settings. Read-only unless you untick it.': '). Ein Key handelt als du, sieht nur deine Seiten und kann weder das Team noch deine Sicherheitseinstellungen ändern. Nur lesen, außer du nimmst den Haken raus.',
|
|
144
|
+
'Signed in as': 'Angemeldet als',
|
|
145
|
+
'Viewer — read only': 'Viewer: nur lesen', 'Admin — sites, share links and funnels (all or picked sites)': 'Admin: Seiten, Share-Links und Funnels (alle oder ausgewählte Seiten)', 'Owner — everything, including the team': 'Owner: alles, auch das Team',
|
|
146
|
+
'In Plausible:': 'In Plausible:', 'Site settings → Imports & Exports → Export data': 'Website-Einstellungen → Imports & Exports → Export data',
|
|
147
|
+
'. Upload the ZIP you get (or the CSV files in it).': '. Lade das ZIP hoch (oder die CSV-Dateien darin).',
|
|
148
|
+
'In Google Analytics 4: open a report (e.g.': 'In Google Analytics 4: Öffne einen Bericht (z. B.', 'Pages and screens': 'Seiten und Bildschirme', 'Traffic acquisition': 'Akquisition von Zugriffen',
|
|
149
|
+
'Demographic details': 'Demografische Details', '), add': ') und füge', 'Date': 'Datum', 'as a secondary dimension, then': 'als sekundäre Dimension hinzu, dann',
|
|
150
|
+
'Share → Download file → CSV': 'Teilen → Datei herunterladen → CSV', '. One file per report; pick several at once.': '. Eine Datei pro Bericht, du kannst mehrere auf einmal wählen.',
|
|
151
|
+
'Privacy flags on the tag:': 'Datenschutz-Optionen am Tag:', 'Track goals & properties from your code:': 'Ziele und Eigenschaften aus deinem Code messen:',
|
|
152
|
+
'Recovery codes left:': 'Übrige Wiederherstellungscodes:', 'server default': 'Server-Standard', 'last sent': 'zuletzt gesendet',
|
|
153
|
+
};
|
|
154
|
+
// strings with numbers / names in them
|
|
155
|
+
var DE_RX = [
|
|
156
|
+
[/^Every change by anyone on the team, newest first\. Kept for a year \(AUDIT_RETENTION_DAYS\)\.$/, 'Jede Änderung im Team, neueste zuerst. Wird ein Jahr aufbewahrt (AUDIT_RETENTION_DAYS).'],
|
|
157
|
+
[/^Signed in as (.+) · (Owner|Admin|Viewer)$/, 'Angemeldet als $1 · $2'],
|
|
158
|
+
[/^You were invited as (owner|admin|viewer)\. Choose a username and a password\.$/, function (m, r) { return 'Du wurdest als ' + { owner: 'Owner', admin: 'Admin', viewer: 'Viewer' }[r] + ' eingeladen. Wähle Benutzername und Passwort.'; }],
|
|
159
|
+
[/^Choose a new password for (.+)\.( Your two-factor code stays on\.)?$/, function (m, u, z) { return 'Wähle ein neues Passwort für ' + u + '.' + (z ? ' Dein Zwei-Faktor-Code bleibt an.' : ''); }],
|
|
160
|
+
[/^(\d+) files? imported \(([\d.,]+) views\)\.\s*(.*)$/, function (m, n, v, rest) { return n + (n === '1' ? ' Datei' : ' Dateien') + ' importiert (' + v + ' Aufrufe). ' + rest; }],
|
|
161
|
+
[/^Includes imported history \(until (.+)\)\.$/, 'Enthält importierte Daten (bis $1).'],
|
|
162
|
+
[/^Own data starts (.+); imported days from then on are ignored\.$/, 'Eigene Daten ab $1. Importierte Tage ab dann werden ignoriert.'],
|
|
163
|
+
[/^Days from (.+) on already have own data — imported numbers count only before that day\.$/, 'Ab $1 gibt es schon eigene Daten. Importierte Zahlen zählen nur vor diesem Tag.'],
|
|
164
|
+
[/^Left the site \(([\d.,]+k?)\)$/, 'Seite verlassen ($1)'],
|
|
165
|
+
[/^(\d+)% deep$/, '$1 % tief'],
|
|
166
|
+
[/^(.+) — ([\d.,]+k?) measured views$/, '$1 · $2 gemessene Aufrufe'],
|
|
167
|
+
[/^· ([\d.,]+k?) sessions, top paths, 30-min session window$/, '· $1 Sitzungen, häufigste Wege, Sitzungsfenster 30 Min.'],
|
|
168
|
+
[/^([\d.,]+) searches · ([\d.,]+)% without results$/, '$1 Suchen · $2 % ohne Treffer'],
|
|
169
|
+
[/^(“[^”]+”)\s+· (\d+)× no results$/, '$1 · $2× ohne Treffer'],
|
|
170
|
+
[/^· (Owner|Admin|Viewer)$/, '· $1'],
|
|
171
|
+
[/^(2FA on|no 2FA)((?: · \d+ passkeys?)?) · last sign-in (.+)$/, function (m, a, pk, d) { return (a === '2FA on' ? '2FA an' : 'ohne 2FA') + pk.replace('passkeys', 'Passkeys').replace('passkey', 'Passkey') + ' · letzte Anmeldung ' + d; }],
|
|
172
|
+
[/^(.+) · (2FA on|no 2FA)((?: · \d+ passkeys?)?) · last sign-in (.+)$/, function (m, s, a, pk, d) { return s.replace('all sites', 'alle Seiten') + ' · ' + (a === '2FA on' ? '2FA an' : 'ohne 2FA') + pk + ' · letzte Anmeldung ' + d; }],
|
|
173
|
+
[/^(\d+) visitors$/, '$1 Besucher'], [/^(\d+) views$/, '$1 Aufrufe'], [/^added (.+)$/, 'hinzugefügt $1'],
|
|
174
|
+
[/^used (.+)$/, 'benutzt $1'], [/^last sign-in (.+)$/, 'letzte Anmeldung $1'], [/^expires (.+)$/, 'läuft ab $1'], [/^Last sent (.+)$/, 'Zuletzt gesendet $1'],
|
|
175
|
+
[/^Server did not answer with JSON \(HTTP (\d+)\)$/, 'Der Server hat nicht mit JSON geantwortet (HTTP $1). Bitte neu laden.'],
|
|
176
|
+
[/^Send this to (.+)\. It works once, for 24 hours\. Their 2FA stays on\.$/, 'Schick das an $1. Funktioniert einmal, 24 Stunden lang. Die 2FA bleibt an.'],
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
function lang() {
|
|
180
|
+
var p; try { p = localStorage.getItem('tacilytics_lang') || 'auto'; } catch (e) { p = 'auto'; }
|
|
181
|
+
if (p === 'auto') p = /^de\b/i.test(navigator.language || '') ? 'de' : 'en';
|
|
182
|
+
return p === 'de' ? 'de' : 'en';
|
|
183
|
+
}
|
|
184
|
+
var L = lang();
|
|
185
|
+
var DE_VALUES = {}; // already-German strings: never "missing", never re-translated
|
|
186
|
+
document.documentElement.setAttribute('lang', L);
|
|
187
|
+
// collecting untranslated strings only on demand (?i18n=debug) — otherwise the set would grow with
|
|
188
|
+
// every page path / source shown during a long session
|
|
189
|
+
var DEBUG = /[?&]i18n=debug\b/.test(location.search);
|
|
190
|
+
if (DEBUG) window.__taciMissing = window.__taciMissing || {};
|
|
191
|
+
function tr(s) {
|
|
192
|
+
if (L !== 'de' || !s) return null;
|
|
193
|
+
var lead = s.match(/^\s*/)[0], tail = s.match(/\s*$/)[0], core = s.trim();
|
|
194
|
+
if (!core || !/[A-Za-z]/.test(core)) return null;
|
|
195
|
+
if (Object.prototype.hasOwnProperty.call(DE, core)) return lead + DE[core] + tail;
|
|
196
|
+
if (DE_VALUES[core]) return null;
|
|
197
|
+
for (var i = 0; i < DE_RX.length; i++) if (DE_RX[i][0].test(core)) return lead + noteResult(core.replace(DE_RX[i][0], DE_RX[i][1])) + tail;
|
|
198
|
+
if (DEBUG) window.__taciMissing[core] = 1;
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
function addValue(v) { if (typeof v === 'string') DE_VALUES[v.trim()] = 1; }
|
|
202
|
+
for (var k in DE) if (Object.prototype.hasOwnProperty.call(DE, k)) addValue(DE[k]);
|
|
203
|
+
function noteResult(t) { if (t) addValue(t); return t;
|
|
204
|
+
}
|
|
205
|
+
var ATTRS = ['placeholder', 'title', 'aria-label'];
|
|
206
|
+
var SKIP = { SCRIPT: 1, STYLE: 1, CODE: 1, PRE: 1, TEXTAREA: 1 };
|
|
207
|
+
function walk(node) {
|
|
208
|
+
if (L !== 'de') return;
|
|
209
|
+
if (node.nodeType === 3) {
|
|
210
|
+
var p = node.parentNode;
|
|
211
|
+
if (!p || SKIP[p.nodeName] || (p.closest && p.closest('.snippet, .bar .name, .feed, #actRows .act-target, [data-no-i18n]'))) return;
|
|
212
|
+
var t = tr(node.nodeValue); if (t != null && t !== node.nodeValue) node.nodeValue = t;
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (node.nodeType !== 1 || SKIP[node.nodeName]) return;
|
|
216
|
+
if (node.closest && node.closest('[data-no-i18n]')) return;
|
|
217
|
+
for (var a = 0; a < ATTRS.length; a++) {
|
|
218
|
+
var v = node.getAttribute && node.getAttribute(ATTRS[a]);
|
|
219
|
+
if (v) { var tv = tr(v); if (tv != null && tv !== v) node.setAttribute(ATTRS[a], tv); } // equal → no write, or the observer loops
|
|
220
|
+
}
|
|
221
|
+
if (node.nodeName === 'OPTION' && node.parentNode && node.parentNode.id === 'siteSelect') return; // domains
|
|
222
|
+
for (var c = node.firstChild; c; c = c.nextSibling) walk(c);
|
|
223
|
+
}
|
|
224
|
+
function start() {
|
|
225
|
+
walk(document.body);
|
|
226
|
+
new MutationObserver(function (list) {
|
|
227
|
+
for (var i = 0; i < list.length; i++) {
|
|
228
|
+
var m = list[i];
|
|
229
|
+
if (m.type === 'characterData') walk(m.target);
|
|
230
|
+
else if (m.type === 'attributes') walk(m.target);
|
|
231
|
+
else for (var j = 0; j < m.addedNodes.length; j++) walk(m.addedNodes[j]);
|
|
232
|
+
}
|
|
233
|
+
}).observe(document.body, { childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: ATTRS });
|
|
234
|
+
}
|
|
235
|
+
if (L === 'de') { if (document.body) start(); else document.addEventListener('DOMContentLoaded', start); }
|
|
236
|
+
window.TaciLang = {
|
|
237
|
+
get: function () { try { return localStorage.getItem('tacilytics_lang') || 'auto'; } catch (e) { return 'auto'; } },
|
|
238
|
+
set: function (v) { try { localStorage.setItem('tacilytics_lang', v); } catch (e) {} location.reload(); },
|
|
239
|
+
current: L,
|
|
240
|
+
t: function (s) { var r = tr(s); return r == null ? s : r; }, // for texts that never reach the DOM (confirm())
|
|
241
|
+
};
|
|
242
|
+
})();
|
package/public/index.html
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
<link rel="icon" href="/assets/tacilytics-mark.svg" type="image/svg+xml">
|
|
9
9
|
<meta name="theme-color" content="#071114">
|
|
10
10
|
<link rel="preload" href="/assets/fonts/inter-latin-wght-normal.woff2" as="font" type="font/woff2" crossorigin>
|
|
11
|
-
<
|
|
11
|
+
<script src="/theme.js?v=1"></script>
|
|
12
|
+
<link rel="stylesheet" href="/style.css?v=2.5.0.5">
|
|
12
13
|
</head>
|
|
13
14
|
<body>
|
|
14
15
|
|
|
@@ -33,6 +34,20 @@
|
|
|
33
34
|
</div>
|
|
34
35
|
</div>
|
|
35
36
|
|
|
37
|
+
<!-- Reset: new password from a one-time link -->
|
|
38
|
+
<div id="reset" class="modal-overlay hide">
|
|
39
|
+
<div class="modal">
|
|
40
|
+
<img class="logo" src="/assets/tacilytics-mark.svg" alt="">
|
|
41
|
+
<h2>New password</h2>
|
|
42
|
+
<p id="resetHint">Choose a new password.</p>
|
|
43
|
+
<div id="resetErr" class="err" role="alert"></div>
|
|
44
|
+
<div id="resetForm">
|
|
45
|
+
<input id="resetPw" type="password" placeholder="New password (at least 10 characters)" autocomplete="new-password">
|
|
46
|
+
<button class="btn" id="resetBtn">Save password</button>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
36
51
|
<!-- Invite: create an account from an invite link -->
|
|
37
52
|
<div id="invite" class="modal-overlay hide">
|
|
38
53
|
<div class="modal">
|
|
@@ -61,6 +76,7 @@
|
|
|
61
76
|
<div class="header-right">
|
|
62
77
|
<div class="realtime" id="realtimeBtn" title="View live visitors" style="cursor:pointer"><span class="dot"></span><span id="realtime">0</span> online now</div>
|
|
63
78
|
<select id="siteSelect"></select>
|
|
79
|
+
<button class="btn ghost tz-btn" id="tzBtn" title="Time zone of this site — days start at midnight here"><svg class="ico ui" aria-hidden="true"><use href="/assets/icons.svg?v=2#i-timer"></use></svg><span id="tzName">UTC</span></button>
|
|
64
80
|
<button class="btn ghost" id="addSiteBtn"><svg class="ico ui" aria-hidden="true"><use href="/assets/icons.svg?v=2#i-plus"></use></svg>Add site</button>
|
|
65
81
|
<button class="btn ghost" id="settingsBtn" title="Account, security and team"><svg class="ico ui" aria-hidden="true"><use href="/assets/icons.svg?v=2#i-settings"></use></svg><span id="meName">Settings</span></button>
|
|
66
82
|
<button class="btn ghost danger" id="logoutBtn">Sign out</button>
|
|
@@ -81,7 +97,9 @@
|
|
|
81
97
|
<button class="btn ghost" id="applyRange">Apply</button>
|
|
82
98
|
</div>
|
|
83
99
|
<button class="btn ghost" id="exportBtn"><svg class="ico ui" aria-hidden="true"><use href="/assets/icons.svg?v=2#i-download"></use></svg>CSV</button>
|
|
100
|
+
<button class="btn ghost" id="alertsBtn" title="Get a message on traffic spikes, drops or reached goals"><svg class="ico ui" aria-hidden="true"><use href="/assets/icons.svg?v=2#i-bell"></use></svg>Alerts</button>
|
|
84
101
|
<button class="btn ghost" id="shareBtn" title="Create a public read-only link"><svg class="ico ui" aria-hidden="true"><use href="/assets/icons.svg?v=2#i-share"></use></svg>Share</button>
|
|
102
|
+
<button class="btn ghost" id="importBtn" title="Bring your history from Google Analytics or Plausible"><svg class="ico ui" aria-hidden="true"><use href="/assets/icons.svg?v=2#i-download"></use></svg>Import</button>
|
|
85
103
|
<button class="btn ghost" id="snippetBtn"><svg class="ico ui" aria-hidden="true"><use href="/assets/icons.svg?v=2#i-code"></use></svg>Embed code</button>
|
|
86
104
|
</div>
|
|
87
105
|
</div>
|
|
@@ -112,9 +130,11 @@
|
|
|
112
130
|
<div class="legend">
|
|
113
131
|
<button type="button" class="leg" id="leg-visitors" data-key="visitors" aria-pressed="true"><span class="swatch" style="background:var(--chart-visitors)"></span>Visitors</button>
|
|
114
132
|
<button type="button" class="leg" id="leg-pageviews" data-key="pageviews" aria-pressed="true"><span class="swatch" style="background:var(--chart-pageviews)"></span>Pageviews</button>
|
|
133
|
+
<button type="button" class="leg" id="annBtn" title="Mark a day in the chart, e.g. a newsletter or a release"><span class="swatch ann"></span>Notes</button>
|
|
115
134
|
</div>
|
|
116
135
|
</div>
|
|
117
136
|
<svg id="chart" viewBox="0 0 800 220" preserveAspectRatio="none"></svg>
|
|
137
|
+
<p class="muted small hide" id="importedNote" style="margin:8px 2px 0"></p>
|
|
118
138
|
</div>
|
|
119
139
|
|
|
120
140
|
<div class="panels">
|
|
@@ -176,26 +196,46 @@
|
|
|
176
196
|
|
|
177
197
|
<!-- custom-event properties (shown when drilled into a single goal) -->
|
|
178
198
|
<div id="propsWrap" class="panels hide" style="margin-top:16px"></div>
|
|
179
|
-
<p class="footer-note">Tacilytics · open source · <a href="https://github.com/Open-Cosmic-Software/tacilytics" target="_blank" rel="noopener">GitHub</a></p>
|
|
199
|
+
<p class="footer-note">Tacilytics<span id="appVersion"></span> · open source · <a href="https://github.com/Open-Cosmic-Software/tacilytics" target="_blank" rel="noopener">GitHub</a></p>
|
|
180
200
|
</div>
|
|
181
201
|
</div>
|
|
182
202
|
|
|
183
203
|
<!-- Add site modal -->
|
|
184
|
-
<div id="addModal" class="modal-overlay hide">
|
|
204
|
+
<div id="addModal" class="modal-overlay hide" data-guard>
|
|
185
205
|
<div class="modal">
|
|
206
|
+
<button type="button" class="modal-x" data-close aria-label="Close" title="Close"><svg class="ico ui" aria-hidden="true"><use href="/assets/icons.svg?v=2#i-x"></use></svg></button>
|
|
186
207
|
<span class="logo em"><svg class="ico big" aria-hidden="true"><use href="/assets/icons.svg?v=2#i-site"></use></svg></span>
|
|
187
208
|
<h2>Add a site</h2>
|
|
188
209
|
<p>Enter the domain you want to track.</p>
|
|
189
210
|
<div id="addErr" class="err"></div>
|
|
190
211
|
<input id="newDomain" type="text" placeholder="example.com" autocomplete="off">
|
|
212
|
+
<label class="field-label" for="newTz">Time zone (days start at midnight here)</label>
|
|
213
|
+
<select id="newTz" class="tz-select"></select>
|
|
191
214
|
<button class="btn" id="createSiteBtn">Create</button>
|
|
192
215
|
<button class="btn ghost" id="cancelAdd" style="margin-top:10px">Cancel</button>
|
|
193
216
|
</div>
|
|
194
217
|
</div>
|
|
195
218
|
|
|
219
|
+
<!-- Time zone of a site (Fabian) -->
|
|
220
|
+
<div id="tzModal" class="modal-overlay hide" data-guard>
|
|
221
|
+
<div class="modal">
|
|
222
|
+
<button type="button" class="modal-x" data-close aria-label="Close" title="Close"><svg class="ico ui" aria-hidden="true"><use href="/assets/icons.svg?v=2#i-x"></use></svg></button>
|
|
223
|
+
<span class="logo em"><svg class="ico big" aria-hidden="true"><use href="/assets/icons.svg?v=2#i-timer"></use></svg></span>
|
|
224
|
+
<h2>Time zone</h2>
|
|
225
|
+
<p><span>Days, hours, the weekly report and alerts follow this time zone for</span> <b id="tzSite"></b></p>
|
|
226
|
+
<p>Stored data does not change — switching back and forth is safe.</p>
|
|
227
|
+
<p class="muted small">Unique visitors per day are an approximation outside UTC: the anonymous visitor ID changes at midnight UTC.</p>
|
|
228
|
+
<div id="tzErr" class="err"></div>
|
|
229
|
+
<select id="tzSelect" class="tz-select"></select>
|
|
230
|
+
<button class="btn" id="saveTz">Save</button>
|
|
231
|
+
<button class="btn ghost" id="cancelTz" style="margin-top:10px">Cancel</button>
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
|
|
196
235
|
<!-- Snippet modal: website + apps (13.10.2026) -->
|
|
197
236
|
<div id="snippetModal" class="modal-overlay hide">
|
|
198
237
|
<div class="modal modal-wide">
|
|
238
|
+
<button type="button" class="modal-x" data-close aria-label="Close" title="Close"><svg class="ico ui" aria-hidden="true"><use href="/assets/icons.svg?v=2#i-x"></use></svg></button>
|
|
199
239
|
<span class="logo em"><svg class="ico big" aria-hidden="true"><use href="/assets/icons.svg?v=2#i-code"></use></svg></span>
|
|
200
240
|
<h2>Add tracking</h2>
|
|
201
241
|
<p style="margin-bottom:12px">for <strong id="snipDomain"></strong>. Same dashboard for website and apps.</p>
|
|
@@ -218,6 +258,7 @@
|
|
|
218
258
|
<!-- Share modal -->
|
|
219
259
|
<div id="shareModal" class="modal-overlay hide">
|
|
220
260
|
<div class="modal">
|
|
261
|
+
<button type="button" class="modal-x" data-close aria-label="Close" title="Close"><svg class="ico ui" aria-hidden="true"><use href="/assets/icons.svg?v=2#i-x"></use></svg></button>
|
|
221
262
|
<span class="logo em"><svg class="ico big" aria-hidden="true"><use href="/assets/icons.svg?v=2#i-share"></use></svg></span>
|
|
222
263
|
<h2>Share dashboard</h2>
|
|
223
264
|
<p>Anyone with this link can view stats for <strong id="shareDomain"></strong> — read-only, no login.</p>
|
|
@@ -229,8 +270,9 @@
|
|
|
229
270
|
</div>
|
|
230
271
|
|
|
231
272
|
<!-- Funnel builder modal -->
|
|
232
|
-
<div id="funnelModal" class="modal-overlay hide">
|
|
273
|
+
<div id="funnelModal" class="modal-overlay hide" data-guard>
|
|
233
274
|
<div class="modal">
|
|
275
|
+
<button type="button" class="modal-x" data-close aria-label="Close" title="Close"><svg class="ico ui" aria-hidden="true"><use href="/assets/icons.svg?v=2#i-x"></use></svg></button>
|
|
234
276
|
<span class="logo em"><svg class="ico big" aria-hidden="true"><use href="/assets/icons.svg?v=2#i-funnel"></use></svg></span>
|
|
235
277
|
<h2>New funnel</h2>
|
|
236
278
|
<p>2–8 ordered steps. A visitor advances only after completing the previous step.</p>
|
|
@@ -246,6 +288,7 @@
|
|
|
246
288
|
<!-- Live visitors modal -->
|
|
247
289
|
<div id="liveModal" class="modal-overlay hide">
|
|
248
290
|
<div class="modal live">
|
|
291
|
+
<button type="button" class="modal-x" data-close aria-label="Close" title="Close"><svg class="ico ui" aria-hidden="true"><use href="/assets/icons.svg?v=2#i-x"></use></svg></button>
|
|
249
292
|
<div class="live-head">
|
|
250
293
|
<h2>Live</h2>
|
|
251
294
|
<span class="realtime"><span class="dot"></span><span id="live-online">0</span> online</span>
|
|
@@ -258,6 +301,73 @@
|
|
|
258
301
|
</div>
|
|
259
302
|
</div>
|
|
260
303
|
|
|
304
|
+
<!-- Import -->
|
|
305
|
+
<div id="importModal" class="modal-overlay hide">
|
|
306
|
+
<div class="modal modal-wide settings">
|
|
307
|
+
<button type="button" class="modal-x" data-close aria-label="Close" title="Close"><svg class="ico ui" aria-hidden="true"><use href="/assets/icons.svg?v=2#i-x"></use></svg></button>
|
|
308
|
+
<h2>Import history into <span id="impSite"></span></h2>
|
|
309
|
+
<p>Bring your old numbers along. They show up in the chart and in the panels for the days <strong>before</strong> Tacilytics started counting — never twice.</p>
|
|
310
|
+
<div id="impErr" class="err" role="alert"></div>
|
|
311
|
+
<div class="tabs set-tabs" id="impTabs" style="margin:0 0 12px">
|
|
312
|
+
<button data-src="plausible" class="active">Plausible</button>
|
|
313
|
+
<button data-src="ga">Google Analytics 4</button>
|
|
314
|
+
</div>
|
|
315
|
+
<div class="set-card">
|
|
316
|
+
<p class="muted small" id="impHowto"></p>
|
|
317
|
+
<div class="set-inline"><input type="file" id="impFile" accept=".zip,.csv" multiple><button class="btn" id="impGo">Import</button></div>
|
|
318
|
+
<p class="muted small" id="impStatus" style="margin:8px 0 0"></p>
|
|
319
|
+
</div>
|
|
320
|
+
<div id="impList"></div>
|
|
321
|
+
<button class="btn ghost" id="closeImport" style="margin-top:12px">Close</button>
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
|
|
325
|
+
<!-- Alerts -->
|
|
326
|
+
<div id="alertsModal" class="modal-overlay hide" data-guard>
|
|
327
|
+
<div class="modal modal-wide settings">
|
|
328
|
+
<button type="button" class="modal-x" data-close aria-label="Close" title="Close"><svg class="ico ui" aria-hidden="true"><use href="/assets/icons.svg?v=2#i-x"></use></svg></button>
|
|
329
|
+
<h2>Alerts for <span id="alertsSite"></span></h2>
|
|
330
|
+
<p>Tacilytics sends a message to a webhook (Discord, Slack, or anything that takes JSON). Checked every 10 minutes.</p>
|
|
331
|
+
<div id="alertsErr" class="err" role="alert"></div>
|
|
332
|
+
<div id="alertsList"></div>
|
|
333
|
+
<div class="set-card">
|
|
334
|
+
<h3>New alert</h3>
|
|
335
|
+
<div class="set-inline">
|
|
336
|
+
<select id="alKind">
|
|
337
|
+
<option value="spike">Traffic spike — about 3× the usual for this hour</option>
|
|
338
|
+
<option value="drop">Traffic drop — under 20 % of usual (tracker broken?)</option>
|
|
339
|
+
<option value="goal">Goal reached — e.g. 10× Purchase</option>
|
|
340
|
+
</select>
|
|
341
|
+
</div>
|
|
342
|
+
<div class="set-inline hide" id="alGoalRow">
|
|
343
|
+
<input id="alGoal" type="text" placeholder="Event name, e.g. Purchase" maxlength="80" list="alGoalList">
|
|
344
|
+
<datalist id="alGoalList"></datalist>
|
|
345
|
+
<input id="alThreshold" type="number" min="1" value="10" style="flex:0 0 90px" title="How many">
|
|
346
|
+
<select id="alWindow" style="flex:0 0 140px"><option value="1">in 1 hour</option><option value="24" selected>in 24 hours</option><option value="168">in 7 days</option></select>
|
|
347
|
+
</div>
|
|
348
|
+
<div class="set-inline">
|
|
349
|
+
<input id="alWebhook" type="url" placeholder="Webhook URL (https://…)">
|
|
350
|
+
<button class="btn" id="alAdd">Add alert</button>
|
|
351
|
+
</div>
|
|
352
|
+
<p class="muted small" id="alHint" style="margin:8px 0 0"></p>
|
|
353
|
+
</div>
|
|
354
|
+
<button class="btn ghost" id="closeAlerts" style="margin-top:12px">Close</button>
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
|
|
358
|
+
<!-- Chart annotations -->
|
|
359
|
+
<div id="annModal" class="modal-overlay hide" data-guard>
|
|
360
|
+
<div class="modal modal-wide settings">
|
|
361
|
+
<button type="button" class="modal-x" data-close aria-label="Close" title="Close"><svg class="ico ui" aria-hidden="true"><use href="/assets/icons.svg?v=2#i-x"></use></svg></button>
|
|
362
|
+
<h2>Notes in the chart</h2>
|
|
363
|
+
<p>Mark a day — a newsletter, a release, a sale — so a spike explains itself later. Everyone with access to this site sees them.</p>
|
|
364
|
+
<div id="annErr" class="err" role="alert"></div>
|
|
365
|
+
<div class="set-inline"><input id="annDay" type="date" style="flex:0 0 170px;color-scheme:dark"><input id="annText" type="text" maxlength="140" placeholder="e.g. Newsletter sent"><button class="btn" id="saveAnn">Add</button></div>
|
|
366
|
+
<div id="annList" style="margin-top:14px"></div>
|
|
367
|
+
<button class="btn ghost" id="closeAnn" style="margin-top:16px">Close</button>
|
|
368
|
+
</div>
|
|
369
|
+
</div>
|
|
370
|
+
|
|
261
371
|
<!-- Settings: account, security, team -->
|
|
262
372
|
<div id="settingsModal" class="modal-overlay hide">
|
|
263
373
|
<div class="modal modal-wide settings">
|
|
@@ -266,17 +376,21 @@
|
|
|
266
376
|
<div class="tabs set-tabs" id="setTabs">
|
|
267
377
|
<button data-t="security" class="active"><svg class="ico ui" aria-hidden="true"><use href="/assets/icons.svg?v=2#i-shield"></use></svg>Security</button>
|
|
268
378
|
<button data-t="team"><svg class="ico ui" aria-hidden="true"><use href="/assets/icons.svg?v=2#i-users"></use></svg>Team</button>
|
|
379
|
+
<button data-t="activity"><svg class="ico ui" aria-hidden="true"><use href="/assets/icons.svg?v=2#i-timer"></use></svg>Activity</button>
|
|
269
380
|
</div>
|
|
381
|
+
<button type="button" class="modal-x" data-close aria-label="Close" title="Close"><svg class="ico ui" aria-hidden="true"><use href="/assets/icons.svg?v=2#i-x"></use></svg></button>
|
|
270
382
|
</div>
|
|
271
383
|
<div id="setErr" class="err" role="alert"></div>
|
|
272
384
|
<div id="setSecurity"></div>
|
|
273
385
|
<div id="setTeam" class="hide"></div>
|
|
386
|
+
<div id="setActivity" class="hide"></div>
|
|
274
387
|
<button class="btn ghost" id="closeSettings" style="margin-top:18px">Close</button>
|
|
275
388
|
</div>
|
|
276
389
|
</div>
|
|
277
390
|
|
|
278
391
|
<div class="tip" id="tip"></div>
|
|
279
|
-
<script src="/
|
|
280
|
-
<script src="/
|
|
392
|
+
<script src="/i18n.js?v=2.2.3"></script>
|
|
393
|
+
<script src="/account.js?v=2.5.6"></script>
|
|
394
|
+
<script src="/app.js?v=2.7.2"></script>
|
|
281
395
|
</body>
|
|
282
396
|
</html>
|
package/public/style.css
CHANGED
|
@@ -451,4 +451,106 @@ select option { background: #0c1a1d; color: var(--text); }
|
|
|
451
451
|
#login .btn.ghost .ico, #settingsBtn .ico { margin-right: 6px; }
|
|
452
452
|
/* viewers: hide everything that would only answer with 403 */
|
|
453
453
|
body.role-viewer #addSiteBtn, body.role-viewer #shareBtn, body.role-viewer #addFunnelBtn { display: none !important; }
|
|
454
|
+
body.site-limited #addSiteBtn { display: none !important; }
|
|
454
455
|
@media (max-width: 560px) { #meName { display: none; } .team-row { flex-wrap: wrap; } }
|
|
456
|
+
a.btn, a.btn:hover { text-decoration: none; display: inline-flex; align-items: center; gap: 6px; color: var(--text); }
|
|
457
|
+
.swatch.ann { width: 9px; height: 9px; transform: rotate(45deg); border-radius: 1px; background: var(--accent-2, #ff7a5c); }
|
|
458
|
+
.tip-note { color: var(--accent-2, #ff7a5c); }
|
|
459
|
+
body.role-viewer #annBtn { display: none !important; }
|
|
460
|
+
.act-row { align-items: baseline; }
|
|
461
|
+
.act-row .act-time { flex: 0 0 96px; }
|
|
462
|
+
.act-row > span:nth-child(2) { flex: 1; display: block; }
|
|
463
|
+
.act-target { color: var(--text); background: rgba(255,255,255,.04); border: 1px solid var(--line); border-radius: 6px; padding: 1px 6px; font-size: 0.8rem; }
|
|
464
|
+
.act-ip { flex: 0 0 auto; font-family: ui-monospace, monospace; }
|
|
465
|
+
@media (max-width: 560px) { .act-ip { display: none; } }
|
|
466
|
+
body.role-viewer #alertsBtn { display: none !important; }
|
|
467
|
+
#alertsList [data-al-test] { min-width: 72px; justify-content: center; }
|
|
468
|
+
body.role-viewer #importBtn { display: none !important; }
|
|
469
|
+
#impFile { flex: 1 1 240px; color: var(--text-dim); font-size: .82rem; }
|
|
470
|
+
#impFile::file-selector-button { font: 600 .8rem inherit; font-family: inherit; color: var(--text); background: rgba(255,255,255,.04); border: 1px solid var(--line-strong); border-radius: 9px; padding: 7px 12px; margin-right: 10px; cursor: pointer; }
|
|
471
|
+
#impFile::file-selector-button:hover { background: rgba(255,255,255,.08); }
|
|
472
|
+
|
|
473
|
+
/* ══ light theme (html[data-theme="light"], chosen in Settings or following the system) ══════════ */
|
|
474
|
+
html[data-theme="light"] {
|
|
475
|
+
color-scheme: light;
|
|
476
|
+
--accent: #0d9488;
|
|
477
|
+
--accent-strong: #0f766e;
|
|
478
|
+
--accent-2: #e8603f;
|
|
479
|
+
--accent-soft: rgba(13, 148, 136, 0.10);
|
|
480
|
+
--accent-line: rgba(13, 148, 136, 0.35);
|
|
481
|
+
--chart-visitors: #0d9488;
|
|
482
|
+
--chart-pageviews: #e8603f;
|
|
483
|
+
--green: #059669; --red: #dc2626; --yellow: #b45309;
|
|
484
|
+
--bg: #f4f8f7; --bg2: #eef3f2;
|
|
485
|
+
--surface: rgba(255, 255, 255, 0.86);
|
|
486
|
+
--surface-2: #eef4f3;
|
|
487
|
+
--line: rgba(10, 40, 38, 0.09);
|
|
488
|
+
--line-strong: rgba(10, 40, 38, 0.16);
|
|
489
|
+
--text: #0f2624; --text-dim: #3f5a56; --text-faint: #647a77;
|
|
490
|
+
--cyan: #0e7490;
|
|
491
|
+
--shadow: 0 1px 0 rgba(255,255,255,.7) inset, 0 10px 28px -16px rgba(15,40,38,.25);
|
|
492
|
+
}
|
|
493
|
+
html[data-theme="light"] body {
|
|
494
|
+
background:
|
|
495
|
+
radial-gradient(1100px 560px at 8% -12%, rgba(13,148,136,.10), transparent 60%),
|
|
496
|
+
radial-gradient(800px 460px at 104% -4%, rgba(232,96,63,.07), transparent 55%),
|
|
497
|
+
var(--bg);
|
|
498
|
+
}
|
|
499
|
+
html[data-theme="light"] header {
|
|
500
|
+
background: linear-gradient(180deg, rgba(255,255,255,.92), rgba(247,251,250,.88));
|
|
501
|
+
box-shadow: 0 1px 0 rgba(255,255,255,.8) inset, 0 16px 34px -24px rgba(15,40,38,.35);
|
|
502
|
+
}
|
|
503
|
+
html[data-theme="light"] .btn { color: #fff; box-shadow: 0 6px 16px -8px rgba(13,148,136,.55); }
|
|
504
|
+
html[data-theme="light"] .btn.ghost,
|
|
505
|
+
html[data-theme="light"] .header-right .btn.ghost,
|
|
506
|
+
html[data-theme="light"] .header-right select { background: rgba(255,255,255,.75); color: var(--text); border-color: var(--line-strong); }
|
|
507
|
+
html[data-theme="light"] .header-right .btn.ghost:hover, html[data-theme="light"] .btn.ghost:hover { background: rgba(13,148,136,.07); }
|
|
508
|
+
html[data-theme="light"] .stats .stat:first-child { background: linear-gradient(160deg, rgba(13,148,136,.12), rgba(255,255,255,.9) 55%); border-color: rgba(13,148,136,.25); }
|
|
509
|
+
html[data-theme="light"] .commerce .stat:first-child { background: linear-gradient(160deg, rgba(232,96,63,.10), rgba(255,255,255,.9) 55%); border-color: rgba(232,96,63,.22); }
|
|
510
|
+
html[data-theme="light"] .bar .fill { background: linear-gradient(90deg, rgba(13,148,136,.16), rgba(13,148,136,.04)); }
|
|
511
|
+
html[data-theme="light"] .bar:hover .fill { background: linear-gradient(90deg, rgba(13,148,136,.24), rgba(232,96,63,.08)); }
|
|
512
|
+
html[data-theme="light"] .realtime { background: rgba(13,148,136,.07); border-color: rgba(13,148,136,.22); color: #0f766e; }
|
|
513
|
+
html[data-theme="light"] .trend.up { color: #0f766e; background: rgba(13,148,136,.10); }
|
|
514
|
+
html[data-theme="light"] .trend.down { color: #c2410c; background: rgba(232,96,63,.10); }
|
|
515
|
+
html[data-theme="light"] #p-pages .bar .name .txt, html[data-theme="light"] #p-404 .bar .name .txt, html[data-theme="light"] .feed-row .path { color: #1d3a37; }
|
|
516
|
+
html[data-theme="light"] .modal { background: rgba(255,255,255,.96); box-shadow: 0 30px 80px rgba(15,40,38,.25), 0 0 0 1px var(--line-strong); }
|
|
517
|
+
html[data-theme="light"] .modal-overlay { background: rgba(220,232,230,.6); }
|
|
518
|
+
html[data-theme="light"] #login { background: radial-gradient(700px 420px at 50% 30%, rgba(13,148,136,.14), transparent 65%), rgba(236,244,242,.92); }
|
|
519
|
+
html[data-theme="light"] .snippet { background: #f2f7f6; color: #17403b; border-color: var(--line); }
|
|
520
|
+
html[data-theme="light"] select option { background: #fff; color: var(--text); }
|
|
521
|
+
html[data-theme="light"] input, html[data-theme="light"] select, html[data-theme="light"] textarea { background-color: #fff; color: var(--text); border-color: var(--line-strong); }
|
|
522
|
+
html[data-theme="light"] .set-card { background: rgba(255,255,255,.7); }
|
|
523
|
+
html[data-theme="light"] .pill, html[data-theme="light"] .act-target { background: rgba(10,40,38,.04); }
|
|
524
|
+
html[data-theme="light"] .flag { box-shadow: 0 0 0 1px rgba(0,0,0,.08); }
|
|
525
|
+
html[data-theme="light"] .tip { background: #fff; color: var(--text); border-color: var(--line-strong); box-shadow: 0 10px 30px -10px rgba(15,40,38,.3); }
|
|
526
|
+
html[data-theme="light"] .range input[type=date], html[data-theme="light"] #annDay { color-scheme: light !important; }
|
|
527
|
+
html[data-theme="light"] .set-tabs button.active, html[data-theme="light"] .panel h3 .tabs button.active { background: #fff; }
|
|
528
|
+
html[data-theme="light"] .pill { color: #36504c; border-color: rgba(10,40,38,.22); }
|
|
529
|
+
html[data-theme="light"] .stat .label { color: #3f5a56; }
|
|
530
|
+
|
|
531
|
+
/* one close button for every closable modal — sticky, so it stays reachable in long modals */
|
|
532
|
+
.modal-x { position: sticky; top: 0; float: right; margin: -14px -12px 0 8px; z-index: 3; width: 34px; height: 34px; display: inline-flex; align-items: center; justify-content: center;
|
|
533
|
+
background: var(--surface-2, rgba(255,255,255,.06)); border: 1px solid var(--line); border-radius: 10px; color: var(--text-dim); cursor: pointer; }
|
|
534
|
+
.modal-x:hover, .modal-x:focus-visible { color: var(--text); border-color: var(--line-strong); }
|
|
535
|
+
.settings-head .modal-x { position: static; float: none; margin: 0 0 0 6px; flex: none; }
|
|
536
|
+
.modal.settings .settings-head { position: sticky; top: -32px; z-index: 3; margin: -32px -30px 12px; padding: 22px 30px 12px;
|
|
537
|
+
background: rgb(23,26,34); border-bottom: 1px solid var(--line); border-radius: 20px 20px 0 0; }
|
|
538
|
+
html[data-theme="light"] .modal.settings .settings-head { background: #fff; }
|
|
539
|
+
|
|
540
|
+
/* scrollbars in Tacilytics style — thin, quiet, follows light/dark (Fabian) */
|
|
541
|
+
:root { --sb-thumb: rgba(180, 255, 240, 0.16); --sb-thumb-hover: rgba(180, 255, 240, 0.32); }
|
|
542
|
+
html[data-theme="light"] { --sb-thumb: rgba(10, 40, 38, 0.18); --sb-thumb-hover: rgba(10, 40, 38, 0.34); }
|
|
543
|
+
* { scrollbar-width: thin; scrollbar-color: var(--sb-thumb) transparent; }
|
|
544
|
+
::-webkit-scrollbar { width: 8px; height: 8px; }
|
|
545
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
546
|
+
::-webkit-scrollbar-thumb { background: var(--sb-thumb); border-radius: 8px; border: 2px solid transparent; background-clip: padding-box; }
|
|
547
|
+
::-webkit-scrollbar-thumb:hover { background: var(--sb-thumb-hover); background-clip: padding-box; }
|
|
548
|
+
::-webkit-scrollbar-corner { background: transparent; }
|
|
549
|
+
.modal::-webkit-scrollbar-track { margin: 14px 0; } /* keeps the thumb off the rounded corners */
|
|
550
|
+
/* login / reset / invite always on top: a 401 while Settings is open must not hide the login behind it */
|
|
551
|
+
#login, #reset, #invite { z-index: 1100; }
|
|
552
|
+
/* ── time zones ── */
|
|
553
|
+
.modal .tz-select { width: 100%; margin-bottom: 14px; }
|
|
554
|
+
.modal .field-label { display: block; color: var(--text-dim); font-size: 0.8rem; margin: -4px 0 6px; text-align: left; }
|
|
555
|
+
.modal p.small { font-size: 0.78rem; margin-top: -14px; }
|
|
556
|
+
.tz-btn span { max-width: 140px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|