- TypeScript 72.3%
- Svelte 21.2%
- Shell 3.6%
- JavaScript 1.7%
- CSS 0.7%
- Other 0.5%
| scripts | ||
| src | ||
| systemd/user | ||
| tests | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| .prettierignore | ||
| .prettierrc | ||
| AI-DISCLOSURE.md | ||
| docker-compose.override.yml | ||
| docker-compose.yml | ||
| Dockerfile | ||
| jsconfig.json | ||
| package-lock.json | ||
| package.json | ||
| playwright.config.ts | ||
| postcss.config.js | ||
| README.md | ||
| svelte.config.js | ||
| tailwind.config.ts | ||
| tsconfig.json | ||
| vite.config.ts | ||
| vitest.config.ts | ||
LoadLog
LoadLog is a lightweight, single-user handloading log for a local network or workstation. Bullets and loads are reusable templates; entries always link to a deduplicated template, matched across display units, so re-recording the same load never creates duplicate rows. Access requires signing in, and API requests without a valid session are rejected.
Setup
Requirements: Node 20+, npm, and Docker Compose.
cp .env.example .env
docker compose up -d
npm install
npm run dev
Open http://localhost:5173; the first page request initializes the schema. With the app open, optional sample content is added from a second terminal with npm run db:seed. Stop MariaDB with docker compose down; data remains in the named volume.
Set LOGIN_USER, LOGIN_PASSWORD, and SESSION_SECRET in .env before starting the app. The default development credentials are admin / change-me; replace them for any network-accessible deployment. Sessions are signed with SESSION_SECRET and expire after 30 days; the app refuses to start in production without a configured secret. Failed logins are logged and throttled per source address (LOGIN_MAX_FAILURES, default 5, with a LOGIN_LOCKOUT_MINUTES lockout, default 15), and non-GET requests from mismatched Origin headers are rejected as cross-site.
For server deployments, docker compose up -d --build reads the same .env file and refuses to start until SESSION_SECRET, LOGIN_PASSWORD, MARIADB_PASSWORD, and MARIADB_ROOT_PASSWORD are set. Production is detected through NODE_ENV or DEPLOYMENT (both default to production in compose); it then rejects the example credentials, requires a 32+ character SESSION_SECRET, and marks the session cookie secure unless COOKIE_SECURE=false. Both containers define healthchecks backed by GET /api/health, and the web service waits for a healthy database before starting.
Caliber supports mm and in; bullet weight and powder charge support g and gr. Every input carries a unit dropdown beside it, so each entry records exactly what was measured — displayed values never change retroactively. Preferences choose the default unit system (metric or imperial) for new entries. Preferences are persisted in MariaDB and the theme follows the system by default, with a local toggle for quick switching. Dates use ISO YYYY-MM-DD values. Counts can be blank or zero, and notes are plain text.
Load templates are managed from Manage templates beside the load selector. Editing a template affects future entries only; entries keep a snapshot of the load values recorded at the time. Deleting a template archives it from future selectors without deleting historical entries.
Bullets follow the same rules: recorded entries always display the bullet details captured at save time, and deleting a bullet with history archives it instead of destroying records. Bullets without entries are removed permanently, and DELETE /api/bullets/[id]?purge=1 force-deletes a bullet together with its recorded entries. Schema changes are applied through versioned migrations recorded in a schema_migrations table.
Backups
The compose stack runs a backup service that dumps the database on a schedule (BACKUP_INTERVAL_HOURS, default 24, first dump at startup), encrypts each dump with BACKUP_ENCRYPTION_KEY (AES-256, PBKDF2), writes it to ${BACKUP_DIR:-./backups} on the host filesystem, and prunes dumps older than BACKUP_RETENTION_DAYS (default 30). If POST_BACKUP_HOOK is set, it runs after every dump — use it to push the encrypted files off-host, e.g. rclone copy /backups remote:loadlog-backups.
Ad-hoc operations against a running stack (MARIADB_CONTAINER overrides the target container, defaults come from .env):
npm run db:backup # encrypted dump into BACKUP_DIR
npm run db:restore -- backups/<file>.sql.gz.enc # destructive: drops and recreates the tables
npm run db:verify -- backups/<file>.sql.gz.enc # restores into a throwaway container and compares row counts
Run db:verify regularly — a backup that has not been restored is only a hope. Recovery point objective: one BACKUP_INTERVAL_HOURS window plus any gap in off-host syncing; lower the interval for tighter RPO. Recovery time objective: a few minutes for restore plus verification on the same host; off-host copies additionally cover host loss.
Scheduled off-host backups with systemd
For weekly encrypted backups into a mounted NAS directory, install the user units (adjust %h/loadlog if the checkout lives elsewhere):
sed -i 's|%h/loadlog|%h/path/to/LoadLog|g' systemd/user/*.service
mkdir -p ~/.config/systemd/user
cp systemd/user/loadlog-backup.{service,timer} systemd/user/loadlog-verify.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now loadlog-backup.timer
sudo loginctl enable-linger "$USER"
The timer runs Sundays at 03:00 (±30 minutes, catch-up enabled for powered-off machines) into ~/nas/backup/loadlog with the standard retention rules, and a successful backup immediately triggers loadlog-verify.service, which restores the newest dump into a throwaway container and compares row counts. Check status with systemctl --user list-timers loadlog-backup.timer and journalctl --user -u loadlog-backup.service.
Verification
npm run check
npm test
npm run test:e2e
npm run build
The Playwright smoke test expects the development server and a running MariaDB instance. npm run db:init is available for manually initializing the preferences table, though the app also initializes all tables automatically.