๐ Developer Workflow & Tools
This guide covers developer-specific workflows, tools, and all CLI commands available in LibreFolio.
Prerequisite: Host Installation
Before using these developer tools, ensure you have set up your local Python, Node.js, and Pipenv environment. For step-by-step setup instructions, see the Host Installation Guide.
๐ฅ๏ธ SvelteKit Frontend Development
For active frontend development with Hot Module Replacement (HMR), start a second terminal and run:
The Vite development server will run at http://localhost:5173 and automatically proxy all /api calls to the FastAPI backend.
Frontend Dependency
The SvelteKit frontend is heavily dependent on the data returned by the backend. You must keep the backend server running in parallel (via ./dev.py server in another terminal) to handle API requests.
๐ Development Workflow CLI Commands
LibreFolio includes a central orchestration script, dev.py, which is your single entry point for all development tasks.
Run ./dev.py --help for the full command tree.
๐ฅ๏ธ Frontend Management
| Command | Description | Details |
|---|---|---|
./dev.py front dev |
Start Vite dev server with HMR | Run at http://localhost:5173 |
./dev.py front build |
Compile frontend into production-ready static assets | static files output to backend/static |
./dev.py front build --debug |
Compile frontend with source maps | Used for debugging frontend issues |
./dev.py front check |
Run svelte-check type validator |
Checks Svelte components and TS types |
./dev.py front preview |
Preview the compiled production build locally | Run Vite preview server |
๐ API Client Synchronization
LibreFolio uses an OpenAPI-first workflow to keep types synchronized between Python (backend) and SvelteKit (frontend):
| Command | Description | Details |
|---|---|---|
./dev.py api schema |
Export OpenAPI JSON schema from FastAPI backend | Generates openapi.json |
./dev.py api client |
Generate TypeScript client from the exported schema | Generates frontend API client |
./dev.py api sync |
Export schema and generate client in one step | Highly recommended after changing models/routes |
๐ Internationalization (i18n)
Manage translations across English, Italian, Spanish, and French. The catalogue the frontend loads, frontend/src/lib/i18n/<lang>.json, is generated and ignored by git: never edit it, the build stops on a hand edit. Its sources are frontend/src/lib/i18n/base/, one folder per subsystem with its prefixes.txt, and each plugin's i18n/ fragment (see Internationalization and A Plugin's Translations). Every command works on the sources, and the ones that change keys regenerate the catalogue.
| Command | Description |
|---|---|
./dev.py i18n audit |
Audit missing or extra translation keys across languages, after a strict check of the sources |
./dev.py i18n audit --duplicates |
Audit and report duplicate translation values |
./dev.py i18n add KEY --en "โฆ" --it "โฆ" --fr "โฆ" --es "โฆ" [--into SOURCE] |
Add a key in the four languages, in the source that owns it; --into names the source: base, a subsystem, or a plugin as <family>/<plugin> |
./dev.py i18n update KEY [--en "โฆ"] [--it "โฆ"] [--fr "โฆ"] [--es "โฆ"] |
Change a key's translations, in the source that holds it |
./dev.py i18n remove KEY |
Remove a key from every source and language |
./dev.py i18n where KEY |
The source file that holds a key, or the one add would write it to |
./dev.py i18n search QUERY |
Search key names or translation values; shows the source of each key |
./dev.py i18n tree [PREFIX] |
Print key tree structure starting with optional prefix |
./dev.py i18n build [--strict] [--force] |
Regenerate the catalogue from the sources: a missing translation falls back to English with a warning (an error with --strict); --force overwrites a hand-edited generated file |
./dev.py i18n check [--lenient] [--against REV] |
Validate the sources, strictly: every language complete (--lenient accepts a missing translation, as the build does); --against also proves the catalogue identical to the one at a git revision, key by key and language by language |
./dev.py i18n move PREFIX --into SOURCE |
Move every key under a prefix, in every language, to another source |
./dev.py i18n split |
Put every key of base and of the subsystems in the folder its declared prefix names (re-runnable) |
./dev.py i18n absorb --base REV --theirs REV [--prefer {ours,theirs}] |
Replay the key changes a branch made on the old single-file layout, as a three-way merge per key and language; conflicts are listed and nothing is written unless --prefer settles them |
The catalogue is regenerated by ./dev.py front build, by the npm hooks of dev, build, build:debug, check and check:watch (so also by ./dev.py front dev and ./dev.py front check; by hand, npm run i18n:build), before every Vitest run, by ./dev.py lint --dead-code before knip, and by the commands above. The development server generates it once, when it starts, and does not watch its sources: a source you edit by hand reaches the running server only after ./dev.py i18n build.
๐๏ธ Database Migrations (Alembic)
Create and apply database schema changes:
| Command | Description |
|---|---|
./dev.py db upgrade |
Apply all pending migrations to the SQLite database |
./dev.py db migrate "MESSAGE" |
Auto-generate a new Alembic migration based on SQLAlchemy models |
./dev.py db downgrade |
Rollback the database schema by one migration step |
./dev.py db create-clean |
Recreate a fresh database and apply all migrations |
./dev.py db current |
Show the current database migration revision |
db migrate, db upgrade, db downgrade and db create-clean refuse to run while a server listens on the port that serves the target database (PORT for the production database, TEST_PORT for the test one): stop that server first. The server itself applies pending migrations at startup (ensure_database_exists() in backend/app/main.py).
๐งช Test Runner
Run backend unit/integration tests and frontend Playwright E2E tests:
| Command | Description |
|---|---|
./dev.py test all |
Run all test categories in the optimal order |
./dev.py test <category> all |
Run tests in a single category (e.g., api, e2e, front-fx) |
./dev.py test <category> --help |
List the units (actions) of a category, each with its description |
./dev.py test <category> <action> --list |
List the tests of one unit without running them; all lists the whole category |
๐งฐ Linting, Formatting & Documentation
| Command | Description |
|---|---|
./dev.py format |
Format backend Python code with black |
./dev.py lint |
Lint and auto-fix backend Python issues using ruff |
./dev.py mkdocs serve |
Start the local MkDocs documentation development server |
./dev.py mkdocs build |
Compile the documentation site into static HTML |
./dev.py shell |
Open a subshell inside the active pipenv virtual environment |
๐ณ Docker Integration
Developers can build and run production-tagged containers locally using the dev CLI:
| Command | Description |
|---|---|
./dev.py docker build |
Build production Docker image (compiles frontend + docs first) |
./dev.py docker up |
Launch the containerized stack in detached mode |
./dev.py docker down |
Stop and remove active Docker containers |
./dev.py docker rebuild |
Build, stop, and restart containers with the new image |
./dev.py docker exec <cmd> |
Execute a dev.py command inside the running container |
For details on local Docker workflows, container settings, and how the host .env file integrates with docker-compose.yml, see the Advanced Docker Guide. How the image itself is built and started: Architecture overview โ Docker image.
๐๏ธ What ./dev.py docker build Does
cmd_docker_build() checks that .env exists, then _docker_ensure_assets_built() prepares what the image copies from the host:
- Resource cache:
scripts/update_js_cache.py, strict. It caches the flags subset of the Noto Color Emoji font (frontend/static/fonts/noto-color-emoji/, drawn by the'LF Flags'face offrontend/static/lf-flags.csson every non-Apple device: Windows has no flag emoji of its own) and MathJax (mkdocs_src/docs/javascripts/vendor/); a resource that cannot be downloaded and has no cached copy fails the build (โ Resource cache incomplete)../dev.py front buildfails only for the resources the frontend ships (--required-for frontend), and./dev.py serveronly warns and falls back to the CDN../dev.py cache js(--force) refreshes the cache by hand. - Frontend: rebuilt for production when
check_frontend_needs_build()finds sources newer thanfrontend/build/index.html. The check reads timestamps, not the build mode: a debug build left by./dev.py server --testpasses it, and theDockerfile'sfrontendstage then refuses it. - Documentation:
mkdocs buildwhenmkdocs_src/site/index.htmlis missing or older than any.mdundermkdocs_src/docs/. requirements.txt: regenerated withpipenv requirementswhen missing or older thanPipfile.lock.VERSION:get_git_version()frozen into a file, since the image has no.git/.
The build then runs docker build -t librefolio:<version> -t librefolio:latest --build-arg UID=<uid> --build-arg GID=<gid> ., where <version> comes from get_git_version() and the UID/GID are those of the user running the command (os.getuid(), os.getgid(); only docker compose build reads UID/GID from .env). --light adds -light to both tags and --build-arg DOCS_VARIANT=light; --no-cache is passed through. ./dev.py docker rebuild runs the same build, then docker compose down and docker compose up -d; a failed build leaves the running containers alone.
๐งช Test Mode in the Container
The repository's docker-compose.yml maps a second port, ${TEST_PORT:-6041}:6041, for a test server, but the current image cannot run test mode, for the two reasons below. Run test mode from a source checkout on the host instead:
./dev.py test db populate --force --with-static # mock data in the test database
./dev.py server --test # test server on :6041
Then open http://localhost:6041 and sign in as a test user, for example e2e_test_user / E2eTestPass123! or e2e_test_admin / E2eAdminPass123!.
While the compose stack runs, Docker holds host port 6041 for that second mapping: test db populate starts no server and is not affected, but ./dev.py server --test cannot start the test server on that port (dev.py stops with Port 6041 is already in use! or, when it cannot see Docker's process, uvicorn fails to bind the port). Stop the stack first (docker compose stop), or start the test server on another port and open that one instead, for example ./dev.py server --test --port 6045 (it must differ from the production PORT). Do not use --force, although the error suggests it: it kills whatever holds the port, here Docker's own port forwarder.
Why the image cannot run test mode
testis not part of the image.dev.pyregisters a command group only when the directories it needs exist (_has()indev.py): withoutbackend/test_scripts/, which.dockerignorekeeps out,teststays listed but answers 'test' is not available in this installation and exits with2, sotest db populatecannot seed the test database.i18n(frontend/scripts/) andmkdocs translate/translate-validate(mkdocs_src/aphra-pipeline/) behave the same way;./dev.py test utils dev-cli-imagepins this behaviour on a copy of the image's file set.server --testforces a debug frontend build, while the image ships a production one:auto_build_frontend()sees the mode mismatch and rebuilds, which runsnpm(API client generation, then the build itself), and the image has no Node.js.
The flow above runs on the host, so its test database is backend/data/test/sqlite/app.db in the source checkout (from LIBREFOLIO_TEST_DATA_DIR, default ./backend/data/test), and ./dev.py test db populate --force deletes and recreates it.