Releases: Abrechen2/TravStats
TravStats v1.1.0
Added
- Birthday Flight achievement — New
BIRTHDAY_FLIGHTachievement counts flown flights that depart on the user's birthday (month + day, year irrelevant). Profile settings now include an optional birthdate field used by the check. - Kurios calendar easter eggs — Six hidden achievements tied to aviation and calendar observances: ICAO Day (7 Dec), Wright Day (19 Aug), May the Fourth (4 May), Pi Day (14 Mar), Pi Precision (3 141 km ± 5 % on Pi Day) and Halloween (31 Oct).
- Branded GlobeLoader — Monochrome spinning-globe loader replaces the generic spinner at every large-area loading surface (maps, stats, flight pages, admin). Paints in < 16 ms so users see feedback before deck.gl / three.js finish booting, holds for ≥ 2 s to avoid flashing, and keeps spinning regardless of
prefers-reduced-motion. Reads live CSS tokens so it reacts to dark-mode toggles without remount. - New logo system (v1.0) — Luggage-tag mark with
TSmonogram and a cross-dotTRAV✛STATSwordmark ship asLogoMark,LogoMarkFilled,LogoWordmarkandLogoLockupcomponents. Visible in the nav header (desktop + mobile), all four auth screens and the favicon (SVG with dark background + PNG fallback). The README and Unraid forum / release docs point atdocs/images/logo.svg. - App version vs build version split — Runtime now exposes the deployed app version (from
backend/VERSION) and the Docker image build version as distinct fields, so About can show1.1.0 (built from 1.1.0-rc.3)during the RC cycle.
Changed
- Achievements modules split —
backend/src/utils/achievements.ts(1 073 lines) reorganised into three files (orchestrator + stats + checks);backend/src/data/achievements.ts(1 306 lines) split into two seed parts plus a thin composition layer. Public API unchanged. Every file now respects the 800-line cap. - Achievement stats are immutable —
checkAndUpdateAchievementsbuilds a freshaugmentedStatsvia spread rather than mutating the object returned fromcalculateUserStats;Sets are cloned, never mutated in place.
Fixed
- GlobeLoader no longer freezes for reduced-motion users — The loader is feedback, not decoration; it now always animates.
- Favicon visible on light browser chrome — The new SVG favicon shipped transparent, which made the amber strokes disappear on Safari / iOS tab bars. A dark background is now baked into the SVG.
LogoMarkFilledsurvives theme switches — Inverse colour used to be hardcoded#0b0d10; now readsvar(--bg-base)so the mark stays legible in light mode.- Screen-reader announcements for the header logo and mobile drawer — Home link now announces as "TravStats — Home" once, not the visible text three times. Mobile drawer Donate / Star links carry the
aria-labelattributes their desktop twins already had. GlobeLoadercanvas no longer churns on parent rerenders — Derivedbufferdimension is memoised so the effect dep array is stable.
Database
users.birthdate— New nullableTIMESTAMPcolumn backing the Birthday Flight achievement. Additive only; no data transform; safe to deploy with zero downtime.
Tests
- Brand/Logo component suite — 9 new Vitest tests cover the four logo components (a11y attributes, theme-token defaults, layout modes). Total frontend coverage is now 257 tests across 54 files.
TravStats v1.1.0-rc.1
Added
- Birthday Flight achievement — New
BIRTHDAY_FLIGHTachievement counts flown flights that depart on the user's birthday (month + day, year irrelevant). Profile settings now include an optional birthdate field used by the check. - Kurios calendar easter eggs — Six hidden achievements tied to aviation and calendar observances: ICAO Day (7 Dec), Wright Day (19 Aug), May the Fourth (4 May), Pi Day (14 Mar), Pi Precision (3 141 km ± 5 % on Pi Day) and Halloween (31 Oct).
- Branded GlobeLoader — Monochrome spinning-globe loader replaces the generic spinner at every large-area loading surface (maps, stats, flight pages, admin). Paints in < 16 ms so users see feedback before deck.gl / three.js finish booting, holds for ≥ 2 s to avoid flashing, and keeps spinning regardless of
prefers-reduced-motion. Reads live CSS tokens so it reacts to dark-mode toggles without remount. - New logo system (v1.0) — Luggage-tag mark with
TSmonogram and a cross-dotTRAV✛STATSwordmark ship asLogoMark,LogoMarkFilled,LogoWordmarkandLogoLockupcomponents. Visible in the nav header (desktop + mobile), all four auth screens and the favicon (SVG with dark background + PNG fallback). The README and Unraid forum / release docs point atdocs/images/logo.svg. - App version vs build version split — Runtime now exposes the deployed app version (from
backend/VERSION) and the Docker image build version as distinct fields, so About can show1.1.0 (built from 1.1.0-rc.3)during the RC cycle.
Changed
- Achievements modules split —
backend/src/utils/achievements.ts(1 073 lines) reorganised into three files (orchestrator + stats + checks);backend/src/data/achievements.ts(1 306 lines) split into two seed parts plus a thin composition layer. Public API unchanged. Every file now respects the 800-line cap. - Achievement stats are immutable —
checkAndUpdateAchievementsbuilds a freshaugmentedStatsvia spread rather than mutating the object returned fromcalculateUserStats;Sets are cloned, never mutated in place.
Fixed
- GlobeLoader no longer freezes for reduced-motion users — The loader is feedback, not decoration; it now always animates.
- Favicon visible on light browser chrome — The new SVG favicon shipped transparent, which made the amber strokes disappear on Safari / iOS tab bars. A dark background is now baked into the SVG.
LogoMarkFilledsurvives theme switches — Inverse colour used to be hardcoded#0b0d10; now readsvar(--bg-base)so the mark stays legible in light mode.- Screen-reader announcements for the header logo and mobile drawer — Home link now announces as "TravStats — Home" once, not the visible text three times. Mobile drawer Donate / Star links carry the
aria-labelattributes their desktop twins already had. GlobeLoadercanvas no longer churns on parent rerenders — Derivedbufferdimension is memoised so the effect dep array is stable.
Database
users.birthdate— New nullableTIMESTAMPcolumn backing the Birthday Flight achievement. Additive only; no data transform; safe to deploy with zero downtime.
Tests
- Brand/Logo component suite — 9 new Vitest tests cover the four logo components (a11y attributes, theme-token defaults, layout modes). Total frontend coverage is now 257 tests across 54 files.
TravStats v1.0.1
Security
- Base images refreshed and patched — Builders moved from
node:20-alpinetonode:22-alpine; production stage moved fromnode:20-slimtonode:22-bookworm-slim. The production image now also runsapt-get upgradeat build time so every rebuild pulls the latest Debian security feed on top of the base layer. Closes the Critical CVE-2026-6100 and eleven High-severity npm CVEs (tar, minimatch, cross-spawn) that Docker Scout reported againstv1.0.0. Drop-in replacement — no schema, config, or runtime behaviour changes. - CI aligned with runtime — GitHub Actions now runs backend and frontend checks on Node 22 to match the container image.
v1.0.1-security-rc.1 (Release Candidate)
Security patch — Release Candidate
Docker Scout flagged the v1.0.0 image for one Critical (CVE-2026-6100, Debian) and eleven High npm CVEs (tar, minimatch, cross-spawn). This RC rebuilds the image with refreshed base layers and pulls all pending Debian security patches.
Changed
- Base images bumped — Dockerfile builders moved from
node:20-alpinetonode:22-alpine; production stage moved fromnode:20-slimtonode:22-bookworm-slim. - Debian security patches applied at build time — added
apt-get upgrade -y --no-install-recommendsso every rebuild pulls the latest Debian security feed on top of the base image. - CI aligned — GitHub Actions runs backend + frontend on Node 22 to match the production runtime.
Verification
- Prod container runs on Node
v22.22.2. /healthreturnsok.- No schema or runtime config changes — drop-in replacement for
v1.0.0.
Next
This RC is deployed on the reference environment for UAT. Promote to v1.0.1 once verified — it will retag the exact same GHCR image (no rebuild), mirror to Docker Hub, and replace :latest / :stable.
Image: ghcr.io/abrechen2/travstats:1.0.1-security-rc.1
Digest: sha256:d344388b584a6e4b0bf02de088064ffb8c3a7da9cd83c4a4fd28f7e92c724ac5
Branch: security/docker-scout-patch (commit 2edcaad)
TravStats v1.0.0
TravStats v1.0.0 — the first public release ✈️
Six months of daily use, 22 resolved pentest findings, a full switch to DB-backed config and a thousand tiny UX tweaks later, TravStats goes stable. One container, one environment variable, the setup wizard handles the rest. No telemetry, no account on anyone else's server, no ads — your flights stay on your own machine.
"I just wanted to know where I've been — without handing that to a company."
That's the whole thesis. TravStats is for 1–10 users who want their travel history to outlive a startup's pivot.
What you can actually do with it
✈️ Track flights the way that fits you
- Manual entry with categories, tags, up to 50 travel companions, cost + currency
- Boarding-pass scanner — QR, PDF417 and OCR fallback with automatic airline/airport resolution
- Email & PDF import — plain text, HTML, Outlook
.msg,.eml, with optional local LLM parsing via Ollama (gemma3:12bbenchmarks 100% accuracy on our test corpus) - Five flight states — flown · scheduled · cancelled · historical · duplicated, each with its own form, validation and stats treatment
- Duplicate detection with a confirmed "save anyway" escape hatch when you actually did fly the same number twice in a day
🗺️ Six map modes on one canvas
Routes · Heatmap · Hexagon (3D) · 3D Columns · animated Trips · 3D Globe. Built on deck.gl 9 and MapLibre 5 through a shared WebGL context, so switching visualisations doesn't flash. Scheduled flights render as cyan arcs and are excluded from stats until they're flown.
📊 Statistics that actually tell a story
Year-over-year comparison, seat/class distribution, top routes, cost tracking, airline loyalty breakdown. Export a PDF year report for your own records, or download a vintage-passport-style PNG certificate with your totals to share.
🏆 58 achievements across 5 categories
Explorer · Distance · Collector · Elite · Special — plus Planner and Survivor tiers for travellers who cancel nothing, and those who survive them. Thresholds are tuned so v1 users don't unlock everything in the first week.
🤖 Automation that never leaves your LAN
- Automatic flight-data lookup via AirLabs (primary), OpenSky OAuth and Aviationstack fallback — with live gate, terminal and actual-times while the flight is in the air
- Pending-update inbox — nothing changes without your approval, and every suggestion shows its statistics impact before you accept it
- Historical enrichment scheduler backfills gate/terminal/aircraft data for past flights
- 24 h / 2 h pre-departure reminders via SMTP (off by default)
- Automated database backups with retention + optional WebDAV sync to Nextcloud, HiDrive or any WebDAV endpoint
- Runtime auto-update checker that pings GHCR for a newer image and lets an admin redeploy with one click
🔐 Security you can audit yourself
- JWT stored in an
HttpOnly,SameSite=Strictcookie — no Bearer fallback, nolocalStorage - 15 rate limiters on auth, external-API-backed routes and admin exports (LAN traffic is skipped so the admin dashboard doesn't throttle itself)
- Zod validation on every input endpoint, Prisma-parameterised queries, Helmet CSP,
server_tokens off - 22 pentest findings (2 CRITICAL, 5 HIGH, 8 MEDIUM, 7 LOW) surfaced and mitigated across the beta — reproducible verification commands in
SECURITY.md - JWT + encryption keys auto-generated on first boot and persisted inside the data volume at
/app/data/secrets/— no secret in your.env
🐛 Friction-free bug reports
One button in the top nav copies an anonymised diagnostic bundle (log tails, flight-state aggregates, settings sans credentials) to your clipboard and opens a pre-filled GitHub Issue Form with your version populated. Two clicks from "something's weird" to "here's a useful report".
Installation
Docker Compose (bundled Postgres):
curl -O https://raw.githubusercontent.com/Abrechen2/TravStats/main/docker-compose.prod.yml
echo "DB_PASSWORD=$(openssl rand -base64 32)" > .env
docker compose -f docker-compose.prod.yml up -d
open http://localhost:3000/setupUnraid: Community Apps templates live at Abrechen2/docker-templates — install travstats-db first, then TravStats, set the password, open /setup.
Images: ghcr.io/abrechen2/travstats:1.0.0 · docker.io/abrechen2/travstats:1.0.0
Full install guide with screenshots: README.md · Unraid step-by-step: docs/unraid/README.md
Zero-config is the default now
For a default Docker install, the only thing you set in a file is DB_PASSWORD. Instance name, public URL, user cap, registration mode, API keys, Ollama endpoint + model, backup schedule, WebDAV — everything is captured by the first-run setup wizard or configured from the admin UI afterwards. The setup form itself is just username + password + confirm; sensible defaults are applied silently and are editable anytime.
This was the single biggest change going into 1.0: nine new admin_settings columns took the place of nine environment variables, and the runtime reads from there first. Pre-1.0 installs keep working — the old env vars are honoured as a one-time fallback until an admin saves from the UI.
Breaking changes from 0.x
- Docker registry: images still live on GHCR (
ghcr.io/abrechen2/travstats) as the primary target. Starting with 1.0,X.Y.0releases are also mirrored bit-identically to Docker Hub (abrechen2/travstats) for discovery. Pre-release candidates and patches stay on GHCR only. - Default LLM model is now
gemma3:12b(wasqwen2.5:7b). Change it in Admin → Parser after login. - Parser-feedback collection removed. The
/admin/parser-feedback/*endpoints and theFeedbackAnalyticsadmin tab are gone. The separateParseTrainingLog-backed hit-rate dashboard is unchanged. - Install footprint: one volume now covers the app data, backups and auto-generated secrets (previously a dedicated
/app/secretsmount). Pre-1.0 installs are auto-migrated at container boot. - Instance-level ENV vars are now optional.
INSTANCE_NAME,MAX_USERS,ALLOW_REGISTRATION,FRONTEND_URL,WEBDAV_*are still read once as a fallback, then superseded by the DB value the first time an admin saves from the UI.
What's next
The v1.x roadmap in ROADMAP.md is driven by real-world use:
- v1.1 Cruises module — track cruise segments alongside flights, same interactive map
- v1.2 Trip planner — draft upcoming travel with budget and gear checklists
- v1.3 CO₂ footprint tracking + compensation suggestions
- v1.4 Social sharing — optional opt-in "my travel year" cards
- v1.7 PWA — installable web app, offline flight entry sync
Priority shifts with feedback. Open an issue or discussion — we read all of them.
About this release
TravStats was built solo over six months — as a personal travel-history tool first, hardened through daily use on a homelab, and carried through a full black-box pentest before going public. v1.0.0 is the first release you're seeing because it's the first one I'd hand to someone else without caveats.
If it ends up useful to you, the nicest ways to say thanks are:
- ⭐ Star the repo on GitHub — helps other self-hosters find it
- Tell the community — post on your favourite homelab/self-hosted subreddit or Mastodon instance
- Open a pull request with the airline template you wish TravStats already shipped
- A small donation via PayPal keeps the AirLabs quota topped up for community users who share the key
Safe travels.
— Dennis
v1.0.0-rc.7 (Release Candidate)
Release Candidate 7 for v1.0.0
Supersedes v1.0.0-rc.6 with three UAT-driven polishes.
Image version surfaced in the UI
The Admin → System-Info card and Einstellungen → About used to show the stable base version (1.0.0) even for RC builds, which made it impossible to tell which RC was running. Fixed in two places:
- Dockerfile writes the build-arg
VERSION(e.g.1.0.0-rc.7) into/app/VERSIONat image build time. - New public
GET /api/v1/versionendpoint. The About section fetches from it and falls back to the bundledpackage.jsonwhile the request is in flight.
No-LLM email-import warning
When the admin hasn't wired up an Ollama endpoint, the email import tab now shows an amber banner before the drop zone: "Regex parser is inaccurate; can't split multi-flight emails. Recommended: Admin → Parser → Ollama, or manual entry." Driven by a new public GET /api/v1/parser-capabilities endpoint.
Correct Ollama model default
The admin parser UI placeholder flipped from qwen2.5:14b to gemma3:12b (the model that actually benchmarks 100% accuracy on the TravStats test corpus).
Docker images
ghcr.io/abrechen2/travstats:1.0.0-rc.7
docker.io/abrechen2/travstats:1.0.0-rc.7
Digest: sha256:69c420a4a3ba11360904713f142b4329ea1399917cb6bceb20819262200835f0
How to update an rc.6 install
Unraid → Docker → TravStats → Edit → change Repository tag 1.0.0-rc.6 to 1.0.0-rc.7 → Apply.
v1.0.0-rc.6 (Release Candidate)
Release Candidate 6 for v1.0.0
Supersedes v1.0.0-rc.5 — UI decluttering and Unraid install simplification.
Dashboard onboarding ripped out
The onboarding checklist sat as a flex-row between the navigation bar and the map. On shorter viewports it pushed the map (and all its overlay buttons — add flight, filter, visualisation picker) off-screen; the user could reach its own checkboxes, but nothing else. Fully removed: OnboardingGuide, OnboardingStep, ContextualHint, HomeAirportOnboardingBanner, the /settings/onboarding-state route, the OnboardingState interface, the onboarding i18n namespace, and all state plumbing in Dashboard / Achievements / Advanced Stats. -736 lines net.
Unraid install simplified — no custom network required
Both templates back on Docker's stock bridge. travstats-db publishes its port to the Unraid host (default 5432, configurable), and TravStats reaches it via host.docker.internal:5432 thanks to an --add-host=host.docker.internal:host-gateway in its ExtraParams. First-time install now: create DB container → create TravStats container → done. No docker network create pre-step.
Docker images
ghcr.io/abrechen2/travstats:1.0.0-rc.6
docker.io/abrechen2/travstats:1.0.0-rc.6
Digest: sha256:9c53b04d376246c76ec226f0efbe600c5d64850b220fabf6030e3cf19488097b
How to update an rc.5 install
Unraid → Docker → TravStats → Edit → change Repository tag 1.0.0-rc.5 to 1.0.0-rc.6 → Apply. Appdata stays, DB stays, JWT stays (persisted in /app/data/secrets/). Migrations run on first boot.
v1.0.0-rc.5 (Release Candidate)
Release Candidate 5 for v1.0.0
Supersedes v1.0.0-rc.4 with three user-visible polishes uncovered while walking through the Unraid install on a clean box.
Setup wizard reduced to admin credentials
Instance name, public URL, user cap and registration mode are dropped from the first-run form. They're filled with sensible defaults (public URL auto-captured from window.location.origin) and editable under Admin → Instanz anytime. First-run is now literally username + password + confirm.
Global rate limit skipped on LAN
The /api/ catch-all limiter no longer throttles loopback, Docker-bridge and RFC 1918 ranges. Self-hosted installs (Unraid, homelab compose) stop 429-ing themselves during the dashboard-polling that follows a fresh install. Public-facing deployments still enforce the 10 000/15 min cap.
Dead developer-mode toggle removed
The old Parser-Entwicklermodus switch under Einstellungen → Developer Options hadn't actually gated anything since the parser page started checking user.isAdmin directly. Fully removed — route, components, confirmation dialog, two user_settings columns (via Prisma migration).
Unraid template polish
- Both containers now default to the user-defined
travstats-netbridge (Unraid creates it on first install); Docker's stockbridgehas no container-name DNS, which was biting installers. - WebUI URL uses
[PORT:80]so the button follows whatever host-port is set, not the default3000. DATABASE_URLunmasked with a clearer 'Database URL' label + step-by-step description.- Companion
travstats-dbtemplate added so PostGIS installs in one click.
Docker images
ghcr.io/abrechen2/travstats:1.0.0-rc.5
docker.io/abrechen2/travstats:1.0.0-rc.5
Digest: sha256:14568918d09e0981c91e2a5962285d7500c729785fdc619b2d06733c220ae4b9
v1.0.0-rc.4 (Release Candidate)
Release Candidate 4 for v1.0.0
Supersedes v1.0.0-rc.2 and v1.0.0-rc.3 with the install surface consolidated and two follow-up fixes.
Install-surface consolidation
- One volume instead of two. Secrets (JWT, AES-GCM encryption key) moved out of the dedicated
/app/secretsmount into/app/data/secrets/inside the main data volume. Both compose and Unraid now need a single/app/datamount; pre-1.0 installs are migrated automatically at boot so existing key material stays in use. - Unraid template slimmed down to
DATABASE_URL+TZonly.OLLAMA_URL,COOKIE_SECUREandCORS_ORIGINdropped from the template — Ollama is configured from the admin UI, cookie-secure auto-detects fromX-Forwarded-Proto, and CORS defaults to same-origin behind a proxy. All three still honour environment overrides for exotic setups.
Fixes since rc.3
- Docker entrypoint writes JWT to
/app/data/secrets/jwt.secretdirectly (rc.3 still wrote to/app/secrets, bypassing the Node-side resolver).
CI right-sized for a solo-dev workflow
ci.ymlruns on push to Main (previously only viaworkflow_call).- Redundant
docker-build.ymlremoved; therelease.ymlDocker job dropped too. Images are built locally by the/deployskill and pushed straight to GHCR.
Docker images
ghcr.io/abrechen2/travstats:1.0.0-rc.4
docker.io/abrechen2/travstats:1.0.0-rc.4
Digest: sha256:8176971472ab95655aa029035f561cd1fc4e7f0898c9b8fef7c53b57adaa7220
Unraid test install
The Community-Apps template is at docs/unraid/travstats.xml. Install PostGIS from CA first, add TravStats from the template, set DATABASE_URL to postgresql://flights:<password>@travstats-db:5432/flights, open /setup.
v1.0.0-rc.2 (Release Candidate)
Release Candidate 2 for v1.0.0
Follow-up to v1.0.0-rc.1 with the following fixes layered on top of the same v1 feature set. Matches the image that is currently deployed on flighttest for UAT.
Security
npm audit fixpatches four moderate-severity transitive CVEs. Lockfile-only; no behaviour change.- GHSA-r4q5-vmmm-2653 —
follow-redirectsleaks custom auth headers on cross-domain redirects (viaaxios, frontend + backend) - GHSA-39q2-94rc-95cp —
dompurifyADD_TAGSbypassesFORBID_TAGS(viajspdf, frontend) - GHSA-j452-xhg8-qg39 —
protocol-buffers-schemaprototype pollution (viamaplibre-gl>pbf, frontend)
- GHSA-r4q5-vmmm-2653 —
- Hardcoded internal LAN IP in the Ollama URL UI placeholder replaced with
http://localhost:11434(German + English). - Private attack-roadmap doc removed from the public repo (present in v1.0.0-rc.1 as
.private/PENTEST_FINDINGS.md); the full git history has been rewritten to remove it everywhere.
Docs
- Legacy root
unraid-template.xmldeleted (pointed to Docker Hub with eight legacy ENV vars that are now DB-stored). docs/unraid/travstats.xmlnow points at the Docker Hub mirror for native Unraid Community Apps discovery.backend/.env.exampleslimmed from 151 to ~60 lines — only the five runtime knobs remain; everything user-configurable post-install is marked as UI-configured.
Docker image
ghcr.io/abrechen2/travstats:1.0.0-rc.2
Digest: sha256:0fbdf2f122ba41157885943e35e8dfb4d52ee0ea55c958cf5eb3388b5a402d15
On green UAT
The tested image will be retagged bit-identically to :1.0.0 / :latest / :stable on GHCR, mirrored to Docker Hub as abrechen2/travstats:1.0.0 / :latest / :stable, and a proper v1.0.0 release published.