# 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 ```bash 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 `. 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 (uploaded banner, generated banner CSS) 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. On start the server copies the `banner.css` template there from the site root and regenerates a missing banner style. 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, `/upload`, `/health` (`src/backend/health.rs`). - `src/app.rs` — ``, 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/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::::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 `), 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`.