Mobile-first PWA for logging fuel fill-ups to a self-hosted LubeLogger instance.
Status: v0.3.3 — stable. Single-user homelab tool, daily-driven. Public repo so anyone can fork and self-host.
This project was developed with Claude Code — design, implementation, tests, infra, and docs were paired with Anthropic's coding assistant. Code is reviewed and decisions are made by a human; the assistant did the typing.
LubeLogger's web UI is great for review and analytics, but entering a fill-up at the gas pump from a phone is fiddly. quicklogger is a one-form, install-as-PWA front door optimised for the pump:
- Auto-selects the last vehicle
- Volume in gallons or liters, cost in any major currency — converted server-side
- Live MPG-since-last-fill preview as you type
- Last-fillup strip above the form + odometer prefill + one-tap
+300 michip — seedocs/user/odometer-prefill.md - Offline queue that auto-syncs when signal returns
- iOS Shortcut integration (voice + deep-link)
- Stays on your network — backend talks to LubeLogger over the internal Docker network, not the public internet
| Log Fuel | History | Maintenance |
![]() |
![]() |
![]() |
| Vehicles | Settings | Photo Preview |
![]() |
![]() |
![]() |
| Stats | ||
![]() |
||
The fastest path to running quicklogger — standalone container, single host, no other services on the network. Five commands:
git clone https://github.com/varunpan/quicklogger.git
cd quicklogger
cp compose.example.yml docker-compose.yml
# Edit docker-compose.yml — set LUBELOGGER_URL to your LubeLogger container/host,
# and ORIGIN to the URL you'll actually open the app from (see note below)
cp .env.example .env
# Edit .env — set LUBELOGGER_API_KEY
docker compose up -dquicklogger now serves on port 3000. Open http://<your-host's-LAN-IP>:3000 on
your phone (same network — a phone can't resolve localhost on another
machine), confirm your fleet appears under Vehicles, log a small dummy
fill, and you're done.
ORIGINmust match the URL you open the app from. SvelteKit rejects a mismatchedOriginon submit with a 403 — GETs (like the Vehicles check above) still work, so a wrongORIGINlooks like a bad API key until you try to log a fill. See Reverse proxy below for the HTTPS-fronted version of this.
For deployment behind a reverse proxy alongside an existing LubeLogger stack, see Self-hosting below.
- Docker (any host with
docker compose) - A running LubeLogger instance with an Editor-scope API key (LubeLogger → Settings → API Keys)
- A way to expose HTTPS to the phone you'll log from (Traefik, Caddy, Cloudflare Tunnel, Tailscale Funnel, etc.). Plain HTTP works on a LAN, but iOS won't install the PWA.
If you already run LubeLogger in a docker compose stack, drop quicklogger in next to it. Talking to LubeLogger over Docker DNS skips a public network round-trip:
services:
quicklogger:
image: ghcr.io/varunpan/quicklogger:latest
container_name: quicklogger
restart: unless-stopped
environment:
- LUBELOGGER_URL=http://<lubelog-service>:8080 # whatever your LubeLogger service is named on this network
- LUBELOGGER_API_KEY=${LUBELOGGER_API_KEY} # in your stack's .env
- LUBELOGGER_VOLUME_UNIT=gallons_us
- LUBELOGGER_DISTANCE_UNIT=miles # or km — match your LubeLogger instance
- LUBELOGGER_CURRENCY=USD
- ORIGIN=https://quicklog.example.com # your public/internal URL
- PORT=3000
volumes:
- /srv/quicklogger/data:/data # bind-mount for the FX cache
# Runtime hardening — see docs/deployment.md § "Hardening the runtime"
read_only: true
tmpfs:
- /tmp:rw,size=16m,mode=1777
cap_drop: [ALL]
security_opt: ['no-new-privileges:true']
pids_limit: 100
mem_limit: 256m
networks:
- <same-network-as-lubelogger>Append LUBELOGGER_API_KEY=<key> to the stack's .env. Then docker compose up -d quicklogger — only that service starts, the others are untouched.
The image listens on plain HTTP :3000. Front it with HTTPS. Traefik label snippet (internal-only host):
labels:
- traefik.enable=true
- traefik.http.services.quicklogger.loadbalancer.server.port=3000
- traefik.http.routers.quicklogger.rule=Host(`quicklog.example.com`)
- traefik.http.routers.quicklogger.entrypoints=websecure
- traefik.http.routers.quicklogger.tls=trueFor Caddy, nginx, or Cloudflare Tunnel: same idea — proxy https://quicklog.example.com → http://quicklogger:3000.
Set
ORIGINto your public URL. SvelteKit uses it for CSRF protection on POSTs; a mismatchedORIGINreturns 403 on submit.
- Open
https://quicklog.example.comon your phone. - iOS: Share → Add to Home Screen.
- Tap ☰ → Vehicles → confirm your fleet from LubeLogger appears.
- Go back to Log fillup, enter a small dummy fill, submit. Confirm it lands in LubeLogger.
Defaults intended to be reasonable for a single-user homelab tool. The deeper write-up lives in docs/deployment.md § Hardening the runtime — short version:
- No app-side auth. quicklogger has no login screen. Front it with HTTPS and either keep it on a private network (Tailscale, LAN, an internal-only hostname) or put it behind a forward-auth middleware (Authentik, Cloudflare Access, etc.).
- CSRF / origin check. Mutating API requests (
/api/fuelup,/api/ocr,/api/log) are rejected with a 403 if they arrive with a browserOriginthat doesn't match your configuredORIGIN— defense-in-depth beyond SvelteKit's form-only default, so a cross-siteapplication/jsonPOST is covered too. Requests with noOrigin(Apple Shortcuts, server-to-server) are unaffected. - Container runs as
node(UID 1000), not root. - Image is multi-stage — runtime layer has only the built
build/output, prod-onlynode_modules, andpackage.json. No build tools, no source. - Image is vulnerability-scanned — every release build is scanned with Trivy and fails on fixable critical/high CVEs before it's published. The base image's OS packages are upgraded and its unused npm is stripped at build time. See
docs/deployment.md§ Vulnerability scanning. - Recommended compose hardening (in both compose patterns above):
read_only: true,cap_drop: [ALL],security_opt: [no-new-privileges:true],pids_limit: 100,mem_limit: 256m, plus a 16 MB tmpfs for/tmp. Verified per-release. - Secrets surface:
LUBELOGGER_API_KEY(Editor-scope on your LubeLogger). Sits in.env, never logged. If it leaks, rotate it in LubeLogger. - What's still your responsibility: rate-limiting / WAF in front (CrowdSec, Traefik middlewares); TLS cert management; network segmentation; LubeLogger's own threat model.
Minimum vars to run:
| Var | Required | Default | Purpose |
|---|---|---|---|
LUBELOGGER_URL |
yes | — | URL of your LubeLogger (use container DNS if same network) |
LUBELOGGER_API_KEY |
yes | — | Editor-scope API key from LubeLogger |
LUBELOGGER_CURRENCY |
no | USD |
Target currency for storage |
ORIGIN |
no | — | SvelteKit CSRF origin (set to your public URL) |
PORT |
no | 3000 |
App listen port |
For the full reference (every var, type, default, override scenarios), see docs/user/configuration.md.
Structured JSON to stdout by default. Set LOG_FILE_PATH to also write a rotating logfile. Full reference in docs/user/configuration.md and the internals in docs/technical/logging.md.
- Node 24 (pin via
nvmorasdf) - npm 10+
- A reachable LubeLogger for integration testing — any of:
- The LubeLogger you already self-host
- A throwaway one:
docker run --rm -p 8080:8080 ghcr.io/hargata/lubelogger:latest
- Docker +
docker compose— only for thecompose.dev.ymlprod-mirror UAT (not needed for the npm dev loop)
The app ships a single runtime npm dependency — rotating-file-stream (log rotation); everything else is devDependencies that get bundled into the production artifact at build time. Exact versions live in package.json; the categories below are the load-bearing pieces.
| Layer | Package | Purpose |
|---|---|---|
| Framework | @sveltejs/kit ^2.68 |
Full-stack framework (file-based routing, SSR, server endpoints) |
svelte ^5.56 |
UI framework (runes-mode component model) | |
@sveltejs/adapter-node ^5.5 |
Production adapter — emits a node build entrypoint |
|
| Build | vite ^8.0 |
Dev server + production bundler |
@sveltejs/vite-plugin-svelte ^7.0 |
Vite integration for Svelte (HMR, compilation) | |
typescript ^6.0 |
Type system | |
svelte-check ^4.6 |
Svelte/TS type-checker | |
| Styling | tailwindcss ^4.2 + @tailwindcss/vite |
Utility-first CSS via Vite plugin |
| Client state | idb ^8.0 |
Promise-based IndexedDB wrapper for the offline submission queue |
| Unit / integration tests | vitest ^4.1 + @vitest/coverage-v8 |
Test runner + coverage |
@testing-library/svelte ^5.3 + @testing-library/jest-dom ^6.9 |
Component testing + DOM matchers | |
msw ^2.14 |
Mock LubeLogger upstream in route-handler tests | |
jsdom ^29.1 |
Browser DOM shim for Node-side unit tests | |
fake-indexeddb ^6.2 |
IndexedDB shim for tests of the offline queue | |
| E2E tests | @playwright/test ^1.60 |
Mobile-Safari profile against the production build |
| Lint / format | eslint ^10.5 + @eslint/js ^10.0 |
Linter (flat config) |
eslint-plugin-svelte ^3.19 + svelte-eslint-parser ^1.6 |
Svelte ESLint integration | |
typescript-eslint ^8.61 |
TypeScript-aware ESLint rules | |
prettier ^3.8 + prettier-plugin-svelte ^4.1 |
Code formatter | |
| Runtime | node:24-alpine (Docker) |
Dockerfile switches to the unprivileged node user (UID 1000) via USER node before the app starts |
git clone https://github.com/varunpan/quicklogger.git
cd quicklogger
npm install
cat > .env <<EOF
LUBELOGGER_URL=http://localhost:8080
LUBELOGGER_API_KEY=<your key>
EOF
npm run dev # http://localhost:5173| Command | Purpose |
|---|---|
npm run dev |
Vite dev server with hot reload (localhost only) |
npm run dev:lan |
Same, exposed on the LAN — for testing on a real phone |
npm run build |
Production build (adapter-node → build/) |
npm run preview |
Run the production build locally |
npm run preview:lan |
Same, exposed on the LAN — for testing the production bundle on a real phone |
npm run uat |
Production-mirror server (node --env-file=.env build) — rebuilds until precompressed assets are complete, smoke-tests, then prints the URL. macOS only — scripts/uat.sh detects the LAN IP via ipconfig getifaddr; on Linux it exits 1 after a successful build. Use npm run uat:docker instead. |
npm run uat:docker |
Build + run the real production image locally (compose.dev.yml) — prod-mirror UAT with the service worker live on localhost |
npm test |
Vitest — unit + route handler tests |
npm run test:watch |
Vitest watch mode |
npm run test:e2e |
Playwright (mobile-Safari profile) |
npm run lint |
ESLint flat config |
npm run check |
svelte-kit sync + svelte-check |
npm run format |
Prettier across the tree (config in .prettierrc) |
npm run format:check |
Prettier in check mode — the CI format gate |
- Vitest (unit + integration) —
src/**/*.test.ts. Server modules (env, currency, lubelogger client, FX cache) and SvelteKit route handlers (with MSW mocking the LubeLogger upstream) are covered here. - Playwright (E2E) —
tests/e2e/*.spec.ts. One mobile-Safari profile to match the target device. The service worker is set toblockper-spec so Playwright route mocks aren't intercepted.
Local dev server, real phone, same WiFi:
-
Find the dev machine's LAN IP:
ipconfig getifaddr en0 # macOS, Wi-Fi hostname -I | awk '{print $1}' # Linux
-
Run the dev or preview server on the LAN:
npm run dev:lan # http://<lan-ip>:5173 — hot reload # or, to test the production bundle: npm run build && npm run preview:lan # http://<lan-ip>:4173
-
On the phone (same WiFi), open
http://<lan-ip>:5173(or:4173) in Safari.
Caveats:
- This is plain HTTP. iOS won't let you "Add to Home Screen" as a real PWA, and the service worker won't activate — so offline-queue behaviour is unverifiable this way. Use it for layout, touch interactions, form flow, and live FX preview. For PWA install + service-worker testing, use the prod-mirror container below —
localhostis a secure context so the SW registers with no deploy (front it with a reverse proxy for on-device HTTPS). LUBELOGGER_URLin.envmust be reachable from the dev machine. If your LubeLogger is on the same network the dev machine is on, point at it directly (https://lubelogger.example.com). Otherwise, run a throwaway LubeLogger locally and point athttp://localhost:8080.- Don't pollute your real fuel log — when testing against a live LubeLogger, create a dedicated
TEST – DELETE MEvehicle and submit fillups against that. Clean it up periodically.
To test the real production image locally — including the service worker and PWA install, which the plain-HTTP preview above can't exercise — run:
docker compose -f compose.dev.yml up --buildOn http://localhost:3000 the service worker registers (localhost is a secure
context), so offline/PWA behaviour is testable in a desktop browser with no
deploy. For on-device phone testing over HTTPS, set the TRAEFIK_* + ORIGIN
vars in .env — see docs/deployment.md § Dev prod-mirror
compose.
User guides:
docs/user/app-pages.md— tour of the six app pages (Log Fuel / History / Maintenance / Stats / Vehicles / Settings)docs/user/configuration.md— full env-var referencedocs/user/currency-fx.md— entering cost in any currency, server-side conversiondocs/user/odometer-prefill.md— odometer prefill + last-fillup stripdocs/user/offline-queue.md— how offline submission and replay works from the user's sidedocs/user/photo-ocr.md— reading your pump display or odometer from a photodocs/user/shortcuts.md— Apple Shortcuts recipesdocs/user/smart-checks.md— the advisory nudges shown before a fillup submitsdocs/user/units.md— matching quicklogger's volume/distance units to your LubeLogger instance
Technical / internals:
docs/architecture.md— high-level map of modules, FX chain, state, service worker (details live in the focused docs below)docs/technical/attach-ocr-photo.md— attaching the exact OCR'd photo to the LubeLogger record on submitdocs/technical/fillup-unit-price.md— per-unit price line on/historycards, actual vs. instance-converteddocs/technical/format.md— locale-driven date/number/currency rendering (format.ts)docs/technical/fx-chain.md— provider chain order, cache, resolution flowdocs/technical/history-page.md—/historypage lifecycle, data flow, edge casesdocs/technical/idb-and-api.md— full IDB schema, every HTTP endpoint enumerated, plus the LubeLogger upstream call mappingdocs/technical/instance-units.md—LUBELOGGER_VOLUME_UNIT/LUBELOGGER_DISTANCE_UNITvalidation and propagationdocs/technical/last-fillup.md— offline resolver for the home page's "last fillup" snapshotdocs/technical/logging.md— structured JSON logger, request IDs, secret redaction, rotating-file sinkdocs/technical/lubelogger-car-images.md— proxying and caching each vehicle's LubeLogger photodocs/technical/maintenance-page.md—/maintenancepage lifecycle, data flow, error handlingdocs/technical/odometer-prefill.md— odometer prefill internals (state model, lifecycle, edge cases)docs/technical/offline-app-shell.md— SPA-shell precache that keeps the app usable on an offline cold-startdocs/technical/offline-odometer-prefill.md— odometer prefill behaviour while offline (cache + queue resolution)docs/technical/offline-queue.md— IDB schema, status state machine, replay pathdocs/technical/photo-ocr.md— vision-LLM provider chain, budget/rate limiting, image validationdocs/technical/server-info.md—/api/server-infohealth probe (LubeLogger reachability, key validity, version)docs/technical/service-worker.md— shell cache, install/activate, fetch decision treedocs/technical/smart-checks.md— the six pre-submit validation checks and where they rundocs/technical/stats-page.md—/statspage lifecycle, data flow, error handlingdocs/technical/vehicle-identifiers.md— license plate / VIN tap-to-copy card internals
Operations:
docs/deployment.md— image build, CI, GHCR release, runtime hardeningdocs/uat.md— manual test planCHANGELOG.md— release history
PRs welcome. The repo is small enough to read in one sitting:
-
Open an issue describing the change before large work — especially anything that touches the server ↔ LubeLogger contract or the mobile form layout.
-
Branch from
main. Conventional-commit-style messages preferred (feat:,fix:,chore:,docs:). -
Lint + check + test must pass locally and in CI before merge:
npm run lint && npm run check && npm test && npm run build
-
Branch protection on
mainrequires a greenlint-and-testcheck and a PR (no direct pushes). -
For changes that touch visible UI, run through
docs/uat.mdon a real phone before requesting review.
MIT — see LICENSE.






