From 5dbe7c36b161d22d3bf3927be0b051ae716e1e92 Mon Sep 17 00:00:00 2001 From: Josef Rokos Date: Fri, 9 Oct 2026 18:18:35 +0200 Subject: [PATCH] Added CLAUDE.md and development rules for Claude Code. Co-Authored-By: Claude Opus 5.5 --- .claude/rules/backend.md | 54 ++++++++++++++++++++++++++++++++++++ .claude/rules/database.md | 15 ++++++++++ .claude/rules/frontend.md | 39 ++++++++++++++++++++++++++ CLAUDE.md | 58 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 166 insertions(+) create mode 100644 .claude/rules/backend.md create mode 100644 .claude/rules/database.md create mode 100644 .claude/rules/frontend.md create mode 100644 CLAUDE.md diff --git a/.claude/rules/backend.md b/.claude/rules/backend.md new file mode 100644 index 0000000..9122abe --- /dev/null +++ b/.claude/rules/backend.md @@ -0,0 +1,54 @@ +--- +paths: + - "src/backend/**" + - "src/main.rs" + - "src/error.rs" + - "src/config.rs" +--- + +# Backend rules + +## Server functions + +- One file per domain in `src/backend/`. Server-only helpers and their imports go in the `cfg_if! { if #[cfg(feature = "ssr")] { ... }}` block at the top; `#[server]` functions follow below it. Extra server-only imports inside a server fn body are written as local `use` statements. +- Use plain `#[server]` for new functions (older ones spell out `#[server(Name, "/api", "Url", "name")]`; both end up under `/api`). +- Return `Result, ServerFnError>`. Expected/business failures are `Ok(ApiResponse::Error("English message".to_string()))` — the client passes that string through `trl()`, so add it to the catalogue. `Err(ServerFnError)` is for real failures (DB down etc.). +- A function used with `` / `` must return `ApiResponse<()>` and take the form entity as a single struct argument; its name is the prefix of the form field names (`company: Company` ↔ `name="company[name]"`). +- For ``, implement `ForValidation` for the generated struct (PascalCase of the fn name) right below the function: + + ```rust + impl ForValidation for UpdateCompany { + fn entity(&self) -> &dyn Validate { &self.company } + } + ``` + + That only validates in the browser. Anything that matters must be checked again inside the server fn. + +## Authorization + +- Every server fn that reads or changes non-public data starts with `perm_check!(is_logged_in)` or `perm_check!(is_admin)`; use `user_check!(login)` when a non-admin may only touch their own record. The macros return early with 403 and `ApiResponse::Error`, so they only work in functions returning `Result, _>`. +- The auth middleware protects only `/admin*` page requests. `/api/*` is not covered by it — a server fn without `perm_check!` is public. Only the public booking form endpoints should be. +- Plain Actix handlers (like `appearance::upload`) must check the session themselves. +- The logged-in `User` is stored in the cookie session under `"user"`; read it with `logged_in_user()`. + +## Database access + +- Get the pool with `get_pool().await?` (and the mailer with `get_mailing().await?`); they come from `AppData` in Actix app data. +- Runtime-checked sqlx only: `query` / `query_as::<_, T>` with `$n` binds. No `query!` macros (there is no `DATABASE_URL`/offline data at build time). +- Always bind values; never format user input into SQL. +- Multi-statement writes go through a transaction (`pool.begin()`, helpers take `&mut Transaction<'_, Postgres>`), as in `create_reservation`. +- The users table is `"user"` — always quoted. + +## Data types (`data.rs`) + +- Types crossing the server/client boundary live in `src/backend/data.rs` and must compile for WASM: sqlx derives only as `#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))]` / `sqlx::Type`. +- `id` fields are private with an `id()` getter; other fields are mostly `pub`. +- Validation uses `validator` 0.16 derive attributes; every `message = "..."` is an English translation key. +- Rust enums mapped to Postgres enums (`SlotType`, `ReservationState`, `MessageType`) must keep variant names identical to the SQL enum labels. + +## Errors, logging, mail + +- Server-side helpers return `AppError`; add a `From` impl in `src/error.rs` for new error sources (ssr-only ones behind `#[cfg(feature = "ssr")]`). The `From` impls already log the error. +- Log with the `log` macros (`info!`, `warn!`, `error!`), not `println!`. +- Send mail through `Mailer::send_mail` (queued to a background sender thread), never directly from a request. Templates are `message` rows keyed by `MessageType` with `#date#` and `#summary#` placeholders. +- A new `[section]`/key in `config.toml` needs the struct in `src/config.rs` and an entry in `config_sample.toml`. diff --git a/.claude/rules/database.md b/.claude/rules/database.md new file mode 100644 index 0000000..d49b65d --- /dev/null +++ b/.claude/rules/database.md @@ -0,0 +1,15 @@ +--- +paths: + - "migrations/**" + - "src/backend/data.rs" +--- + +# Database rules + +- PostgreSQL. Schema changes are plain SQL files in `migrations/`, named `NN_short_name.sql` with the next two-digit number. They are embedded with `sqlx::migrate!()` and applied automatically at server start. +- Never edit or rename a migration that has been committed — sqlx stores a checksum per version and existing installations will refuse to start. Fix things with a new migration. +- Migrations must work on a database that already holds production data: add columns as nullable or with a `DEFAULT`, backfill in the same file if needed. +- `migrate!()` reads the directory at compile time and there is no `build.rs`, so cargo may not notice a new `.sql` file. If a new migration doesn't run, touch `src/main.rs` and rebuild. +- Default rows (admin user, company, message templates, appearance) are seeded in Rust at startup (`create_admin`, `check_*`), not in migrations. A new singleton table or `message_type` value needs the matching seeding code. +- Keep the schema and `src/backend/data.rs` in sync in the same change: column names = struct field names (`SELECT *` + `FromRow`), Postgres enum labels = Rust variant names. A new enum value needs `ALTER TYPE ... ADD VALUE` plus the Rust variant, its `Display` arm and, for `message_type`, the list in `check_messages`. +- Naming: singular snake_case tables (`reservation_sum`, `closing_time`), `id SERIAL PRIMARY KEY`, `"user"` always quoted, money as `NUMERIC(9, 2)` ↔ `rust_decimal::Decimal`, dates/times as `DATE`/`TIME` ↔ `chrono::NaiveDate`/`NaiveTime`. diff --git a/.claude/rules/frontend.md b/.claude/rules/frontend.md new file mode 100644 index 0000000..f9c6bd2 --- /dev/null +++ b/.claude/rules/frontend.md @@ -0,0 +1,39 @@ +--- +paths: + - "src/pages/**" + - "src/components/**" + - "src/locales/**" + - "src/app.rs" + - "src/validator.rs" + - "style/**" +--- + +# Frontend rules + +## Components + +- Leptos 0.6 API only. Components are `#[component]` functions returning `impl IntoView`, one page or dialog per file in `src/pages/`; reusable building blocks go to `src/components/`. Register new files in the respective `mod.rs` (`pub mod` only if used from `app.rs`). +- This code runs on the server (SSR) and in the browser (hydration). Browser-only APIs (`window()`, `web_sys`) must not run during SSR — keep them in event handlers/effects or behind `#[cfg(not(feature = "ssr"))]`. Markup must render the same on both sides or hydration breaks. +- A new admin page needs a `` in `src/app.rs` wrapped in ``, and a menu entry in `src/components/admin_portal.rs`. Paths starting with `/admin` are login-protected by the middleware; data access is still guarded by `perm_check!` in the server fn. + +## Data loading and forms + +- Load data with `create_blocking_resource` / `create_resource` calling the server fn, rendered inside `` with `{trl("Loading...")}`. Use a dialog's `opener.visible()` as the resource source so lists reload after the dialog closes (see `src/pages/users.rs`). +- Handle all three outcomes of a server call: `Err(e)`, `Ok(ApiResponse::Error(msg))`, `Ok(ApiResponse::Data(d))`. +- Edit/create dialogs: `DialogOpener::new()` in the parent, `create_server_action::()` in the dialog, wrapped in ``. It supplies the modal, Save/Close buttons, client validation and server error display. Confirmations use ``. +- Form field names follow the server fn argument: `name="company[name]"`. Pass the id as a hidden input. Fill inputs with `prop:value={move || signal.get().field}`. +- `DataForm`/`ModalDialog` take `title` as an untranslated English key (`&'static str`); they translate it themselves. + +## Translations + +- Every user-visible string goes through `trl("English text")`; never hard-code Czech (or any text) in a view. The English text is the key and the fallback. +- When you add or change a key, add the Czech translation to the `cs` map in `src/locales/catalogues.rs`. This includes validation `message`s and `ApiResponse::Error` strings from the backend. +- `trl()` returns a closure — use it directly as `{trl("...")}` or attribute value; call it (`trl("...")()`) when a `String` is needed. +- Dates shown to users go through `loc_date()`, weekday names through `show_day()`. + +## Styling + +- UI is the Bootstrap-based admin template in `assets/vendor/` plus Boxicons (``). Reuse its classes (`card`, `table`, `btn btn-primary`, `form-control`, `row`/`col`) before writing CSS. +- Custom styles go to `style/main.scss`. Never edit `assets/vendor/`. +- The public page banner style is `assets/data/banner.css`, rewritten at runtime by the appearance upload (`/*bg-img*/` marker) — keep that marker / the `background-image: url('...')` form intact. +- Stylesheets and meta tags are declared in `src/components/header.rs`. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..ed13ce2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,58 @@ +# Rezervovator + +Booking system for sport/service facilities. Full-stack Rust: Leptos 0.6 (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 +``` + +Run both `cargo check` commands after any change; a plain `cargo check` (no features) checks neither side properly. + +There are no unit tests, and `end2end/` is still the untouched Leptos template (it asserts "Welcome to Leptos") — it does not test this app. Don't claim something is tested because these pass. + +## 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`. +- Migrations in `migrations/` are embedded by `sqlx::migrate!()` and run on startup. +- First start seeds an `admin` / `admin` user, 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. `assets/data/` (banner.css, uploaded banner) is untracked but required: the page links `/data/` and uploads are written to `target/site/data/` at runtime, relative to the working directory. A rebuild wipes `target/site`, so runtime uploads there are lost in dev. + +## Layout + +- `src/main.rs` — Actix setup: config, pool, migrations, seeding, session + auth middleware, `/api/*` server fns, `/upload`. +- `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`). +- `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. +- `assets/vendor/` — third-party admin template (Bootstrap based). Don't edit or reformat it; app styles go to `style/main.scss`. + +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.6 — use the 0.6 API (`create_signal`, `create_resource`, `create_server_action`, `#[component]` returning `impl IntoView`), not 0.7/0.8 (`signal()`, `Resource::new`, …). +- `wasm-bindgen` is pinned to `=0.2.100`; it must match the wasm-bindgen version of the installed cargo-leptos. Don't bump it on its own. +- Version lives in `Cargo.toml` and is shown in the page footer. + +Detailed rules are in `.claude/rules/`: `backend.md`, `frontend.md`, `database.md`.