1
0
Fork
You've already forked karrage
0
No description
  • TypeScript 51.2%
  • Python 47.3%
  • Dockerfile 0.7%
  • JavaScript 0.4%
  • Shell 0.3%
matti 2ac1e42cf0
Some checks failed
CI / Lint frontend (push) Has been cancelled
CI / Check backend (push) Has been cancelled
CI / Build and push images (push) Has been cancelled
Merge pull request 'Release 0.5.1: legal pages toggle, per-tenant Tankerkoenig key, rate limiting' ( #3 ) from fix/issue-2-mobile-bottom-nav into main
2026年07月07日 21:25:24 +02:00
.forgejo/workflows ci(fix): runner does not support username spaces 2026年06月10日 07:42:16 +02:00
backend feat(tenants): per-tenant Tankerkoenig API key with rate limiting 2026年07月07日 21:19:35 +02:00
frontend feat(tenants): per-tenant Tankerkoenig API key with rate limiting 2026年07月07日 21:19:35 +02:00
.env.example feat: Add NEXT_PUBLIC_LEGAL_PAGES_ENABLED env var to toggle legal pages 2026年07月07日 21:16:00 +02:00
.gitignore first dev app 2026年05月16日 15:46:56 +02:00
.trivyignore feat(tenants): per-tenant Tankerkoenig API key with rate limiting 2026年07月07日 21:19:35 +02:00
backup-db.sh feat(tenants): per-tenant Tankerkoenig API key with rate limiting 2026年07月07日 21:19:35 +02:00
CHANGELOG.md docs(changelog): add initial changelog for recent feature work 2026年05月18日 23:25:44 +02:00
DEPLOYMENT.md feat(tenants): per-tenant Tankerkoenig API key with rate limiting 2026年07月07日 21:19:35 +02:00
docker-compose.yml feat(auth): add secure cookie sessions and OIDC login flow 2026年05月18日 23:23:45 +02:00
README.md feat(tenants): per-tenant Tankerkoenig API key with rate limiting 2026年07月07日 21:19:35 +02:00
renovate.json quality changes 2026年05月17日 23:13:29 +02:00
VERSION chore(release): bump version to 0.5.1 2026年07月07日 21:20:24 +02:00

Karrage

Karrage is a full-stack web app for vehicle management, expenses, reminders, Paperless-ngx document linking, optional OCR fuel receipt extraction, and fleet statistics.

Stack

  • Backend: Python 3.14, FastAPI, async SQLAlchemy 2.0, PostgreSQL 16, Alembic, fastapi-users, Pillow, Pandas, httpx, optional pytesseract.
  • Frontend: Next.js 16 App Router, TypeScript, Tailwind CSS, Framer Motion, Recharts, TanStack Query v5, Zustand, React Hook Form, Zod.
  • Infrastructure: Docker Compose with backend, frontend, PostgreSQL, and a local media volume for development.

Quick Start

cp .env.example .env
sudo docker compose up --build

Open:

Create the first user:

sudo docker compose exec backend python -m app.cli create-user user@example.com --migrate

Use --password "..." for non-interactive setup.

Backend Overview

The backend is a FastAPI application in backend/app. It is intentionally split into thin HTTP routers, Pydantic schemas, SQLAlchemy models, and service modules for business logic.

flowchart LR
 Browser[Next.js frontend] -->|/api/*| API[FastAPI backend]
 API --> Auth[fastapi-users JWT auth]
 API --> Routers[API routers]
 Routers --> Services[Domain services]
 Services --> DB[(PostgreSQL)]
 Routers --> DB
 Services --> Media[(Media volume)]
 Services --> Paperless[Paperless-ngx]
 Services --> Tankerkoenig[Tankerkoenig API]
 Services --> OCR[Tesseract OCR]

Backend Modules

  • app/main.py: application setup, CORS, logging middleware, static media mount, exception handling, health endpoints, router registration.
  • app/config.py: runtime configuration from environment variables and .env; production guard for SECRET_KEY and CORS.
  • app/database.py: async SQLAlchemy engine/session and shared declarative base.
  • app/dependencies.py: fastapi-users setup, JWT authentication, current-user dependency.
  • app/models/: SQLAlchemy ORM models.
  • app/schemas/: Pydantic request/response schemas.
  • app/routers/: HTTP API endpoints grouped by domain.
  • app/services/: business logic for fuel metrics, data quality, migrations, Paperless, storage, OCR, statistics, and external APIs.
  • app/cli.py: operational commands such as user creation and Hammond migration.

Request Flow

sequenceDiagram
 participant UI as Frontend
 participant API as FastAPI Router
 participant Auth as current_active_user
 participant Service as Service Layer
 participant DB as PostgreSQL
 UI->>API: Request with Bearer token
 API->>Auth: Resolve active user
 Auth-->>API: User
 API->>DB: Load owned vehicle/resource
 API->>Service: Validate and process domain logic
 Service->>DB: Read/write data
 DB-->>Service: Result
 Service-->>API: Domain result
 API-->>UI: Pydantic response

Main Domains

erDiagram
 USER ||--o{ TENANT_MEMBER : joins
 TENANT ||--o{ TENANT_MEMBER : has
 TENANT ||--o{ VEHICLE : owns
 VEHICLE ||--o{ FUEL_LOG : has
 VEHICLE ||--o{ MAINTENANCE_LOG : has
 VEHICLE ||--o{ INSURANCE_POLICY : has
 INSURANCE_POLICY ||--o{ INSURANCE_CLAIM : has
 VEHICLE ||--o{ LICENSING_RECORD : has
 VEHICLE ||--o{ OTHER_EXPENSE : has
 VEHICLE ||--o| TECHNICAL_INSPECTION : has
 USER ||--o{ DOCUMENT_LINK : owns

Core behavior:

  • Vehicles belong to tenants. Every user has at least one tenant membership, and personal use is represented by a private tenant.
  • Tenant roles are OWNER, ADMIN, MEMBER, and VIEWER. Viewing starts at VIEWER, creating fuel/cost entries starts at MEMBER, editing operational data starts at ADMIN, and tenant administration is reserved for OWNER.
  • Tenant owners manage members and tenant-wide Paperless-ngx settings. Paperless tokens are stored on the tenant and are never returned to the frontend.
  • Tenant owners can map OIDC group names to tenant roles. When OIDC_GROUPS_CLAIM is configured, OIDC logins synchronize matching mapped groups as OIDC memberships while leaving local memberships untouched.
  • Existing single-user data is migrated into a shared tenant named by INITIAL_TENANT_NAME and owned by INITIAL_TENANT_OWNER_EMAIL when multiple users exist.
  • Sold or archived vehicles block new fuel and cost entries while keeping history visible.
  • Fuel entries normalize liters, total cost, and price per liter when two of the three values are provided.
  • Fuel metrics are recalculated after create/update/delete and after CSV correction imports.
  • Maintenance, licensing, insurance, and other expenses contribute to statistics and reminders.
  • Technical inspection (HU) is tracked per vehicle with due-date calculation from last inspection or first registration.
  • Reminders include insurance deadlines, cancellation windows, vehicle tax, maintenance, and HU milestones with urgency levels.
  • Insurance policies are contract-like records: only one active/current policy is allowed until the existing one is marked cancelled.
  • Paperless links store document IDs/URLs, not document contents.
  • Admins and owners can export a vehicle as CSV or JSON from the vehicle detail view. JSON exports include vehicle data and related fuel, maintenance, insurance, licensing, other cost, and HU records without Paperless credentials.

Reminder and calendar behavior:

  • The dashboard shows one unified reminder list, sorted by severity (danger -> warning -> success) and urgency.
  • A signed calendar subscription endpoint is available for fixed-cost due dates (insurance, cancellation deadline, vehicle tax, HU):
    • GET /api/calendar/subscription returns subscription links for the authenticated user.
    • GET /api/calendar/fixcosts.ics?token=... serves an iCalendar feed that can be subscribed externally.

Runtime Configuration

Important environment variables:

  • DATABASE_URL: async PostgreSQL connection string.
  • SECRET_KEY: JWT/reset/verification secret. In production it must be strong and at least 32 characters.
  • APP_ENV: set to production to enable stricter runtime guards.
  • BACKEND_CORS_ORIGINS: comma-separated allowed origins.
  • MEDIA_ROOT: storage path for uploaded media inside the backend container.
  • AUTO_MIGRATE: 1 runs Alembic migrations during backend startup; set 0 when migrations are handled separately.
  • UVICORN_RELOAD: 1 enables reload for local development.
  • UVICORN_WORKERS: worker count for non-reload Uvicorn startup.
  • TANKERKOENIG_API_KEY: optional global fallback key for nearby fuel-station price lookup; only suitable for single-tenant selfhosting (the free Tankerkoenig API is licensed for personal, non-commercial use, so one shared key across tenants is not allowed). In multi-tenant setups each tenant owner stores their own free key (creativecommons.tankerkoenig.de) in the settings UI.
  • TANKERKOENIG_RATE_LIMIT_REQUESTS / TANKERKOENIG_RATE_LIMIT_WINDOW_SECONDS: per-tenant throttle for station lookups (defaults: 10 requests per 300 s) so a tenant key is not overused and blocked upstream.
  • LOGIN_RATE_LIMIT_ATTEMPTS / LOGIN_RATE_LIMIT_WINDOW_SECONDS: in-memory login throttle per client IP.
  • AUTH_COOKIE_NAME: httpOnly session cookie name.
  • AUTH_COOKIE_SECURE: leave unset to use non-secure cookies in local development and secure cookies in production; set true when serving behind HTTPS.
  • AUTH_COOKIE_SAMESITE: defaults to lax; use none only for cross-site deployments and only together with HTTPS.
  • ALLOW_LOCAL_LOGIN_IN_PRODUCTION: production refuses to start with local password login unless this is true; the expected production setup is OIDC-only (OIDC_ENABLED=true plus OIDC_DISABLE_LOCAL_LOGIN=true).
  • PAPERLESS_ALLOW_PRIVATE_URLS: leave empty to allow private/internal Paperless URLs in development and block them in production (SSRF protection); set true for self-hosted setups with Paperless on a private network.
  • INITIAL_TENANT_NAME: shared tenant name used once when existing single-user data is migrated; defaults to Familie.
  • INITIAL_TENANT_OWNER_EMAIL: required for the tenant migration when more than one user already exists.

Security defaults:

  • API and frontend responses set CSP, frame, content-type, referrer, and permissions headers.
  • Authentication uses an httpOnly session cookie instead of storing JWTs in localStorage.
  • Production mode adds HSTS, rejects weak/default SECRET_KEY values, and expects OIDC-only login unless ALLOW_LOCAL_LOGIN_IN_PRODUCTION=true.
  • Login attempts are rate-limited before password verification.
  • Uploaded vehicle images and receipt PDFs are type-checked and size-limited.
  • Vehicle images are served only through the authenticated /api/vehicles/{id}/image endpoint; there is no public static media route.
  • Calendar subscription links are signed with a per-user secret and can be revoked/rotated from the dashboard.
  • Paperless URLs must be HTTP(S) URLs without embedded credentials; in production they must not point to private/internal addresses, and proxied previews/thumbnails are size-limited and do not follow redirects.

Privacy (GDPR) features:

  • Users can export all of their data as JSON and delete their account (including owned single-member tenants, vehicles, records, and images) from the settings page (GET /api/account/export, DELETE /api/account).
  • /datenschutz and /impressum pages ship as templates; replace the bracketed placeholders with the operator's details before launch.
  • Only technically necessary cookies are used; vehicle image uploads are re-encoded, which strips EXIF/GPS metadata.

OpenID Connect Login

Karrage can add OIDC as a second login path next to the local email/password login. OIDC users are matched by their verified email address and receive the same internal JWT as local users.

Set these variables in .env:

  • OIDC_ENABLED=true
  • OIDC_PROVIDER_NAME: button label, for example Keycloak or Authentik.
  • OIDC_CLIENT_ID and OIDC_CLIENT_SECRET: client credentials from the provider.
  • OIDC_DISCOVERY_URL: provider .well-known/openid-configuration URL.
  • OIDC_REDIRECT_URI: callback registered with the provider, for local compose usually http://localhost:8000/api/auth/oidc/callback.
  • OIDC_FRONTEND_REDIRECT_URL: frontend login URL, usually http://localhost:3000/login.
  • OIDC_ALLOW_SIGNUP: when true, first OIDC login creates a user automatically.
  • OIDC_REQUIRE_VERIFIED_EMAIL: when true, the provider must return email_verified: true.
  • OIDC_GROUPS_CLAIM: optional claim name containing OIDC groups, for example groups.
  • OIDC_GROUPS_PREFIX: optional prefix filter for OIDC groups, for example karrage:.

If the provider has no discovery document, configure OIDC_AUTHORIZATION_ENDPOINT, OIDC_TOKEN_ENDPOINT, and OIDC_USERINFO_ENDPOINT instead of OIDC_DISCOVERY_URL.

Migrations

The backend image starts through backend/entrypoint.sh. By default it runs:

alembic upgrade head
uvicorn app.main:app --host 0.0.0.0 --port 8000

You can run migrations manually:

sudo docker compose exec backend alembic upgrade head

For Kubernetes, prefer a dedicated migration Job or release step and set AUTO_MIGRATE=0 on the API Deployment when running multiple replicas.

flowchart TD
 Start[Container start] --> Check{AUTO_MIGRATE=1?}
 Check -->|yes| Alembic[alembic upgrade head]
 Check -->|no| Skip[skip migrations]
 Alembic --> Uvicorn[Start Uvicorn]
 Skip --> Uvicorn
 Uvicorn --> API[FastAPI serving traffic]

Container Deployment

The backend image has a useful default command, so it can run outside Docker Compose, for example with Podman Quadlets or Kubernetes.

Local Compose keeps development behavior separate:

  • Backend source is bind-mounted.
  • UVICORN_RELOAD=1 is set only in Compose.
  • Postgres and media are local development dependencies.

Production/container guidance:

  • Inject secrets as environment variables or orchestrator secrets.
  • Do not bake .env into images.
  • Mount persistent media storage at MEDIA_ROOT, or replace it later with object storage.
  • Use /api/health for liveness.
  • Use /api/ready for readiness because it verifies DB connectivity.
  • Run migrations once per release when using more than one backend replica.

Kubernetes Readiness

Karrage is close to cloud-native for a small stateful web app, but production Kubernetes should provide the following pieces:

  • Deployment for backend.
  • Deployment for frontend.
  • External PostgreSQL or a Postgres operator.
  • Secret for DATABASE_URL, SECRET_KEY, API keys, and Paperless token-related configuration.
  • ConfigMap for non-sensitive settings.
  • PersistentVolumeClaim for MEDIA_ROOT, unless media is moved to object storage.
  • Migration Job for Alembic when running multiple backend replicas.
  • Ingress or Gateway with TLS termination.

Example probe shape:

livenessProbe:httpGet:path:/api/healthport:8000readinessProbe:httpGet:path:/api/readyport:8000

Redis

Redis is not required for the current application. The backend is stateless apart from PostgreSQL and media storage.

Add Redis only when one of these appears:

  • background jobs or scheduled workers,
  • server-side caching,
  • rate limiting,
  • distributed locks,
  • websocket fan-out,
  • shared session/state coordination.

If Redis is added, keep it optional behind REDIS_URL so single-node, Quadlet, and small Compose deployments remain simple.

Fuel Data Quality

Rules for automatic outlier handling, smoothing, and CSV correction:

  • Fuel metrics are recalculated after each relevant write.
  • Full-tank routes produce raw consumption values.
  • Implausible values are marked and smoothed.
  • Manual corrections and CSV correction imports trigger recalculation.

Smart Mobile Fueling

The fuel form supports one-tap location lookup for mobile usage:

  1. Browser geolocation gets current coordinates.
  2. Backend requests nearby stations from the Tankerkoenig API using the tenant's own API key (settings UI), falling back to the global TANKERKOENIG_API_KEY for single-tenant selfhosting.
  3. Form auto-fills station_name and price_per_liter based on vehicle fuel type.
  4. You only enter odometer and either liters or total cost.

Lookups are cached (about 1 km grid, 1 h TTL) and rate-limited per tenant so the upstream key is not overused. Price data: Tankerkoenig / MTS-K, licensed CC BY 4.0 (attribution shown in the settings UI and privacy page).

Paperless and OCR

Paperless integration is optional.

  • Settings store Paperless URL/token on the user record.
  • Maintenance and insurance forms can pick matching Paperless documents.
  • The backend fetches Paperless previews through authenticated requests.
  • OCR accepts authenticated image uploads, limits upload size/type, and extracts fuel receipt hints with Tesseract.

Users

There is no public registration flow in the UI. Create users from the backend CLI:

sudo docker compose exec backend python -m app.cli create-user user@example.com

Useful options:

  • --password "..." sets a password non-interactively.
  • --migrate runs migrations before creating the user.

Migration from Hammond

Karrage includes a staged migration path from Hammond backups:

  1. Export a JSON dump from Hammond backup (.tar.gz) or hammond.db.
  2. Import vehicles from dump.
  3. Import fuel fillups into Karrage.
  4. Import expenses into maintenance, licensing, and other expense records.

1. Create Dump

sudo docker compose exec backend python -m app.cli dump-hammond-fillups \
	--source /app/media/hammond_backup_2026年05月15日_120000.tar.gz \
	--output /app/media/hammond_fillups_dump.json

2. One-Step Migration

Create dump, show summary, then confirm import with [y/N]:

sudo docker compose exec backend python -m app.cli migrate-hammond \
	--source /app/media/hammond.db \
	--user-email user@example.com

Optional flags:

  • --import-vehicles imports vehicles first.
  • --vehicle-map /app/media/vehicle_map.json applies manual mapping.
  • -y skips confirmation prompt.

3. Vehicle Mapping

If no vehicles exist yet in Karrage, import them from the dump first:

sudo docker compose exec backend python -m app.cli import-hammond-vehicles \
	--dump /app/media/hammond_fillups_dump.json \
	--user-email user@example.com \
	--dry-run

Then execute without --dry-run.

If license plate or make/model/year cannot be matched automatically for fillups, create a map file:

{
	"<hammond_vehicle_id>": "<karrage_vehicle_uuid>"
}

4. Import Fillups

sudo docker compose exec backend python -m app.cli import-hammond-fillups \
	--dump /app/media/hammond_fillups_dump.json \
	--user-email user@example.com

5. Import Expenses

Expenses from Hammond are imported into Karrage categories with a heuristic mapping:

  • Maintenance-related expenses, such as inspection, tires, or repair, become maintenance logs.
  • Tax/registration-like expenses become licensing records.
  • Remaining expenses become other expenses.
sudo docker compose exec backend python -m app.cli import-hammond-expenses \
	--dump /app/media/hammond_fillups_dump.json \
	--user-email user@example.com \
	--dry-run

Then execute without --dry-run.

The importer is idempotent for existing entries and skips duplicates based on vehicle, date, odometer, and total cost.

Recommended order:

  1. Vehicles
  2. Fillups
  3. Expenses

Insurance policies are managed directly in Karrage because Hammond expense exports do not contain enough contract metadata for provider, policy number, contract status, and cancellation deadlines.