diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 2d86f1fc..54516344 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -1,32 +1,19 @@
-# To get started with Dependabot version updates, you'll need to specify which
-# package ecosystems to update and where the package manifests are located.
-# Please see the documentation for all configuration options:
-# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
-# Basic `dependabot.yml` file with
-# minimum configuration for three package managers
-
version: 2
updates:
- # Enable version updates for npm
- package-ecosystem: "npm"
- # Look for `package.json` and `lock` files in the `root` directory
directory: "/"
- # Check the npm registry for updates every day (weekdays)
+ target-branch: "development"
schedule:
interval: "daily"
- # Enable version updates for Docker
- package-ecosystem: "docker"
- # Look for a `Dockerfile` in the `root` directory
directory: "/"
- # Check for updates once a week
+ target-branch: "development"
schedule:
interval: "weekly"
- # Enable version updates for GitHub Actions
- package-ecosystem: "github-actions"
- # Workflow files stored in the default location of `.github/workflows`
- # You don't need to specify `/.github/workflows` for `directory`. You can use `directory: "/"`.
directory: "/"
+ target-branch: "development"
schedule:
interval: "weekly"
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e258e973..75ba6b62 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -4,7 +4,7 @@ on:
push:
branches: [main]
pull_request:
- branches: [main]
+ branches: [main, development]
permissions:
contents: read
@@ -17,7 +17,7 @@ jobs:
steps:
- uses: actions/checkout@v6
- - uses: pnpm/action-setup@v4
+ - uses: pnpm/action-setup@v5
- uses: actions/setup-node@v6
with:
diff --git a/.github/workflows/dev-image.yml b/.github/workflows/dev-image.yml
index 785cb046..71e322b0 100644
--- a/.github/workflows/dev-image.yml
+++ b/.github/workflows/dev-image.yml
@@ -27,6 +27,12 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
+ - name: Log in to Docker Hub
+ uses: docker/login-action@v4
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
@@ -39,6 +45,8 @@ jobs:
context: .
push: true
platforms: linux/amd64,linux/arm64
- tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:development
+ tags: |
+ ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:development
+ docker.io/jordyjordy/tracker-tracker:development
cache-from: type=gha
cache-to: type=gha,mode=max
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index 9b9d176f..75670544 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -2,7 +2,7 @@ name: Docker Build & Scan
on:
pull_request:
- branches: [main]
+ branches: [main, development]
permissions:
contents: read
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 685d1c85..135d926d 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -1,5 +1,5 @@
# .github/workflows/docs.yml
-name: Deploy Docs
+name: Docs
on:
push:
@@ -7,13 +7,37 @@ on:
paths:
- "docs/kb/**"
- ".github/workflows/docs.yml"
+ pull_request:
+ branches: [main]
+ paths:
+ - "docs/kb/**"
+ - ".github/workflows/docs.yml"
workflow_dispatch:
permissions:
contents: write
jobs:
+ build:
+ name: Docs Build
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v6
+
+ - uses: actions/setup-python@v6
+ with:
+ python-version: "3.x"
+
+ - name: Install MkDocs Material
+ run: pip install -r docs/kb/requirements.txt
+
+ - name: Build docs
+ run: mkdocs build --strict --config-file docs/kb/mkdocs.yml
+
deploy:
+ name: Docs Deploy
+ needs: build
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
@@ -25,5 +49,5 @@ jobs:
- name: Install MkDocs Material
run: pip install -r docs/kb/requirements.txt
- - name: Build and deploy
+ - name: Deploy to GitHub Pages
run: mkdocs gh-deploy --force --config-file docs/kb/mkdocs.yml
diff --git a/.github/workflows/knip-report.yml b/.github/workflows/knip-report.yml
index 3b85fdd0..cbebce4a 100644
--- a/.github/workflows/knip-report.yml
+++ b/.github/workflows/knip-report.yml
@@ -16,7 +16,7 @@ jobs:
uses: actions/checkout@v6
- name: Setup pnpm
- uses: pnpm/action-setup@v4
+ uses: pnpm/action-setup@v5
- name: Setup Node.js
uses: actions/setup-node@v6
diff --git a/.github/workflows/ossar.yml b/.github/workflows/ossar.yml
index e646749c..c66c1862 100644
--- a/.github/workflows/ossar.yml
+++ b/.github/workflows/ossar.yml
@@ -32,7 +32,7 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
# Ensure a compatible version of dotnet is installed.
# The [Microsoft Security Code Analysis CLI](https://aka.ms/mscadocs) is built with dotnet v3.1.201.
@@ -50,6 +50,6 @@ jobs:
# Upload results to the Security tab
- name: Upload OSSAR results
- uses: github/codeql-action/upload-sarif@v3
+ uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: ${{ steps.ossar.outputs.sarifFile }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 23cd7c3f..bb3fe6fb 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -137,7 +137,7 @@ jobs:
- name: Sync README to Docker Hub
if: steps.tag.outputs.should_release == 'true'
- uses: peter-evans/dockerhub-description@v4
+ uses: peter-evans/dockerhub-description@v5
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
diff --git a/.github/workflows/security-audit.yml b/.github/workflows/security-audit.yml
index 102074f8..d8119db8 100644
--- a/.github/workflows/security-audit.yml
+++ b/.github/workflows/security-audit.yml
@@ -4,7 +4,7 @@ on:
push:
branches: [main]
pull_request:
- branches: [main]
+ branches: [main, development]
permissions:
contents: read
@@ -19,7 +19,7 @@ jobs:
steps:
- uses: actions/checkout@v6
- - uses: pnpm/action-setup@v4
+ - uses: pnpm/action-setup@v5
- uses: actions/setup-node@v6
with:
diff --git a/.github/workflows/tracker-validation.yml b/.github/workflows/tracker-validation.yml
index a39cb5cb..5e569cb3 100644
--- a/.github/workflows/tracker-validation.yml
+++ b/.github/workflows/tracker-validation.yml
@@ -2,7 +2,7 @@ name: Tracker Validation
on:
pull_request:
- branches: [main]
+ branches: [main, development]
permissions:
contents: read
@@ -17,7 +17,7 @@ jobs:
steps:
- uses: actions/checkout@v6
- - uses: pnpm/action-setup@v4
+ - uses: pnpm/action-setup@v5
- uses: actions/setup-node@v6
with:
diff --git a/.gitignore b/.gitignore
index 61911d76..4603b9f3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -51,6 +51,9 @@ docs/superpowers/specs/
.interface-design/
.worktrees/
+# docker local overrides
+docker-compose.override.yml
+
# vercel
.vercel
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5431fbc3..9bdcc602 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,91 @@
# Changelog
-## [2.3.0](https://github.com/jordanlambrecht/tracker-tracker/compare/v2.1.1...v2.3.0) (2026-03-21)
+## [2.4.1](https://github.com/jordanlambrecht/tracker-tracker/compare/v2.3.0...v2.4.1) (2026-03-23)
+
+
+### Features
+
+- add development image to docker hub ([8785094](https://github.com/jordanlambrecht/tracker-tracker/commit/87850942f118ccfd23c0a04d537928cd3db82976))
+- add fetchTrackerStats for future live transit paper data ([ed6662f](https://github.com/jordanlambrecht/tracker-tracker/commit/ed6662f0ac0c9990b6c96e8ae5616452385b6c26))
+- add GitHub Actions workflow for building and pushing development Docker image ([dca0af0](https://github.com/jordanlambrecht/tracker-tracker/commit/dca0af0a75a4ce21fbe14414d86d4ecbe4d459d3))
+- add per-tracker pause polling ([14a6c43](https://github.com/jordanlambrecht/tracker-tracker/commit/14a6c43cd6764924aba6e2fa592ef13d208b528a))
+- add system events viewer and log management ([b01ed22](https://github.com/jordanlambrecht/tracker-tracker/commit/b01ed22ec2ecfefe7f12bb448998d2726cb6b7f9))
+- remote image upload ([a480c34](https://github.com/jordanlambrecht/tracker-tracker/commit/a480c3470b7e97e44764c1d9c6d1bee356d22728))
+- **ui:** add pause/resume button ([c89299e](https://github.com/jordanlambrecht/tracker-tracker/commit/c89299ee80b9ce5ab22743039b6abd40be5a27b6))
+- **ui:** lazy-load chart sections, prefetch sidebar links, and fix scroll-to-top on navigation ([ba8f59e](https://github.com/jordanlambrecht/tracker-tracker/commit/ba8f59ee33a21be72034f296f5da1ae795e03c70))
+
+
+### Bug Fixes
+
+- **api:** orpheus was not matching seeding/leeching to response ([4569238](https://github.com/jordanlambrecht/tracker-tracker/commit/456923879b2970c46432bc9a0b604da2685bc31d))
+- better regex for splitting comparison values in timing safe check ([8c67a50](https://github.com/jordanlambrecht/tracker-tracker/commit/8c67a50cb64196831d7b021249eb76e84766e009))
+- convert bold numbered rules to markdown list items ([6e96454](https://github.com/jordanlambrecht/tracker-tracker/commit/6e964541330cca791818aca5d703e03ac2165694))
+- deploy issues ([cf45ea1](https://github.com/jordanlambrecht/tracker-tracker/commit/cf45ea19726b8f31db5080ebe93909dd0825e995))
+- preload fleet dashboard tab ([5f08951](https://github.com/jordanlambrecht/tracker-tracker/commit/5f0895192f39a740927594ab6617f2c5c04b5708))
+- resolve biome lint warnings ([af8807d](https://github.com/jordanlambrecht/tracker-tracker/commit/af8807d72847e139be396778a096a7695fc49123))
+- **trackers:** markdown rendering ([a5fbdde](https://github.com/jordanlambrecht/tracker-tracker/commit/a5fbdde7056848bb58fdbe6f1e77a765a543842d))
+- update type imports for CollapsibleCard ([23979d1](https://github.com/jordanlambrecht/tracker-tracker/commit/23979d1f6323bd3fa209c9ed19172dbd7d05b6db))
+- update workflow triggers to include development branch for pull requests ([d159775](https://github.com/jordanlambrecht/tracker-tracker/commit/d15977566a9dbd341c0db6f3b67e0ace9bb70f16))
+- wrong postgres setup in docker-compose (closes [#78](https://github.com/jordanlambrecht/tracker-tracker/issues/78)) ([a0a3e0e](https://github.com/jordanlambrecht/tracker-tracker/commit/a0a3e0e16fe4e3b97dea9c7ebc5616cb54e22332))
+
+
+### Performance
+
+- add 5s per-client fetch deadline ([558c4be](https://github.com/jordanlambrecht/tracker-tracker/commit/558c4be0f9b05b198fd09ca5df4aad0dc6cde637))
+- **settings:** settings page optimizations ([63aabab](https://github.com/jordanlambrecht/tracker-tracker/commit/63aabab9afdfde3d492b81b86f581c4c035269d1))
+
+
+### Refactoring
+
+- **charts:** consolidate duplicate Fleet/Torrent chart pairs and normalize upstream data flow ([ca051a8](https://github.com/jordanlambrecht/tracker-tracker/commit/ca051a8f4284565f6b0c09e4c9f0522a70ff7e2c))
+- **charts:** migrate time-series charts to time axis with shared helpers and quality fixes ([596396e](https://github.com/jordanlambrecht/tracker-tracker/commit/596396e205fe387799986af7f1ff8386e8f77d13))
+- **charts:** reorganize chart support files into lib/ subfolder ([98a26d4](https://github.com/jordanlambrecht/tracker-tracker/commit/98a26d4fb4bf2fb7c3b02c4d38151a6b07fbb887))
+- **settings:** extract CollapsibleCard ([5487d19](https://github.com/jordanlambrecht/tracker-tracker/commit/5487d19d1ac2dd56c6bf2136f072edbcb7868fe5))
+- **settings:** extract SettingsSection wrapper ([ea0572c](https://github.com/jordanlambrecht/tracker-tracker/commit/ea0572c603ef191494a37cd9fe7ca64447bce1d4))
+
+## [2.4.0](https://github.com/jordanlambrecht/tracker-tracker/compare/v2.3.0...v2.4.0) (2026-03-23)
+### Features
+
+- add fetchTrackerStats for future live transit paper data ([ed6662f](https://github.com/jordanlambrecht/tracker-tracker/commit/ed6662f0ac0c9990b6c96e8ae5616452385b6c26))
+- add GitHub Actions workflow for building and pushing development Docker image ([dca0af0](https://github.com/jordanlambrecht/tracker-tracker/commit/dca0af0a75a4ce21fbe14414d86d4ecbe4d459d3))
+- add per-tracker pause polling ([14a6c43](https://github.com/jordanlambrecht/tracker-tracker/commit/14a6c43cd6764924aba6e2fa592ef13d208b528a))
+- add system events viewer and log management ([b01ed22](https://github.com/jordanlambrecht/tracker-tracker/commit/b01ed22ec2ecfefe7f12bb448998d2726cb6b7f9))
+- remote image upload ([a480c34](https://github.com/jordanlambrecht/tracker-tracker/commit/a480c3470b7e97e44764c1d9c6d1bee356d22728))
+- **ui:** add pause/resume button ([c89299e](https://github.com/jordanlambrecht/tracker-tracker/commit/c89299ee80b9ce5ab22743039b6abd40be5a27b6))
+- **ui:** lazy-load chart sections, prefetch sidebar links, and fix scroll-to-top on navigation ([ba8f59e](https://github.com/jordanlambrecht/tracker-tracker/commit/ba8f59ee33a21be72034f296f5da1ae795e03c70))
+
+
+### Bug Fixes
+
+- **api:** orpheus was not matching seeding/leeching to response ([4569238](https://github.com/jordanlambrecht/tracker-tracker/commit/456923879b2970c46432bc9a0b604da2685bc31d))
+- better regex for splitting comparison values in timing safe check ([8c67a50](https://github.com/jordanlambrecht/tracker-tracker/commit/8c67a50cb64196831d7b021249eb76e84766e009))
+- convert bold numbered rules to markdown list items ([6e96454](https://github.com/jordanlambrecht/tracker-tracker/commit/6e964541330cca791818aca5d703e03ac2165694))
+- deploy issues ([cf45ea1](https://github.com/jordanlambrecht/tracker-tracker/commit/cf45ea19726b8f31db5080ebe93909dd0825e995))
+- resolve biome lint warnings ([af8807d](https://github.com/jordanlambrecht/tracker-tracker/commit/af8807d72847e139be396778a096a7695fc49123))
+- **trackers:** markdown rendering ([a5fbdde](https://github.com/jordanlambrecht/tracker-tracker/commit/a5fbdde7056848bb58fdbe6f1e77a765a543842d))
+- update type imports for CollapsibleCard ([23979d1](https://github.com/jordanlambrecht/tracker-tracker/commit/23979d1f6323bd3fa209c9ed19172dbd7d05b6db))
+- update workflow triggers to include development branch for pull requests ([d159775](https://github.com/jordanlambrecht/tracker-tracker/commit/d15977566a9dbd341c0db6f3b67e0ace9bb70f16))
+- wrong postgres setup in docker-compose (closes [#78](https://github.com/jordanlambrecht/tracker-tracker/issues/78)) ([a0a3e0e](https://github.com/jordanlambrecht/tracker-tracker/commit/a0a3e0e16fe4e3b97dea9c7ebc5616cb54e22332))
+
+
+### Performance
+
+- add 5s per-client fetch deadline ([558c4be](https://github.com/jordanlambrecht/tracker-tracker/commit/558c4be0f9b05b198fd09ca5df4aad0dc6cde637))
+- **settings:** settings page optimizations ([63aabab](https://github.com/jordanlambrecht/tracker-tracker/commit/63aabab9afdfde3d492b81b86f581c4c035269d1))
+
+
+### Refactoring
+
+- **charts:** consolidate duplicate Fleet/Torrent chart pairs and normalize upstream data flow ([ca051a8](https://github.com/jordanlambrecht/tracker-tracker/commit/ca051a8f4284565f6b0c09e4c9f0522a70ff7e2c))
+- **charts:** migrate time-series charts to time axis with shared helpers and quality fixes ([596396e](https://github.com/jordanlambrecht/tracker-tracker/commit/596396e205fe387799986af7f1ff8386e8f77d13))
+- **charts:** reorganize chart support files into lib/ subfolder ([98a26d4](https://github.com/jordanlambrecht/tracker-tracker/commit/98a26d4fb4bf2fb7c3b02c4d38151a6b07fbb887))
+- **settings:** extract CollapsibleCard ([5487d19](https://github.com/jordanlambrecht/tracker-tracker/commit/5487d19d1ac2dd56c6bf2136f072edbcb7868fe5))
+- **settings:** extract SettingsSection wrapper ([ea0572c](https://github.com/jordanlambrecht/tracker-tracker/commit/ea0572c603ef191494a37cd9fe7ca64447bce1d4))
+
+## [2.3.0](https://github.com/jordanlambrecht/tracker-tracker/compare/v2.1.1...v2.3.0) (2026-03-21)
+
### Features
- add alertSlideIn keyframe animation ([f59ab88](https://github.com/jordanlambrecht/tracker-tracker/commit/f59ab8854669d8592422da27f8e74a9c7d643216))
@@ -20,7 +103,6 @@
- replace manual polling with TanStack Query ([0129020](https://github.com/jordanlambrecht/tracker-tracker/commit/0129020d3caba64431485bd4d7fb7b3647853fab))
- wire notification dispatch into tracker polling scheduler ([1d8f4fc](https://github.com/jordanlambrecht/tracker-tracker/commit/1d8f4fc622d79400f0bf52b7fc7678fd123dea42))
-
### Bug Fixes
- added size props to dialog comp ([46a8160](https://github.com/jordanlambrecht/tracker-tracker/commit/46a816081276ab068310b36a040ee6ada8fd4441))
@@ -28,7 +110,6 @@
- update notificationDeliveryState schema to add foreign key constraint for targetId ([1319b8d](https://github.com/jordanlambrecht/tracker-tracker/commit/1319b8d168cd192627ab9c874120174317034738))
- update timestamp format ([2d01aba](https://github.com/jordanlambrecht/tracker-tracker/commit/2d01abaf76a46e7f93353ce1e17461d3d112e22a))
-
### Refactoring
- add getProxyTrackers function to fetch proxy-enabled trackers ([17cf490](https://github.com/jordanlambrecht/tracker-tracker/commit/17cf4903c1047a00dc62ea3d2e8614cdc74f06ec))
@@ -48,7 +129,6 @@
## [2.2.0](https://github.com/jordanlambrecht/tracker-tracker/compare/v2.1.1...v2.2.0) (2026-03-20)
-
### Features
- add notification delivery pipeline with circuit breaker and cooldowns ([14d3982](https://github.com/jordanlambrecht/tracker-tracker/commit/14d3982c19e017b5d4b932953bcc8ae6fcfbd839))
@@ -64,7 +144,6 @@
- replace manual polling with TanStack Query ([0129020](https://github.com/jordanlambrecht/tracker-tracker/commit/0129020d3caba64431485bd4d7fb7b3647853fab))
- wire notification dispatch into tracker polling scheduler ([1d8f4fc](https://github.com/jordanlambrecht/tracker-tracker/commit/1d8f4fc622d79400f0bf52b7fc7678fd123dea42))
-
### Refactoring
- add getProxyTrackers function to fetch proxy-enabled trackers ([17cf490](https://github.com/jordanlambrecht/tracker-tracker/commit/17cf4903c1047a00dc62ea3d2e8614cdc74f06ec))
@@ -87,7 +166,6 @@
## [2.0.2](https://github.com/jordanlambrecht/tracker-tracker/compare/v1.11.3...v2.0.2) (2026-03-18)
-
### Features
- add boot-time scheduler recovery ([b1b1499](https://github.com/jordanlambrecht/tracker-tracker/commit/b1b1499d4cce5cfd3a621c9023ffd1f47c04322f))
@@ -104,20 +182,17 @@
- persist scheduler key on login, keep running through logout ([eca1cad](https://github.com/jordanlambrecht/tracker-tracker/commit/eca1cad3ff7d7ef86cae43876e99b5cb1f75d9d9))
- postgresql 18 infrastructure with migration script ([4178cad](https://github.com/jordanlambrecht/tracker-tracker/commit/4178cad4d78dc210323850e5623ebc7b16505cb0))
-
### Bug Fixes
- add icons metadata for favicon ([d048355](https://github.com/jordanlambrecht/tracker-tracker/commit/d04835500a5d8071215a3613d678c1aaba51c7cd))
- biome filter for noImportantStyles ([ae2fd08](https://github.com/jordanlambrecht/tracker-tracker/commit/ae2fd08b542a2c87cd080ce59dffe8e47ffcaace))
- hopefully fixed un/pw field flashing on login screen ([2482473](https://github.com/jordanlambrecht/tracker-tracker/commit/2482473b950519f9bffe6cb459ec82f3bb2a9a73))
-
### Performance
- add database indexes, column type improvements, and connection pool tuning ([c949145](https://github.com/jordanlambrecht/tracker-tracker/commit/c949145f502fe6e549bc060e15af3f2f33eb59ca))
- distinct on query, column projections, batch inserts, jsonb/array cleanup ([f5cc7ca](https://github.com/jordanlambrecht/tracker-tracker/commit/f5cc7ca22d4c72740f627f321c0258abbd4fa96c))
-
### Refactoring
- centralize localStorage keys into storage-keys.ts ([501288e](https://github.com/jordanlambrecht/tracker-tracker/commit/501288e6bd5a5c1e3d0d9f0d2bffc32fd16ffdcb))
@@ -130,7 +205,6 @@
## [2.0.1](https://github.com/jordanlambrecht/tracker-tracker/compare/v1.11.3...v2.0.1) (2026-03-18)
-
### Features
- add boot-time scheduler recovery ([b1b1499](https://github.com/jordanlambrecht/tracker-tracker/commit/b1b1499d4cce5cfd3a621c9023ffd1f47c04322f))
@@ -147,20 +221,17 @@
- persist scheduler key on login, keep running through logout ([eca1cad](https://github.com/jordanlambrecht/tracker-tracker/commit/eca1cad3ff7d7ef86cae43876e99b5cb1f75d9d9))
- postgresql 18 infrastructure with migration script ([4178cad](https://github.com/jordanlambrecht/tracker-tracker/commit/4178cad4d78dc210323850e5623ebc7b16505cb0))
-
### Bug Fixes
- add icons metadata for favicon ([d048355](https://github.com/jordanlambrecht/tracker-tracker/commit/d04835500a5d8071215a3613d678c1aaba51c7cd))
- biome filter for noImportantStyles ([ae2fd08](https://github.com/jordanlambrecht/tracker-tracker/commit/ae2fd08b542a2c87cd080ce59dffe8e47ffcaace))
- hopefully fixed un/pw field flashing on login screen ([2482473](https://github.com/jordanlambrecht/tracker-tracker/commit/2482473b950519f9bffe6cb459ec82f3bb2a9a73))
-
### Performance
- add database indexes, column type improvements, and connection pool tuning ([c949145](https://github.com/jordanlambrecht/tracker-tracker/commit/c949145f502fe6e549bc060e15af3f2f33eb59ca))
- distinct on query, column projections, batch inserts, jsonb/array cleanup ([f5cc7ca](https://github.com/jordanlambrecht/tracker-tracker/commit/f5cc7ca22d4c72740f627f321c0258abbd4fa96c))
-
### Refactoring
- centralize localStorage keys into storage-keys.ts ([501288e](https://github.com/jordanlambrecht/tracker-tracker/commit/501288e6bd5a5c1e3d0d9f0d2bffc32fd16ffdcb))
@@ -173,7 +244,6 @@
## [2.0.0](https://github.com/jordanlambrecht/tracker-tracker/compare/v1.11.3...v2.0.0) (2026-03-18)
-
### Features
- add boot-time scheduler recovery ([b1b1499](https://github.com/jordanlambrecht/tracker-tracker/commit/b1b1499d4cce5cfd3a621c9023ffd1f47c04322f))
@@ -190,20 +260,17 @@
- persist scheduler key on login, keep running through logout ([eca1cad](https://github.com/jordanlambrecht/tracker-tracker/commit/eca1cad3ff7d7ef86cae43876e99b5cb1f75d9d9))
- postgresql 18 infrastructure with migration script ([4178cad](https://github.com/jordanlambrecht/tracker-tracker/commit/4178cad4d78dc210323850e5623ebc7b16505cb0))
-
### Bug Fixes
- add icons metadata for favicon ([d048355](https://github.com/jordanlambrecht/tracker-tracker/commit/d04835500a5d8071215a3613d678c1aaba51c7cd))
- biome filter for noImportantStyles ([ae2fd08](https://github.com/jordanlambrecht/tracker-tracker/commit/ae2fd08b542a2c87cd080ce59dffe8e47ffcaace))
- hopefully fixed un/pw field flashing on login screen ([2482473](https://github.com/jordanlambrecht/tracker-tracker/commit/2482473b950519f9bffe6cb459ec82f3bb2a9a73))
-
### Performance
- add database indexes, column type improvements, and connection pool tuning ([c949145](https://github.com/jordanlambrecht/tracker-tracker/commit/c949145f502fe6e549bc060e15af3f2f33eb59ca))
- distinct on query, column projections, batch inserts, jsonb/array cleanup ([f5cc7ca](https://github.com/jordanlambrecht/tracker-tracker/commit/f5cc7ca22d4c72740f627f321c0258abbd4fa96c))
-
### Refactoring
- centralize localStorage keys into storage-keys.ts ([501288e](https://github.com/jordanlambrecht/tracker-tracker/commit/501288e6bd5a5c1e3d0d9f0d2bffc32fd16ffdcb))
@@ -512,7 +579,7 @@
- Login timer cards link to tracker site with hover external-link indicator
- Rank timeline: promotion/demotion chevrons (green/red), anniversary milestones, horizontal scroll
- Swipe/drag gestures on sidebar client carousel (pointer events with capture)
-- 4 new draft trackers: AsianCinema, Bibliotik, UHDBits, Seed Pool
+- 4 new draft trackers: AsianCinema, Bibliotik, UHDBits, SeedPool
- TrackerHub slugs and status page URLs populated across 19 existing trackers
- Download client uptime tracking: 24h heartbeat history displayed as a horizontal status bar in each client's settings card, with 5-minute bucket granularity and long-term retention for future chart overlays
- Live active torrents: 5-second polling of actively transferring torrents on tracker detail page with live speed/state updates
diff --git a/README.md b/README.md
index a7a9f8ce..1791a47a 100644
--- a/README.md
+++ b/README.md
@@ -49,6 +49,7 @@ You can check out a few other, longer, screenshots in the docs folder.
| Redacted (RED) | Gazelle | ✅ Verified | |
| ReelFlix | UNIT3D | ✅ Verified | |
| SkipTheCommercials | UNIT3D | ✅ Verified | |
+| Seedpool | UNIT3D | ✅ Verified | |
| Upload.cx | UNIT3D | ✅ Verified | |
| AlphaRatio | Gazelle | 🟡 Unverified ⛔ Stuck | |
| AnimeBytes | Gazelle | 🟡 Unverified ⛔ Stuck | |
@@ -174,7 +175,6 @@ All other settings — polling interval, privacy mode, proxy, backups — are co
| `./data/logs` | `/data/logs` | Application log files |
| `pgdata` (named) | PG data dir | PostgreSQL database (managed by Docker) |
-
## Adding a Tracker
1. Click **+ Add Tracker** in the sidebar
diff --git a/biome.json b/biome.json
index 6483d122..a5af73b4 100644
--- a/biome.json
+++ b/biome.json
@@ -1,5 +1,5 @@
{
- "$schema": "https://biomejs.dev/schemas/2.4.7/schema.json",
+ "$schema": "https://biomejs.dev/schemas/2.4.8/schema.json",
"files": {
"includes": ["**", "!.next", "!node_modules", "!public", "!.history", "!.claude"]
},
diff --git a/docker-compose.yml b/docker-compose.yml
index d5a96280..583cbb6f 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -42,7 +42,7 @@ services:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
# POSTGRES_PORT: ${POSTGRES_PORT} uncomment if you want to customize
volumes:
- - pgdata:/var/lib/postgresql/data # Don't ever put a database on a network drive
+ - pgdata:/var/lib/postgresql # Don't ever put a database on a network drive
- ./postgres/postgresql.conf:/etc/postgresql/postgresql.conf:ro # Needed for loading fancy PG18-specific optimizations
command: ["postgres", "-c", "config_file=/etc/postgresql/postgresql.conf"] # ^Same
healthcheck:
diff --git a/docs/kb/.prettierignore b/docs/kb/.prettierignore
new file mode 100644
index 00000000..de056073
--- /dev/null
+++ b/docs/kb/.prettierignore
@@ -0,0 +1 @@
+**/*.md
diff --git a/docs/kb/docs/contributing/adding-a-tracker.md b/docs/kb/docs/contributing/adding-a-tracker.md
index 6dcb59e4..e255060a 100644
--- a/docs/kb/docs/contributing/adding-a-tracker.md
+++ b/docs/kb/docs/contributing/adding-a-tracker.md
@@ -6,6 +6,28 @@ If the tracker you want to add runs on UNIT3D, Gazelle, GGn, or Nebulance, you o
---
+## Standardization Philosophy
+
+Every tracker file in `src/data/trackers/` follows the same field order and completeness rules. This makes files easy to compare, review, and diff.
+
+**Every field must be present in every tracker file, even if empty.** Use `""` for empty strings, `[]` for empty arrays, and `false` for booleans. Do not omit fields and do not use `undefined` as a value. Presence in the file shows the field was considered.
+
+```typescript
+abbreviation: "" // not: abbreviation: undefined
+logo: "" // not: logo: undefined
+trackerHubSlug: "" // not: trackerHubSlug: undefined
+bannedGroups: [] // not: bannedGroups: undefined
+warning: false // not: warning: undefined
+```
+
+**There are three exceptions to this rule:**
+
+1. The `stats` block is omitted entirely when no real data exists. Do not include the block with `undefined` values.
+2. `rules.fulfillmentPeriodHours`, `rules.hnrBanLimit`, and `rules.fullRulesMarkdown` are truly optional — omit them when unknown rather than setting them to `undefined`.
+3. Platform-specific fields (`gazelleAuthStyle`, `gazelleEnrich`, `unit3dAuthStyle`) only appear in tracker files for their respective platform. Do not add them to tracker files on other platforms.
+
+---
+
## 1. Copy the Template
The template lives at `src/data/trackers/_template.ts`. Copy it to a new file named after your tracker's slug. The slug must be lowercase with hyphens only — no underscores, no uppercase, no special characters.
@@ -22,63 +44,95 @@ Here is the full template for reference:
// Copy this file to add a new tracker to the registry.
//
// 1. Duplicate this file and rename it to your tracker's slug (e.g. mytracker.ts)
-// 2. Fill in the fields below — see inline comments for guidance
+// 2. Fill in all fields below — every field must be present (use "" / [] / false
+// rather than omitting). See inline comments for guidance.
// 3. Export from src/data/trackers/index.ts (add to the barrel + ALL_TRACKERS array)
// 4. Run `pnpm test` to validate your entry
//
// Set draft: true while the entry is incomplete. Draft trackers skip strict
// validation in CI, so you can submit a PR with partial data.
//
-// Content categories must be from the allowed list:
+// Allowed content categories:
// Movies, TV, Music, Games, Apps, Sports, Books, Audiobooks, Comics,
// Manga, Anime, XXX, Documentaries, Education, Tutorials, Fanres,
// iOS Apps, Graphics, Audio
+//
+// Validator checks:
+// - slug: lowercase letters and hyphens only
+// - platform: "unit3d" | "gazelle" | "ggn" | "nebulance" | "custom"
+// - apiPath must match platform default:
+// unit3d → "/api/user"
+// gazelle → "/ajax.php"
+// ggn → "/api.php"
+// - url: https only
+// - contentCategories: values must come from the allowed list above
+// - language: required
+// - rules: required (minimumRatio, seedTimeHours, loginIntervalDays as numbers)
import type { TrackerRegistryEntry } from "@/data/tracker-registry"
export const mytracker: TrackerRegistryEntry = {
- // ── Required ────────────────────────────────────────────────────────
+ // ── Identity ────────────────────────────────────────────────────────
slug: "mytracker", // lowercase, hyphens only (e.g. "my-tracker")
name: "My Tracker", // display name
- url: "https://mytracker.example", // base URL (https only)
+ abbreviation: "", // short code (e.g. "ATH", "RED") — "" if none
+ url: "https://example.com", // base URL (https only)
description: "TODO", // 1-2 sentence overview
+
+ // ── Platform & API ──────────────────────────────────────────────────
platform: "unit3d", // "unit3d" | "gazelle" | "ggn" | "nebulance" | "custom"
- apiPath: "/api/user", // must match platform default (unit3d: "/api/user", gazelle: "/ajax.php")
- specialty: "", // what the tracker is known for (e.g. "HD Movies", "Anime")
- contentCategories: [], // see allowed list above
- color: "#00d4ff", // hex accent color for the tracker's detail page
+ // Platform-specific fields (uncomment for your platform):
+ // gazelleAuthStyle: "token", // gazelle only — "token" | "raw"
+ // gazelleEnrich: true, // gazelle only — enables enrichment call
+ // unit3dAuthStyle: "bearer", // unit3d only — "bearer" | "query"
+ apiPath: "/api/user", // unit3d: "/api/user" | gazelle: "/ajax.php" | ggn: "/api.php"
- // ── Optional (fill in what you know) ────────────────────────────────
- abbreviation: undefined, // short code (e.g. "ATH", "RED")
+ // ── Content ─────────────────────────────────────────────────────────
+ specialty: "", // what the tracker is known for (e.g. "HD Movies", "Anime")
+ contentCategories: [], // see allowed list in header
language: "English",
- logo: undefined, // "/tracker-logos/mytracker_logo.svg" — file must exist in public/
- trackerHubSlug: undefined, // slug on trackerhub.xyz, if listed
- statusPageUrl: undefined, // external status page URL
+
+ // ── Visual ──────────────────────────────────────────────────────────
+ color: "#000000", // hex accent color for the tracker's detail page
+ logo: "", // "/tracker-logos/mytracker_logo.svg" — file must exist in public/ — "" if none
+
+ // ── External Links ──────────────────────────────────────────────────
+ trackerHubSlug: "", // slug on trackerhub.xyz, if listed — "" if none
+ statusPageUrl: "", // external status page URL — "" if none
+
+ // ── Community ───────────────────────────────────────────────────────
+ userClasses: [], // [{ name: "Power User", requirements: "Upload ≥ 100 GiB" }]
+ releaseGroups: [], // [{ name: "GrpName", description: "Encodes" }] or ["GrpName"]
+ bannedGroups: [], // ["GroupName"] — groups explicitly banned by the tracker
+ notableMembers: [], // ["handle"] — notable community figures
// ── Rules ───────────────────────────────────────────────────────────
rules: {
minimumRatio: 0, // 0 = no minimum
seedTimeHours: 0, // 0 = no minimum
loginIntervalDays: 0, // 0 = no login interval policy
- // fulfillmentPeriodHours: undefined, // hours to complete H&R seeding
- // hnrBanLimit: undefined, // number of H&Rs before ban
- // fullRulesMarkdown: undefined, // detailed rules as markdown string
+ // fulfillmentPeriodHours: 72, // optional — hours to complete H&R seeding
+ // hnrBanLimit: 3, // optional — number of H&Rs before ban
+ // fullRulesMarkdown: `...`, // optional — detailed rules as markdown string
},
- // ── Community data (arrays can be empty) ────────────────────────────
- userClasses: [], // [{ name: "Power User", requirements: "Upload ≥ 100 GiB" }]
- releaseGroups: [], // [{ name: "GrpName", description: "Encodes" }] or ["GrpName"]
- notableMembers: [],
- bannedGroups: [],
-
- // ── Stats (leave undefined if unknown) ──────────────────────────────
- stats: {
- userCount: undefined,
- torrentCount: undefined,
- },
+ // ── Status ──────────────────────────────────────────────────────────
+ warning: false, // true if the tracker has a known issue or is at risk
+ warningNote: "", // short description of the warning — "" if none
// ── Flags ───────────────────────────────────────────────────────────
- draft: true, // remove once all required fields are filled in
+ draft: true, // remove (or set false) once all required fields are filled in
+ supportsTransitPapers: false, // true if the tracker supports transit papers export
+ profileUrlPattern: "", // e.g. "/user.php?id={id}" — required when supportsTransitPapers: true
+
+ // ── Stats (omit this block entirely if no real data is available) ───
+ // stats: {
+ // userCount: undefined,
+ // activeUsers: undefined,
+ // torrentCount: undefined,
+ // seedSize: undefined, // e.g. "500 TiB"
+ // statsUpdatedAt: undefined, // ISO 8601 date string
+ // },
}
```
@@ -86,7 +140,9 @@ export const mytracker: TrackerRegistryEntry = {
## 2. Field Reference
-### Required Fields
+Fields are documented in the same order they appear in the template, grouped by section.
+
+### Identity
#### `slug`
@@ -110,6 +166,18 @@ name: "Blutopia"
name: "My Tracker"
```
+#### `abbreviation`
+
+Type: `string`
+
+A short code for the tracker, used in compact UI contexts. Use `""` if none.
+
+```typescript
+abbreviation: "ATH" // Aither
+abbreviation: "RED" // REDacted
+abbreviation: "" // no abbreviation
+```
+
#### `url`
Type: `string`
@@ -132,6 +200,10 @@ One or two sentences describing what the tracker is about — content focus, com
description: "The largest general music tracker (also has some software). Has an interview to join, although the wait can be notoriously long."
```
+---
+
+### Platform & API
+
#### `platform`
Type: `"unit3d" | "gazelle" | "ggn" | "nebulance" | "custom"`
@@ -146,6 +218,46 @@ Which adapter handles API requests for this tracker. This controls how the sched
| `"nebulance"` | Nebulance-specific API |
| `"custom"` | Placeholder, not implemented — do not use |
+#### `gazelleAuthStyle`
+
+Type: `"token" | "raw"` — Gazelle trackers only
+
+Controls how the API token is sent in the request.
+
+- `"token"` — sends the token in an `Authorization: token TOKEN` header (used by REDacted, Orpheus)
+- `"raw"` — sends the token directly in the `Authorization` header without a prefix
+
+Only include this field for Gazelle trackers. If you are unsure which style a Gazelle tracker uses, check `docs/kb/docs/contributing/tracker-responses-gazelle.md`.
+
+#### `gazelleEnrich`
+
+Type: `boolean` — Gazelle trackers only
+
+When `true`, the adapter makes a second API call (`action=user&id=X`) after the initial `action=index` call to fetch seeding/leeching counts, warned status, joined date, avatar, ranks, and community stats. **All Gazelle trackers must set this to `true`** — without it, seeding and leeching counts will always be 0.
+
+```typescript
+gazelleEnrich: true
+```
+
+Only include this field for Gazelle trackers.
+
+#### `unit3dAuthStyle`
+
+Type: `"bearer" | "query"` — UNIT3D trackers only
+
+Controls how the API token is sent in the request.
+
+- `"bearer"` — sends the token in an `Authorization: Bearer TOKEN` header (required by UNIT3D v8+)
+- `"query"` — sends the token as a `?api_token=TOKEN` query parameter (legacy UNIT3D)
+
+Omit this field to use the default query parameter method. Set to `"bearer"` if the tracker's UNIT3D instance has been updated to v8+ and returns 401 with query param auth.
+
+```typescript
+unit3dAuthStyle: "bearer" // Blutopia (UNIT3D v8+)
+```
+
+Only include this field for UNIT3D trackers.
+
#### `apiPath`
Type: `string`
@@ -169,16 +281,20 @@ apiPath: "/ajax.php"
Do not change this from the platform default unless you have verified that the tracker uses a non-standard path. Almost no trackers deviate from the defaults above.
+---
+
+### Content
+
#### `specialty`
Type: `string`
-A short phrase describing what the tracker specializes in. Shown in the UI and used for filtering.
+A short phrase describing what the tracker specializes in. Shown in the UI and used for filtering. Use `""` if none.
```typescript
specialty: "HD Movies"
specialty: "Music"
-specialty: "General / HD content"
+specialty: ""
```
#### `contentCategories`
@@ -196,128 +312,144 @@ iOS Apps, Graphics, Audio
```typescript
contentCategories: ["Movies", "TV"]
contentCategories: ["Music", "Apps"]
-contentCategories: ["Games"]
+contentCategories: []
```
-#### `color`
+#### `language`
Type: `string`
-A hex color code used to theme the tracker's detail page — chart colors, scrollbar, stat card accents. Pick something that represents the tracker's visual identity.
-
-```typescript
-color: "#00d4ff" // Aither cyan
-color: "#f44336" // REDacted red
-color: "#7b1fa2" // GazelleGames purple
-color: "#1a4fc2" // Nebulance blue
-```
+Primary language of the tracker. Use `"English"` for English-language trackers.
---
-### Optional Fields
+### Visual
-#### `abbreviation`
+#### `color`
-Type: `string | undefined`
+Type: `string`
-A short code for the tracker, used in compact UI contexts.
+A hex color code used to theme the tracker's detail page — chart colors, scrollbar, stat card accents. Pick something that represents the tracker's visual identity.
```typescript
-abbreviation: "ATH" // Aither
-abbreviation: "RED" // REDacted
-abbreviation: "GGn" // GazelleGames
+color: "#00d4ff" // Aither cyan
+color: "#f44336" // REDacted red
+color: "#7b1fa2" // GazelleGames purple
+color: "#1a4fc2" // Nebulance blue
```
-#### `language`
-
-Type: `string | undefined`
-
-Primary language of the tracker. Defaults to `"English"` if omitted.
-
#### `logo`
-Type: `string | undefined`
+Type: `string`
-Path to the tracker's logo file under the `public/` directory. The file must actually exist — do not set this field if you have not added the logo.
+Path to the tracker's logo file under the `public/` directory. The file must actually exist — do not set this field to a path unless you have added the logo. Use `""` if none.
```typescript
logo: "/tracker-logos/aither_logo.svg"
logo: "/tracker-logos/nebulance_logo.png"
+logo: ""
```
SVG is preferred. PNG is acceptable.
+---
+
+### External Links
+
#### `trackerHubSlug`
-Type: `string | undefined`
+Type: `string`
-The tracker's slug on [trackerhub.xyz](https://trackerhub.xyz), if the tracker is listed there. Used to link to its Trackerhub profile.
+The tracker's slug on [trackerhub.xyz](https://trackerhub.xyz), if the tracker is listed there. Used to link to its Trackerhub profile. Use `""` if not listed.
```typescript
trackerHubSlug: "aither"
-trackerHubSlug: "gazelle-games"
+trackerHubSlug: ""
```
#### `statusPageUrl`
-Type: `string | undefined`
+Type: `string`
-URL to an external status page. Many trackers have one at `trackerstatus.info`.
+URL to an external status page. Many trackers have one at `trackerstatus.info`. Use `""` if none.
```typescript
statusPageUrl: "https://status.aither.cc/status/aither"
-statusPageUrl: "https://red.trackerstatus.info/"
+statusPageUrl: ""
```
-#### `gazelleAuthStyle`
+---
-Type: `"token" | "raw" | undefined`
+### Status
-Gazelle trackers only. Controls how the API token is sent in the request.
+#### `warning`
-- `"token"` — sends the token in an `Authorization: token TOKEN` header (used by REDacted, Orpheus)
-- `"raw"` — sends the token directly in the `Authorization` header without a prefix
+Type: `boolean`
-Omit this field for non-Gazelle trackers. If you are unsure which style a Gazelle tracker uses, check `docs/kb/docs/contributing/tracker-responses-gazelle.md`.
+Set to `true` if there is something users should know before adding this tracker (for example, known API instability). Use `false` when there is no known issue.
-#### `gazelleEnrich`
+#### `warningNote`
+
+Type: `string`
-Type: `boolean | undefined`
+Short description of the warning. Use `""` if `warning` is `false`.
-Gazelle trackers only. When `true`, the adapter makes a second API call (`action=user&id=X`) after the initial `action=index` call to fetch additional fields like `warned`. Set to `true` for trackers where this extra call is needed and documented to work.
+---
-```typescript
-gazelleEnrich: true // REDacted
-```
+### Flags
#### `draft`
-Type: `boolean | undefined`
+Type: `boolean`
-When `true`, the tracker is excluded from `TRACKER_REGISTRY` and skips strict validation in CI. Use this while you are filling in fields. Remove the field (or set to `false`) once the entry is complete.
+When `true`, the tracker is excluded from `TRACKER_REGISTRY` and skips strict validation in CI. Use this while you are filling in fields. Set to `false` (or remove the field) once the entry is complete.
-#### `warning` and `warningNote`
+#### `supportsTransitPapers`
-Type: `boolean | undefined` and `string | undefined`
+Type: `boolean`
-Set `warning: true` and provide a `warningNote` if there is something users should know before adding this tracker (for example, known API instability).
+Set to `true` if the tracker supports the transit papers export feature. Use `false` otherwise.
+
+#### `profileUrlPattern`
+
+Type: `string`
+
+The URL pattern used to construct a user's profile link. Required when `supportsTransitPapers` is `true`. Use `""` if not applicable.
+
+```typescript
+profileUrlPattern: "/user.php?id={id}"
+profileUrlPattern: ""
+```
---
### Stats
+The `stats` block is **omitted entirely** when no real data is available. Do not include the block with `undefined` values — the absence of the block signals that no stats have been sourced yet.
+
+When you do have data, include only the fields you know:
+
+```typescript
+stats: {
+ userCount: 12000,
+ torrentCount: 450000,
+ seedSize: "8.2 PiB",
+ statsUpdatedAt: "2025-09-01",
+}
+```
+
+Available fields:
+
```typescript
stats: {
userCount?: number
activeUsers?: number
torrentCount?: number
- seedSize?: string
- statsUpdatedAt?: string
+ seedSize?: string // e.g. "500 TiB"
+ statsUpdatedAt?: string // ISO 8601 date string
}
```
-All fields are optional. Fill in what you know. If the tracker does not publish these numbers publicly, leave the object with all values as `undefined`.
-
---
## 3. User Classes
@@ -474,22 +606,7 @@ rules: {
}
```
-For `fullRulesMarkdown`, you can use a template literal for readability:
-
-```typescript
-fullRulesMarkdown: `## Ratio Requirements
-
-| Downloaded | Required Ratio |
-|---|---|
-| 0-5 GB | 0.00 |
-| 100+ GB | 0.60 |
-
-## Seeding Rules
-
-- Torrents must be seeded for **72 hours** after snatching.`,
-```
-
-Or join an array of strings, which works well for long rule sets:
+For `fullRulesMarkdown`, use the array-join format. This keeps diffs clean and avoids multiline template literal indentation issues:
```typescript
fullRulesMarkdown: [
@@ -499,6 +616,9 @@ fullRulesMarkdown: [
"",
"## Ratio System",
"Required ratio starts at 0.00 and rises as you download more.",
+ "",
+ "## Seeding Rules",
+ "- Torrents must be seeded for **72 hours** after snatching.",
].join("\n"),
```
@@ -599,4 +719,4 @@ The `color` field must be a full six-digit hex string starting with `#`. Shortha
### Logo path points to a missing file
-If you set `logo: "/tracker-logos/mytracker.svg"` but the file does not exist under `public/`, the logo image will silently 404 and show a broken image in the UI. Either add the file or leave `logo` as `undefined`.
+If you set `logo: "/tracker-logos/mytracker.svg"` but the file does not exist under `public/`, the logo image will silently 404 and show a broken image in the UI. Either add the file or set `logo: ""`.
diff --git a/docs/kb/docs/contributing/index.md b/docs/kb/docs/contributing/index.md
index 84ddf621..b5246eaf 100644
--- a/docs/kb/docs/contributing/index.md
+++ b/docs/kb/docs/contributing/index.md
@@ -35,10 +35,10 @@ The app will be available at `http://localhost:3000`. On first run it redirects
### Key environment variables
-| Variable | Description |
-|---|---|
-| `DATABASE_URL` | Postgres connection string |
-| `NEXTAUTH_SECRET` | Random secret for JWE session signing |
+| Variable | Description |
+| ------------------------- | ------------------------------------------------ |
+| `DATABASE_URL` | Postgres connection string |
+| `NEXTAUTH_SECRET` | Random secret for JWE session signing |
| `NEXT_PUBLIC_APP_VERSION` | Auto-populated from `package.json` at build time |
---
diff --git a/docs/kb/docs/contributing/slot-system.md b/docs/kb/docs/contributing/slot-system.md
index 63692f9c..dfb27566 100644
--- a/docs/kb/docs/contributing/slot-system.md
+++ b/docs/kb/docs/contributing/slot-system.md
@@ -22,11 +22,11 @@ Every slot belongs to one of three categories defined in `src/lib/slot-types.ts`
export type SlotCategory = "badge" | "stat-card" | "progress"
```
-| Category | What it renders | Where it appears |
-|---|---|---|
-| `stat-card` | A `StatCard` component (basic, stacked, or ring variant) | Inside the bento grid |
-| `badge` | A `SlotBadge` pill (Warned, Donor, Parked, etc.) | Collected and displayed as a badge row above the grid |
-| `progress` | An arbitrary component (achievement progress bars, share score, buffs) | Rendered as a flex column above the bento grid via `SlotRenderer` |
+| Category | What it renders | Where it appears |
+| ----------- | ---------------------------------------------------------------------- | ----------------------------------------------------------------- |
+| `stat-card` | A `StatCard` component (basic, stacked, or ring variant) | Inside the bento grid |
+| `badge` | A `SlotBadge` pill (Warned, Donor, Parked, etc.) | Collected and displayed as a badge row above the grid |
+| `progress` | An arbitrary component (achievement progress bars, share score, buffs) | Rendered as a flex column above the bento grid via `SlotRenderer` |
This document focuses on **`stat-card`** slots, as they are the most common thing to add.
@@ -36,10 +36,10 @@ This document focuses on **`stat-card`** slots, as they are the most common thin
Each stat-card slot has a `span` field that determines how many grid rows it occupies.
-| `span` | CardType in layout | Description |
-|---|---|---|
-| `1` (default) | `single` | Standard 1×1 card — one row tall, one column wide |
-| `2` | `double` (or `triple` if promoted) | Tall card — two rows tall, one column wide. Can be promoted to `triple` (three rows) by the algorithm when it produces a better layout. |
+| `span` | CardType in layout | Description |
+| ------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
+| `1` (default) | `single` | Standard 1×1 card — one row tall, one column wide |
+| `2` | `double` (or `triple` if promoted) | Tall card — two rows tall, one column wide. Can be promoted to `triple` (three rows) by the algorithm when it produces a better layout. |
The algorithm may promote a `double` to a `triple` (three rows) when doing so reduces gaps or eliminates an orphan. This is purely a layout decision — the slot itself only declares `span: 1` or `span: 2`. The `triple` type exists in `CardType` but is never assigned directly by slot authors.
@@ -54,12 +54,12 @@ The object passed to every slot's `resolve` function:
```ts
// src/lib/slot-types.ts
export interface SlotContext {
- tracker: TrackerSummary // DB row + computed fields
+ tracker: TrackerSummary // DB row + computed fields
latestSnapshot: Snapshot | null
snapshots: Snapshot[]
meta: GGnPlatformMeta | GazellePlatformMeta | NebulancePlatformMeta | null
registry: TrackerRegistryEntry | undefined
- accentColor: string // tracker's hex color, e.g. "#00d4ff"
+ accentColor: string // tracker's hex color, e.g. "#00d4ff"
}
```
@@ -89,9 +89,9 @@ interface SlotDefinition
- resolve: (ctx: SlotContext) => P | null // null = hide this slot
+ resolve: (ctx: SlotContext) => P | null // null = hide this slot
priority: number
- span?: 1 | 2 // omit for 1 (default)
+ span?: 1 | 2 // omit for 1 (default)
}
```
@@ -109,18 +109,18 @@ Single hero value. Use for any scalar metric.
```ts
interface StatCardBasicProps {
- type?: "basic" // optional — "basic" is the default
- label: string // card title, displayed uppercase
- value: string | number // large hero number
- unit?: string // displayed small next to value, e.g. "BON", "GiB"
- subtitle?: string // small text below the value
- subValue?: string // secondary line, mono font, tertiary color
+ type?: "basic" // optional — "basic" is the default
+ label: string // card title, displayed uppercase
+ value: string | number // large hero number
+ unit?: string // displayed small next to value, e.g. "BON", "GiB"
+ subtitle?: string // small text below the value
+ subValue?: string // secondary line, mono font, tertiary color
trend?: "up" | "down" | "flat"
- tooltip?: string // adds a "?" button with a popover
- icon?: ReactNode // 16×16 icon in the top-right corner
- accentColor?: string // hex color for the glow effect
+ tooltip?: string // adds a "?" button with a popover
+ icon?: ReactNode // 16×16 icon in the top-right corner
+ accentColor?: string // hex color for the glow effect
alert?: "warn" | "danger"
- alertReason?: string // shown in a "!" tooltip when alert is set
+ alertReason?: string // shown in a "!" tooltip when alert is set
}
```
@@ -130,21 +130,21 @@ Multiple label/value rows. Use when a concept has two or three related figures (
```ts
interface StatCardStackedProps {
- type: "stacked" // required
- title: string // card title
+ type: "stacked" // required
+ title: string // card title
rows: Array<{
label: string
value: string | number
- prefix?: string // prepended to value display
+ prefix?: string // prepended to value display
unit?: string
- colorClass?: string // Tailwind class, e.g. "text-success"
+ colorClass?: string // Tailwind class, e.g. "text-success"
}>
total?: {
- label: string // displayed below a divider
+ label: string // displayed below a divider
value: string
unit?: string
}
- sumIsHero?: boolean // promote the total to a large hero above the rows
+ sumIsHero?: boolean // promote the total to a large hero above the rows
tooltip?: string
icon?: ReactNode
accentColor?: string
@@ -161,9 +161,9 @@ Countdown progress ring. Used exclusively for the Login Deadline card. Renders a
```ts
interface StatCardRingProps {
- type: "ring" // required
- title?: string // defaults to "Login Deadline"
- lastAccessAt: string // ISO date string of last tracker visit
+ type: "ring" // required
+ title?: string // defaults to "Login Deadline"
+ lastAccessAt: string // ISO date string of last tracker visit
loginIntervalDays: number // from registry entry's rules.loginIntervalDays
tooltip?: string
accentColor?: string
@@ -189,16 +189,16 @@ Add a new constant in `src/components/tracker-detail/slot-registry.ts`. Place it
```ts
// Example: a basic card showing invite count for a hypothetical platform
const myTrackerInvitesSlot: SlotDefinition = {
- id: "my-tracker-invites", // must be unique across all slots
+ id: "my-tracker-invites", // must be unique across all slots
category: "stat-card",
component: StatCard as ComponentType,
- priority: 50, // lower = renders earlier (leftmost/topmost)
+ priority: 50, // lower = renders earlier (leftmost/topmost)
// span: 1, // omit for default 1-tall card
resolve(ctx) {
const { meta, accentColor } = ctx
- if (!meta || !("invites" in meta)) return null // guard: wrong platform
+ if (!meta || !("invites" in meta)) return null // guard: wrong platform
const invites = (meta as MyPlatformMeta).invites
- if (typeof invites !== "number" || invites <= 0) return null // hide when empty
+ if (typeof invites !== "number" || invites <= 0) return null // hide when empty
return {
label: "Invites",
value: invites,
@@ -217,13 +217,13 @@ const myTrackerTokensSlot: SlotDefinition = {
category: "stat-card",
component: StatCard as ComponentType,
priority: 30,
- span: 2, // 2-row tall card
+ span: 2, // 2-row tall card
resolve(ctx) {
const { meta, accentColor } = ctx
if (!meta || !("giftTokens" in meta)) return null
const m = meta as MyPlatformMeta
return {
- type: "stacked" as const, // required for stacked
+ type: "stacked" as const, // required for stacked
title: "Tokens",
rows: [
{ label: "Gift", value: m.giftTokens ?? 0 },
@@ -246,7 +246,7 @@ export const SLOT_DEFINITIONS: AnySlotDefinition[] = [
loginDeadlineSlot,
goldSlot,
// ... existing slots ...
- myTrackerInvitesSlot, // add here
+ myTrackerInvitesSlot, // add here
myTrackerTokensSlot,
// badge slots
// ...
@@ -282,11 +282,11 @@ File: `src/lib/grid-layout.ts`
The algorithm operates on three internal card sizes:
-| Type | Row span | Assigned to |
-|---|---|---|
-| `single` | 1 | Core stats + `span: 1` slot cards |
-| `double` | 2 | `span: 2` slot cards |
-| `triple` | 3 | A `double` that was promoted to fill a triple-height gap |
+| Type | Row span | Assigned to |
+| -------- | -------- | -------------------------------------------------------- |
+| `single` | 1 | Core stats + `span: 1` slot cards |
+| `double` | 2 | `span: 2` slot cards |
+| `triple` | 3 | A `double` that was promoted to fill a triple-height gap |
The first N cards in the `single` pool are marked `fixed` (N = number of columns). Fixed cards are always placed in row 1 and are never moved.
@@ -302,6 +302,7 @@ This is the primary desktop layout. It brute-forces all valid combinations of co
The winning configuration's cards each receive a `{ row, col, span }` placement. `getCardClasses` turns these into static Tailwind classes (`row-start-N col-start-N row-span-N`). The row/col start classes are pre-enumerated as lookup tables (up to 30 rows) rather than generated dynamically, because Tailwind v4 requires static class names for its JIT scanner.
**Placement order within the winner:**
+
- Row 1: core stat singles (up to 4)
- Triple-height blocks: promoted doubles fill columns left to right; remaining columns in the same row block are filled with singles stacked 3-tall
- Double-height blocks: doubles fill columns left to right; remaining columns filled with pairs of singles
@@ -317,11 +318,11 @@ Fixed 2 columns. Deterministic: promotes at most one double to a triple when the
### Breakpoint wiring (current status)
-| Breakpoint | Tailwind class | Algorithm | Status |
-|---|---|---|---|
-| Mobile (`< md`) | `grid-cols-2` | `findOptimalLayout2Col` | Wired and active |
-| Medium (`md` to `lg`) | `md:grid-cols-3` | `findOptimalLayout3Col` | Wired and active |
-| Large (`>= lg`) | `lg:grid-cols-3` or `lg:grid-cols-4` | `findOptimalLayout4Col` | Wired and active |
+| Breakpoint | Tailwind class | Algorithm | Status |
+| --------------------- | ------------------------------------ | ----------------------- | ---------------- |
+| Mobile (`< md`) | `grid-cols-2` | `findOptimalLayout2Col` | Wired and active |
+| Medium (`md` to `lg`) | `md:grid-cols-3` | `findOptimalLayout3Col` | Wired and active |
+| Large (`>= lg`) | `lg:grid-cols-3` or `lg:grid-cols-4` | `findOptimalLayout4Col` | Wired and active |
The large grid uses `lg:grid-cols-4` when the algorithm selects 4 columns, or `lg:grid-cols-3` when it finds 3 columns produces fewer gaps.
@@ -360,7 +361,7 @@ Badge slots follow the same `SlotDefinition` shape but use `SlotBadge` as the co
```ts
const myBadgeSlot: SlotDefinition = {
id: "my-badge",
- category: "badge", // not "stat-card"
+ category: "badge", // not "stat-card"
component: SlotBadge,
priority: 40,
resolve(ctx) {
@@ -376,12 +377,12 @@ const myBadgeSlot: SlotDefinition = {
## Key files reference
-| File | Purpose |
-|---|---|
-| `src/lib/slot-types.ts` | `SlotCategory`, `SlotContext`, `ResolvedSlot` types |
-| `src/lib/grid-layout.ts` | `findOptimalLayout4Col`, `findOptimalLayout3Col`, `findOptimalLayout2Col`, `getCardClasses` |
-| `src/components/tracker-detail/slot-registry.ts` | All slot definitions + `SLOT_DEFINITIONS` array + `renderSlotElement` |
-| `src/components/ui/StatCard.tsx` | `StatCard` component (basic / stacked / ring) |
-| `src/components/tracker-detail/AnalyticsTab.tsx` | Grid renderer — calls layout algorithms, maps card IDs to elements |
-| `src/components/tracker-detail/CoreStatCards.tsx` | `buildCoreStatDescriptors` — the 8 fixed core stats |
-| `src/components/tracker-detail/SlotRenderer.tsx` | Renders `progress` category slots above the grid |
+| File | Purpose |
+| ------------------------------------------------- | ------------------------------------------------------------------------------------------- |
+| `src/lib/slot-types.ts` | `SlotCategory`, `SlotContext`, `ResolvedSlot` types |
+| `src/lib/grid-layout.ts` | `findOptimalLayout4Col`, `findOptimalLayout3Col`, `findOptimalLayout2Col`, `getCardClasses` |
+| `src/components/tracker-detail/slot-registry.ts` | All slot definitions + `SLOT_DEFINITIONS` array + `renderSlotElement` |
+| `src/components/ui/StatCard.tsx` | `StatCard` component (basic / stacked / ring) |
+| `src/components/tracker-detail/AnalyticsTab.tsx` | Grid renderer — calls layout algorithms, maps card IDs to elements |
+| `src/components/tracker-detail/CoreStatCards.tsx` | `buildCoreStatDescriptors` — the 8 fixed core stats |
+| `src/components/tracker-detail/SlotRenderer.tsx` | Renders `progress` category slots above the grid |
diff --git a/docs/kb/docs/contributing/tracker-responses-gazelle.md b/docs/kb/docs/contributing/tracker-responses-gazelle.md
index f6622124..0ffe9382 100644
--- a/docs/kb/docs/contributing/tracker-responses-gazelle.md
+++ b/docs/kb/docs/contributing/tracker-responses-gazelle.md
@@ -54,22 +54,22 @@ Byte values are **raw integers** (bytes), not formatted strings. The `id` field
## Field Mapping (action=index)
-| TrackerStats field | Gazelle path | Type | Notes |
-|---|---|---|---|
-| `username` | `response.username` | `string` | Direct copy |
-| `group` | `response.userstats.class` | `string` | Falls back to `"Unknown"` |
-| `uploadedBytes` | `response.userstats.uploaded` | `number` | `BigInt(Math.floor(...))` |
-| `downloadedBytes` | `response.userstats.downloaded` | `number` | `BigInt(Math.floor(...))` |
-| `ratio` | `response.userstats.ratio` | `number` | Defaults to `0` if not a number |
-| `bufferBytes` | — | — | Calculated: `uploadedBytes - downloadedBytes` (min `0`) |
-| `seedingCount` | `response.userstats.seedingcount` | `number?` | Defaults to `0` — many forks omit this |
-| `leechingCount` | `response.userstats.leechingcount` | `number?` | Defaults to `0` — many forks omit this |
-| `seedbonus` | `response.userstats.bonusPoints` or `.bonuspoints` | `number?` | Checks both casing variants |
-| `hitAndRuns` | — | — | Always `null` — not in Gazelle index response |
-| `requiredRatio` | `response.userstats.requiredratio` | `number?` | `null` if absent |
-| `warned` | — | — | Defaults to `false` from index; overridden if enrichment runs |
-| `freeleechTokens` | `response.userstats.freeleechTokens` or `response.giftTokens` | `number?` | Checks `userstats` first, falls back to top-level `giftTokens` |
-| `remoteUserId` | `response.id` | `number` | Cached to skip re-parsing on future polls |
+| TrackerStats field | Gazelle path | Type | Notes |
+| ------------------ | ------------------------------------------------------------- | --------- | -------------------------------------------------------------- |
+| `username` | `response.username` | `string` | Direct copy |
+| `group` | `response.userstats.class` | `string` | Falls back to `"Unknown"` |
+| `uploadedBytes` | `response.userstats.uploaded` | `number` | `BigInt(Math.floor(...))` |
+| `downloadedBytes` | `response.userstats.downloaded` | `number` | `BigInt(Math.floor(...))` |
+| `ratio` | `response.userstats.ratio` | `number` | Defaults to `0` if not a number |
+| `bufferBytes` | — | — | Calculated: `uploadedBytes - downloadedBytes` (min `0`) |
+| `seedingCount` | `response.userstats.seedingcount` | `number?` | Defaults to `0` — many forks omit this |
+| `leechingCount` | `response.userstats.leechingcount` | `number?` | Defaults to `0` — many forks omit this |
+| `seedbonus` | `response.userstats.bonusPoints` or `.bonuspoints` | `number?` | Checks both casing variants |
+| `hitAndRuns` | — | — | Always `null` — not in Gazelle index response |
+| `requiredRatio` | `response.userstats.requiredratio` | `number?` | `null` if absent |
+| `warned` | — | — | Defaults to `false` from index; overridden if enrichment runs |
+| `freeleechTokens` | `response.userstats.freeleechTokens` or `response.giftTokens` | `number?` | Checks `userstats` first, falls back to top-level `giftTokens` |
+| `remoteUserId` | `response.id` | `number` | Cached to skip re-parsing on future polls |
---
@@ -144,16 +144,16 @@ This call fetches the full user profile including warned status, join date, seed
### What the enrichment step overrides
-| TrackerStats field | Source in action=user response | Notes |
-|---|---|---|
-| `warned` | `personal.warned` | Overrides the `false` default from index |
-| `joinedDate` | `stats.joinedDate` | Not available from index |
-| `lastAccessDate` | `stats.lastAccess` | Not available from index |
-| `bufferBytes` | `stats.buffer` | Richer than the calculated value |
-| `seedingCount` | `community.seeding` | More reliable than index for many forks |
-| `leechingCount` | `community.leeching` | More reliable than index for many forks |
-| `avatarUrl` | `avatar` | Not available from index |
-| `platformMeta` | `personal`, `ranks`, `community` | Full `GazellePlatformMeta` object |
+| TrackerStats field | Source in action=user response | Notes |
+| ------------------ | -------------------------------- | ---------------------------------------- |
+| `warned` | `personal.warned` | Overrides the `false` default from index |
+| `joinedDate` | `stats.joinedDate` | Not available from index |
+| `lastAccessDate` | `stats.lastAccess` | Not available from index |
+| `bufferBytes` | `stats.buffer` | Richer than the calculated value |
+| `seedingCount` | `community.seeding` | More reliable than index for many forks |
+| `leechingCount` | `community.leeching` | More reliable than index for many forks |
+| `avatarUrl` | `avatar` | Not available from index |
+| `platformMeta` | `personal`, `ranks`, `community` | Full `GazellePlatformMeta` object |
If the enrichment call fails for any reason, the adapter continues with core stats from the index response — the failure is non-fatal.
@@ -161,13 +161,13 @@ If the enrichment call fails for any reason, the adapter continues with core sta
## Gazelle Fork Variations
-| Site | bonusPoints field | freeleechTokens | seedingcount in index |
-|---|---|---|---|
-| Redacted (RED) | `bonusPoints` | Sometimes | No |
-| Orpheus (OPS) | `bonusPoints` | Sometimes | No |
-| BroadcasTheNet (BTN) | Varies | No | No |
-| PassThePopcorn (PTP) | Varies | No | No |
-| AnimeBytes (AB) | Varies | Varies | No |
+| Site | bonusPoints field | freeleechTokens | seedingcount in index |
+| -------------------- | ----------------- | --------------- | --------------------- |
+| Redacted (RED) | `bonusPoints` | Sometimes | No |
+| Orpheus (OPS) | `bonusPoints` | Sometimes | No |
+| BroadcasTheNet (BTN) | Varies | No | No |
+| PassThePopcorn (PTP) | Varies | No | No |
+| AnimeBytes (AB) | Varies | Varies | No |
GazelleGames (GGn) is handled by its own separate adapter — see the [GGn page](tracker-responses-ggn.md).
diff --git a/docs/kb/docs/contributing/tracker-responses-ggn.md b/docs/kb/docs/contributing/tracker-responses-ggn.md
index b2743fd4..62c3f193 100644
--- a/docs/kb/docs/contributing/tracker-responses-ggn.md
+++ b/docs/kb/docs/contributing/tracker-responses-ggn.md
@@ -128,26 +128,26 @@ The only purpose of this call is to resolve the user's numeric ID. Once the adap
## Field Mapping
-| TrackerStats field | GGn path | Type | Notes |
-|---|---|---|---|
-| `username` | `response.username` | `string` | From user response; quick_user value used as fallback |
-| `group` | `response.personal.class` | `string` | Falls back to `"Unknown"` |
-| `remoteUserId` | `response.id` | `number` | Stored after first poll to skip quick_user |
-| `uploadedBytes` | `response.stats.uploaded` | `number` | `BigInt(Math.floor(...))` |
-| `downloadedBytes` | `response.stats.downloaded` | `number` | `BigInt(Math.floor(...))` |
-| `ratio` | `response.stats.ratio` | `string \| number` | Parsed via `parseFloat()` if string |
-| `bufferBytes` | — | — | Calculated: `uploadedBytes - downloadedBytes` (min `0`) |
-| `seedingCount` | `response.community.seeding` | `number \| null` | Defaults to `0`; null when paranoia hides it |
-| `leechingCount` | `response.community.leeching` | `number \| null` | Defaults to `0`; null when paranoia hides it |
-| `seedbonus` | `response.stats.gold` | `number` | GGn uses "gold" as its currency, not "bonus points" |
-| `hitAndRuns` | `response.personal.hnrs` | `number \| null` | `null` when not tracked or hidden |
-| `requiredRatio` | `response.stats.requiredRatio` | `number` | Available directly in the user call |
-| `warned` | `response.personal.warned` | `boolean` | Available directly — no second call needed |
-| `freeleechTokens` | — | — | Always `null` — GGn does not expose FL tokens |
-| `joinedDate` | `response.stats.joinedDate` | `string \| null` | ISO-style datetime string |
-| `lastAccessDate` | `response.stats.lastAccess` | `string \| null` | ISO-style datetime string |
-| `shareScore` | `response.stats.shareScore` | `number \| null` | GGn-specific metric |
-| `platformMeta` | `response.personal`, `response.community`, `response.buffs`, `response.achievements` | — | `GGnPlatformMeta` object |
+| TrackerStats field | GGn path | Type | Notes |
+| ------------------ | ------------------------------------------------------------------------------------ | ------------------ | ------------------------------------------------------- |
+| `username` | `response.username` | `string` | From user response; quick_user value used as fallback |
+| `group` | `response.personal.class` | `string` | Falls back to `"Unknown"` |
+| `remoteUserId` | `response.id` | `number` | Stored after first poll to skip quick_user |
+| `uploadedBytes` | `response.stats.uploaded` | `number` | `BigInt(Math.floor(...))` |
+| `downloadedBytes` | `response.stats.downloaded` | `number` | `BigInt(Math.floor(...))` |
+| `ratio` | `response.stats.ratio` | `string \| number` | Parsed via `parseFloat()` if string |
+| `bufferBytes` | — | — | Calculated: `uploadedBytes - downloadedBytes` (min `0`) |
+| `seedingCount` | `response.community.seeding` | `number \| null` | Defaults to `0`; null when paranoia hides it |
+| `leechingCount` | `response.community.leeching` | `number \| null` | Defaults to `0`; null when paranoia hides it |
+| `seedbonus` | `response.stats.gold` | `number` | GGn uses "gold" as its currency, not "bonus points" |
+| `hitAndRuns` | `response.personal.hnrs` | `number \| null` | `null` when not tracked or hidden |
+| `requiredRatio` | `response.stats.requiredRatio` | `number` | Available directly in the user call |
+| `warned` | `response.personal.warned` | `boolean` | Available directly — no second call needed |
+| `freeleechTokens` | — | — | Always `null` — GGn does not expose FL tokens |
+| `joinedDate` | `response.stats.joinedDate` | `string \| null` | ISO-style datetime string |
+| `lastAccessDate` | `response.stats.lastAccess` | `string \| null` | ISO-style datetime string |
+| `shareScore` | `response.stats.shareScore` | `number \| null` | GGn-specific metric |
+| `platformMeta` | `response.personal`, `response.community`, `response.buffs`, `response.achievements` | — | `GGnPlatformMeta` object |
## Quirks
@@ -155,9 +155,7 @@ The only purpose of this call is to resolve the user's numeric ID. Once the adap
```typescript
const ratio =
- typeof resp.stats.ratio === "number"
- ? resp.stats.ratio
- : parseFloat(resp.stats.ratio) || 0
+ typeof resp.stats.ratio === "number" ? resp.stats.ratio : parseFloat(resp.stats.ratio) || 0
```
**Seedbonus is called `gold`.** GGn has a currency called gold, not bonus points. The adapter maps `stats.gold` → `seedbonus` in the `TrackerStats` output so the dashboard can display it consistently.
diff --git a/docs/kb/docs/contributing/tracker-responses-unit3d.md b/docs/kb/docs/contributing/tracker-responses-unit3d.md
index a7b87190..f261cfc0 100644
--- a/docs/kb/docs/contributing/tracker-responses-unit3d.md
+++ b/docs/kb/docs/contributing/tracker-responses-unit3d.md
@@ -33,21 +33,21 @@ All byte values are **formatted strings** (`"500.25 GiB"`), not integers. The `r
## Field Mapping
-| TrackerStats field | UNIT3D field | Type | Notes |
-|---|---|---|---|
-| `username` | `username` | `string` | Direct copy |
-| `group` | `group` | `string` | User class / rank label |
-| `uploadedBytes` | `uploaded` | `string` | Parsed via `parseBytes()` → `bigint` |
-| `downloadedBytes` | `downloaded` | `string` | Parsed via `parseBytes()` → `bigint` |
-| `ratio` | `ratio` | `string` | `parseFloat()`, defaults to `0` |
-| `bufferBytes` | `buffer` | `string` | Parsed via `parseBytes()` → `bigint` |
-| `seedingCount` | `seeding` | `number` | Direct copy |
-| `leechingCount` | `leeching` | `number` | Direct copy |
-| `seedbonus` | `seedbonus` | `string` | `parseFloat()`, defaults to `0` |
-| `hitAndRuns` | `hit_and_runs` | `number` | Direct copy |
-| `requiredRatio` | — | — | Always `null` — not in UNIT3D API |
-| `warned` | — | — | Always `null` — not in UNIT3D API |
-| `freeleechTokens` | — | — | Always `null` — not in UNIT3D API |
+| TrackerStats field | UNIT3D field | Type | Notes |
+| ------------------ | -------------- | -------- | ------------------------------------ |
+| `username` | `username` | `string` | Direct copy |
+| `group` | `group` | `string` | User class / rank label |
+| `uploadedBytes` | `uploaded` | `string` | Parsed via `parseBytes()` → `bigint` |
+| `downloadedBytes` | `downloaded` | `string` | Parsed via `parseBytes()` → `bigint` |
+| `ratio` | `ratio` | `string` | `parseFloat()`, defaults to `0` |
+| `bufferBytes` | `buffer` | `string` | Parsed via `parseBytes()` → `bigint` |
+| `seedingCount` | `seeding` | `number` | Direct copy |
+| `leechingCount` | `leeching` | `number` | Direct copy |
+| `seedbonus` | `seedbonus` | `string` | `parseFloat()`, defaults to `0` |
+| `hitAndRuns` | `hit_and_runs` | `number` | Direct copy |
+| `requiredRatio` | — | — | Always `null` — not in UNIT3D API |
+| `warned` | — | — | Always `null` — not in UNIT3D API |
+| `freeleechTokens` | — | — | Always `null` — not in UNIT3D API |
UNIT3D makes a single API call per poll. No enrichment step.
diff --git a/docs/kb/docs/contributing/trackers/aither.md b/docs/kb/docs/contributing/trackers/aither.md
index 4cd0428d..5400de6d 100644
--- a/docs/kb/docs/contributing/trackers/aither.md
+++ b/docs/kb/docs/contributing/trackers/aither.md
@@ -1,13 +1,13 @@
# Aither (ATH)
-| Field | Value |
-|---|---|
-| Platform | UNIT3D |
-| Base URL | `https://aither.cc` |
-| API Endpoint | `https://aither.cc/api/user` |
-| Auth Method | Query parameter: `?api_token=TOKEN` |
-| Enrichment | N/A |
-| Auth Style | N/A |
+| Field | Value |
+| ------------ | ----------------------------------- |
+| Platform | UNIT3D |
+| Base URL | `https://aither.cc` |
+| API Endpoint | `https://aither.cc/api/user` |
+| Auth Method | Query parameter: `?api_token=TOKEN` |
+| Enrichment | N/A |
+| Auth Style | N/A |
## Notes
diff --git a/docs/kb/docs/contributing/trackers/alpharatio.md b/docs/kb/docs/contributing/trackers/alpharatio.md
index b87c3fab..7caa54df 100644
--- a/docs/kb/docs/contributing/trackers/alpharatio.md
+++ b/docs/kb/docs/contributing/trackers/alpharatio.md
@@ -1,13 +1,13 @@
# AlphaRatio (AR)
-| Field | Value |
-|---|---|
-| Platform | Gazelle |
-| Base URL | `https://alpharatio.cc` |
-| API Endpoint | `https://alpharatio.cc/ajax.php` |
-| Auth Method | HTTP header: `Authorization: token TOKEN` |
-| Enrichment | No |
-| Auth Style | standard |
+| Field | Value |
+| ------------ | ----------------------------------------- |
+| Platform | Gazelle |
+| Base URL | `https://alpharatio.cc` |
+| API Endpoint | `https://alpharatio.cc/ajax.php` |
+| Auth Method | HTTP header: `Authorization: token TOKEN` |
+| Enrichment | No |
+| Auth Style | standard |
## Notes
diff --git a/docs/kb/docs/contributing/trackers/animebytes.md b/docs/kb/docs/contributing/trackers/animebytes.md
index 66893b30..487a86c7 100644
--- a/docs/kb/docs/contributing/trackers/animebytes.md
+++ b/docs/kb/docs/contributing/trackers/animebytes.md
@@ -1,13 +1,13 @@
# AnimeBytes (AB)
-| Field | Value |
-|---|---|
-| Platform | Gazelle |
-| Base URL | `https://animebytes.tv` |
-| API Endpoint | `https://animebytes.tv/ajax.php` |
-| Auth Method | HTTP header: `Authorization: token TOKEN` |
-| Enrichment | No |
-| Auth Style | standard |
+| Field | Value |
+| ------------ | ----------------------------------------- |
+| Platform | Gazelle |
+| Base URL | `https://animebytes.tv` |
+| API Endpoint | `https://animebytes.tv/ajax.php` |
+| Auth Method | HTTP header: `Authorization: token TOKEN` |
+| Enrichment | No |
+| Auth Style | standard |
## Notes
diff --git a/docs/kb/docs/contributing/trackers/anthelion.md b/docs/kb/docs/contributing/trackers/anthelion.md
index e53ceb0f..c2753705 100644
--- a/docs/kb/docs/contributing/trackers/anthelion.md
+++ b/docs/kb/docs/contributing/trackers/anthelion.md
@@ -1,13 +1,13 @@
# Anthelion (ANT)
-| Field | Value |
-|---|---|
-| Platform | Nebulance |
-| Base URL | `https://anthelion.me` |
-| API Endpoint | `https://anthelion.me/api.php` |
-| Auth Method | Query parameter: `?api_token=TOKEN` |
-| Enrichment | N/A |
-| Auth Style | N/A |
+| Field | Value |
+| ------------ | ----------------------------------- |
+| Platform | Nebulance |
+| Base URL | `https://anthelion.me` |
+| API Endpoint | `https://anthelion.me/api.php` |
+| Auth Method | Query parameter: `?api_token=TOKEN` |
+| Enrichment | N/A |
+| Auth Style | N/A |
## Notes
diff --git a/docs/kb/docs/contributing/trackers/blutopia.md b/docs/kb/docs/contributing/trackers/blutopia.md
index 0e5ec8dc..2224239f 100644
--- a/docs/kb/docs/contributing/trackers/blutopia.md
+++ b/docs/kb/docs/contributing/trackers/blutopia.md
@@ -1,13 +1,13 @@
# Blutopia (BLU)
-| Field | Value |
-|---|---|
-| Platform | UNIT3D |
-| Base URL | `https://blutopia.cc` |
-| API Endpoint | `https://blutopia.cc/api/user` |
-| Auth Method | Query parameter: `?api_token=TOKEN` |
-| Enrichment | N/A |
-| Auth Style | N/A |
+| Field | Value |
+| ------------ | ----------------------------------- |
+| Platform | UNIT3D |
+| Base URL | `https://blutopia.cc` |
+| API Endpoint | `https://blutopia.cc/api/user` |
+| Auth Method | Query parameter: `?api_token=TOKEN` |
+| Enrichment | N/A |
+| Auth Style | N/A |
## Notes
diff --git a/docs/kb/docs/contributing/trackers/broadcasthenet.md b/docs/kb/docs/contributing/trackers/broadcasthenet.md
index 5da31976..328cade8 100644
--- a/docs/kb/docs/contributing/trackers/broadcasthenet.md
+++ b/docs/kb/docs/contributing/trackers/broadcasthenet.md
@@ -1,13 +1,13 @@
# BroadcasTheNet (BTN)
-| Field | Value |
-|---|---|
-| Platform | Gazelle |
-| Base URL | `https://broadcasthe.net` |
-| API Endpoint | `https://broadcasthe.net/ajax.php` |
-| Auth Method | HTTP header: `Authorization: token TOKEN` |
-| Enrichment | No |
-| Auth Style | standard |
+| Field | Value |
+| ------------ | ----------------------------------------- |
+| Platform | Gazelle |
+| Base URL | `https://broadcasthe.net` |
+| API Endpoint | `https://broadcasthe.net/ajax.php` |
+| Auth Method | HTTP header: `Authorization: token TOKEN` |
+| Enrichment | No |
+| Auth Style | standard |
## Notes
diff --git a/docs/kb/docs/contributing/trackers/concertos.md b/docs/kb/docs/contributing/trackers/concertos.md
index 1ac63507..e6d18f57 100644
--- a/docs/kb/docs/contributing/trackers/concertos.md
+++ b/docs/kb/docs/contributing/trackers/concertos.md
@@ -1,13 +1,13 @@
# Concertos (CON)
-| Field | Value |
-|---|---|
-| Platform | UNIT3D |
-| Base URL | `https://concertos.live` |
-| API Endpoint | `https://concertos.live/api/user` |
-| Auth Method | Query parameter: `?api_token=TOKEN` |
-| Enrichment | N/A |
-| Auth Style | N/A |
+| Field | Value |
+| ------------ | ----------------------------------- |
+| Platform | UNIT3D |
+| Base URL | `https://concertos.live` |
+| API Endpoint | `https://concertos.live/api/user` |
+| Auth Method | Query parameter: `?api_token=TOKEN` |
+| Enrichment | N/A |
+| Auth Style | N/A |
## Notes
diff --git a/docs/kb/docs/contributing/trackers/empornium.md b/docs/kb/docs/contributing/trackers/empornium.md
index ba174aa2..7420ba7b 100644
--- a/docs/kb/docs/contributing/trackers/empornium.md
+++ b/docs/kb/docs/contributing/trackers/empornium.md
@@ -1,13 +1,13 @@
# Empornium (EMP)
-| Field | Value |
-|---|---|
-| Platform | Gazelle |
-| Base URL | `https://empornium.is` |
-| API Endpoint | `https://empornium.is/ajax.php` |
-| Auth Method | HTTP header: `Authorization: token TOKEN` |
-| Enrichment | No |
-| Auth Style | standard |
+| Field | Value |
+| ------------ | ----------------------------------------- |
+| Platform | Gazelle |
+| Base URL | `https://empornium.is` |
+| API Endpoint | `https://empornium.is/ajax.php` |
+| Auth Method | HTTP header: `Authorization: token TOKEN` |
+| Enrichment | No |
+| Auth Style | standard |
## Notes
diff --git a/docs/kb/docs/contributing/trackers/fearnopeer.md b/docs/kb/docs/contributing/trackers/fearnopeer.md
index 4b4a754f..c88948c2 100644
--- a/docs/kb/docs/contributing/trackers/fearnopeer.md
+++ b/docs/kb/docs/contributing/trackers/fearnopeer.md
@@ -1,13 +1,13 @@
# FearNoPeer (FNP)
-| Field | Value |
-|---|---|
-| Platform | UNIT3D |
-| Base URL | `https://fearnopeer.com` |
-| API Endpoint | `https://fearnopeer.com/api/user` |
-| Auth Method | Query parameter: `?api_token=TOKEN` |
-| Enrichment | N/A |
-| Auth Style | N/A |
+| Field | Value |
+| ------------ | ----------------------------------- |
+| Platform | UNIT3D |
+| Base URL | `https://fearnopeer.com` |
+| API Endpoint | `https://fearnopeer.com/api/user` |
+| Auth Method | Query parameter: `?api_token=TOKEN` |
+| Enrichment | N/A |
+| Auth Style | N/A |
## Notes
diff --git a/docs/kb/docs/contributing/trackers/gazellegames.md b/docs/kb/docs/contributing/trackers/gazellegames.md
index 5b0afd1d..ed18c852 100644
--- a/docs/kb/docs/contributing/trackers/gazellegames.md
+++ b/docs/kb/docs/contributing/trackers/gazellegames.md
@@ -1,13 +1,13 @@
# GazelleGames (GGn)
-| Field | Value |
-|---|---|
-| Platform | GGn |
-| Base URL | `https://gazellegames.net` |
-| API Endpoint | `https://gazellegames.net/api.php` |
-| Auth Method | Query parameter: `?key=TOKEN` |
-| Enrichment | N/A (two-step fetch built into the GGn adapter) |
-| Auth Style | N/A |
+| Field | Value |
+| ------------ | ----------------------------------------------- |
+| Platform | GGn |
+| Base URL | `https://gazellegames.net` |
+| API Endpoint | `https://gazellegames.net/api.php` |
+| Auth Method | Query parameter: `?key=TOKEN` |
+| Enrichment | N/A (two-step fetch built into the GGn adapter) |
+| Auth Style | N/A |
## Notes
diff --git a/docs/kb/docs/contributing/trackers/greatposterwall.md b/docs/kb/docs/contributing/trackers/greatposterwall.md
index 1ca4689a..ed5f676a 100644
--- a/docs/kb/docs/contributing/trackers/greatposterwall.md
+++ b/docs/kb/docs/contributing/trackers/greatposterwall.md
@@ -1,13 +1,13 @@
# Great Poster Wall (GPW)
-| Field | Value |
-|---|---|
-| Platform | Gazelle |
-| Base URL | `https://greatposterwall.com` |
-| API Endpoint | `https://greatposterwall.com/ajax.php` |
-| Auth Method | HTTP header: `Authorization: token TOKEN` |
-| Enrichment | No |
-| Auth Style | standard |
+| Field | Value |
+| ------------ | ----------------------------------------- |
+| Platform | Gazelle |
+| Base URL | `https://greatposterwall.com` |
+| API Endpoint | `https://greatposterwall.com/ajax.php` |
+| Auth Method | HTTP header: `Authorization: token TOKEN` |
+| Enrichment | No |
+| Auth Style | standard |
## Notes
diff --git a/docs/kb/docs/contributing/trackers/lst.md b/docs/kb/docs/contributing/trackers/lst.md
index 011aacff..9f7c9331 100644
--- a/docs/kb/docs/contributing/trackers/lst.md
+++ b/docs/kb/docs/contributing/trackers/lst.md
@@ -1,13 +1,13 @@
# LST (LST)
-| Field | Value |
-|---|---|
-| Platform | UNIT3D |
-| Base URL | `https://lst.gg` |
-| API Endpoint | `https://lst.gg/api/user` |
-| Auth Method | Query parameter: `?api_token=TOKEN` |
-| Enrichment | N/A |
-| Auth Style | N/A |
+| Field | Value |
+| ------------ | ----------------------------------- |
+| Platform | UNIT3D |
+| Base URL | `https://lst.gg` |
+| API Endpoint | `https://lst.gg/api/user` |
+| Auth Method | Query parameter: `?api_token=TOKEN` |
+| Enrichment | N/A |
+| Auth Style | N/A |
## Notes
diff --git a/docs/kb/docs/contributing/trackers/morethantv.md b/docs/kb/docs/contributing/trackers/morethantv.md
index 06cfb4e0..deb6a5a4 100644
--- a/docs/kb/docs/contributing/trackers/morethantv.md
+++ b/docs/kb/docs/contributing/trackers/morethantv.md
@@ -1,13 +1,13 @@
# MoreThanTV (MTV)
-| Field | Value |
-|---|---|
-| Platform | Gazelle |
-| Base URL | `https://morethantv.me` |
-| API Endpoint | `https://morethantv.me/ajax.php` |
-| Auth Method | HTTP header: `Authorization: token TOKEN` |
-| Enrichment | No |
-| Auth Style | standard |
+| Field | Value |
+| ------------ | ----------------------------------------- |
+| Platform | Gazelle |
+| Base URL | `https://morethantv.me` |
+| API Endpoint | `https://morethantv.me/ajax.php` |
+| Auth Method | HTTP header: `Authorization: token TOKEN` |
+| Enrichment | No |
+| Auth Style | standard |
## Notes
diff --git a/docs/kb/docs/contributing/trackers/nebulance.md b/docs/kb/docs/contributing/trackers/nebulance.md
index a7d95fc9..a4b69b86 100644
--- a/docs/kb/docs/contributing/trackers/nebulance.md
+++ b/docs/kb/docs/contributing/trackers/nebulance.md
@@ -1,13 +1,13 @@
# Nebulance (NBL)
-| Field | Value |
-|---|---|
-| Platform | Nebulance |
-| Base URL | `https://nebulance.io` |
-| API Endpoint | `https://nebulance.io/api.php` |
-| Auth Method | Query parameter: `?api_token=TOKEN` |
-| Enrichment | N/A |
-| Auth Style | N/A |
+| Field | Value |
+| ------------ | ----------------------------------- |
+| Platform | Nebulance |
+| Base URL | `https://nebulance.io` |
+| API Endpoint | `https://nebulance.io/api.php` |
+| Auth Method | Query parameter: `?api_token=TOKEN` |
+| Enrichment | N/A |
+| Auth Style | N/A |
## Notes
diff --git a/docs/kb/docs/contributing/trackers/oldtoons.md b/docs/kb/docs/contributing/trackers/oldtoons.md
index 7023eded..0cd3dc58 100644
--- a/docs/kb/docs/contributing/trackers/oldtoons.md
+++ b/docs/kb/docs/contributing/trackers/oldtoons.md
@@ -1,13 +1,13 @@
# OldToons (OT)
-| Field | Value |
-|---|---|
-| Platform | UNIT3D |
-| Base URL | `https://oldtoons.world` |
-| API Endpoint | `https://oldtoons.world/api/user` |
-| Auth Method | Query parameter: `?api_token=TOKEN` |
-| Enrichment | N/A |
-| Auth Style | N/A |
+| Field | Value |
+| ------------ | ----------------------------------- |
+| Platform | UNIT3D |
+| Base URL | `https://oldtoons.world` |
+| API Endpoint | `https://oldtoons.world/api/user` |
+| Auth Method | Query parameter: `?api_token=TOKEN` |
+| Enrichment | N/A |
+| Auth Style | N/A |
## Notes
diff --git a/docs/kb/docs/contributing/trackers/onlyencodes.md b/docs/kb/docs/contributing/trackers/onlyencodes.md
index a3505150..83b5a945 100644
--- a/docs/kb/docs/contributing/trackers/onlyencodes.md
+++ b/docs/kb/docs/contributing/trackers/onlyencodes.md
@@ -1,13 +1,13 @@
# OnlyEncodes (OE)
-| Field | Value |
-|---|---|
-| Platform | UNIT3D |
-| Base URL | `https://onlyencodes.cc` |
-| API Endpoint | `https://onlyencodes.cc/api/user` |
-| Auth Method | Query parameter: `?api_token=TOKEN` |
-| Enrichment | N/A |
-| Auth Style | N/A |
+| Field | Value |
+| ------------ | ----------------------------------- |
+| Platform | UNIT3D |
+| Base URL | `https://onlyencodes.cc` |
+| API Endpoint | `https://onlyencodes.cc/api/user` |
+| Auth Method | Query parameter: `?api_token=TOKEN` |
+| Enrichment | N/A |
+| Auth Style | N/A |
## Notes
diff --git a/docs/kb/docs/contributing/trackers/orpheus.md b/docs/kb/docs/contributing/trackers/orpheus.md
index ced3b2ac..8f7265f5 100644
--- a/docs/kb/docs/contributing/trackers/orpheus.md
+++ b/docs/kb/docs/contributing/trackers/orpheus.md
@@ -1,13 +1,13 @@
# Orpheus (OPS)
-| Field | Value |
-|---|---|
-| Platform | Gazelle |
-| Base URL | `https://orpheus.network` |
-| API Endpoint | `https://orpheus.network/ajax.php` |
-| Auth Method | HTTP header: `Authorization: token TOKEN` |
-| Enrichment | No |
-| Auth Style | standard |
+| Field | Value |
+| ------------ | ----------------------------------------- |
+| Platform | Gazelle |
+| Base URL | `https://orpheus.network` |
+| API Endpoint | `https://orpheus.network/ajax.php` |
+| Auth Method | HTTP header: `Authorization: token TOKEN` |
+| Enrichment | No |
+| Auth Style | standard |
## Notes
diff --git a/docs/kb/docs/contributing/trackers/passthepopcorn.md b/docs/kb/docs/contributing/trackers/passthepopcorn.md
index 6dfc90c6..00ec2010 100644
--- a/docs/kb/docs/contributing/trackers/passthepopcorn.md
+++ b/docs/kb/docs/contributing/trackers/passthepopcorn.md
@@ -1,13 +1,13 @@
# PassThePopcorn (PTP)
-| Field | Value |
-|---|---|
-| Platform | Gazelle |
-| Base URL | `https://passthepopcorn.me` |
-| API Endpoint | `https://passthepopcorn.me/ajax.php` |
-| Auth Method | HTTP header: `Authorization: token TOKEN` |
-| Enrichment | No |
-| Auth Style | standard |
+| Field | Value |
+| ------------ | ----------------------------------------- |
+| Platform | Gazelle |
+| Base URL | `https://passthepopcorn.me` |
+| API Endpoint | `https://passthepopcorn.me/ajax.php` |
+| Auth Method | HTTP header: `Authorization: token TOKEN` |
+| Enrichment | No |
+| Auth Style | standard |
## Notes
diff --git a/docs/kb/docs/contributing/trackers/phoenixproject.md b/docs/kb/docs/contributing/trackers/phoenixproject.md
index ee9029f5..a3a9ec6a 100644
--- a/docs/kb/docs/contributing/trackers/phoenixproject.md
+++ b/docs/kb/docs/contributing/trackers/phoenixproject.md
@@ -1,13 +1,13 @@
# Phoenix Project (PP)
-| Field | Value |
-|---|---|
-| Platform | Gazelle |
-| Base URL | `https://phoenixproject.app` |
-| API Endpoint | `https://phoenixproject.app/ajax.php` |
-| Auth Method | HTTP header: `Authorization: token TOKEN` |
-| Enrichment | Yes (`gazelleEnrich: true`) |
-| Auth Style | standard |
+| Field | Value |
+| ------------ | ----------------------------------------- |
+| Platform | Gazelle |
+| Base URL | `https://phoenixproject.app` |
+| API Endpoint | `https://phoenixproject.app/ajax.php` |
+| Auth Method | HTTP header: `Authorization: token TOKEN` |
+| Enrichment | Yes (`gazelleEnrich: true`) |
+| Auth Style | standard |
## Notes
diff --git a/docs/kb/docs/contributing/trackers/racing4everyone.md b/docs/kb/docs/contributing/trackers/racing4everyone.md
index 6490b352..49e702a2 100644
--- a/docs/kb/docs/contributing/trackers/racing4everyone.md
+++ b/docs/kb/docs/contributing/trackers/racing4everyone.md
@@ -1,13 +1,13 @@
# Racing4Everyone (R4E)
-| Field | Value |
-|---|---|
-| Platform | UNIT3D |
-| Base URL | `https://racing4everyone.eu` |
+| Field | Value |
+| ------------ | ------------------------------------- |
+| Platform | UNIT3D |
+| Base URL | `https://racing4everyone.eu` |
| API Endpoint | `https://racing4everyone.eu/api/user` |
-| Auth Method | Query parameter: `?api_token=TOKEN` |
-| Enrichment | N/A |
-| Auth Style | N/A |
+| Auth Method | Query parameter: `?api_token=TOKEN` |
+| Enrichment | N/A |
+| Auth Style | N/A |
## Notes
diff --git a/docs/kb/docs/contributing/trackers/redacted.md b/docs/kb/docs/contributing/trackers/redacted.md
index 770d8932..f5d74cdc 100644
--- a/docs/kb/docs/contributing/trackers/redacted.md
+++ b/docs/kb/docs/contributing/trackers/redacted.md
@@ -1,13 +1,13 @@
# REDacted (RED)
-| Field | Value |
-|---|---|
-| Platform | Gazelle |
-| Base URL | `https://redacted.sh` |
-| API Endpoint | `https://redacted.sh/ajax.php` |
-| Auth Method | HTTP header: `Authorization: token TOKEN` |
-| Enrichment | Yes (`gazelleEnrich: true`) |
-| Auth Style | raw (`gazelleAuthStyle: "token"`) |
+| Field | Value |
+| ------------ | ----------------------------------------- |
+| Platform | Gazelle |
+| Base URL | `https://redacted.sh` |
+| API Endpoint | `https://redacted.sh/ajax.php` |
+| Auth Method | HTTP header: `Authorization: token TOKEN` |
+| Enrichment | Yes (`gazelleEnrich: true`) |
+| Auth Style | raw (`gazelleAuthStyle: "token"`) |
## Notes
diff --git a/docs/kb/docs/contributing/trackers/reelflix.md b/docs/kb/docs/contributing/trackers/reelflix.md
index 5c189840..c1af8a8e 100644
--- a/docs/kb/docs/contributing/trackers/reelflix.md
+++ b/docs/kb/docs/contributing/trackers/reelflix.md
@@ -1,13 +1,13 @@
# Reelflix (RF)
-| Field | Value |
-|---|---|
-| Platform | UNIT3D |
-| Base URL | `https://reelflix.xyz` |
-| API Endpoint | `https://reelflix.xyz/api/user` |
-| Auth Method | Query parameter: `?api_token=TOKEN` |
-| Enrichment | N/A |
-| Auth Style | N/A |
+| Field | Value |
+| ------------ | ----------------------------------- |
+| Platform | UNIT3D |
+| Base URL | `https://reelflix.xyz` |
+| API Endpoint | `https://reelflix.xyz/api/user` |
+| Auth Method | Query parameter: `?api_token=TOKEN` |
+| Enrichment | N/A |
+| Auth Style | N/A |
## Notes
diff --git a/docs/kb/docs/contributing/trackers/seedpool.md b/docs/kb/docs/contributing/trackers/seedpool.md
new file mode 100644
index 00000000..ccc48a7e
--- /dev/null
+++ b/docs/kb/docs/contributing/trackers/seedpool.md
@@ -0,0 +1,30 @@
+# Seed Pool (SP)
+
+| Field | Value |
+| ------------ | -------------------------------------- |
+| Platform | UNIT3D |
+| Base URL | `https://seedpool.org` |
+| API Endpoint | `https://seedpool.org/api/user` |
+| Auth Method | Query parameter: `?api_token=TOKEN` |
+| Enrichment | N/A |
+| Auth Style | N/A |
+
+## Notes
+
+Standard UNIT3D configuration. No tracker-specific quirks.
+
+Seed Pool uses a seedsize-based promotion system rather than upload amount. All class names are pool-themed (User → Pool → PowerPool → SuperPool → UberPool → MegaPool → GodPool). There is also a purchasable ProPool class available via IRC.
+
+The `Cesspool` class is a demotion for users whose ratio drops below 1 — download privileges are revoked. `KiddiePool` is a timeout zone.
+
+Status page available at `https://status.seedpool.org/`.
+
+## Slots
+
+**Profile Card:** username · group (no avatar or join date — UNIT3D platform)
+
+**Badges:** `warned` (conditional — only resolves when `warned === true` in snapshot)
+
+**Stat Cards:** `seedbonus`
+
+**Progress:** none
diff --git a/docs/kb/docs/contributing/trackers/skipthecommercials.md b/docs/kb/docs/contributing/trackers/skipthecommercials.md
index 12416fd4..158b87f1 100644
--- a/docs/kb/docs/contributing/trackers/skipthecommercials.md
+++ b/docs/kb/docs/contributing/trackers/skipthecommercials.md
@@ -1,13 +1,13 @@
# SkipTheCommercials (STC)
-| Field | Value |
-|---|---|
-| Platform | UNIT3D |
-| Base URL | `https://skipthecommercials.xyz` |
+| Field | Value |
+| ------------ | ----------------------------------------- |
+| Platform | UNIT3D |
+| Base URL | `https://skipthecommercials.xyz` |
| API Endpoint | `https://skipthecommercials.xyz/api/user` |
-| Auth Method | Query parameter: `?api_token=TOKEN` |
-| Enrichment | N/A |
-| Auth Style | N/A |
+| Auth Method | Query parameter: `?api_token=TOKEN` |
+| Enrichment | N/A |
+| Auth Style | N/A |
## Notes
diff --git a/docs/kb/docs/contributing/trackers/uploadcx.md b/docs/kb/docs/contributing/trackers/uploadcx.md
index 9406e528..4f4fa0a3 100644
--- a/docs/kb/docs/contributing/trackers/uploadcx.md
+++ b/docs/kb/docs/contributing/trackers/uploadcx.md
@@ -1,13 +1,13 @@
# Upload.cx (UCX)
-| Field | Value |
-|---|---|
-| Platform | UNIT3D |
-| Base URL | `https://upload.cx` |
-| API Endpoint | `https://upload.cx/api/user` |
-| Auth Method | Query parameter: `?api_token=TOKEN` |
-| Enrichment | N/A |
-| Auth Style | N/A |
+| Field | Value |
+| ------------ | ----------------------------------- |
+| Platform | UNIT3D |
+| Base URL | `https://upload.cx` |
+| API Endpoint | `https://upload.cx/api/user` |
+| Auth Method | Query parameter: `?api_token=TOKEN` |
+| Enrichment | N/A |
+| Auth Style | N/A |
## Notes
diff --git a/docs/kb/docs/features/backups.md b/docs/kb/docs/features/backups.md
index 3ec49882..b7a7c8a2 100644
--- a/docs/kb/docs/features/backups.md
+++ b/docs/kb/docs/features/backups.md
@@ -88,7 +88,7 @@ If you're restoring a backup from a different Tracker Tracker installation — o
If a field can't be re-encrypted (for example, because the backup was encrypted with a password you no longer know), that field is cleared rather than saved in a broken state. For TOTP, the restore screen will tell you if 2FA was turned off as a result. You can re-enable it after the restore completes.
!!! warning "TOTP after a cross-instance restore"
- If 2FA was active on the source instance but can't be carried over, it will be disabled. Re-enroll in **Settings → Security** after the restore.
+If 2FA was active on the source instance but can't be carried over, it will be disabled. Re-enroll in **Settings → Security** after the restore.

diff --git a/docs/kb/docs/features/download-clients.md b/docs/kb/docs/features/download-clients.md
index a3dca79a..0dfab7e5 100644
--- a/docs/kb/docs/features/download-clients.md
+++ b/docs/kb/docs/features/download-clients.md
@@ -30,7 +30,7 @@ Go to **Settings → Download Clients** and fill in the connection details:
| Use SSL | Enable if your qBittorrent Web UI is served over HTTPS |
!!! warning "SSL/port mismatch"
- The form will warn you if SSL is on with port 80, or SSL is off with port 443. These combinations are usually misconfigured. You can still save, but double-check your settings.
+The form will warn you if SSL is on with port 80, or SSL is off with port 443. These combinations are usually misconfigured. You can still save, but double-check your settings.
After saving, use the **Test Connection** button to confirm Tracker Tracker can reach and authenticate with qBittorrent.
diff --git a/docs/kb/docs/features/image-hosting.md b/docs/kb/docs/features/image-hosting.md
new file mode 100644
index 00000000..bb2ed715
--- /dev/null
+++ b/docs/kb/docs/features/image-hosting.md
@@ -0,0 +1,71 @@
+---
+title: Image Hosting
+description: Upload screenshots to PTPImg, OnlyImage, or ImgBB directly from Tracker Tracker.
+---
+
+# Image Hosting
+
+!!! danger "Functionality has not been implemented and this currently does nothing"
+ This is in preparation for the Tracker Transit Papers that will be launched in an upcoming release. No harm is done from adding your api keys now, but they won't do anything.
+
+Tracker Tracker can upload images to external hosting services and return a direct link. This is useful for uploading screenshots when creating or editing torrent listings on trackers that require images hosted on approved services.
+
+## Supported Hosts
+
+| Host | URL | Expiration | Notes |
+| ------------- | ------------- | --------------------------- | ------------------------------------------ |
+| **PTPImg** | ptpimg.me | Not supported | Required by PTP, accepted by most trackers |
+| **OnlyImage** | onlyimage.org | Time-based (ISO 8601) | Used by OnlyEncodes and other trackers |
+| **ImgBB** | imgbb.com | Time-based (60s - 180 days) | Widely accepted free host |
+
+None of the three services support "burn after reading" (view-count-based self-destruct). PTPImg hosts images permanently. OnlyImage and ImgBB support time-based auto-deletion.
+
+## Setting Up API Keys
+
+Go to **Settings → General → Image Hosting** to add your API keys.
+
+Each key is encrypted at rest using the same AES-256-GCM encryption used for tracker API tokens. The app only stores whether a key is configured (shown as a "configured" badge) — the plaintext key is never returned to the browser after saving.
+
+### Where to Get Your API Key
+
+**PTPImg:** Log into ptpimg.me, view page source, and find the `api_key` value. It looks like a UUID: `44171be4-eb87-444f-9c49-11268f470e12`.
+
+**OnlyImage:** Go to your user settings at onlyimage.org and find the API section. The key is a long hex string starting with `chv_`.
+
+**ImgBB:** Create a free account at api.imgbb.com and generate an API key from the dashboard. It's a 32-character hex string.
+
+### Managing Keys
+
+- **Save Key** — Paste the key and click Save. It's encrypted before storage.
+- **Replace Key** — Click "Replace Key" to enter a new one. The old key is overwritten.
+- **Remove** — Click "Remove" to delete the stored key. This does not delete any images already uploaded.
+
+## Expiration
+
+When uploading to ImgBB or OnlyImage, you can optionally set images to auto-delete after a set time. PTPImg does not support expiration — all PTPImg uploads are permanent.
+
+| Duration | ImgBB | OnlyImage |
+| -------- | ------------- | --------- |
+| 1 minute | Yes (minimum) | Yes |
+| 1 hour | Yes | Yes |
+| 1 day | Yes | Yes |
+| 1 week | Yes | Yes |
+| 30 days | Yes | Yes |
+| 180 days | Yes (maximum) | Yes |
+
+## Supported File Types
+
+JPEG, PNG, GIF, WebP, BMP, and AVIF. Maximum file size is 32 MB.
+
+## Backups
+
+Image hosting API keys are included in backups as encrypted ciphertext. When restoring from a backup created on a different instance (different encryption salt), the keys are automatically re-encrypted using the current instance's key. If re-encryption fails, the keys are silently cleared — you'll need to re-enter them after restore.
+
+Backups created before the image hosting feature was added will not contain these keys. Restoring from such a backup will not affect any keys you've already configured.
+
+## Security
+
+- API keys are encrypted at rest using the same encryption as your tracker tokens
+- Keys are never shown after saving — the app only displays whether a key is configured
+- Uploads require you to be logged in
+- Keys are sent securely to the hosting services (never in URLs where they could leak into logs)
diff --git a/docs/kb/docs/features/proxies.md b/docs/kb/docs/features/proxies.md
index 482d8775..d23abe0b 100644
--- a/docs/kb/docs/features/proxies.md
+++ b/docs/kb/docs/features/proxies.md
@@ -6,7 +6,7 @@ description: Route tracker polling through SOCKS5, HTTP, or HTTPS proxies on a p
# Proxies
!!! warning "Experimental"
- Proxy support is experimental and may not work with all trackers or proxy configurations. Use at your own risk.
+Proxy support is experimental and may not work with all trackers or proxy configurations. Use at your own risk.
You can route outbound tracker API requests through a proxy. Proxy support is opt-in — you configure one global proxy, and then individually enable it per tracker.
@@ -23,10 +23,10 @@ The proxy type controls how your traffic reaches the proxy server — SOCKS5 for
When a proxy is enabled for a tracker, the tracker sees the proxy's IP address — not yours.
!!! warning "Some trackers ban proxy and VPN traffic"
- Many private trackers explicitly prohibit accessing the site from VPNs, proxies, or shared IPs. Using a proxy for API polling may trigger automated security flags or get your account disabled. Check your tracker's rules before enabling this. If a tracker allows API access from a different IP than your browsing IP, you're probably fine — but not all trackers make that distinction.
+Many private trackers explicitly prohibit accessing the site from VPNs, proxies, or shared IPs. Using a proxy for API polling may trigger automated security flags or get your account disabled. Check your tracker's rules before enabling this. If a tracker allows API access from a different IP than your browsing IP, you're probably fine — but not all trackers make that distinction.
!!! info "DNS resolution"
- HTTP and HTTPS proxies resolve the tracker's hostname on the proxy side — your local DNS provider never sees the domain. SOCKS5 behavior depends on configuration: most SOCKS5 proxies also resolve remotely, but some setups resolve locally first. If DNS privacy matters to you, verify your SOCKS5 proxy does remote resolution.
+HTTP and HTTPS proxies resolve the tracker's hostname on the proxy side — your local DNS provider never sees the domain. SOCKS5 behavior depends on configuration: most SOCKS5 proxies also resolve remotely, but some setups resolve locally first. If DNS privacy matters to you, verify your SOCKS5 proxy does remote resolution.
## Setup
diff --git a/docs/kb/docs/features/qbitmanage.md b/docs/kb/docs/features/qbitmanage.md
index 3f65b12c..5cd522b6 100644
--- a/docs/kb/docs/features/qbitmanage.md
+++ b/docs/kb/docs/features/qbitmanage.md
@@ -23,14 +23,14 @@ Tracker Tracker has built-in support for qbitmanage's status tags. Enable it in
Map each status to the tag name from your qbitmanage config. Here are qbitmanage's **out-of-the-box defaults**:
-| Status | Default tag | Config key | What it means |
-|--------|------------|------------|---------------|
-| Issue | `issue` | `tracker_error_tag` | Torrent has a tracker error (unregistered, dead, etc.) |
-| Min Time Not Reached | `MinTimeNotReached` | `share_limits_min_seeding_time_tag` | Hasn't hit the minimum seeding time for its share limit group |
-| No Hardlinks | `noHL` | `nohardlinks_tag` | No hardlinks found outside the root directory |
-| Min Seeds Not Met | `MinSeedsNotMet` | `share_limits_min_num_seeds_tag` | Not enough other seeders to safely remove yet |
-| Last Active Limit Not Reached | `LastActiveLimitNotReached` | `share_limits_last_active_tag` | Hasn't been inactive long enough for removal |
-| Last Active Not Reached | `LastActiveNotReached` | `share_limits_last_active_tag` | Last activity hasn't crossed the threshold |
+| Status | Default tag | Config key | What it means |
+| ----------------------------- | --------------------------- | ----------------------------------- | ------------------------------------------------------------- |
+| Issue | `issue` | `tracker_error_tag` | Torrent has a tracker error (unregistered, dead, etc.) |
+| Min Time Not Reached | `MinTimeNotReached` | `share_limits_min_seeding_time_tag` | Hasn't hit the minimum seeding time for its share limit group |
+| No Hardlinks | `noHL` | `nohardlinks_tag` | No hardlinks found outside the root directory |
+| Min Seeds Not Met | `MinSeedsNotMet` | `share_limits_min_num_seeds_tag` | Not enough other seeders to safely remove yet |
+| Last Active Limit Not Reached | `LastActiveLimitNotReached` | `share_limits_last_active_tag` | Hasn't been inactive long enough for removal |
+| Last Active Not Reached | `LastActiveNotReached` | `share_limits_last_active_tag` | Last activity hasn't crossed the threshold |
Many people customize these with emoji prefixes (e.g., `⚠️ Issue` instead of `issue`). If you've changed them in your qbitmanage `config.yml`, enter **your** tag names — not the defaults above.
@@ -39,7 +39,7 @@ Here's what the qbitmanage status breakdown looks like on a tracker's Torrents t

!!! tip "Match your config exactly"
- Tag names must match character-for-character, including any emoji or special characters. Copy them directly from your qbitmanage `config.yml`.
+Tag names must match character-for-character, including any emoji or special characters. Copy them directly from your qbitmanage `config.yml`.
## Tag Group Examples
diff --git a/docs/kb/docs/features/tag-groups.md b/docs/kb/docs/features/tag-groups.md
index e721908d..046b1a95 100644
--- a/docs/kb/docs/features/tag-groups.md
+++ b/docs/kb/docs/features/tag-groups.md
@@ -61,7 +61,7 @@ When enabled, the chart includes an extra segment for torrents that don't match
Tags must match **exactly** — same capitalization, same spacing, same characters. If your qBittorrent tag is `High Priority` and you type `high priority` in the group, it won't match.
!!! tip "Check your qBittorrent tags"
- Open qBittorrent and look at the tag list in the sidebar to see the exact tag names. Copy them character-for-character into Tracker Tracker.
+Open qBittorrent and look at the tag list in the sidebar to see the exact tag names. Copy them character-for-character into Tracker Tracker.
## Automating with qbitmanage
diff --git a/docs/kb/docs/features/totp.md b/docs/kb/docs/features/totp.md
index ad657533..b2f4882f 100644
--- a/docs/kb/docs/features/totp.md
+++ b/docs/kb/docs/features/totp.md
@@ -33,7 +33,7 @@ When you enable 2FA, you get 8 backup codes. Each one looks like this:
Use a backup code at the TOTP prompt the same way you'd use a 6-digit code — there's a "Use a backup code" option on the login screen.
!!! warning "Save your backup codes now"
- If you lose your authenticator app and don't have backup codes, you cannot log in. There is no account recovery. Store the codes in a password manager or print them and keep them somewhere secure.
+If you lose your authenticator app and don't have backup codes, you cannot log in. There is no account recovery. Store the codes in a password manager or print them and keep them somewhere secure.
## Logging In With 2FA
@@ -61,7 +61,7 @@ Failed TOTP attempts count toward the same lockout limit as failed password atte
## After a Backup Restore
!!! warning "2FA may be disabled after restoring a backup"
- If you restore a backup from a different Tracker Tracker instance — one that was set up with a different password — the 2FA secret can't be carried over. In that case, 2FA will be turned off automatically as part of the restore.
+If you restore a backup from a different Tracker Tracker instance — one that was set up with a different password — the 2FA secret can't be carried over. In that case, 2FA will be turned off automatically as part of the restore.
The restore confirmation screen will tell you if this happened. You'll need to go back to **Settings → Security** and set up 2FA again.
diff --git a/docs/kb/docs/features/transit-papers.md b/docs/kb/docs/features/transit-papers.md
new file mode 100644
index 00000000..ddb14e3f
--- /dev/null
+++ b/docs/kb/docs/features/transit-papers.md
@@ -0,0 +1,204 @@
+---
+title: Tracker Transit Papers
+description: Generate tamper-resistant proof-of-membership images for private tracker applications.
+---
+
+# Tracker Transit Papers
+
+!!! danger "This feature is currently unreleased"
+
+!!! warning "Beta — Highly Experimental"
+Transit Papers are under active development. The report format, encoding scheme, and verification behavior may change between versions. Reports generated with one version are not guaranteed to verify correctly with a future version. Use at your own risk.
+
+Transit Papers generate a tamper-resistant PNG image showing your stats on a single tracker. The image is designed to be shared with tracker moderators as proof of your membership and standing when applying to other trackers.
+
+Instead of a browser screenshot — which can be faked in seconds with inspect element or Photoshop — Transit Papers produce a server-rendered image with cryptographically linked visual elements. Editing any part of the image (the stats, the fractal seal, or the data strip) breaks the link between them, and the verification tool detects it. Each report is called a **Proof of Citizenship**.
+
+!!! warning "What Transit Papers are NOT"
+Transit Papers are **not zero-trust cryptographic proof** that the stats are real. Tracker Tracker is self-hosted. You control the machine, the database, and the network. A technically motivated user could theoretically fabricate data before generation.
+
+ What this system does is raise the cost of forgery from **trivial** (inspect element, 30 seconds) to **impractical** (clone the project, set up a database, fabricate internally consistent stats across 10+ mathematically related fields, understand the target tracker's API schema).
+
+ Transit Papers are a tool to assist a mod's judgment, not replace it. If a mod has direct access to your profile on the source tracker, that is always more trustworthy than any report.
+
+---
+
+## Generating a Proof of Citizenship
+
+Navigate to a tracker's detail page and generate a report. Tracker Tracker uses your most recent polled snapshot to render the report server-side and returns a PNG for download.
+
+The report is a 1200x630 image containing:
+
+- **Tracker name and platform type** — which tracker and what software it runs
+- **Your identity** — username, class/rank, and member-since date appear in a header section above the stats grid (when available).
+- **Your stats** — uploaded bytes, downloaded bytes, ratio, buffer, seeding count, seedbonus, and hit & runs. Which fields appear depends on what the platform reports.
+- **Fractal Seal** — a unique fractal image derived from your stats. Different stats produce a completely different fractal. This is both decorative and functional — it serves as the encryption key for the data strip.
+- **Spirograph** — a decorative hypotrochoid pattern derived from the generation timestamp. Three layered curves, each 120 degrees apart on the color wheel.
+- **Data Strip** — a horizontal colored barcode near the bottom of the image. Your stats are serialized, encrypted using a key derived from the fractal, and encoded as colored bands.
+- **Footer** — generation timestamp, report version, and the full SHA-256 seed hash.
+
+---
+
+## Sharing Your Report
+
+### Built-in upload integrations (recommended)
+
+Tracker Tracker can upload your Proof of Citizenship directly to an image host and give you a shareable URL. This is the most reliable method because the image is transferred losslessly.
+
+| Host | Notes |
+| ------------- | ---------------------------------------------------------------------------------- |
+| **PTPImg** | Most widely accepted across private trackers. Serves PNGs untouched. |
+| **OnlyImage** | onlyimage.org. Accepted by OnlyEncodes and other trackers. |
+| **ImgBB** | Supports auto-delete timers if you want expiring links. May recompress large PNGs. |
+
+Configure API keys in **Settings → General → Image Hosting**. See the [Image Hosting](image-hosting.md) docs for setup instructions.
+
+### Manual sharing
+
+If you are not using an integration, share the original PNG file directly:
+
+- Attach it to a forum PM or recruitment thread
+- Send via IRC DCC
+- Upload to any lossless image host manually (ptpimg.me, imgbox.com, catbox.moe)
+
+!!! danger "Do not screenshot the report"
+The verification system reads pixel data from the image. A screenshot of the report is not the report. It will degrade verification or cause it to fail entirely.
+
+---
+
+## Compression and Image Quality
+
+The data strip and fractal seal are verified at the pixel level. If the image passes through a lossy compression step (JPEG conversion, resize, re-encoding), pixel colors shift. This can cause verification to fail or require fuzzy recovery that reduces confidence.
+
+### Lossless (verification works perfectly)
+
+- ptpimg.me
+- OnlyImage (onlyimage.org)
+- imgbox.com
+- catbox.moe
+- Direct file attachment (forum PM, IRC, email)
+- Discord file attachment (the actual file download, not the embedded preview)
+- Telegram "send as file" (not "send as photo")
+- Direct download links (Google Drive, Dropbox)
+
+### Lossy (may degrade verification)
+
+- ImgBB (can recompress large PNGs — test your results)
+- Discord embedded preview (proxied and resized by Discord's CDN)
+- iMessage / SMS
+- Twitter / X
+- Imgur (sometimes recompresses PNGs)
+- Any mobile "send as photo" option
+- Screenshotting the image
+- Downloading from a web preview instead of the original file
+
+### What happens if the image was compressed
+
+The verifier uses fuzzy recovery: it tries nearby perceptual hash keys until it finds one that decodes successfully. The result shows how many bit corrections were needed:
+
+| Bit flips | What it means |
+| --------- | ----------------------------------------------------------------------------------- |
+| 0 | Perfect. Image arrived losslessly. Highest confidence. |
+| 1-3 | Mild compression. Verification is still reliable. |
+| 4+ | Heavy compression or re-encoding. Result is valid but confidence is reduced. |
+| Failed | Image too degraded to recover. Request the original file or a lossless-hosted link. |
+
+**For users:** Use a built-in upload integration or share the original file directly.
+
+**For mods:** If verification shows more than 0 bit flips, consider asking the user to reshare via ptpimg or OnlyImage.
+
+---
+
+## For Tracker Mods: Verifying a Report
+
+The verification tool is a standalone static page — completely separate from any Tracker Tracker installation. It runs entirely in your browser. No account, no server connection, no data leaves your machine. You do not need to be a Tracker Tracker user to verify a report.
+
+Upload or drag-and-drop the PNG onto the verification page.
+
+### What the verification page shows
+
+- Whether the data strip was successfully decoded
+- The decoded stats: tracker name, platform, username, class, upload, download, ratio, buffer, seeding count, seedbonus, hit & runs, join date
+- A side-by-side comparison of the fractal extracted from the image and the fractal regenerated from the decoded stats
+- A regenerated spirograph from the decoded timestamp
+- How many bit corrections were needed (0 = lossless)
+- Overall verification status
+
+### Verification results
+
+**Verified — Seal + Data Match**
+: The stats in the strip match the fractal. The image has not been tampered with after generation. This is the best result.
+
+**Data Decoded — Seal Mismatch**
+: The strip decoded but the fractal does not match. The image may have been edited after generation, or it experienced heavy compression. Ask for the original file.
+
+**Decode Failed**
+: The image is too degraded or has been fundamentally altered. Cannot verify.
+
+!!! info "What verification does NOT tell you"
+Verification confirms the image is internally consistent and has not been tampered with **after generation**. It does not confirm the stats are truthful — the user controls their instance and could have fabricated data before generating the report. Use it as one input alongside your own judgment.
+
+---
+
+## How It Works
+
+1. When you generate a report, Tracker Tracker reads your most recent polled snapshot from its database.
+2. It serializes the stats into a compact binary format and hashes them to produce a unique fingerprint.
+3. That fingerprint determines the appearance of the fractal seal. Different stats produce a completely different fractal.
+4. The fractal image is used to derive an encryption key.
+5. The binary stats are encrypted with that key, scrambled, and encoded into the colored bands of the data strip.
+6. The fractal and strip are bound together: you cannot change one without breaking the other.
+7. When a verifier uploads the image, the system extracts the fractal, derives the decryption key from it, decrypts the strip, recovers the stats, regenerates what the fractal _should_ look like from those stats, and checks that it matches what's actually in the image.
+
+---
+
+## Trust Model
+
+### What Transit Papers defend against
+
+| Attack | Defense |
+| ----------------------------------- | ------------------------------------------------------------------------------------------ |
+| Inspect element / edit browser page | No DOM — the report is a flat PNG rendered server-side |
+| Edit stats text in Photoshop | Data strip still contains original values — decoded stats will not match visible text |
+| Edit the data strip colors | Checksum fails — decode is rejected |
+| Replace the fractal seal | Strip becomes undecryptable — wrong key |
+| Edit both strip and fractal | New fractal's key will not match the encryption used on the original strip |
+| Screenshot and re-share | Fuzzy recovery tolerates minor compression — perceptual hashing corrects small differences |
+
+### What Transit Papers do NOT defend against
+
+| Attack | Why | Mitigation |
+| -------------------------------------------------------------------- | --------------------------------------- | ----------------------------------------------------------------------------------------- |
+| User modifies their database before generation | Self-hosted — user controls the machine | Faker must fabricate internally consistent stats across 10+ mathematically related fields |
+| User intercepts/modifies API response before it reaches the renderer | User controls the network stack | Faker must produce plausible data matching the exact API schema of the target platform |
+| User modifies the source code | Open source — code is public | Faker must reproduce pixel-perfect output from the full rendering pipeline |
+
+!!! note "Honest positioning"
+This system raises forgery effort from trivial to impractical. It does not make forgery impossible. A determined attacker with technical skills who controls their instance can theoretically fabricate a valid report. The practical threat — someone trying to bluff their way into a tracker invite — is effectively blocked.
+
+---
+
+## Privacy Considerations
+
+- The report contains your username, tracker name, platform type, and stats. Once shared, you cannot control its distribution.
+- Image host URLs (ptpimg, imgbox, etc.) are typically unguessable but publicly accessible if someone has the link.
+- ImgBB supports auto-delete timers if you want the image to expire after sharing.
+- The verification tool runs entirely in the browser. No images are uploaded to any server — processing happens locally in memory and nothing is stored or transmitted.
+
+---
+
+## Image Host Setup
+
+Configure image hosting API keys in **Settings → General → Image Hosting**. See the [Image Hosting](image-hosting.md) page for full setup instructions.
+
+| Host | Where to find your key | Notes |
+| --------- | ---------------------------------------------------------- | --------------------------------------------------------------------------------- |
+| PTPImg | Log into ptpimg.me, view page source, find `api_key` value | Most widely accepted. Lossless PNG hosting. |
+| OnlyImage | onlyimage.org → user settings → API section | Key starts with `chv_` |
+| ImgBB | api.imgbb.com → create account → generate key | 32-char hex. Supports auto-delete (1 min to 6 months). May recompress large PNGs. |
+
+---
+
+## How Verification Recovers From Compression
+
+When an image has been mildly compressed (passed through Discord, re-saved, etc.), the fractal's visual fingerprint may shift by a few bits. The verifier compensates by testing nearby fingerprint variants until it finds one that successfully decrypts the data strip. A checksum embedded in the payload acts as a stop condition. This process is automatic and typically completes in under a second.
diff --git a/docs/kb/docs/features/webhooks.md b/docs/kb/docs/features/webhooks.md
index 8fbd1d99..fac917f0 100644
--- a/docs/kb/docs/features/webhooks.md
+++ b/docs/kb/docs/features/webhooks.md
@@ -59,7 +59,7 @@ Each target subscribes to any combination of these events:
Cooldowns prevent spam — if a condition persists across multiple polls, you get one alert per cooldown period, not one per poll.
!!! info "First-poll behavior"
- Events that compare snapshots (ratio drop, hit-and-run) need at least two polls and won't fire on the first one. Events like "account warned" fire immediately if the condition is already true.
+Events that compare snapshots (ratio drop, hit-and-run) need at least two polls and won't fire on the first one. Events like "account warned" fire immediately if the condition is already true.
## Thresholds
diff --git a/docs/kb/docs/getting-started/docker-config.md b/docs/kb/docs/getting-started/docker-config.md
index 30b0b428..ff5bf27f 100644
--- a/docs/kb/docs/getting-started/docker-config.md
+++ b/docs/kb/docs/getting-started/docker-config.md
@@ -34,7 +34,7 @@ Everything you need to customize how Tracker Tracker runs: environment variables
| `LOG_FILE` | _(none)_ | Absolute path inside the container to write logs to disk, e.g. `/data/logs/tracker-tracker.log`. |
!!! info "Settings vs environment variables"
- Most day-to-day settings — polling interval, privacy mode, proxy config, backup schedule, lockout policy — live inside the app under **Settings**, not in environment variables. Environment variables are just for infrastructure stuff like database connections and ports.
+Most day-to-day settings — polling interval, privacy mode, proxy config, backup schedule, lockout policy — live inside the app under **Settings**, not in environment variables. Environment variables are just for infrastructure stuff like database connections and ports.
---
@@ -49,7 +49,7 @@ Everything you need to customize how Tracker Tracker runs: environment variables
| `./postgres/postgresql.conf` | `/etc/postgresql/postgresql.conf` | Custom PostgreSQL config. Included in the repo and required for the bundled database to start. |
!!! warning "Back up the pgdata volume"
- The `pgdata` named volume holds your entire database. Use the built-in backup feature (Settings → Backups) for app-level backups, and separately snapshot the Docker volume or use `pg_dump` if you want a database-level backup.
+The `pgdata` named volume holds your entire database. Use the built-in backup feature (Settings → Backups) for app-level backups, and separately snapshot the Docker volume or use `pg_dump` if you want a database-level backup.
---
@@ -69,7 +69,7 @@ PORT=8080
```
!!! tip "Behind a reverse proxy?"
- If Tracker Tracker sits behind Nginx, Caddy, or Traefik, you don't need to expose port 3000 to the outside world at all. Remove the `ports:` block from `docker-compose.yml` and let the reverse proxy talk to the container over the Docker network directly.
+If Tracker Tracker sits behind Nginx, Caddy, or Traefik, you don't need to expose port 3000 to the outside world at all. Remove the `ports:` block from `docker-compose.yml` and let the reverse proxy talk to the container over the Docker network directly.
---
@@ -143,7 +143,7 @@ PORT=8080
This assumes Traefik is already running with a `websecure` entrypoint and a `letsencrypt` certificate resolver.
!!! info "Set BASE_URL when using a reverse proxy"
- Set `BASE_URL=https://trackertracker.example.com` in `.env` so backup files and notification links use your public address instead of localhost.
+Set `BASE_URL=https://trackertracker.example.com` in `.env` so backup files and notification links use your public address instead of localhost.
---
@@ -169,7 +169,7 @@ docker compose pull && docker compose up -d
The database schema updates automatically on startup. No manual steps required.
!!! tip "Check the changelog first"
- Read the [CHANGELOG](https://github.com/jordanlambrecht/tracker-tracker/blob/main/CHANGELOG.md) before pulling a new image — especially for major version bumps, which may include breaking changes to backup formats or environment variables.
+Read the [CHANGELOG](https://github.com/jordanlambrecht/tracker-tracker/blob/main/CHANGELOG.md) before pulling a new image — especially for major version bumps, which may include breaking changes to backup formats or environment variables.
To pin to a specific version and update deliberately:
diff --git a/docs/kb/docs/getting-started/first-setup.md b/docs/kb/docs/getting-started/first-setup.md
index c52958fb..a0182ec7 100644
--- a/docs/kb/docs/getting-started/first-setup.md
+++ b/docs/kb/docs/getting-started/first-setup.md
@@ -19,10 +19,10 @@ Your password does two things:
2. **Protects your stored API tokens.** All tracker API tokens are encrypted at rest using a key derived from your password. If you lose your password, those tokens can't be recovered — but you can re-enter them manually.
!!! warning "Choose strong credentials"
- There is no recovery mechanism. If you forget your password, you'll need to reset the database and start fresh. Keep it in a password manager.
+There is no recovery mechanism. If you forget your password, you'll need to reset the database and start fresh. Keep it in a password manager.
!!! info "Your password stays on your machine"
- It's hashed on the server before being stored. The raw password is never saved anywhere.
+It's hashed on the server before being stored. The raw password is never saved anywhere.
Click **Create Account**. You'll be logged in and land on the dashboard.
@@ -41,7 +41,7 @@ The left sidebar has three sections:
The main area shows the tracker overview grid, charts, and leaderboard. Charts fill in automatically as polling history builds up over time.
!!! tip "Polling starts right away"
- The moment you add a tracker, the app polls it and records a snapshot. Stats start charting from that first poll. The default polling interval is 60 minutes — you can change it in **Settings → General**.
+The moment you add a tracker, the app polls it and records a snapshot. Stats start charting from that first poll. The default polling interval is 60 minutes — you can change it in **Settings → General**.
---
diff --git a/docs/kb/docs/getting-started/installation.md b/docs/kb/docs/getting-started/installation.md
index 6229418b..a66e544f 100644
--- a/docs/kb/docs/getting-started/installation.md
+++ b/docs/kb/docs/getting-started/installation.md
@@ -15,7 +15,7 @@ Tracker Tracker runs as a Docker image. The easiest way to get it running is wit
Nothing else needs to be installed on your host.
!!! info "Architecture support"
- The image supports **linux/amd64** and **linux/arm64**. It runs on x86-64 servers and ARM machines like Raspberry Pi 4/5 or Apple Silicon in Linux VMs — Docker picks the right version automatically.
+The image supports **linux/amd64** and **linux/arm64**. It runs on x86-64 servers and ARM machines like Raspberry Pi 4/5 or Apple Silicon in Linux VMs — Docker picks the right version automatically.
---
@@ -53,7 +53,7 @@ TZ=America/Chicago
```
!!! warning "Don't reuse these values"
- `SESSION_SECRET` protects your session cookies. `POSTGRES_PASSWORD` protects your database. Generate fresh values — never copy the placeholder text from `.env.example`.
+`SESSION_SECRET` protects your session cookies. `POSTGRES_PASSWORD` protects your database. Generate fresh values — never copy the placeholder text from `.env.example`.
## Step 4 — Start the stack
@@ -126,7 +126,7 @@ The same image is on two registries — either works:
| Registry | Image |
| ------------------------- | ------------------------------------------------ |
| GitHub Container Registry | `ghcr.io/jordanlambrecht/tracker-tracker:latest` |
-| Docker Hub | `jordyjordy/tracker-tracker:latest` |
+| Docker Hub | `jordyjordy/tracker-tracker:latest` |
Pin to a specific version if you want predictable updates:
@@ -149,4 +149,4 @@ TZ=America/Chicago
```
!!! tip
- You only need to create the database itself beforehand. The app handles the rest on first startup — no manual SQL required.
+You only need to create the database itself beforehand. The app handles the rest on first startup — no manual SQL required.
diff --git a/docs/kb/docs/trackers/adding-a-tracker.md b/docs/kb/docs/trackers/adding-a-tracker.md
index 52a7d1a6..595f4111 100644
--- a/docs/kb/docs/trackers/adding-a-tracker.md
+++ b/docs/kb/docs/trackers/adding-a-tracker.md
@@ -29,6 +29,7 @@ Click the **+** button next to "Trackers" in the sidebar, or go to `/trackers/ne
| Racing4Everyone | R4E |
| ReelFlix | |
| SkipTheCommercials | STC |
+ | Seedpool | SP |
| Upload.cx | |
=== "Gazelle"
@@ -60,7 +61,7 @@ Click the **+** button next to "Trackers" in the sidebar, or go to `/trackers/ne
| Nebulance | NBL |
!!! note "Draft entries"
- Some trackers in the registry are marked as drafts — dashed border, "Stats tracking not yet supported." You can pin them as quicklinks but no stats will be polled.
+Some trackers in the registry are marked as drafts — dashed border, "Stats tracking not yet supported." You can pin them as quicklinks but no stats will be polled.
Trackers you've already added are hidden from the list automatically.
@@ -100,7 +101,7 @@ Where to find it depends on the platform:
GGn keys don't expire on their own, but they can be regenerated from your settings.
!!! warning "Keep your token private"
- Your API token acts like a password for your account. Tracker Tracker encrypts it before storing it.
+Your API token acts like a password for your account. Tracker Tracker encrypts it before storing it.
## Optional fields
@@ -112,15 +113,15 @@ If the tracker needs a proxy (e.g., for geo-restrictions), toggle **Use Proxy**
1. The tracker is saved and an immediate poll runs.
2. The **PulseDot** on the tracker card shows the result:
- - Breathing cyan — poll succeeded
- - Amber — warning or partial data
- - Red — poll failed (bad token, network error, etc.)
+ - Breathing cyan — poll succeeded
+ - Amber — warning or partial data
+ - Red — poll failed (bad token, network error, etc.)
3. The tracker appears in the dashboard and sidebar.

!!! tip "Red dot right after adding?"
- Check your API token. The most common cause is a copy-paste error or a token that was rotated after you copied it.
+Check your API token. The most common cause is a copy-paste error or a token that was rotated after you copied it.
## Polling manually
@@ -134,19 +135,19 @@ The global schedule (default: every 60 minutes) runs all trackers on a shared ti
Not every platform exposes the same stats. Here's what you'll see:
-| Stat | UNIT3D | Gazelle | GGn | Notes |
-|------|--------|---------|-----|-------|
-| Upload | Yes | Yes | Yes | |
-| Download | Yes | Yes | Yes | |
-| Ratio | Yes | Yes | Yes | GGn shows extra decimal precision |
-| Buffer | Yes | Yes | Yes | UNIT3D returns this directly; others calculate it from upload minus download |
-| Seeding count | Yes | Partial | Partial | Some Gazelle forks and GGn may return 0 even when you're seeding |
-| Leeching count | Yes | Partial | Partial | Same as above |
-| Bonus points | Yes | Yes | Yes | GGn calls this "gold" — it maps automatically |
-| Hit & Runs | Yes | No | Partial | GGn shows unknown for Elite Gamer+ (HNR immunity) |
-| Required ratio | No | Yes | Yes | Not in the UNIT3D API |
-| Warned status | No | Partial | Yes | Most Gazelle trackers default to false; RED has extended data |
-| Freeleech tokens | No | Partial | No | Not all Gazelle forks expose this |
+| Stat | UNIT3D | Gazelle | GGn | Notes |
+| ---------------- | ------ | ------- | ------- | ---------------------------------------------------------------------------- |
+| Upload | Yes | Yes | Yes | |
+| Download | Yes | Yes | Yes | |
+| Ratio | Yes | Yes | Yes | GGn shows extra decimal precision |
+| Buffer | Yes | Yes | Yes | UNIT3D returns this directly; others calculate it from upload minus download |
+| Seeding count | Yes | Partial | Partial | Some Gazelle forks and GGn may return 0 even when you're seeding |
+| Leeching count | Yes | Partial | Partial | Same as above |
+| Bonus points | Yes | Yes | Yes | GGn calls this "gold" — it maps automatically |
+| Hit & Runs | Yes | No | Partial | GGn shows unknown for Elite Gamer+ (HNR immunity) |
+| Required ratio | No | Yes | Yes | Not in the UNIT3D API |
+| Warned status | No | Partial | Yes | Most Gazelle trackers default to false; RED has extended data |
+| Freeleech tokens | No | Partial | No | Not all Gazelle forks expose this |
### Gazelle: enriched data on RED
@@ -164,7 +165,7 @@ REDacted (and Phoenix Project) fetch additional data beyond the standard stats
### Token not working
-Make sure you copied the full token. UNIT3D tokens are typically 60–80 characters. Gazelle keys are shown only once when created.
+Make sure you copied the full token. UNIT3D tokens are typically 60-80 characters. Gazelle keys are shown only once when created.
### Poll fails with 401
diff --git a/docs/kb/docs/troubleshooting/common-errors.md b/docs/kb/docs/troubleshooting/common-errors.md
index 2db4421f..df447152 100644
--- a/docs/kb/docs/troubleshooting/common-errors.md
+++ b/docs/kb/docs/troubleshooting/common-errors.md
@@ -26,11 +26,7 @@ These appear in the **Poll Error Banner** on the tracker detail page, under the
- Your account's API access was revoked or restricted.
- The tracker requires a specific IP allowlist for API access.
-!!! success "Solution"
- 1. Log into the tracker website and go to your profile or security settings.
- 2. Regenerate or copy your current API token.
- 3. Open the tracker settings in Tracker Tracker and replace the token.
- 4. Click **Resume Polling** on the tracker detail page, then **Poll Now** to verify.
+!!! success "Solution" 1. Log into the tracker website and go to your profile or security settings. 2. Regenerate or copy your current API token. 3. Open the tracker settings in Tracker Tracker and replace the token. 4. Click **Resume Polling** on the tracker detail page, then **Poll Now** to verify.
---
@@ -38,11 +34,7 @@ These appear in the **Poll Error Banner** on the tracker detail page, under the
**Cause:** DNS resolution failed for the tracker's hostname. The system could not translate the domain name to an IP address.
-!!! success "Solution"
- 1. Check the tracker's base URL for typos (e.g. `tracker.exmaple.com` instead of `tracker.example.com`).
- 2. Test DNS from your Docker host: `nslookup `.
- 3. If you use a VPN or custom DNS, confirm DNS is accessible inside the container. Add a `dns:` directive to `docker-compose.yml` if needed.
- 4. If the tracker's domain has changed, update the base URL in tracker settings.
+!!! success "Solution" 1. Check the tracker's base URL for typos (e.g. `tracker.exmaple.com` instead of `tracker.example.com`). 2. Test DNS from your Docker host: `nslookup `. 3. If you use a VPN or custom DNS, confirm DNS is accessible inside the container. Add a `dns:` directive to `docker-compose.yml` if needed. 4. If the tracker's domain has changed, update the base URL in tracker settings.
---
@@ -50,10 +42,7 @@ These appear in the **Poll Error Banner** on the tracker detail page, under the
**Cause:** The hostname resolved but the host could not be reached at the network layer. The route to the IP does not exist, or a firewall is silently dropping packets.
-!!! success "Solution"
- 1. Check whether the tracker is accessible from your browser.
- 2. If you use a VPN or firewall to route tracker traffic, verify those are up.
- 3. Confirm there is no egress firewall on your Docker host blocking outbound connections.
+!!! success "Solution" 1. Check whether the tracker is accessible from your browser. 2. If you use a VPN or firewall to route tracker traffic, verify those are up. 3. Confirm there is no egress firewall on your Docker host blocking outbound connections.
---
@@ -61,10 +50,7 @@ These appear in the **Poll Error Banner** on the tracker detail page, under the
**Cause:** The host was reached but actively refused the connection. The most common cause is a wrong port — nothing is listening there — or the tracker's web server is down.
-!!! success "Solution"
- 1. Verify the base URL includes the correct port if the tracker uses a non-standard one.
- 2. Check the scheme (`https://` vs `http://`) — a mismatch will produce a refused or reset connection.
- 3. Confirm the tracker site is responding normally in a browser.
+!!! success "Solution" 1. Verify the base URL includes the correct port if the tracker uses a non-standard one. 2. Check the scheme (`https://` vs `http://`) — a mismatch will produce a refused or reset connection. 3. Confirm the tracker site is responding normally in a browser.
---
@@ -72,9 +58,7 @@ These appear in the **Poll Error Banner** on the tracker detail page, under the
**Cause:** The connection was established and then immediately terminated by the remote host. Often caused by SSL/TLS mismatches, invalid certificates, or the remote server closing the connection unexpectedly.
-!!! success "Solution"
- 1. Confirm the scheme in the base URL matches what the tracker requires (`https://`).
- 2. Self-signed certificates are not currently supported — the TLS handshake will fail.
+!!! success "Solution" 1. Confirm the scheme in the base URL matches what the tracker requires (`https://`). 2. Self-signed certificates are not currently supported — the TLS handshake will fail.
---
@@ -82,10 +66,7 @@ These appear in the **Poll Error Banner** on the tracker detail page, under the
**Cause:** The tracker API accepted the connection but did not return a response within 15 seconds. This can happen during maintenance windows or when the API is under heavy load.
-!!! success "Solution"
- 1. Wait a few minutes and use **Poll Now** to retry manually.
- 2. If timeouts are persistent, check whether a proxy is adding latency.
- 3. The 15-second timeout is fixed and cannot be changed.
+!!! success "Solution" 1. Wait a few minutes and use **Poll Now** to retry manually. 2. If timeouts are persistent, check whether a proxy is adding latency. 3. The 15-second timeout is fixed and cannot be changed.
---
@@ -93,11 +74,7 @@ These appear in the **Poll Error Banner** on the tracker detail page, under the
**Cause:** The tracker's rate-limiting or abuse detection blocked your IP. This can happen if the poll interval is too short, or if the tracker treats automated requests as abusive.
-!!! success "Solution"
- 1. Wait for the ban to expire — this is enforced on the tracker side and typically lasts minutes to hours depending on the site's policy.
- 2. Once the ban clears, go to **Settings → General** and increase the poll interval. 60 minutes is the recommended default; some trackers enforce stricter limits.
- 3. Resume polling only after the ban has likely expired.
- 4. Do **not** set the poll interval below 30 minutes on trackers known to be sensitive to API traffic.
+!!! success "Solution" 1. Wait for the ban to expire — this is enforced on the tracker side and typically lasts minutes to hours depending on the site's policy. 2. Once the ban clears, go to **Settings → General** and increase the poll interval. 60 minutes is the recommended default; some trackers enforce stricter limits. 3. Resume polling only after the ban has likely expired. 4. Do **not** set the poll interval below 30 minutes on trackers known to be sensitive to API traffic.
---
@@ -105,10 +82,7 @@ These appear in the **Poll Error Banner** on the tracker detail page, under the
**Cause:** The tracker has **Use Proxy** enabled, and the proxy server was unreachable or returned an error. Tracker Tracker will not silently bypass a required proxy and make a direct connection.
-!!! success "Solution"
- 1. Go to **Settings → Proxy** and verify the proxy host, port, type, username, and password.
- 2. Confirm the proxy server is running and reachable from the Docker container.
- 3. To disable the proxy for a specific tracker, edit the tracker settings and turn off **Use Proxy**.
+!!! success "Solution" 1. Go to **Settings → Proxy** and verify the proxy host, port, type, username, and password. 2. Confirm the proxy server is running and reachable from the Docker container. 3. To disable the proxy for a specific tracker, edit the tracker settings and turn off **Use Proxy**.
---
@@ -116,10 +90,7 @@ These appear in the **Poll Error Banner** on the tracker detail page, under the
**Cause:** The tracker's API responded with an unexpected HTTP status code. For example, `API returned 429` indicates HTTP-level rate limiting; `API returned 500` indicates a tracker-side server error.
-!!! success "Solution"
- - **429:** Reduce the poll interval and wait for the tracker to clear the rate limit.
- - **500 / 503:** The tracker API is having issues — wait and retry later.
- - **Other codes:** Check the tracker's status page or community forums.
+!!! success "Solution" - **429:** Reduce the poll interval and wait for the tracker to clear the rate limit. - **500 / 503:** The tracker API is having issues — wait and retry later. - **Other codes:** Check the tracker's status page or community forums.
---
@@ -128,7 +99,7 @@ These appear in the **Poll Error Banner** on the tracker detail page, under the
**Cause:** An unexpected error occurred that did not match any of the known patterns above.
!!! success "Solution"
- Check the Tracker Tracker server logs (`docker compose logs app`) for the full error. Look for a line containing `Poll failed for tracker` followed by the raw error string.
+Check the Tracker Tracker server logs (`docker compose logs app`) for the full error. Look for a line containing `Poll failed for tracker` followed by the raw error string.
---
@@ -142,10 +113,7 @@ These appear on the **Download Clients** panel in Settings, on individual client
**Cause:** qBittorrent rejected the username/password combination.
-!!! success "Solution"
- 1. In qBittorrent's Web UI settings, confirm the username and password.
- 2. Update the credentials in **Settings → Download Clients** (edit the client card).
- 3. Use the **Test Connection** button after saving to verify.
+!!! success "Solution" 1. In qBittorrent's Web UI settings, confirm the username and password. 2. Update the credentials in **Settings → Download Clients** (edit the client card). 3. Use the **Test Connection** button after saving to verify.
---
@@ -154,7 +122,7 @@ These appear on the **Download Clients** panel in Settings, on individual client
**Cause:** qBittorrent returned a 403 on an authenticated request, indicating the session has expired. Tracker Tracker handles this automatically by re-authenticating on the next request.
!!! info "No action needed"
- Session expiry is handled transparently. If you see persistent errors in the **Download Clients** panel, the issue is more likely wrong credentials rather than session expiry.
+Session expiry is handled transparently. If you see persistent errors in the **Download Clients** panel, the issue is more likely wrong credentials rather than session expiry.
---
@@ -162,11 +130,7 @@ These appear on the **Download Clients** panel in Settings, on individual client
**Cause:** The qBittorrent Web UI is not accessible at the configured host and port.
-!!! success "Solution"
- 1. Verify the host and port match the qBittorrent Web UI configuration.
- 2. Confirm qBittorrent is running with the Web UI enabled.
- 3. If qBittorrent is on a different machine or container, confirm the port is reachable from the Tracker Tracker container.
- 4. Check the SSL toggle — a mismatch between qBittorrent's actual scheme and what Tracker Tracker expects will cause failures.
+!!! success "Solution" 1. Verify the host and port match the qBittorrent Web UI configuration. 2. Confirm qBittorrent is running with the Web UI enabled. 3. If qBittorrent is on a different machine or container, confirm the port is reachable from the Tracker Tracker container. 4. Check the SSL toggle — a mismatch between qBittorrent's actual scheme and what Tracker Tracker expects will cause failures.
---
@@ -175,7 +139,7 @@ These appear on the **Download Clients** panel in Settings, on individual client
**Cause:** DNS resolution failed for the qBittorrent host.
!!! success "Solution"
- Use the container name (if qBittorrent is in the same Docker Compose stack) or an IP address instead of a hostname that depends on external DNS.
+Use the container name (if qBittorrent is in the same Docker Compose stack) or an IP address instead of a hostname that depends on external DNS.
---
@@ -184,7 +148,7 @@ These appear on the **Download Clients** panel in Settings, on individual client
**Cause:** qBittorrent accepted the connection but did not respond within 15 seconds. This can happen during heavy indexing operations.
!!! success "Solution"
- Wait for qBittorrent to finish processing and retry. If timeouts are persistent, check qBittorrent's CPU and memory usage.
+Wait for qBittorrent to finish processing and retry. If timeouts are persistent, check qBittorrent's CPU and memory usage.
---
@@ -197,7 +161,7 @@ These appear on the **Download Clients** panel in Settings, on individual client
**Cause:** The auto-lockout feature triggered. After a configurable number of consecutive failed login attempts, the app blocks further attempts until the lockout duration expires.
!!! success "Solution"
- Wait for the lockout duration to expire — the remaining time is shown on the login page. If you are locked out and cannot wait, you will need to connect to the database directly and clear the `lockedUntil` field in the `appSettings` table.
+Wait for the lockout duration to expire — the remaining time is shown on the login page. If you are locked out and cannot wait, you will need to connect to the database directly and clear the `lockedUntil` field in the `appSettings` table.
---
@@ -205,10 +169,7 @@ These appear on the **Download Clients** panel in Settings, on individual client
**Cause:** The 6-digit code was incorrect or expired. TOTP codes are valid for 30 seconds. Clock drift between your authenticator app and the server can cause valid-looking codes to fail.
-!!! success "Solution"
- 1. Ensure your device's clock is synchronized (NTP). Even a 30-60 second drift can invalidate codes.
- 2. Wait for your authenticator to cycle to the next code and try again.
- 3. If your clock is correct and codes still fail, use a **backup code** from when you set up TOTP. Backup codes are one-time use and follow the format `XXXX-XXXX`.
+!!! success "Solution" 1. Ensure your device's clock is synchronized (NTP). Even a 30-60 second drift can invalidate codes. 2. Wait for your authenticator to cycle to the next code and try again. 3. If your clock is correct and codes still fail, use a **backup code** from when you set up TOTP. Backup codes are one-time use and follow the format `XXXX-XXXX`.
---
@@ -217,7 +178,7 @@ These appear on the **Download Clients** panel in Settings, on individual client
**Cause:** You no longer have access to the authenticator app and have no backup codes.
!!! warning "Database access required"
- There is no in-app recovery path for this situation. You will need to connect to the PostgreSQL database directly and run:
+There is no in-app recovery path for this situation. You will need to connect to the PostgreSQL database directly and run:
```sql
UPDATE app_settings SET totp_secret = NULL, totp_backup_codes = NULL;
@@ -236,7 +197,7 @@ These appear on the **Download Clients** panel in Settings, on individual client
**Cause:** The URL you entered uses an unsupported scheme (e.g. `ftp://`) or is a bare hostname without a scheme.
!!! success "Solution"
- Prefix the URL with `https://` or `http://`.
+Prefix the URL with `https://` or `http://`.
---
@@ -245,4 +206,4 @@ These appear on the **Download Clients** panel in Settings, on individual client
**Cause:** The URL resolves to a private IP range (192.168.x.x, 10.x.x.x, 172.16-31.x.x) or localhost. Tracker Tracker blocks these to prevent your internal network from being probed through the app.
!!! info "By design"
- Tracker URLs must be publicly routable hostnames. Internal or self-hosted trackers only accessible via private IPs cannot be added.
+Tracker URLs must be publicly routable hostnames. Internal or self-hosted trackers only accessible via private IPs cannot be added.
diff --git a/docs/kb/docs/troubleshooting/ratio-not-updating.md b/docs/kb/docs/troubleshooting/ratio-not-updating.md
index a63bbccd..4359d59c 100644
--- a/docs/kb/docs/troubleshooting/ratio-not-updating.md
+++ b/docs/kb/docs/troubleshooting/ratio-not-updating.md
@@ -16,7 +16,7 @@ Go to **Settings → General** and find the **Tracker Poll Interval** field. The
If this is set to 240, stats will only update every 4 hours. That is expected behavior, not a bug.
!!! info "Minimum update rate"
- Setting the interval to 15 minutes is the fastest update rate available. Stats will not update more frequently than this regardless of how often you reload the page.
+Setting the interval to 15 minutes is the fastest update rate available. Stats will not update more frequently than this regardless of how often you reload the page.
---
@@ -82,4 +82,4 @@ If you have configured a **Snapshot Retention** period in Settings → General,
If retention is set to 7 days and you are looking at a 30-day chart, the older portion of the chart will be empty. That data was pruned.
!!! tip "Retention defaults"
- If retention is not configured, snapshots are kept forever. If you see gaps in older data, check the retention setting in Settings → General.
+If retention is not configured, snapshots are kept forever. If you see gaps in older data, check the retention setting in Settings → General.
diff --git a/docs/kb/docs/troubleshooting/tracker-offline.md b/docs/kb/docs/troubleshooting/tracker-offline.md
index c5474d62..db485b05 100644
--- a/docs/kb/docs/troubleshooting/tracker-offline.md
+++ b/docs/kb/docs/troubleshooting/tracker-offline.md
@@ -38,7 +38,7 @@ When this happens:
Paused trackers are skipped entirely until you manually resume them.
!!! warning "Verify the cause before resuming"
- The banner reads: _"Polling was paused after repeated failures. Verify your API key is correct before resuming."_ If you resume without fixing the underlying problem, the tracker will fail again immediately and re-pause within the same poll cycle.
+The banner reads: _"Polling was paused after repeated failures. Verify your API key is correct before resuming."_ If you resume without fixing the underlying problem, the tracker will fail again immediately and re-pause within the same poll cycle.
---
@@ -61,13 +61,7 @@ This is the most common cause. Your tracker API key may have been regenerated, r
**Symptom:** The Poll Error Banner shows `Authentication failed`.
-!!! success "Solution"
- 1. Log into your tracker's website and go to your profile or security settings.
- 2. Find your API token (often under "Security", "API", or "Edit Profile").
- 3. Copy the current token.
- 4. In Tracker Tracker, open the tracker's settings (edit icon on the tracker detail page or tracker list).
- 5. Paste the new token into the API Token field and save.
- 6. Click **Resume Polling**, then **Poll Now** to verify.
+!!! success "Solution" 1. Log into your tracker's website and go to your profile or security settings. 2. Find your API token (often under "Security", "API", or "Edit Profile"). 3. Copy the current token. 4. In Tracker Tracker, open the tracker's settings (edit icon on the tracker detail page or tracker list). 5. Paste the new token into the API Token field and save. 6. Click **Resume Polling**, then **Poll Now** to verify.
---
@@ -77,11 +71,7 @@ If a tracker has **Use Proxy** enabled, Tracker Tracker will not fall back to a
**Symptom:** The Poll Error Banner shows `Proxy connection failed`. The tracker has "Use Proxy" toggled on in its settings.
-!!! success "Solution"
- 1. Go to **Settings → Proxy** and verify the proxy host, port, type (SOCKS5/HTTP/HTTPS), and credentials.
- 2. Confirm the proxy server is running and reachable from your Docker host.
- 3. If you want to bypass the proxy for this tracker, edit the tracker settings and disable **Use Proxy**.
- 4. Resume polling after fixing.
+!!! success "Solution" 1. Go to **Settings → Proxy** and verify the proxy host, port, type (SOCKS5/HTTP/HTTPS), and credentials. 2. Confirm the proxy server is running and reachable from your Docker host. 3. If you want to bypass the proxy for this tracker, edit the tracker settings and disable **Use Proxy**. 4. Resume polling after fixing.
---
@@ -91,11 +81,7 @@ The tracker's hostname cannot be resolved. This may be a temporary DNS outage, a
**Symptom:** The Poll Error Banner shows `Host not found`.
-!!! success "Solution"
- 1. Check the tracker's base URL for typos in the hostname.
- 2. From your Docker host, test resolution: `nslookup tracker.example.com`.
- 3. If you use custom DNS or a VPN, confirm DNS is available inside the container network. You may need to add a `dns:` entry to your `docker-compose.yml`.
- 4. If the tracker's domain has changed, update the base URL in the tracker settings.
+!!! success "Solution" 1. Check the tracker's base URL for typos in the hostname. 2. From your Docker host, test resolution: `nslookup tracker.example.com`. 3. If you use custom DNS or a VPN, confirm DNS is available inside the container network. You may need to add a `dns:` entry to your `docker-compose.yml`. 4. If the tracker's domain has changed, update the base URL in the tracker settings.
---
@@ -105,11 +91,7 @@ The hostname resolved but the connection was refused or the host was unreachable
**Symptom:** The Poll Error Banner shows `Connection refused` or `Host unreachable`.
-!!! success "Solution"
- 1. Check whether the tracker site loads in your browser.
- 2. If the tracker is down, wait for it to recover, then resume polling.
- 3. If you route tracker traffic through a VPN, confirm the VPN is up.
- 4. Verify the base URL uses the correct scheme (`https://` vs `http://`) and the right port if the tracker uses a non-standard one.
+!!! success "Solution" 1. Check whether the tracker site loads in your browser. 2. If the tracker is down, wait for it to recover, then resume polling. 3. If you route tracker traffic through a VPN, confirm the VPN is up. 4. Verify the base URL uses the correct scheme (`https://` vs `http://`) and the right port if the tracker uses a non-standard one.
---
@@ -119,10 +101,7 @@ The connection was established but the tracker API did not respond within 15 sec
**Symptom:** The Poll Error Banner shows `Request timed out`.
-!!! success "Solution"
- 1. This is often transient. Try **Poll Now** again after a few minutes.
- 2. If timeouts are persistent, check whether a proxy is adding significant latency.
- 3. If the tracker's API is consistently slow, there is no configurable timeout override.
+!!! success "Solution" 1. This is often transient. Try **Poll Now** again after a few minutes. 2. If timeouts are persistent, check whether a proxy is adding significant latency. 3. If the tracker's API is consistently slow, there is no configurable timeout override.
---
@@ -132,10 +111,7 @@ Some trackers block repeated requests from a single IP if polls come in too freq
**Symptom:** The Poll Error Banner shows `IP temporarily banned by tracker`.
-!!! success "Solution"
- 1. Go to **Settings → General** and increase the **Poll Interval**. The minimum is 15 minutes, but 60 minutes (the default) is recommended for most trackers.
- 2. Wait for the IP ban to expire on the tracker side — this varies by site, typically minutes to hours.
- 3. Resume polling only after the ban has likely cleared.
+!!! success "Solution" 1. Go to **Settings → General** and increase the **Poll Interval**. The minimum is 15 minutes, but 60 minutes (the default) is recommended for most trackers. 2. Wait for the IP ban to expire on the tracker side — this varies by site, typically minutes to hours. 3. Resume polling only after the ban has likely cleared.
---
@@ -144,4 +120,4 @@ Some trackers block repeated requests from a single IP if polls come in too freq
If polling has not paused but you still see a **Last Error** banner — without the "Polling Paused" heading — it means a recent poll failed but not enough times to trigger a pause. The banner clears automatically on the next successful poll.
!!! info "No action needed if the last poll succeeded"
- The Last Error banner always shows the most recent error, even if subsequent polls recovered. If the PulseDot is `healthy`, `warning`, or `critical` (not `error` or `paused`), polling has already recovered on its own.
+The Last Error banner always shows the most recent error, even if subsequent polls recovered. If the PulseDot is `healthy`, `warning`, or `critical` (not `error` or `paused`), polling has already recovered on its own.
diff --git a/docs/kb/mkdocs.yml b/docs/kb/mkdocs.yml
index c6bcb036..79d2f96a 100644
--- a/docs/kb/mkdocs.yml
+++ b/docs/kb/mkdocs.yml
@@ -13,84 +13,85 @@ theme:
primary: cyan
accent: cyan
features:
- - navigation.instant
- - navigation.sections
- - navigation.top
- - navigation.expand
- - content.code.copy
- - content.action.edit
+ - navigation.instant
+ - navigation.sections
+ - navigation.top
+ - navigation.expand
+ - content.code.copy
+ - content.action.edit
icon:
repo: fontawesome/brands/github
markdown_extensions:
-- admonition
-- pymdownx.details
-- pymdownx.superfences
-- pymdownx.tabbed:
- alternate_style: true
-- pymdownx.highlight:
- anchor_linenums: true
-- pymdownx.inlinehilite
-- attr_list
-- md_in_html
-- toc:
- permalink: true
+ - admonition
+ - pymdownx.details
+ - pymdownx.superfences
+ - pymdownx.tabbed:
+ alternate_style: true
+ - pymdownx.highlight:
+ anchor_linenums: true
+ - pymdownx.inlinehilite
+ - attr_list
+ - md_in_html
+ - toc:
+ permalink: true
nav:
-- Home: index.md
-- Getting Started:
- - Installation: getting-started/installation.md
- - First Setup: getting-started/first-setup.md
- - Docker Configuration: getting-started/docker-config.md
-- Trackers:
- - Adding a Tracker: trackers/adding-a-tracker.md
-- Features:
- - Proxy Support: features/proxies.md
- - Two-Factor Auth (TOTP): features/totp.md
- - Backups & Restore: features/backups.md
- - Download Clients: features/download-clients.md
- - Tag Groups: features/tag-groups.md
- - qbitmanage Integration: features/qbitmanage.md
- - Webhooks: features/webhooks.md
-- Reference:
- - Settings Guide: reference/settings.md
- - Stats Explained: reference/stats-explained.md
- - Platform Differences: reference/platform-differences.md
-- Troubleshooting:
- - Tracker Shows Offline: troubleshooting/tracker-offline.md
- - Ratio Not Updating: troubleshooting/ratio-not-updating.md
- - Common Errors: troubleshooting/common-errors.md
-- Contributing:
- - Overview: contributing/index.md
- - Adding a Tracker: contributing/adding-a-tracker.md
- - Bento Grid Slot System: contributing/slot-system.md
- - Tracker API Responses:
- - Overview: contributing/tracker-responses.md
- - UNIT3D: contributing/tracker-responses-unit3d.md
- - Gazelle: contributing/tracker-responses-gazelle.md
- - GGn: contributing/tracker-responses-ggn.md
- - Individual Trackers:
- - Aither: contributing/trackers/aither.md
- - AlphaRatio: contributing/trackers/alpharatio.md
- - AnimeBytes: contributing/trackers/animebytes.md
- - Anthelion: contributing/trackers/anthelion.md
- - Blutopia: contributing/trackers/blutopia.md
- - BroadcasTheNet: contributing/trackers/broadcasthenet.md
- - Concertos: contributing/trackers/concertos.md
- - Empornium: contributing/trackers/empornium.md
- - FearNoPeer: contributing/trackers/fearnopeer.md
- - GazelleGames: contributing/trackers/gazellegames.md
- - Great Poster Wall: contributing/trackers/greatposterwall.md
- - LST: contributing/trackers/lst.md
- - MoreThanTV: contributing/trackers/morethantv.md
- - Nebulance: contributing/trackers/nebulance.md
- - OldToons: contributing/trackers/oldtoons.md
- - OnlyEncodes: contributing/trackers/onlyencodes.md
- - Orpheus: contributing/trackers/orpheus.md
- - PassThePopcorn: contributing/trackers/passthepopcorn.md
- - Phoenix Project: contributing/trackers/phoenixproject.md
- - Racing4Everyone: contributing/trackers/racing4everyone.md
- - REDacted: contributing/trackers/redacted.md
- - Reelflix: contributing/trackers/reelflix.md
- - SkipTheCommercials: contributing/trackers/skipthecommercials.md
- - Upload.cx: contributing/trackers/uploadcx.md
+ - Home: index.md
+ - Getting Started:
+ - Installation: getting-started/installation.md
+ - First Setup: getting-started/first-setup.md
+ - Docker Configuration: getting-started/docker-config.md
+ - Trackers:
+ - Adding a Tracker: trackers/adding-a-tracker.md
+ - Features:
+ - Proxy Support: features/proxies.md
+ - Two-Factor Auth (TOTP): features/totp.md
+ - Backups & Restore: features/backups.md
+ - Download Clients: features/download-clients.md
+ - Tag Groups: features/tag-groups.md
+ - qbitmanage Integration: features/qbitmanage.md
+ - Webhooks: features/webhooks.md
+ - Reference:
+ - Settings Guide: reference/settings.md
+ - Stats Explained: reference/stats-explained.md
+ - Platform Differences: reference/platform-differences.md
+ - Troubleshooting:
+ - Tracker Shows Offline: troubleshooting/tracker-offline.md
+ - Ratio Not Updating: troubleshooting/ratio-not-updating.md
+ - Common Errors: troubleshooting/common-errors.md
+ - Contributing:
+ - Overview: contributing/index.md
+ - Adding a Tracker: contributing/adding-a-tracker.md
+ - Bento Grid Slot System: contributing/slot-system.md
+ - Tracker API Responses:
+ - Overview: contributing/tracker-responses.md
+ - UNIT3D: contributing/tracker-responses-unit3d.md
+ - Gazelle: contributing/tracker-responses-gazelle.md
+ - GGn: contributing/tracker-responses-ggn.md
+ - Individual Trackers:
+ - Aither: contributing/trackers/aither.md
+ - AlphaRatio: contributing/trackers/alpharatio.md
+ - AnimeBytes: contributing/trackers/animebytes.md
+ - Anthelion: contributing/trackers/anthelion.md
+ - Blutopia: contributing/trackers/blutopia.md
+ - BroadcasTheNet: contributing/trackers/broadcasthenet.md
+ - Concertos: contributing/trackers/concertos.md
+ - Empornium: contributing/trackers/empornium.md
+ - FearNoPeer: contributing/trackers/fearnopeer.md
+ - GazelleGames: contributing/trackers/gazellegames.md
+ - Great Poster Wall: contributing/trackers/greatposterwall.md
+ - LST: contributing/trackers/lst.md
+ - MoreThanTV: contributing/trackers/morethantv.md
+ - Nebulance: contributing/trackers/nebulance.md
+ - OldToons: contributing/trackers/oldtoons.md
+ - OnlyEncodes: contributing/trackers/onlyencodes.md
+ - Orpheus: contributing/trackers/orpheus.md
+ - PassThePopcorn: contributing/trackers/passthepopcorn.md
+ - Phoenix Project: contributing/trackers/phoenixproject.md
+ - Racing4Everyone: contributing/trackers/racing4everyone.md
+ - REDacted: contributing/trackers/redacted.md
+ - Seed Pool: contributing/trackers/seedpool.md
+ - Reelflix: contributing/trackers/reelflix.md
+ - SkipTheCommercials: contributing/trackers/skipthecommercials.md
+ - Upload.cx: contributing/trackers/uploadcx.md
diff --git a/package.json b/package.json
index 7c3f0b44..09df15c7 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "private-tracker-tracker",
- "version": "2.3.0",
+ "version": "2.4.1",
"description": "Self-hosted dashboard for monitoring private tracker stats over time",
"license": "GPL-3.0",
"repository": {
@@ -60,7 +60,7 @@
"emoji-picker-react": "^4.18.0",
"https-proxy-agent": "^8.0.0",
"jose": "^6.2.1",
- "next": "16.1.7",
+ "next": "16.2.0",
"node-cron": "^4.2.1",
"otpauth": "^9.5.0",
"pino": "^10.3.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 543117e8..21b10099 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -63,8 +63,8 @@ importers:
specifier: ^6.2.1
version: 6.2.1
next:
- specifier: 16.1.7
- version: 16.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ specifier: 16.2.0
+ version: 16.2.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
node-cron:
specifier: ^4.2.1
version: 4.2.1
@@ -771,57 +771,57 @@ packages:
'@napi-rs/wasm-runtime@1.1.1':
resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==}
- '@next/env@16.1.7':
- resolution: {integrity: sha512-rJJbIdJB/RQr2F1nylZr/PJzamvNNhfr3brdKP6s/GW850jbtR70QlSfFselvIBbcPUOlQwBakexjFzqLzF6pg==}
+ '@next/env@16.2.0':
+ resolution: {integrity: sha512-OZIbODWWAi0epQRCRjNe1VO45LOFBzgiyqmTLzIqWq6u1wrxKnAyz1HH6tgY/Mc81YzIjRPoYsPAEr4QV4l9TA==}
- '@next/swc-darwin-arm64@16.1.7':
- resolution: {integrity: sha512-b2wWIE8sABdyafc4IM8r5Y/dS6kD80JRtOGrUiKTsACFQfWWgUQ2NwoUX1yjFMXVsAwcQeNpnucF2ZrujsBBPg==}
+ '@next/swc-darwin-arm64@16.2.0':
+ resolution: {integrity: sha512-/JZsqKzKt01IFoiLLAzlNqys7qk2F3JkcUhj50zuRhKDQkZNOz9E5N6wAQWprXdsvjRP4lTFj+/+36NSv5AwhQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
- '@next/swc-darwin-x64@16.1.7':
- resolution: {integrity: sha512-zcnVaaZulS1WL0Ss38R5Q6D2gz7MtBu8GZLPfK+73D/hp4GFMrC2sudLky1QibfV7h6RJBJs/gOFvYP0X7UVlQ==}
+ '@next/swc-darwin-x64@16.2.0':
+ resolution: {integrity: sha512-/hV8erWq4SNlVgglUiW5UmQ5Hwy5EW/AbbXlJCn6zkfKxTy/E/U3V8U1Ocm2YCTUoFgQdoMxRyRMOW5jYy4ygg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
- '@next/swc-linux-arm64-gnu@16.1.7':
- resolution: {integrity: sha512-2ant89Lux/Q3VyC8vNVg7uBaFVP9SwoK2jJOOR0L8TQnX8CAYnh4uctAScy2Hwj2dgjVHqHLORQZJ2wH6VxhSQ==}
+ '@next/swc-linux-arm64-gnu@16.2.0':
+ resolution: {integrity: sha512-GkjL/Q7MWOwqWR9zoxu1TIHzkOI2l2BHCf7FzeQG87zPgs+6WDh+oC9Sw9ARuuL/FUk6JNCgKRkA6rEQYadUaw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
- '@next/swc-linux-arm64-musl@16.1.7':
- resolution: {integrity: sha512-uufcze7LYv0FQg9GnNeZ3/whYfo+1Q3HnQpm16o6Uyi0OVzLlk2ZWoY7j07KADZFY8qwDbsmFnMQP3p3+Ftprw==}
+ '@next/swc-linux-arm64-musl@16.2.0':
+ resolution: {integrity: sha512-1ffhC6KY5qWLg5miMlKJp3dZbXelEfjuXt1qcp5WzSCQy36CV3y+JT7OC1WSFKizGQCDOcQbfkH/IjZP3cdRNA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
- '@next/swc-linux-x64-gnu@16.1.7':
- resolution: {integrity: sha512-KWVf2gxYvHtvuT+c4MBOGxuse5TD7DsMFYSxVxRBnOzok/xryNeQSjXgxSv9QpIVlaGzEn/pIuI6Koosx8CGWA==}
+ '@next/swc-linux-x64-gnu@16.2.0':
+ resolution: {integrity: sha512-FmbDcZQ8yJRq93EJSL6xaE0KK/Rslraf8fj1uViGxg7K4CKBCRYSubILJPEhjSgZurpcPQq12QNOJQ0DRJl6Hg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
- '@next/swc-linux-x64-musl@16.1.7':
- resolution: {integrity: sha512-HguhaGwsGr1YAGs68uRKc4aGWxLET+NevJskOcCAwXbwj0fYX0RgZW2gsOCzr9S11CSQPIkxmoSbuVaBp4Z3dA==}
+ '@next/swc-linux-x64-musl@16.2.0':
+ resolution: {integrity: sha512-HzjIHVkmGAwRbh/vzvoBWWEbb8BBZPxBvVbDQDvzHSf3D8RP/4vjw7MNLDXFF9Q1WEzeQyEj2zdxBtVAHu5Oyw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
- '@next/swc-win32-arm64-msvc@16.1.7':
- resolution: {integrity: sha512-S0n3KrDJokKTeFyM/vGGGR8+pCmXYrjNTk2ZozOL1C/JFdfUIL9O1ATaJOl5r2POe56iRChbsszrjMAdWSv7kQ==}
+ '@next/swc-win32-arm64-msvc@16.2.0':
+ resolution: {integrity: sha512-UMiFNQf5H7+1ZsZPxEsA064WEuFbRNq/kEXyepbCnSErp4f5iut75dBA8UeerFIG3vDaQNOfCpevnERPp2V+nA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
- '@next/swc-win32-x64-msvc@16.1.7':
- resolution: {integrity: sha512-mwgtg8CNZGYm06LeEd+bNnOUfwOyNem/rOiP14Lsz+AnUY92Zq/LXwtebtUiaeVkhbroRCQ0c8GlR4UT1U+0yg==}
+ '@next/swc-win32-x64-msvc@16.2.0':
+ resolution: {integrity: sha512-DRrNJKW+/eimrZgdhVN1uvkN1OI4j6Lpefwr44jKQ0YQzztlmOBUUzHuV5GxOMPK3nmodAYElUVCY8ZXo/IWeA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@@ -1526,8 +1526,8 @@ packages:
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
- baseline-browser-mapping@2.10.8:
- resolution: {integrity: sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ==}
+ baseline-browser-mapping@2.10.9:
+ resolution: {integrity: sha512-OZd0e2mU11ClX8+IdXe3r0dbqMEznRiT4TfbhYIbcRPZkqJ7Qwer8ij3GZAmLsRKa+II9V1v5czCkvmHH3XZBg==}
engines: {node: '>=6.0.0'}
hasBin: true
@@ -2651,8 +2651,8 @@ packages:
neo-async@2.6.2:
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
- next@16.1.7:
- resolution: {integrity: sha512-WM0L7WrSvKwoLegLYr6V+mz+RIofqQgVAfHhMp9a88ms0cFX8iX9ew+snpWlSBwpkURJOUdvCEt3uLl3NNzvWg==}
+ next@16.2.0:
+ resolution: {integrity: sha512-NLBVrJy1pbV1Yn00L5sU4vFyAHt5XuSjzrNyFnxo6Com0M0KrL6hHM5B99dbqXb2bE9pm4Ow3Zl1xp6HVY9edQ==}
engines: {node: '>=20.9.0'}
hasBin: true
peerDependencies:
@@ -3920,7 +3920,7 @@ snapshots:
'@img/sharp-wasm32@0.34.5':
dependencies:
- '@emnapi/runtime': 1.9.0
+ '@emnapi/runtime': 1.9.1
optional: true
'@img/sharp-win32-arm64@0.34.5':
@@ -3958,30 +3958,30 @@ snapshots:
'@tybys/wasm-util': 0.10.1
optional: true
- '@next/env@16.1.7': {}
+ '@next/env@16.2.0': {}
- '@next/swc-darwin-arm64@16.1.7':
+ '@next/swc-darwin-arm64@16.2.0':
optional: true
- '@next/swc-darwin-x64@16.1.7':
+ '@next/swc-darwin-x64@16.2.0':
optional: true
- '@next/swc-linux-arm64-gnu@16.1.7':
+ '@next/swc-linux-arm64-gnu@16.2.0':
optional: true
- '@next/swc-linux-arm64-musl@16.1.7':
+ '@next/swc-linux-arm64-musl@16.2.0':
optional: true
- '@next/swc-linux-x64-gnu@16.1.7':
+ '@next/swc-linux-x64-gnu@16.2.0':
optional: true
- '@next/swc-linux-x64-musl@16.1.7':
+ '@next/swc-linux-x64-musl@16.2.0':
optional: true
- '@next/swc-win32-arm64-msvc@16.1.7':
+ '@next/swc-win32-arm64-msvc@16.2.0':
optional: true
- '@next/swc-win32-x64-msvc@16.1.7':
+ '@next/swc-win32-x64-msvc@16.2.0':
optional: true
'@noble/hashes@2.0.1': {}
@@ -4484,7 +4484,7 @@ snapshots:
balanced-match@1.0.2: {}
- baseline-browser-mapping@2.10.8: {}
+ baseline-browser-mapping@2.10.9: {}
bidi-js@1.0.3:
dependencies:
@@ -5728,25 +5728,25 @@ snapshots:
neo-async@2.6.2: {}
- next@16.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
+ next@16.2.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
- '@next/env': 16.1.7
+ '@next/env': 16.2.0
'@swc/helpers': 0.5.15
- baseline-browser-mapping: 2.10.8
+ baseline-browser-mapping: 2.10.9
caniuse-lite: 1.0.30001780
postcss: 8.4.31
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
styled-jsx: 5.1.6(react@19.2.4)
optionalDependencies:
- '@next/swc-darwin-arm64': 16.1.7
- '@next/swc-darwin-x64': 16.1.7
- '@next/swc-linux-arm64-gnu': 16.1.7
- '@next/swc-linux-arm64-musl': 16.1.7
- '@next/swc-linux-x64-gnu': 16.1.7
- '@next/swc-linux-x64-musl': 16.1.7
- '@next/swc-win32-arm64-msvc': 16.1.7
- '@next/swc-win32-x64-msvc': 16.1.7
+ '@next/swc-darwin-arm64': 16.2.0
+ '@next/swc-darwin-x64': 16.2.0
+ '@next/swc-linux-arm64-gnu': 16.2.0
+ '@next/swc-linux-arm64-musl': 16.2.0
+ '@next/swc-linux-x64-gnu': 16.2.0
+ '@next/swc-linux-x64-musl': 16.2.0
+ '@next/swc-win32-arm64-msvc': 16.2.0
+ '@next/swc-win32-x64-msvc': 16.2.0
sharp: 0.34.5
transitivePeerDependencies:
- '@babel/core'
diff --git a/scripts/security-audit.ts b/scripts/security-audit.ts
index 64c06297..c28f84f2 100644
--- a/scripts/security-audit.ts
+++ b/scripts/security-audit.ts
@@ -906,7 +906,7 @@ function checkTimingSafeComparison(): CheckResult {
if (!eqMatch) continue
const lhs = eqMatch[1].trim()
- const rhs = eqMatch[2].split(/[;,)]/)[0].trim()
+ const rhs = eqMatch[2].split(/[;,)?\s]/)[0].trim()
const lhsIsSecret = SECRET_VAR_RE.test(lhs)
const rhsIsSecret = SECRET_VAR_RE.test(rhs)
diff --git a/scripts/validate-trackers.ts b/scripts/validate-trackers.ts
index 7b9cb2f3..d83c44e3 100644
--- a/scripts/validate-trackers.ts
+++ b/scripts/validate-trackers.ts
@@ -181,6 +181,60 @@ function validate(slugFilter?: string[]): TrackerResult[] {
}
}
+ // ── Canonical field presence (every field must be explicitly set) ─
+ const obj = tracker as unknown as Record
+ const CANONICAL_FIELDS = [
+ "slug",
+ "name",
+ "abbreviation",
+ "url",
+ "description",
+ "platform",
+ "apiPath",
+ "specialty",
+ "contentCategories",
+ "language",
+ "color",
+ "logo",
+ "trackerHubSlug",
+ "statusPageUrl",
+ "userClasses",
+ "releaseGroups",
+ "bannedGroups",
+ "notableMembers",
+ "rules",
+ "warning",
+ "warningNote",
+ "draft",
+ "supportsTransitPapers",
+ "profileUrlPattern",
+ ] as const
+ for (const field of CANONICAL_FIELDS) {
+ if (!(field in obj)) {
+ errors.push(`Missing canonical field "${field}" — all fields must be explicitly present`)
+ }
+ }
+
+ // ── Platform-specific field checks ─────────────────────────────────
+ if (tracker.platform === "gazelle" && !tracker.gazelleEnrich) {
+ errors.push("Gazelle trackers must have gazelleEnrich: true")
+ }
+
+ // ── Transit Papers validation ────────────────────────────────────
+ if (tracker.supportsTransitPapers) {
+ if (!tracker.profileUrlPattern) {
+ errors.push("supportsTransitPapers is true but profileUrlPattern is missing")
+ } else {
+ const pattern = tracker.profileUrlPattern
+ if (!pattern.includes("{id}") && !pattern.includes("{username}")) {
+ errors.push(`profileUrlPattern must contain {id} or {username} (got "${pattern}")`)
+ }
+ }
+ }
+ if (tracker.profileUrlPattern && !tracker.supportsTransitPapers) {
+ warnings.push("profileUrlPattern defined but supportsTransitPapers is not true")
+ }
+
// ── Warn-level fields ─────────────────────────────────────────────
if (isEmpty(tracker.abbreviation)) warnings.push("Missing abbreviation")
if (isEmpty(tracker.specialty)) warnings.push("Missing specialty")
diff --git a/src/app/(auth)/DashboardClient.tsx b/src/app/(auth)/DashboardClient.tsx
index 6c6afd7d..a3e70c16 100644
--- a/src/app/(auth)/DashboardClient.tsx
+++ b/src/app/(auth)/DashboardClient.tsx
@@ -4,9 +4,9 @@
"use client"
-import { useMemo, useState } from "react"
import { H1, H2 } from "@typography"
-import { CHART_THEME } from "@/components/charts/theme"
+import { useMemo, useState } from "react"
+import { CHART_THEME } from "@/components/charts/lib/theme"
import { AlertsBanner } from "@/components/dashboard/AlertsBanner"
import { AnalyticsSection } from "@/components/dashboard/AnalyticsSection"
import { DashboardSettingsSheet } from "@/components/dashboard/DashboardSettingsSheet"
@@ -155,11 +155,12 @@ export function DashboardClient({ initialTrackers }: DashboardClientProps) {
{/* Analytics / Fleet */}