Added CLAUDE.md and development rules for Claude Code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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<ApiResponse<T>, 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 `<DataForm>` / `<QuestionDialog>` 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 `<DataForm>`, 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<ApiResponse<_>, _>`.
|
||||
- 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`.
|
||||
@@ -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`.
|
||||
@@ -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 `<Route path="admin/..."/>` in `src/app.rs` wrapped in `<AdminPortal>`, 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 `<Transition fallback=...>` 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::<ServerFnStruct>()` in the dialog, wrapped in `<DataForm opener=.. action=.. title="..">`. It supplies the modal, Save/Close buttons, client validation and server error display. Confirmations use `<QuestionDialog>`.
|
||||
- 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 (`<i class="bx bx-...">`). 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`.
|
||||
Reference in New Issue
Block a user