Skip to content

Commit d65e46d

Browse files
author
ca.mathieu
committed
feat(stats): add counter-backed usage and trending stats
Add DB-backed usage counters for users, tokens, and server-wide stats, including current and lifetime upload/file/size totals, feature usage, TTL distributions, download counters, and daily download rollups. Expose the new stats through user, token, admin, metrics, and trending APIs, update the Vue admin/home/download views, and add sorting by stats-backed size/download fields. Keep stats safe for multi-instance deployments with atomic metadata mutations, import/export coverage, bounded daily rollup cleanup, and best-effort download stat writes that do not block valid downloads. Document the stats architecture, cleanup semantics, download counting policy, Prometheus metrics, API fields, and agent review expectations.
1 parent 4fbe4fe commit d65e46d

77 files changed

Lines changed: 9477 additions & 354 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agents/workflows/review-changes.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ For every modified file, evaluate against ALL of the following checklist:
5454
- [ ] Does the change follow existing patterns in the codebase?
5555
- [ ] Are naming conventions consistent (variables, functions, types)?
5656
- [ ] Does it match the code style of surrounding files?
57+
- [ ] Are non-obvious policy helpers commented? Check public API validators, persistent-state mutations, complex SQL/GORM queries, stats/business policy, security/privacy behavior, and concurrency assumptions.
58+
- [ ] Do comments explain intent, accepted inputs, defaults, return meaning, or invariants where those are not obvious from nearby code?
5759

5860
#### Completeness
5961
- [ ] Are all necessary files modified (tests, docs, config)?
@@ -75,6 +77,8 @@ For every modified file, evaluate against ALL of the following checklist:
7577
- [ ] Are new behaviors covered by tests?
7678
- [ ] Do existing tests still pass with these changes?
7779
- [ ] Are test assertions meaningful (not just "no error")?
80+
- [ ] For large backend changes, compare package coverage before/after and identify new uncovered functions.
81+
- [ ] Are complex SQL/GORM queries, migrations/backfills, counter mutations, concurrency paths, and public API validation covered by focused backend tests? E2E coverage is useful but not a substitute for these.
7882

7983
#### Documentation (`docs/`, README, `--help`)
8084
- [ ] Do user-facing docs reflect the new/changed behavior?
@@ -89,6 +93,7 @@ For every modified file, evaluate against ALL of the following checklist:
8993
- [ ] Are new/changed request/response fields documented with examples?
9094
- [ ] Are removed or renamed fields noted for backward compatibility?
9195
- [ ] Are new query parameters listed in the relevant filter table?
96+
- [ ] Treat HTTP API documentation drift as a critical review finding.
9297

9398
#### CSS / Theming (if webapp styles were changed)
9499
- [ ] Do changes use design tokens (`surface-*`, `accent-*`) instead of hardcoded colors?
@@ -114,6 +119,7 @@ For every modified file, evaluate against ALL of the following checklist:
114119
- Verify the change aligns with documented patterns
115120
- Check if AGENTS.md needs updating
116121
- **If any HTTP handler, route, or API response struct was modified**: check `docs/reference/api.md` and flag any drift as a 🔴 Critical issue
122+
- **If stats, counters, migrations, or SQL/GORM queries changed**: verify direct backend tests exist for the new logic and flag E2E-only coverage as a review issue
117123

118124
### 4. Lint and build
119125

@@ -207,4 +213,4 @@ Do NOT auto-fix without asking. Present the findings first, let the user decide.
207213
- Don't just rubber-stamp changes with "LGTM" unless they're truly clean
208214
- Pay special attention to subtle issues: off-by-one, missing error handling, stdout/stderr confusion, goroutine leaks
209215
- If a change seems incomplete (e.g., missing tests for new behavior), flag it
210-
- Compare against ARCHITECTURE.md and project conventions
216+
- Compare against ARCHITECTURE.md and project conventions

