Files
rezervovator/CLAUDE.md
T

7.6 KiB

Rezervovator

Booking system for sport/service facilities. Full-stack Rust: Leptos 0.8 (SSR + hydration) on Actix Web, PostgreSQL via sqlx. Crate name is rezervator, product name is "Rezervovator".

One crate, compiled twice by cargo-leptos:

  • server binary — feature ssr (Actix, sqlx, lettre, charts-rs)
  • browser WASM — feature hydrate, target wasm32-unknown-unknown

Every change must compile for both. Server-only code and imports go behind #[cfg(feature = "ssr")] / cfg_if!.

Commands

cargo leptos watch                 # dev server with reload (needs config.toml + running Postgres)
cargo leptos build --release       # production build -> target/release/rezervator + target/site
cargo check --features ssr         # type-check the server side
cargo check --lib --features hydrate --target wasm32-unknown-unknown   # type-check the WASM side
cargo test --features ssr          # unit tests

Run both cargo check commands and the tests after any change. They don't catch hydration errors, so after changing a view also load the page in a browser with a full reload (the browser caches the old WASM otherwise) and check the console; a plain cargo check / cargo test (no features) checks neither side properly.

Unit tests are #[cfg(test)] mod tests at the bottom of the file they test (#[cfg(all(test, feature = "ssr"))] for ssr-only code). They cover pure logic only — parsing, validation, paging, slot merging, mail templates, config. Nothing touches the database, so server functions, permissions and SQL are untested. Add tests for new pure logic; keep them free of DB and network.

end2end/ is still the untouched Leptos template (it asserts "Welcome to Leptos") — it does not test this app.

Running locally

  • config.toml (gitignored) is read from the working directory, or from -c <path>. Copy config_sample.toml. The bind address comes from [network] there, not from LEPTOS_SITE_ADDR.
  • The config file holds secrets and must stay outside the site root (target/site), which is served as static files; the server refuses to start otherwise (is_publicly_served in src/config.rs). In Docker it is mounted to /app/config/config.toml.
  • Migrations in migrations/ are embedded by sqlx::migrate!() and run on startup.
  • First start creates the admin user with the password from REZERVATOR_ADMIN_PASSWORD or [admin] initial_password in the config (no default; the server refuses to start when no admin exists and no password is given). It also seeds a placeholder company, empty mail templates and an appearance row (create_admin, check_company, check_messages, check_appearance in src/main.rs).
  • With transport = "File" e-mails are written as .eml into Maildir/ instead of being sent.
  • assets/ is copied to target/site on build and served as static files.
  • Files created at runtime (the uploaded banner and logo) go to the data directory: data/ in the working directory by default (gitignored), [storage] data_dir in the config, /app/data volume in Docker. It is served under /data and survives rebuilds. Server code gets the path with get_data_dir(); never write into target/site.

Layout

  • src/main.rs — Actix setup: config, pool, migrations, seeding, session + auth middleware, /api/* server fns, /health (src/backend/health.rs).
  • src/app.rs — <App/>, router, shared contexts (MenuHelper, MenuOpener, DialogHelper).
  • src/backend/ — server functions per domain, plus data.rs (all DTOs/entities shared by server and client) and auth_middleware.rs (redirects unauthenticated /admin* page requests to /login).
  • src/pages/ — page and dialog components; src/components/ — reusable pieces (DataForm, QuestionDialog, ModalDialog, ServerErr, ValidationErr, AdminPortal, Theme for the light/dark theme of the admin portal).
  • src/backend/banner.rs — options of the public page banner (BannerOptions) and the checks of uploaded images; shared by server and browser.
  • src/backend/color.rs — colours of the public page set by the customer (Colors), contrast checks and the ready-made palettes; shared by server and browser.
  • src/locales/ — trl() and the translation catalogue.
  • src/error.rs — AppError; src/config.rs — config.toml structs (ssr only); src/validator.rs — client-side form validation helper.
  • deploy/ — running one container per customer on a Docker host: rezervator.sh (add, import of an existing installation, upgrade with rollback, backup, restore, remove), docker-compose.yml (optional Traefik and PostgreSQL containers), README.md. Proxy and PostgreSQL are each either a container or the one on the host (REZERVATOR_PROXY=traefik|nginx, REZERVATOR_POSTGRES=container|host); a change to the script must work in both. The script relies on the /health endpoint, the REZERVATOR_ADMIN_PASSWORD variable, the /app/config and /app/data mounts and the max_connections config key — keep them working when changing startup or config code, and test script changes against a real Docker stack with another REZERVATOR_PREFIX.
  • assets/vendor/ — third-party admin template (Bootstrap based). Don't edit or reformat it. App styles are plain CSS files in assets/css/, linked in src/components/header.rs.
  • style/main.scss is compiled to /pkg/rezervator.css, but nothing links that file, so styles written there have no effect.

Routes: / public booking form, /login, /admin, /admin/settings, /admin/mail_settings, /admin/bookings, /admin/customers, /admin/appearance.

Domain model

property is a bookable thing with a price and slot length. A booking is one reservation_sum (date, customer, total price, state New → Approved / Canceled, public uuid) with one or more reservation rows (property + from/to time). opening_hour rows are per weekday, closing_time rows are date ranges when booking is closed. Customers are matched by e-mail. The public form is protected by a proof-of-work captcha (leptos-captcha).

Conventions

  • Code, comments, identifiers, log messages and commit messages are in English. UI strings are English keys translated at runtime (see .claude/rules/frontend.md).
  • Commit messages are always written in English, even when the conversation is in Czech: short past-tense sentences ending with a period, e.g. "Added setting for closing days."
  • The project has no rustfmt config and the code is not rustfmt-formatted. Don't run cargo fmt over existing files; match the surrounding style and keep diffs limited to what you changed.
  • Leptos is 0.8 — use the 0.8 API (use leptos::prelude::*, signal(), RwSignal::new, Resource::new_blocking, ServerAction::<T>::new()), not the 0.6 create_* functions.
  • wasm-bindgen is pinned to an exact version in Cargo.toml. The wasm-bindgen CLI that cargo-leptos runs must be exactly the same version (cargo install -f wasm-bindgen-cli --version <same>), otherwise the front-end build fails with a schema version mismatch. Leptos 0.8 needs at least 0.2.108. Change the pin and the CLI together, never one of them.
  • #![recursion_limit = "512"] at the top of src/lib.rs and src/main.rs is required: without it the release build fails with "queries overflow the depth limit".
  • .cargo/config.toml selects the getrandom backend for the WASM target (needed by the captcha's proof of work). getrandom and uuid have their js features enabled in Cargo.toml for the same reason; don't remove them.
  • Version lives in Cargo.toml and is shown in the page footer.

Detailed rules are in .claude/rules/: backend.md, frontend.md, database.md.