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 CHANGED
@@ -2,9 +2,15 @@
2
2
  # Port (bound to 127.0.0.1; put a reverse proxy / TLS in front)
3
3
  PORT=3920
4
4
 
5
- # Dashboard passphrase (plaintext; keep .env at chmod 600)
5
+ # Dashboard passphrase (plaintext; keep .env at chmod 600).
6
+ # Only used until you create the first account in Settings → Security (then it stops working).
6
7
  WEB_PASSWORD=change-me-to-a-strong-passphrase
7
8
 
9
+ # Public URL of the dashboard. Pins the passkey domain (recommended behind a reverse proxy).
10
+ # TACILYTICS_ORIGIN=https://analytics.example.com
11
+ # Reverse proxies in front (default 1). 0 = no proxy, the socket address is used for rate limits.
12
+ # TRUST_PROXY=1
13
+
8
14
  # Secret for signing session tokens — long random string
9
15
  # node -e "console.log(require('crypto').randomBytes(24).toString('hex'))"
10
16
  SESSION_SECRET=change-me
@@ -21,3 +27,8 @@ API_KEY=change-me
21
27
 
22
28
  # Data minimisation: auto-purge raw events older than N days (0 = keep forever)
23
29
  DATA_RETENTION_DAYS=0
30
+
31
+ # Weekly report by e-mail (optional). smtps://…:465 or smtp://…:587 (STARTTLS)
32
+ # SMTP_URL=smtps://user:password@mail.example.com:465
33
+ # REPORT_FROM=Tacilytics <reports@example.com>
34
+ # REPORT_HOUR_UTC=6
package/README.md CHANGED
@@ -111,9 +111,12 @@ Opt-in flags on the `<script>` tag:
111
111
  <script defer data-domain="example.com"
112
112
  data-honor-dnt <!-- skip tracking if DNT / Global Privacy Control is on -->
113
113
  data-track-outbound <!-- auto-record clicks to other domains as events -->
114
+ data-track-downloads <!-- auto-record file downloads (pdf, zip, docx …) as "File Download"; or ="pdf,epub" -->
114
115
  src="https://analytics.example.com/tacilytics.js"></script>
115
116
  ```
116
117
 
118
+ **Your own visits:** open your site once with `?tacilytics=off` (e.g. `https://example.com/?tacilytics=off`) and that browser is no longer counted on that site. `?tacilytics=on` undoes it. It is stored in the browser's localStorage, nothing is sent; repeat it on every browser and phone you use. Settings → Security has the links for all your sites.
119
+
117
120
  ## Mobile & desktop apps
118
121
 
119
122
  Track your **native app in the same dashboard as your website**. Every row gets a platform
@@ -152,6 +155,52 @@ The server hashes the launch id with a random daily salt that is deleted when th
152
155
  Without `sid`, the usual IP + user-agent daily hash is used. Region comes from the timezone or
153
156
  device locale, never from IP geolocation.
154
157
 