AGENTS.md

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ plik/
4343
├── README.md ← project README (concise)
4444
├── Makefile ← build orchestration
4545
├── Dockerfile
46-
├── .agent/ ← agentic workflows (/review-changes, /prepare-pr, /cut-release)
46+
├── .agents/ ← agentic workflows (/review-changes, /prepare-pr, /cut-release)
4747
├── server/ ← Go server (see server/ARCHITECTURE.md)
4848
│ ├── main.go ← entry point
4949
│ ├── plikd.cfg ← default config
@@ -139,8 +139,24 @@ make vuln # govulncheck (report only)
139139
- **Helm persistence**: the chart has two independent PVCs — `persistence` for uploaded files (`/home/plik/server/files`) and `dbPersistence` for the SQLite database (`/home/plik/server/db`). Both default to `emptyDir` when disabled. The default `MetadataBackendConfig.ConnectionString` is `/home/plik/server/db/plik.db`.
140140
- **Run tests before committing**: `make lint && make test`
141141
- **Keep ARCHITECTURE.md files in sync**: Each root folder has its own — update the one closest to your change
142+
- **Comment non-obvious policy and invariants**: Add concise comments for helpers that validate public API input, mutate persistent state, build complex SQL/GORM queries, encode security/privacy behavior, rely on concurrency assumptions, or implement statistics/business policy. Comments should explain accepted input domains, default behavior, return meaning, and why the rule exists when those details are not obvious from the name and surrounding call site. Prefer comments that explain why the rule exists over comments that restate the code.
143+
- **Test at the right layer**: E2E tests complement, but do not replace, focused backend tests for SQL/GORM queries, migrations/backfills, counter mutations, concurrency, and public API validation.
142144
- **Release process**: Before creating a GitHub release, update the version in `README.md` and move `charts/plik/CHANGELOG.md` entries from `[Unreleased]` to the new version heading
143145

146+
## Agent Workflows
147+
148+
Project workflows live in `.agents/workflows/`. Use them when the matching task comes up:
149+
150+
| Workflow | Purpose |
151+
|----------|---------|
152+
| `plan.md` | Create an implementation plan before non-trivial features or refactors |
153+
| `review-changes.md` | Critically review local diffs before committing or preparing a PR |
154+
| `commit.md` | Commit or push changes with the mandatory user approval gate |
155+
| `prepare-pr.md` | Run review/pre-flight checks, commit, push, and draft a PR |
156+
| `cut-release.md` | Walk through the release checklist |
157+
| `add-language.md` | Add a new webapp locale end to end |
158+
| `review-language.md` | Review locale quality, placeholders, plurals, and semantic distinctions |
159+
144160
## Documentation
145161

146162
The documentation lives in two places:
@@ -171,4 +187,3 @@ make docs # Build docs (builds client, injects help+plikrc, b
171187
| [releaser/ARCHITECTURE.md](releaser/ARCHITECTURE.md) | Release tooling: build pipeline, Docker stages, client/server compilation |
172188
| [charts/plik/ARCHITECTURE.md](charts/plik/ARCHITECTURE.md) | Helm chart: structure, config/secrets separation, persistence, versioning |
173189
| [.github/ARCHITECTURE.md](.github/ARCHITECTURE.md) | GitHub Actions workflows, CI/CD, Helm chart release flow |
174-

ARCHITECTURE.md

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -253,9 +253,9 @@ When processing a request, limits are resolved via the custom `Context`:
253253
| GET | `/me/token` | `GetUserTokens` | List tokens (paginated) |
254254
| POST | `/me/token` | `CreateToken` | Create upload token |
255255
| DELETE | `/me/token/{token}` | `RevokeToken` | Revoke token |
256-
| GET | `/me/uploads` | `GetUserUploads` | List uploads (paginated, sortable by `sort` (`date`/`size`) and `order` (`desc`/`asc`), filterable by `token` and badge settings: `oneShot`, `removable`, `stream`, `extendTTL`, `password`, `e2ee`) |
256+
| GET | `/me/uploads` | `GetUserUploads` | List uploads (paginated, sortable by `sort` (`date`/`size`/`downloads`) and `order` (`desc`/`asc`), filterable by `token` and badge settings: `oneShot`, `removable`, `stream`, `extendTTL`, `password`, `e2ee`) |
257257
| DELETE | `/me/uploads` | `RemoveUserUploads` | Remove all uploads |
258-
| GET | `/me/stats` | `GetUserStatistics` | User stats |
258+
| GET | `/me/stats` | `GetUserStatistics` | Counter-backed user current + lifetime stats, including stats tracking start timestamp; `token` filters to current API token stats |
259259

260260
### User Management Endpoints (authenticated — session cookie required)
261261

@@ -270,10 +270,12 @@ When processing a request, limits are resolved via the custom `Context`:
270270
| Method | Path | Handler | Description |
271271
|--------|------|---------|-------------|
272272
| POST | `/user` | `CreateUser` | Create user |
273-
| GET | `/stats` | `GetServerStatistics` | Server stats |
274-
| GET | `/users` | `GetUsers` | List users (paginated, filterable by `provider` and `admin` status) |
273+
| GET | `/stats` | `GetServerStatistics` | Counter-backed server current + lifetime stats, downloads, and feature usage counters |
274+
| GET | `/stats/trending/uploads` | `GetTrendingUploads` | Trending uploads (`window=all\|1d\|7d\|30d`, `limit`) |
275+
| GET | `/stats/trending/files` | `GetTrendingFiles` | Trending files (`window=all\|1d\|7d\|30d`, `limit`) |
276+
| GET | `/users` | `GetUsers` | List users (paginated, sortable by `sort` (`date`/`size`/`everSize`), filterable by `provider` and `admin` status) |
275277
| GET | `/users/search` | `SearchUsers` | Search users by login/name/email (LIKE query, `q`, `limit`, `provider`, `admin`) |
276-
| GET | `/uploads` | `GetUploads` | List all uploads (paginated, filterable by `user`, `token`, `sort`, badge settings: `oneShot`, `removable`, `stream`, `extendTTL`, `password`, `e2ee`) |
278+
| GET | `/uploads` | `GetUploads` | List all uploads (paginated, sortable by `sort` (`date`/`size`/`downloads`), filterable by `user`, `token`, and badge settings: `oneShot`, `removable`, `stream`, `extendTTL`, `password`, `e2ee`) |
277279

