Skip to content

๐Ÿ”„ 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:

./dev.py front dev

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:

  1. 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 of frontend/static/lf-flags.css on 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 build fails only for the resources the frontend ships (--required-for frontend), and ./dev.py server only warns and falls back to the CDN. ./dev.py cache js (--force) refreshes the cache by hand.
  2. Frontend: rebuilt for production when check_frontend_needs_build() finds sources newer than frontend/build/index.html. The check reads timestamps, not the build mode: a debug build left by ./dev.py server --test passes it, and the Dockerfile's frontend stage then refuses it.
  3. Documentation: mkdocs build when mkdocs_src/site/index.html is missing or older than any .md under mkdocs_src/docs/.
  4. requirements.txt: regenerated with pipenv requirements when missing or older than Pipfile.lock.
  5. 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

  • test is not part of the image. dev.py registers a command group only when the directories it needs exist (_has() in dev.py): without backend/test_scripts/, which .dockerignore keeps out, test stays listed but answers 'test' is not available in this installation and exits with 2, so test db populate cannot seed the test database. i18n (frontend/scripts/) and mkdocs translate / translate-validate (mkdocs_src/aphra-pipeline/) behave the same way; ./dev.py test utils dev-cli-image pins this behaviour on a copy of the image's file set.
  • server --test forces a debug frontend build, while the image ships a production one: auto_build_frontend() sees the mode mismatch and rebuilds, which runs npm (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.