158
+ ## Import from Google Analytics or Plausible
159
+
160
+ **Import** (toolbar) brings your history along, so switching doesn't mean starting at zero.
161
+
162
+ - **Plausible:** Site settings → Imports & Exports → *Export data*. Upload the ZIP (or the CSVs in it).
163
+ - **Google Analytics 4:** open a report (*Pages and screens*, *Traffic acquisition*, *Demographic details*, *Tech*), add **Date** as a secondary dimension, *Share → Download file → CSV*. One file per report; English or German column names.
164
+
165
+ Imported numbers are daily totals per dimension (pages, sources, countries, browsers, OS, devices, events) and count **only for days before your own data starts** — never twice. They appear in the chart, the totals and the panels; while a filter is active they are left out (they can't be combined), and the dashboard says so. Every import can be removed again. No visitor-level data is imported or stored.
166
+
167
+ ## Notes in the chart
168
+
169
+ Click **Notes** in the chart legend to mark a day — a newsletter, a release, a sale. A coral line and a diamond appear on that day, the note shows in the tooltip, and everyone with access to the site (including share links) sees it. Admins and owners add and remove notes. `GET /api/annotations?site=…` lists them; `/stats` returns the ones in its range as `annotations`.
170
+
171
+ ## Time zone per site
172
+
173
+ Every site has its own time zone (any IANA name, e.g. `Europe/Berlin`; the browser's zone is preselected when you add a site, existing sites stay on `UTC`). Click the clock next to the site picker to change it. Days, the hourly view, date ranges, notes, the weekly report, the daily digest and the alert baseline all follow it, with daylight-saving days as 23 or 25 hours and offsets like +5:45 kept exact. Stored data is not touched, so switching back and forth is safe.
174
+
175
+ One limit: the anonymous visitor ID rotates at midnight **UTC** (that is what keeps it unlinkable across days). Outside UTC a local day therefore spans two IDs, and *unique visitors per day* are an approximation — pageviews, goals and revenue are exact.
176
+
177
+ `PATCH /api/sites/:id {"timezone":"Europe/Berlin"}` (admin) changes it via the API; unknown names are rejected with 400.
178
+
179
+ ## Weekly report by e-mail
180
+
181
+ Settings → Security → **Weekly report**: enter an address and tick *every Monday*. Each person gets one mail per week for **the sites they can see** — visitors and pageviews vs. the week before, top pages, sources, goals and a link to the dashboard. *Send one now* sends a sample.
182
+
183
+ Needs mail settings in `.env`:
184
+
185
+ ```env
186
+ SMTP_URL=smtps://user:password@mail.example.com:465 # or smtp://…:587 (STARTTLS)
187
+ REPORT_FROM=Tacilytics <reports@example.com>
188
+ REPORT_HOUR_UTC=6 # Monday, this hour (UTC)
189
+ TACILYTICS_ORIGIN=https://analytics.example.com # for the dashboard link in the mail
190
+ ```
191
+
192
+ ## Alerts
193
+
194
+ **Alerts** (toolbar) sends a message to a webhook — Discord, Slack, or anything that takes JSON — when:
195
+
196
+ - **Traffic spike:** about 3× the usual for this hour of the day,
197
+ - **Traffic drop:** under 20 % of usual (often: the tracker fell off the site),
198
+ - **Goal reached:** e.g. 10× `Purchase` within 1 hour, 24 hours or 7 days.
199
+
200
+ "Usual" is the median of the same hour over the last 14 days, so quiet nights don't look like incidents. Checked every 10 minutes, with a cooldown so one long spike is one message. **Test** sends a sample message. Admins and owners manage alerts.
201
+
202
+ Webhooks entered in the dashboard must be public `https://` addresses — no localhost, private ranges or redirects, so nobody on the team can use alerts to reach internal services. `DIGEST_WEBHOOK_URL` from `.env` is the default target (and may be internal); sites without own alerts keep the default spike/drop alerts there.
203
+
155
204
  ## Live visitors
156
205
 
157
206
  Click **“● online now”** for a realtime view: a 30-minute pulse and a live feed of
@@ -189,6 +238,10 @@ tacilytics sites
189
238
  tacilytics stats example.com 30d
190
239
  tacilytics live example.com --watch
191
240
  tacilytics add example.com
241
+
242
+ # on the server itself (reads the database, no API key)
243
+ tacilytics users
244
+ tacilytics reset-login <username>
192
245
  ```
193
246
 
194
247
  Add `--json` to any command for machine-readable output — perfect for agents and scripts.
@@ -206,16 +259,54 @@ counts unique visitors *per day* and can’t follow the same person across days
206
259
  devices. You get the 95 % that matters (how many, which pages, from where) with 0 %
207
260
  cookie banner — the same approach as Plausible/Fathom.
208
261
 
262
+ ## Light mode & German
263
+
264
+ Settings → Security → **Appearance**: *Theme* (System / Light / Dark) and *Language* (Automatic / English / Deutsch), saved per browser. *System* follows the device's light/dark setting live; *Automatic* picks German when the browser is German. Your data (paths, sources, event names) is never translated.
265
+
266
+ ## Team, 2FA & passkeys
267
+
268
+ Out of the box the dashboard opens with one shared passphrase (`WEB_PASSWORD`). When you want more:
269
+
270
+ 1. Sign in, open **Settings → Security** and create your **owner account**. From then on `WEB_PASSWORD` no longer opens anything, so nobody can walk around your 2FA with it.
271
+ 2. **Settings → Team → Invite someone** creates a link that works **once, for 7 days**. Send it yourself; Tacilytics sends no e-mail.
272
+
273
+ | Role | Can |
274
+ |---|---|
275
+ | **Owner** | everything, including the team |
276
+ | **Admin** | add/remove sites, share links, funnels |
277
+ | **Viewer** | read only — optionally limited to **specific sites** |
278
+
279
+ Everyone can turn on, **optionally**:
280
+
281
+ - **Passkeys** (Face ID, Touch ID, Windows Hello, security keys). A passkey replaces password *and* code and cannot be phished.
282
+ - **One-time codes (TOTP)** from any authenticator app, with 10 single-use **recovery codes**. Codes cannot be reused.
283
+
284
+ **Forgot the password?** An owner clicks **Password link** next to the person in Settings → Team: a one-time link (24 h) to set a new one. Their 2FA stays on and all their sessions end.
285
+
286
+ **Activity log:** owners see every change in Settings → Activity — who added a site, invited someone, changed a role, created an API key, turned 2FA off … with time and address. Failed attempts and sign-ins are not logged, secrets never. Kept for a year (`AUDIT_RETENTION_DAYS`, minimum 30).
287
+
288
+ **Locked out?** On the server: `tacilytics users` lists accounts, `tacilytics reset-login <username>` prints a new password, turns 2FA off and ends that person's sessions. An owner can also reset a teammate's 2FA in Settings → Team.
289
+
290
+ **API keys per person:** Settings → Security → API keys creates a personal key (`tk_…`) for scripts, agents and the CLI. It acts as you and never more: it sees only your sites, is **read-only unless you untick it** (a viewer's key always is), and cannot manage the team or change security settings. Keys are stored as SHA-256, shown once, and can be revoked; removing a member revokes theirs.
291
+
292
+ The server-wide `API_KEY` from `.env` keeps owner rights for agents and the CLI on your own box.
293
+
209
294
  ## Configuration
210
295
 
211
296
  | Env var | Default | Purpose |
212
297
  |---|---|---|
213
298
  | `PORT` | `3920` | Port (bound to 127.0.0.1) |
214
- | `WEB_PASSWORD` | `tacilytics` | Dashboard passphrase |
215
- | `SESSION_SECRET` | random | Session-token secret |
299
+ | `WEB_PASSWORD` | `tacilytics` | Dashboard passphrase — **only until the first account exists** |
300
+ | `TACILYTICS_ORIGIN` | request host | Public URL of the dashboard, e.g. `https://analytics.example.com`. Pins the passkey domain; recommended behind a proxy |
301
+ | `TRUST_PROXY` | `1` | Number of HTTP reverse proxies in front of Tacilytics that append `X-Forwarded-For` (e.g. Cloudflare + Caddy = `2`; a plain TCP forwarder like socat or `ssh -L` does not count). Rate limits **and visitor hashes** use the address the outermost trusted proxy saw, never the client-written first `X-Forwarded-For` entry. Too low: with two proxies and `1`, every visitor looks like the Cloudflare address and unique visitors collapse. Too high: clients can pick their own address and bypass the limits. Tacilytics binds to 127.0.0.1, so a proxy is always in front and `0` is almost always wrong (everyone becomes 127.0.0.1). A request without `X-Forwarded-For` logs a one-time warning |
216
302
  | `INGEST_SALT` | fixed | Extra server-side secret mixed into the visitor hash (on top of the random daily salt) |
217
303
  | `API_KEY` | — | Bearer key for the API / CLI |
218
304
  | `DB_PATH` | `./data/analytics.db` | SQLite path |
305
+ | `DIGEST_WEBHOOK_URL` | — | Daily digest + default alert target (Discord/Slack-compatible). `off` disables it |
306
+ | `WATCH_MIN_VISITORS` | `10` | Spike/drop alerts stay silent below this many visitors per hour |
307
+ | `SMTP_URL` / `REPORT_FROM` | — | Mail server + sender for the weekly report |
308
+ | `REPORT_HOUR_UTC` | `6` | Monday hour (UTC) the weekly report goes out; it covers the last 7 full days in each site's time zone |
309
+ | `AUDIT_RETENTION_DAYS` | `365` | How long the team activity log is kept (min. 30) |
219
310
  | `DATA_RETENTION_DAYS` | `0` | `0` = keep forever; `>0` purges raw events older than N days, daily (data minimisation) |
220
311
 
221
312
  ## Privacy & security
@@ -228,7 +319,9 @@ cookie banner — the same approach as Plausible/Fathom.
228
319
  - **Respects DNT / Global Privacy Control** when you add `data-honor-dnt` to the tag.
229
320
  - Engagement time adds **no new identifier** — it’s matched to the visitor’s own daily hash server-side.
230
321
  - Custom event properties are capped (scalars only, size-limited) so they can’t become a PII dumping ground.
231
- - Timing-safe auth; login and the collect endpoint are rate-limited.
322
+ - Passwords hashed with scrypt; session tokens, invite links and recovery codes are stored only as SHA-256. Sessions live in the database (survive restarts, expire after 30 days).
323
+ - Timing-safe auth; login (per IP and per account), the 2FA step and the collect endpoint are rate-limited. Unknown usernames take as long as wrong passwords.
324
+ - Every per-site endpoint checks access: share links see exactly their site, viewers only the sites they were given.
232
325
  - All visitor-supplied values are HTML-escaped before they reach the DOM.
233
326
  - Run behind TLS; keep `.env` at `chmod 600`.
234
327