278280
---
279281

@@ -421,8 +423,23 @@ GORM-based with auto-migrations via gormigrate.
421423
| `tokens` | `Token` | Upload tokens, FK to users |
422424
| `settings` | `Setting` | Server settings (e.g., auth signing key) |
423425
| `cli_auth_sessions` | `CLIAuthSession` | Ephemeral CLI device auth sessions (auto-cleaned) |
426+
| `user_usage_stats` | `UserUsageStats` | Current/lifetime upload, file, and size counters keyed by user ID, with a stats tracking start timestamp; anonymous usage uses the `__anonymous__` sentinel |
427+
| `server_usage_stats` | `ServerUsageStats` | Single-row server current/lifetime counters, download totals, upload feature counters, TTL buckets, and file-size buckets |
428+
| `token_usage_stats` | `TokenUsageStats` | Current/lifetime upload, file, and size counters for current API tokens |
429+
| `download_stats_daily` | `DownloadStatsDaily` | Daily download rollups keyed by day/entity type/entity ID for trending windows; cleanup keeps today plus 30 previous UTC-day buckets |
424430
| `migrations` | (gormigrate) | Schema migration history |
425431

432+
### Stats Model
433+
434+
Plik's stats system is database-backed so several server instances can safely share one metadata database. Counters are updated in the same metadata transactions that create uploads, complete files, remove retained data, and record counted downloads. The detailed implementation contract lives in [server/ARCHITECTURE.md](server/ARCHITECTURE.md#stats-architecture); the system-level invariants are:
435+
436+
- **Current stats** describe retained state now: active uploads, uploaded files, retained size, current feature usage, current TTL distribution, and current file-size distribution.
437+
- **Lifetime stats** describe events since tracking started: ever uploads, ever files, ever size, lifetime feature usage, lifetime TTL distribution, and lifetime file-size distribution.
438+
- **StartedAt** is the "Stats since ..." timestamp. Migration-created rows start at migration/backfill time because already purged metadata cannot be reconstructed.
439+
- **Anonymous usage** is tracked through a synthetic `__anonymous__` usage row instead of a real user.
440+
- **Downloads** are logical intent events, not bytes. Full-file `GET` requests count, start-range `GET` requests count, non-start ranges and malformed ranges do not. Direct file download stats are recorded after auth/status/backend-open/range-policy checks and before streaming completes; archive download stats are recorded after a successful archive close.
441+
- **Trending and server download windows** use upload/file download counts and daily rollups for `1d`, `7d`, and `30d` UTC-day windows. Trending is admin-only. Cleanup keeps today plus 30 previous UTC-day rollup buckets, one extra bucket beyond the 30-day display window.
442+
426443
---
427444

428445
## Documentation

docs/operations/server-cli.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ plikd file delete --all
146146

147147
## Cleanup
148148

149-
Remove expired uploads and purge deleted files from the data backend:
149+
Remove expired uploads, purge deleted files from the data backend, and prune old download statistics used for trends:
150150

151151
```bash
152152
plikd clean

docs/reference/api.md

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -129,12 +129,13 @@ Requires authenticated session cookie.
129129
| `GET` | `/me` | Current user info |
130130
| `PATCH` | `/me` | Update self-editable profile fields (`name`, `email`, `theme`, `language`) |
131131
| `DELETE` | `/me` | Delete own account |
132-
| `GET` | `/me/token` | List tokens (paginated) |
132+
| `GET` | `/me/token` | List tokens (paginated, sortable by `date`, `size`, `everSize`) |
133133
| `POST` | `/me/token` | Create upload token `{ "comment": "..." }` |
134134
| `DELETE` | `/me/token/{token}` | Revoke token |
135-
| `GET` | `/me/uploads` | List uploads (paginated, filterable) |
135+
| `GET` | `/me/uploads` | List uploads (paginated, filterable, sortable by `date`, `size`, `downloads`) |
136136
| `DELETE` | `/me/uploads` | Remove all uploads |
137-
| `GET` | `/me/stats` | User statistics |
137+
| `GET` | `/me/stats` | User current/lifetime statistics |
138+
| `GET` | `/me/stats?token=...` | Statistics for one API token |
138139

139140
## Admin Endpoints
140141

@@ -146,10 +147,46 @@ Requires admin session cookie.
146147
| `GET` | `/user/{userID}` | Get user info |
147148
| `POST` | `/user/{userID}` | Update user |
148149
| `DELETE` | `/user/{userID}` | Delete user |
149-
| `GET` | `/stats` | Server statistics |
150-
| `GET` | `/users` | List all users (paginated, filterable) |
150+
| `GET` | `/stats` | Server current/lifetime statistics and feature usage counters |
151+
| `GET` | `/stats/trending/uploads?window=7d&limit=20` | Trending uploads (`window`: `all`, `1d`, `7d`, `30d`) |
152+
| `GET` | `/stats/trending/files?window=7d&limit=20` | Trending files (`window`: `all`, `1d`, `7d`, `30d`) |
153+
| `GET` | `/users` | List all users (paginated, filterable, sortable by `date`, `size`, `everSize`) |
151154
| `GET` | `/users/search?q=...` | Search users (optional: `provider`, `admin`, `limit`) |
152-
| `GET` | `/uploads` | List all uploads (paginated, filterable) |
155+
| `GET` | `/uploads` | List all uploads (paginated, filterable, sortable by `date`, `size`, `downloads`) |
156+
157+
### GET /me/stats
158+
159+
Returns current and lifetime usage for the authenticated user. The optional `token` query parameter returns stats for an API token owned by the user; revoked tokens do not retain historical token stats.
160+
161+
Selected response fields: `uploads`, `files`, `totalSize`, `everUploads`, `everFiles`, `everTotalSize`, `startedAt`.
162+
163+
### GET /me/token
164+
165+
Each token returned by `GET /me/token` includes a `stats` object with current and lifetime token usage (`uploads`, `files`, `totalSize`, `everUploads`, `everFiles`, `everTotalSize`, `startedAt`) plus `lastUploadAt` when the token has created uploads. Supported sort fields are `date` (token creation), `size` (current retained size), and `everSize` (lifetime uploaded size).
166+
167+
### GET /stats
168+
169+
Returns server-wide usage statistics. Current usage fields describe retained data at the time of the request: `users`, `uploads`, `anonymousUploads`, `files`, `totalSize`, `anonymousTotalSize`. Lifetime fields describe activity since `startedAt`: `everUsers`, `everUploads`, `everFiles`, `everTotalSize`, `everAnonymousUploads`, `everAnonymousTotalSize`.
170+
171+
Download fields count logical download events, not transferred bytes: `downloads`, `downloads1d`, `downloads7d`, `downloads30d`.
172+
173+
Download windows are based on UTC calendar days: `downloads1d` is the current UTC day, `downloads7d` is today plus the previous 6 UTC days, and `downloads30d` is today plus the previous 29 UTC days. `downloads` remains all-time.
174+
175+
Feature usage counters are individual upload counters, not combinations: `passwordUploads`, `removableUploads`, `oneShotUploads`, `streamUploads`, `extendTTLUploads`, `e2eeUploads`, `commentUploads`, plus matching `ever*` fields.
176+
177+
TTL distribution counters track creation-time TTL choices for current and lifetime uploads: `ttlNoneUploads`, `ttlLt1hUploads`, `ttl1h1dUploads`, `ttl1d7dUploads`, `ttl7d30dUploads`, `ttlGt30dUploads`, plus matching `everTTL*` fields.
178+
179+
File size distribution counters count files, not bytes, with binary MiB/GiB thresholds: `fileSizeLt1mFiles`, `fileSize1m10mFiles`, `fileSize10m100mFiles`, `fileSize100m1gFiles`, `fileSize1g10gFiles`, `fileSize10g100gFiles`, `fileSizeGt100gFiles`, plus matching `everFileSize*` fields.
180+
181+
### GET /users
182+
183+
Each user returned by `GET /users` includes a `stats` object with current and lifetime usage. Supported sort fields are `date` (user creation), `size` (current retained size), and `everSize` (lifetime uploaded size).
184+
185+
### Trending Stats
186+
187+
Admin-only trending endpoints return current uploads/files ordered by download count. If omitted, `window` defaults to `all`; accepted values are `all`, `1d`, `7d`, and `30d`. `limit` defaults to `20` and is capped at `100`.
188+
189+
Trending items include IDs, display name/comment, user ID, size or file count, `downloadCount`, and `lastDownloadedAt`.
153190

154191
## Pagination
155192

0 commit comments

Comments
 (0)