diff --git a/.gitattributes b/.gitattributes index 7508860b..5ffd347d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -15,6 +15,9 @@ *.yml text eol=lf *.yaml text eol=lf +# PowerShell scripts keep Windows line endings +*.ps1 text eol=crlf + # Denote all files that are truly binary and should not be modified *.png binary *.jpg binary diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a66f858..87e776ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,16 +21,13 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 22 + node-version-file: backend/.nvmrc - run: corepack enable - run: pnpm install --frozen-lockfile # tests/frontend loads pdf-lib from the frontend's node_modules - run: pnpm install --frozen-lockfile working-directory: frontend - - run: pnpm lint - - run: pnpm format:check - - run: pnpm typecheck - - run: pnpm test + - run: pnpm run verify frontend: runs-on: ubuntu-latest @@ -41,9 +38,11 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 22 + node-version-file: frontend/.nvmrc - run: corepack enable - run: pnpm install --frozen-lockfile - - run: pnpm lint - - run: pnpm format:check - - run: pnpm typecheck + - run: pnpm run verify + # Every tracked Markdown file in the repo, each with its nearest .prettierrc + - name: Check Markdown format + working-directory: . + run: git ls-files -z '*.md' | xargs -0 frontend/node_modules/.bin/prettier --check --ignore-path .prettierignore --ignore-path frontend/.prettierignore diff --git a/.gitignore b/.gitignore index 6ed0bba5..1df23c78 100644 --- a/.gitignore +++ b/.gitignore @@ -58,7 +58,6 @@ backend/scheduling-solver/venv/ # Claude Code Templates & Skills # -------------------------------------------- .claude/ -**/skills-lock.json # -------------------------------------------- # Local Scripts diff --git a/AGENTS.md b/AGENTS.md index e4f60136..4ae746d8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,9 +1,9 @@ # AGENTS.md -This file provides guidance to agent CLIs — **OpenCode** and Codex — when -working with code in this repository. Claude Code reads `CLAUDE.md` instead; -both files must stay in sync on shared policy (Context7 usage, DB migrations, -commit conventions). +This file provides guidance to agent CLIs — Claude Code (through the +`@AGENTS.md` import in `CLAUDE.md`) and **OpenCode** — when working with code in +this repository. It holds only what the whole repo shares; each subproject and +each documented module has its own `AGENTS.md`. ## Project Overview @@ -14,41 +14,55 @@ cashier, backoffice, and more. ⚠️ **`frontend/` and `backend/` are two independent pnpm projects, not a workspace.** There is no root `package.json` and no `pnpm-workspace.yaml`: each -side has its own `package.json` and its own `pnpm-lock.yaml`. Always `cd` into -one of them before running `pnpm`, and never assume a dependency present on one -side exists on the other — that is exactly how the two Zod majors coexist. - -## Tech Stack - -### Frontend - -- **Framework**: Next.js 16 (App Router), React 19 -- **Language**: TypeScript -- **Styling**: Tailwind CSS -- **State Management**: Zustand, React Query (@tanstack/react-query) -- **UI Components**: NextUI, Headless UI, Heroicons -- **Form Handling**: React Hook Form + Zod validation -- **Package Manager**: pnpm - -### Backend - -- **Framework**: Express 5.1.0 -- **Language**: TypeScript (ES modules, `type: "module"`) -- **Database**: MySQL (local + Aiven cloud) -- **Runtime**: tsx (no build step needed) -- **Authentication**: JWT in HttpOnly cookies (access 15 min + refresh 7 d), - same `SECRET_JWT_KEY` -- **Validation**: Zod -- **Testing**: Vitest -- **External Services**: Cloudinary (images/PDFs). No email sending -- **Package Manager**: pnpm +side has its own `package.json`, `pnpm-lock.yaml`, `pnpm-workspace.yaml` and +`.nvmrc`. Always `cd` into one of them before running `pnpm`, and never assume a +dependency present on one side exists on the other — that is exactly how the two +Zod majors coexist. They do not even share the Node major: `frontend/` runs 24, +`backend/` 22. The root has no `.nvmrc`: a session started here activates each +side's Node before running anything there (Rule 9 under `## Exceptions` in each +subproject's `AGENTS.md`). + +## How the docs are layered + +Three levels. Read **downwards** before touching code — root, then subproject, +then module — and follow the links **across** when a task crosses from one side +to the other. + +- **Root `AGENTS.md`** (this file) — shared by both subprojects: Context7 + policy, Markdown formatting, commits and hooks, the security level, the + contract between frontend and backend, and the module map. +- **[`backend/AGENTS.md`](backend/AGENTS.md) and + [`frontend/AGENTS.md`](frontend/AGENTS.md)** — the harness rules (layer 1), + then the subproject's stack, installed versions, commands, architecture, + security areas and the list of its module docs. +- **Module `AGENTS.md`** — one per documented module, inside the subproject it + belongs to. Each one links up to its subproject and across to its counterpart + on the other side, when there is one. + +Every `AGENTS.md` has a one-line `CLAUDE.md` next to it (`@AGENTS.md`), so +Claude Code loads it (ADR-042). + +**What gets loaded:** + +- **Claude Code** — this file at start; a subproject or module `AGENTS.md` as + soon as it reads a file in that directory. +- **OpenCode** — at start, every `AGENTS.md` from the directory where it was + started up to the repo root (started in `backend/`: `backend/AGENTS.md` and + this file); a module one as soon as it reads a file below it. Checked in its + source at the installed version, 1.18.12. +- **Neither loads the other side on its own.** A backend module doc does not + bring in `frontend/AGENTS.md`, nor the reverse: when a task crosses sides, + follow the links. + +Public, human-facing docs per module live in `docs//` (English). State +and decisions live in `docs/SESSION.md` and `docs/DECISIONS.md`, shared by both +subprojects. ## Up-to-date Documentation (Context7 MCP) This repository assumes the **Context7** MCP is registered at the user level in -the client (not a project dependency and not in any `package.json`). These rules -are the counterpart to the same section in `CLAUDE.md`: **the policy is the -same, only the client changes.** +the client (not a project dependency and not in any `package.json`). The policy +is the same whichever client runs it. Check that it is available before relying on it: @@ -80,79 +94,27 @@ pretend to have consulted documentation. ### When to Use It **Reactive and scoped** activation: only when writing or modifying code that -touches one of the libraries below. Never as a sweep ("review the whole project -against current docs") — that generates unrequested cascading refactors. - -Flow: `resolve-library-id` → `query-docs` with the ID, **the version from the -table**, and the specific topic. If that exact version is not indexed, use the -closest minor below and state which one you consulted. - -### Installed Versions (source of truth: the **lockfiles**, not the `^` ranges - -in `package.json`) - -- **`next`** 16.0.8 (frontend) — App Router + Turbopack. React 19.1.1 -- **`@tanstack/react-query`** 5.90.11 (frontend) — v5: `isPending`, single - object in `useQuery({...})` -- **`zod`** 3.25.76 (frontend) — ⚠️ v3. Range is `^3.25.17`: resolves to - 3.25.76, check the lockfile -- **`zod`** 4.0.5 (backend) — ⚠️ v4 — different API from frontend -- **`next-intl`** 4.6.1 (frontend) — v4: locale resolved in - `frontend/app/i18n/request.ts`: cookie `NEXT_LOCALE`, then Vercel header - `x-vercel-ip-country`, then `Accept-Language`, default `es`. No `routing.ts`, - no locale in the URL -- **`pdf-lib`** 1.17.1 (frontend) — Verify font/image embedding before use -- **`cloudinary`** 2.8.0 (backend) — SDK v2, API different from v1 in most - examples -- **`tailwindcss`** 3.4.17 (frontend) — **v3, not v4** — no - `@import "tailwindcss"` or `@theme` -- **`express`** 5.1.0 (backend) — v5: async errors automatic, path-to-regexp 8, - `req.query` getter -- **`zustand`** 5.0.8 (frontend) — v5 without default selectors -- **`react-hook-form`** 7.66.0 (frontend) — With `@hookform/resolvers` 5.x - (breaking vs 3.x) -- **`react-day-picker`** 9.11.1 (frontend) — v9 renamed props and CSS vars -- **`multer`** 2.0.2 (backend) — v2 breaks from the 1.x in common examples -- **`pdf-parse`** 1.1.1 (backend) — **Pinned to v1** (v2 → OOM on Render). See - `backend/services/fnb/CLAUDE.md` -- **`vitest`** 4.0.16 (backend) — Mock API and `environment` different from - v1/v2 -- **`framer-motion`** 12.23.22 (frontend) — ⚠️ v12 still publishes as - `framer-motion`, but the current upstream package is `motion`. Import from - `framer-motion`, **never** from `motion/react` -- **`date-fns`** 4.1.0 (frontend) — v4 adds timezone system, changes imports - from v2/v3 -- **`@nextui-org/react`** 2.6.11 (frontend) — Package `@nextui-org`, **not** - `@heroui` rebrand -- **`recharts`** 3.5.1 (frontend) — v3 changed types and some defaults from v2 -- **`pdfjs-dist`** 5.4.449 (frontend) — Worker configuration differs by major; - bundler-sensitive -- **`xlsx`** 0.18.5 (frontend) — Outside public npm registry; do not assume - newer API -- **`typescript`** 5.7.3 (frontend) / 5.9.3 (backend) — Different versions per - project - -Lower risk, but verify when touched: `node-cron` 4.2.1, -`express-rate-limit` 8.2.1, `helmet` 8.1.0, `pino` 10.3.1, `archiver` 7.0.1, -`mysql2` 3.14.2 (backend) / 3.15.0 (frontend). - -**Zod is in two different majors.** Never copy a schema between -`frontend/app/lib/schemas/` and `backend/validations/` without consulting docs -for both: in v4, custom error messages, `z.ZodError`, and several `z.string()` -helpers changed. Always indicate the workspace when querying. - -Auth is custom JWT handled by the backend, not NextAuth. The unused frontend -dependencies (`next-auth`, `bcrypt`, `mysql2`, `postgres`, `uuid`) were removed -on 2026-10-02. +touches a library listed in the installed versions of `backend/AGENTS.md` or +`frontend/AGENTS.md`. Never as a sweep ("review the whole project against +current docs") — that generates unrequested cascading refactors. + +Flow: `resolve-library-id` → `query-docs` with the ID, **the installed version** +(the lockfile, not the `^` range) and the specific topic. If that exact version +is not indexed, use the closest minor below and state which one you consulted. + +**Zod is in two different majors** — v3 in `frontend/`, v4 in `backend/`. Never +copy a schema between the two without consulting docs for both: in v4, custom +error messages, `z.ZodError`, and several `z.string()` helpers changed. Always +indicate the subproject when querying. ### Limits - Context7 is for **verification**, not migration. Do not upgrade versions or "modernize" existing working code without explicit request. -- A module's `CLAUDE.md` may pin a version for operational reasons (Render - memory, host compatibility…). **That note overrides this table and official - docs** — these are infrastructure constraints Context7 does not know about. - `pdf-parse` in `services/fnb/` is the living example. +- A module's `AGENTS.md` may pin a version for operational reasons (Render + memory, host compatibility…). **That note overrides the version lists and + official docs** — these are infrastructure constraints Context7 does not know + about. `pdf-parse` in `backend/services/fnb/` is the living example. - If you detect a deprecated API in existing code, **mention it in the response but do not change it**. - If current docs recommend a different pattern than the repo uses, follow the @@ -160,42 +122,46 @@ on 2026-10-02. - Does not apply to business logic, internal refactors, or project-specific debugging — only to library APIs. -## Common Commands - -### Frontend (`/frontend`) - -```bash -pnpm dev # Start Next.js dev server with Turbopack (port 3000) -pnpm build # Build production bundle -pnpm start # Start production server -pnpm lint # Run ESLint -pnpm format # Format code with Prettier -``` - -### Backend (`/backend`) - -```bash -pnpm dev # Start backend with tsx watch (port 4000) -pnpm dev:local # Use local MySQL database -pnpm dev:aiven # Use Aiven cloud database -pnpm start # Start backend without watch -pnpm typecheck # Run TypeScript type checking -pnpm test # Run all tests with Vitest -pnpm test:watch # Run tests in watch mode -pnpm test:coverage # Run tests with coverage report -``` - -### Database +## Development Workflow -- SQL scripts are in `backend/db-mysql/`. Policy and schema overview: - `backend/db-mysql/CLAUDE.md` -- ⚠️ **Never** run `MASTER_INSTALL*.sql` or `aiven/NN_*.sql` against a DB with - data — both local and Aiven are populated. First-install scripts only. -- Any schema change goes in a new idempotent - `backend/db-mysql/scripts/YYYYMMDD_*.sql`, plus a row in the "Migraciones - incrementales" table of `backend/db-mysql/INDEX.md`. +1. Start backend: `cd backend && pnpm dev:local` +2. Start frontend: `cd frontend && pnpm dev` +3. Access app at `http://localhost:3000` +4. API available at `http://localhost:4000` -### Markdown Formatting +A fresh local install (tables, first admin, departments, mock data) is +`cd backend && pnpm setup:local`. Each subproject's full command list is in its +own `AGENTS.md`. + +## Frontend ↔ Backend Contract + +What both halves must agree on. The details of each side live in its own +`AGENTS.md`. + +- **Auth is custom JWT, not NextAuth.** The backend sets two HttpOnly cookies, + `access_token` (15 min) and `refresh_token` (7 d), `SameSite=Lax`, both signed + with `SECRET_JWT_KEY`; in production they are scoped to + `.four-points.stackbp.es`. The frontend sends `credentials: 'include'` on + every call and, on a 401, retries through `POST /api/auth/refresh-token`. + There is no `express-session`, no `sessions` table and no dev-mode bypass. +- **The frontend calls the backend directly** — `NEXT_PUBLIC_API_URL` from the + browser, `NEXT_SERVER_API_URL` from the server — with no Next.js API routes in + between. A new frontend origin has to be added to the CORS allowlist in + `backend/config/cors.ts` (or passed as `FRONTEND_URL`): CORS is also the CSRF + defence, and any other origin gets a 403. +- **Roles** — `admin`, `recepcionista`, `mantenimiento`, `group-admin` — are + enforced in the backend. The frontend only hides what a role cannot use. +- **Demo:** one admin account with `users.is_demo = 1`, limited by + `backend/middlewares/demoRestriction.ts`. It exists only with `DEMO_MODE=true` + in the backend; `NEXT_PUBLIC_DEMO_MODE=true` shows its button on the login + page (ADR-038). +- **Shapes are copied by hand, never shared:** Zod schemas (v3 / v4, see above), + the TS types mirrored in `frontend/app/lib//types.ts`, and the + checklist JSON duplicated in `backend/content/` and `frontend/content/`. + Changing one side means changing the other. +- **The UI is in Spanish**, and so are some code comments. + +## Markdown Formatting Prettier with the root config (`.prettierrc`): aligned GitHub-flavored tables, `proseWrap: always` (auto re-flowing prose to 80 columns) and @@ -204,10 +170,10 @@ blocks — respect the original style). **The same Markdown rule applies everywhere.** Prettier uses the nearest config, so `backend/.prettierrc` and `frontend/.prettierrc` repeat it in a `*.md` -override (80 columns, `proseWrap: always`); their code settings are unchanged. -`frontend/.prettierignore` leaves out the checklist guides and references in -`frontend/content/checklist/`, which the app renders, until they are edited on -purpose. +override (80 columns, `proseWrap: always`, `embeddedLanguageFormatting: off`); +their code settings are unchanged. `frontend/.prettierignore` leaves out the +checklist guides and references in `frontend/content/checklist/`, which the app +renders, until they are edited on purpose. **Lines over 80 columns are only acceptable** inside code blocks and where the line is a single unbreakable piece (a long inline code span or link). Prose and @@ -220,8 +186,14 @@ a list (`- **key**: description`) instead of a table. Applies to any `.md` in the repo (including `docs/`) when creating or editing a table; old ones are fixed when touched. +**CI checks every tracked `.md`** with the frontend's Prettier. Both projects +pin the same version: a different one wraps some lines differently, so upgrade +them together. Run the same check from the root before committing, and format +only the files you changed: + ```bash -./frontend/node_modules/.bin/prettier --write "*.md" "docs/**/*.md" "**/AGENTS.md" +git ls-files -z '*.md' | xargs -0 ./frontend/node_modules/.bin/prettier --check --ignore-path .prettierignore --ignore-path frontend/.prettierignore +./frontend/node_modules/.bin/prettier --write ``` ## Commit and Push Conventions @@ -234,8 +206,8 @@ this is not a recommendation. Proper names, technical identifiers and paths can remain in their original language. - **Format: Conventional Commits** — `feat:`, `fix:`, `docs:`, `chore:`, - `refactor:`, `test:`, `perf:`, `build:`, `ci:`, `style:`. Optional scope: - `feat(scheduling/solver): …` + `refactor:`, `test:`, `perf:`, `build:`, `ci:`, `style:`, `revert:`. Optional + scope: `feat(scheduling/solver): …` - **Subject** in imperative mood, ≤ 72 characters, no trailing period. - **Body**: explain the _why_, not the _what_ (the diff already tells that). Bullets when there are 3+ independent points. No vague messages (`fix bug`, @@ -283,643 +255,61 @@ is declared by the owner as the target (`docs/DECISIONS.md` ADR-017), not the current state. **Re-estimate when:** Aiven starts storing real guest or staff data. -**Areas:** - -- Authentication, Session, JWT tokens → `backend/controllers/auth/`, - `backend/services/auth/`, `backend/middlewares/authenticateToken.ts`, - `frontend/app/lib/auth/`, `frontend/proxy.ts` -- Authorization → `backend/middlewares/roleCheck.ts`, - `backend/middlewares/demoRestriction.ts`; object-level authorization lives in - controllers (e.g. `backend/controllers/messages/`) and in `WHERE` clauses of - `backend/repositories/` -- API, Validation / errors → `backend/routes/`, `backend/controllers/`, - `backend/validations/`, `backend/index.ts` -- File upload → routes with multer in `backend/routes/`: `auth`, `backoffice`, - `blacklist`, `checklist`, `fnb`, `maintenance` -- Data protection / privacy → `blacklist`, `cashier`, users - (`backend/repositories/auth/`), parking vehicles (`parking_vehicles`) and - messaging (`backend/repositories/messages/`) -- Configuration / DevOps → `backend/config/`, `frontend/vercel.json`, - `frontend/next.config.ts`, `.env*` -- Logging / monitoring → `backend/config/logger.ts`; business audit trail in - `backend/services/logbook/` and - `backend/repositories/logbook/logbookHistory-repository.ts` -- Validation / errors (data access) → `backend/repositories/` and the triggers - and procedures in `backend/db-mysql/` (parking availability, booking code) - procedimientos de `backend/db-mysql/` (disponibilidad de parking, código de - reserva) -- Frontend → `frontend/app/` - -## Architecture - -### Backend Architecture - -The backend follows a layered architecture pattern: - -``` -index.ts → routes → controllers → services → repositories → models - ↓ - validations (Zod) -``` - -**Key Patterns:** - -- **ES Modules**: All files use `.js` extensions in imports despite being - TypeScript -- **Modular Services**: Each domain (auth, logbook, parking, scheduling, etc.) - has its own: - - `controllers/` - HTTP request handling - - `services/` - Business logic - - `repositories/` - Database access - - `routes/` - Express route definitions - - `validations/` - Zod schemas - - `models/` - TypeScript types/interfaces - -**Database Access:** - -- Two MySQL configurations: local and Aiven (cloud) -- Environment variable `DB_ENVIRONMENT` controls which DB to use -- Connection pool managed in `config/config.ts` - -**Authentication System:** - -- JWT (`jsonwebtoken`) signed with `SECRET_JWT_KEY`. Access token 15 min, - refresh 7 d (same secret). -- Both tokens travel in **HttpOnly cookies** (`access_token`, `refresh_token`); - `Authorization: Bearer` is accepted as fallback. -- In production cookies are scoped to `.four-points.stackbp.es` - (`controllers/auth/auth-controllers.ts:31`). -- Authentication middleware: `middlewares/authenticateToken.ts` (verifies JWT, - then delegates to `demoRestriction`). -- Role-based access control via `middlewares/roleCheck.ts`. -- Frontend sends `credentials: 'include'` in all fetch requests. -- **No `express-session`, no MySQL `sessions` table, no - `authenticateSession.ts`.** Older docs referenced a planned JWT→sessions - migration that was never implemented; the system is and stays JWT. - -**CORS Configuration:** - -- Allows `localhost:3000`, Vercel domains, and production domains -- Dynamic origin validation with Vercel preview pattern matching -- Credentials enabled for cookie transmission - -### Frontend Architecture - -The frontend uses Next.js App Router with server and client components: - -**Directory Structure:** - -``` -app/ -├── (auth)/ # Auth routes (login) -├── dashboard/ # Protected dashboard routes -│ ├── logbooks/ -│ ├── parking/ -│ ├── scheduling/ -│ └── ... -├── components/ # Reusable UI components -├── lib/ # Utilities, API clients, types -│ ├── apiClient.ts # Client-side API wrapper -│ ├── serverFetch.ts # Server-side API wrapper -│ ├── auth/ # Auth utilities -│ ├── schemas/ # Zod validation schemas -│ └── [domain]/ # Domain-specific utilities -├── stores/ # Zustand stores -└── ui/ # Design system components -``` - -**Key Patterns:** - -- **Server vs Client Components**: By default components are server components. - Use `'use client'` directive when: - - Component has interactivity (onClick, onChange, etc.) - - Component uses React hooks (useState, useEffect, etc.) - - Component accesses browser APIs -- **Data Fetching**: Use React Query for client-side data fetching, server - actions for mutations -- **API Communication**: - - Client-side: `apiClient.ts` (wraps fetch with auth) - - Server-side: `serverFetch.ts` (for Server Components/Actions) -- **Authentication**: - - Global `AuthContext` provides `user`, `login`, `logout`, `isAuthenticated` - - Protected routes use `useAuthContext()` hook - - Dashboard layout handles auth redirect logic - -**Font System:** - -- Fonts configured in `app/ui/fonts-design/` -- Change active font in `fonts.helper.ts` (ACTIVE_FONTS variable) -- Apply via Tailwind classes: `font-sans`, `font-display` - -### Scheduling System (Complex Feature) - -The scheduling module manages monthly staff schedules through **manual -cell-by-cell editing** with **real-time validation**, with optional **automatic -generation** via a CP-SAT solver. - -**Core Concepts:** - -- **Month states**: `draft` (editable) ↔ `published` (locked for staff view) -- **Cell locking**: Assignments with `source_constraint_id` are locked (seeded - from approved constraints) -- **Constraint flow**: Create constraint → Approve → assignments auto-synced - (shift applied + cell locked) -- **Retroactive constraints**: Admins can add constraints for past dates (e.g., - sick days retroactively) -- **Auto-generation**: CP-SAT solver (`scheduling-solver/`) generates a full - month respecting all hard constraints and locked cells - -**Backend Components:** - -- **Validator**: `services/scheduling/schedule-validator.ts` - Validates the - full schedule on demand -- **Constraints (validator)**: `services/scheduling/constraints/` - Validation - rules: - - `consecutive-rest.constraint.ts` - Min rest hours between shifts - - `monthly-libre.constraint.ts` - Min/max free days per month - - `rotation-continuity.constraint.ts` - Rotation pattern rules - - `employee-rules.constraint.ts` - Per-employee rules (fixed shift, no - weekends, etc.) -- **Solver**: `scheduling-solver/` - Python CP-SAT solver (Google OR-Tools) - - `daemon.py` - Proceso Python persistente; importa OR-Tools una vez al - arrancar. Acepta peticiones JSON por stdin (newline-delimited), responde por - stdout. Evita el coste de arranque por petición. - - `model.py` - Builds and solves the CP-SAT model; incluye función objetivo - soft (S1 balanceo noches, S2 libres sueltos, S3 preferencia turno) - - `constraints/` - Hard constraint modules (coverage, rest, night_block, - transitions, day_blocks, libres, locked_cells, employee_rules) - - `schemas.py` - Pydantic input/output contracts (SolverInput / SolverOutput) - - `tests/test_corpus.py` - Tests Python: ningún fixture crashea, fixtures - resolubles dan status='ok' - - `tests/test_daemon_stress.py` - Tests del daemon: arranque, requests - válidas/inválidas, recuperación -- **Solver client**: `services/scheduling/solver-client.ts` - Gestiona el daemon - Python: estado (idle/starting/ready), semáforo (peticiones secuenciales), - abort-safe, warm-up al arrancar -- **Solver input builder**: `services/scheduling/build-solver-input.ts` - - Fetches DB data, builds SolverInput JSON (locked cells, cross-month tail, - employee rules, fixedDays pre-expansion, minNightBlock/maxNightBlock del - config) - -**Key Endpoints:** - -- `GET /months/:id` - Full month data (days, assignments, constraints, stats) -- `GET /months/:id/info` - Approved constraints + employee rules panel data -- `POST /months/:id/generate` - Invoke CP-SAT solver, apply result to - assignments -- `POST /months/:id/reset` - Wipe all assignments, re-seed from approved - constraints -- `PATCH /assignments/:id` - Update single cell (returns 409 if cell is locked) -- `POST /assignments/bulk` - Bulk update assignments -- `POST /constraints/:id/approve` - Approve/reject + auto-sync affected - assignments - -**Solver rules (CP-SAT):** - -- Approved requests (vacation V, bonificable B, baja_temporal IT) become locked - cells — the solver respects them and never freely assigns special codes - (V/B/IT/E/FO/A) to other employees -- Cross-month continuity: last 7 days of the previous month (draft or published) - feed into the solver as virtual days (indices -7…-1), bloqueados desde el - tail. Todos los constraints iteran sobre all_days = virtual_days + real_days - de forma uniforme. -- Monthly libre minimum is reduced by the count of locked special-rest days per - employee (V/B/E/IT/FO/A) -- **Night block rules (night_block.py)**: - - Each employee who does nights does exactly 1 block of - minNightBlock..maxNightBlock consecutive nights per month - - `trailing_N = 0` → puede empezar bloque nuevo normalmente - - `0 < trailing_N < minNightBlock` → bloque incompleto del mes anterior; - prohibido iniciar bloque nuevo (debe completarlo al inicio del mes). - **Excepción**: si las celdas necesarias para completar el mínimo están - bloqueadas (ej: vacación), se levanta la restricción y puede iniciar bloque - nuevo más adelante - - `trailing_N >= minNightBlock` → bloque ya completado; puede iniciar bloque - nuevo en el mes actual - - `trailing_N >= maxNightBlock` → día 1 forzado a no-N (bloque en máximo) -- **Función objetivo soft** (minimiza suma ponderada): - - S1 (W=10): balanceo de noches — minimiza rango(max-min) de noches acumuladas - por empleado usando `nightsHistory` del repo - - S2 (W=2): penaliza L aislado (sin L adyacente el día anterior o siguiente) - - S3 (W=1): penaliza turnos distintos al `shiftPriority` del empleado -- `noWeekends` rule: solver forces L on Saturday/Sunday for the affected - employee -- `fixedShift` rule: solver prohibits other work shifts (M/T/N) for the - employee; only their fixed shift or L is allowed -- `fixedDays` rule: handled in `build-solver-input.ts` — the L-V/S-D pattern is - pre-expanded and injected as lockedCells before the solver runs (approved - requests/vacations take priority over fixedDays). Employees with - `fixedShift=P` but no `fixedDays` are excluded from the solver entirely - (unknown pattern). -- **Config**: todos los parámetros de `scheduling_config` (incluidos - `minNightBlock`, `maxNightBlock`, `prefNightBlock`) se envían al solver desde - `build-solver-input.ts`; Python usa sus valores de schema como fallback si - falta alguno - -**Shift Types:** - -- Work shifts: M (Morning), T (Afternoon), N (Night), PI (Internal Support), P - (Presencia) -- Off states: L (Free), V (Vacation), B (Bonificable/Holiday), E (Sick day), IT - (Temp Disability), FO (Day Off), A (Unjustified absence) - -**Frontend Components:** - -- `SchedulingClient.tsx` - Main orchestrator -- `ScheduleGrid.tsx` - Interactive monthly grid (locked cells visually distinct) -- `MonthInfoPanel.tsx` - Shows approved constraints + employee rules -- `ValidationWarnings.tsx` - Real-time validation feedback - (errors/warnings/info) -- `SchedulingConfigClient.tsx` - Config UI (employees, shifts, rules, requests - tabs) -- PDF export in `lib/scheduling/export-pdf.ts` - -**Configuration:** - -- Schedule parameters (min/max staff per shift, rest hours, libre ranges) stored - in `scheduling_config` table -- Employee-specific rules in `scheduling_employee_rules` table - -#### Running the solver locally - -The solver is a persistent Python daemon that the Node backend launches on -startup and keeps alive for the entire server lifetime. Each generation request -is one line of JSON sent over stdin; the response comes back as one line of JSON -on stdout. OR-Tools is imported only once when the daemon starts, so subsequent -generations are fast. - -**One-time setup:** - -```bash -cd backend/scheduling-solver -python -m venv venv # create local venv -venv/Scripts/pip install ortools pydantic pytest # Windows -# venv/bin/pip install ortools pydantic pytest # Linux/Mac -``` - -The venv lives under `backend/scheduling-solver/venv/` and is gitignored. On -Render (production deploy), the build command needs to recreate it — see -`TODO.md` "Deploy en Render". - -**Day-to-day workflow:** - -```bash -cd backend -pnpm dev:local # backend + daemon Python launch automatically -``` - -When the backend boots, `services/scheduling/solver-client.ts` spawns -`scheduling-solver/daemon.py` and calls `warmupSolver()` from `index.ts` so -OR-Tools imports while the server is starting up. By the time the first request -arrives, the daemon is ready (state `ready`). - -The daemon's lifecycle states are managed in `solver-client.ts`: - -- `idle` — not yet spawned -- `starting` — process spawned, waiting for "READY" handshake on stdout -- `ready` — accepting requests -- `busy` — currently solving a request (semaphore enforces sequential - processing) - -If the daemon dies unexpectedly (Python crash, OR-Tools error), the next call -will detect the broken pipe and respawn it transparently. - -**Triggering a generation from the UI:** - -1. Open `http://localhost:3000/dashboard/scheduling`. -2. Pick a month in `draft` state (or create one via the month picker). -3. Click the "Generar horario" button. Frontend calls - `POST /months/:id/generate`, which invokes - `solver-client.ts → runSolver(input)`. -4. The solver returns either `{status: 'ok', matrix, stats}` or - `{status: 'infeasible', conflictingConstraints, suggestedRelaxations}`. The - controller in `controllers/scheduling/schedule-generate.controller.ts` - applies the matrix to `scheduling_assignments` inside a transaction. - -**Testing the solver in isolation (no Node):** - -The Python solver has its own pytest suite that runs the entire fixture corpus -directly through `model.solve()`. Use this when iterating on a constraint to -skip the Node round-trip: - -```bash -cd backend/scheduling-solver -venv/Scripts/python -m pytest tests/ # full Python suite (~15s, 103 tests) -venv/Scripts/python -m pytest tests/test_corpus.py # only the corpus -venv/Scripts/python -m pytest tests/test_benchmark.py --runbenchmark # 30×31 stress test -``` - -For the parity test that runs the Python solver against the TS validator (the -actual cross-language sync check), use vitest from the backend root: - -```bash -cd backend -pnpm vitest run tests/scheduling/solver-parity.test.ts # ~5s -pnpm vitest run tests/scheduling/corpus.test.ts # ~1s, no Python -``` - -**Debugging a specific month:** - -There are three Node helpers in `backend/` (not in tests/) that connect directly -to the local DB and inspect state without running the full server: - -- `debug-compare.js` — list months in DB with their state. -- `debug-month.js` — print all assignments for a given month ID. -- `debug-sept.js` — full debug dump for a hardcoded month: employees, locked - cells, the SolverInput JSON that would be sent, and the solver output. - -Edit the month ID inside the script and run with `node debug-sept.js`. Useful -when the solver returns `infeasible` in production and you want to inspect -exactly what got sent to it. - -#### Adding a new constraint - -The system maintains the same rule logic in three places: the TS validator -(real-time editing feedback), the Python solver (generation), and the JSON -corpus (regression tests). Skipping any of them creates drift, which the parity -test will eventually catch but slowly. The order below is the cheapest path to a -closed loop. - -**Step 1 — Decide hard vs soft and document it.** - -Open `docs/scheduling/constraints.md`. If the rule is hard (a violation -invalidates the schedule), add it to §2 with an `H` ID. If it is soft (a -violation is acceptable but penalised), add it to §3 with an `S` ID, choose a -tentative weight in the 1-10 range, and explain _why_ that weight relative to -the others. The weight will be tuned in production; the relative ordering is -what matters here. - -If the rule depends on continuity across the month boundary, also list it in -§9.5 (cross-month invariant) so future readers know to wire -`previousMonthHistory` into it. - -**Step 2 — Implement in the TS validator.** - -Create `backend/services/scheduling/constraints/.constraint.ts` -extending `BaseConstraint`. The constraint receives a `GeneratorContext` with -`matrix`, `employees`, `days`, `config`, and `previousMonthHistory` (null on the -first month). For hard rules, push entries through -`this.warn(message, {severity: 'error', ...})`. For soft rules, use -`this.softWarn(weightKey, units, message, ...)` which automatically pulls the -weight from `soft-weights.ts` and accumulates `softPenalty` and -`softPenaltyBreakdown`. - -If you added a new soft weight, declare it in -`services/scheduling/soft-weights.ts` with a JSDoc tag `@emitter` pointing to -the constraint file. Constraints emit warnings with `severity: 'error'` for hard -violations and `severity: 'warning'` for soft penalties. The split is: -`validate()` returns `isValid: errors.length === 0`, plus a separate -`softPenalty` total. - -Register the constraint in `schedule-validator.ts`, inside the -`ConstraintRegistry` block of `validate()`, alongside `CoverageConstraint` and -`EmployeeRulesConstraint`. If your rule fits more naturally inside the -per-employee loop of `runFinalValidation` (because it needs cumulative state per -employee, like libre counts), inline it there instead. - -**Step 3 — Implement in the Python solver.** - -Create `backend/scheduling-solver/constraints/.py` exposing an -`apply(model, x, input, employees, days, virtual_days_by_emp=None)` function. -The signature matches every other constraint in that directory (cross-reference -`coverage.py` or `rest.py` for templates). - -The CP-SAT model variables are `x[employee_index, day_number, shift_code]` — -boolean. For days inside the month, `day_number` ranges 1..N. For cross-month -context, virtual days `-7..-1` are pre-blocked from `previousMonthTail` in -`model.py`. To make a constraint cross-month aware, iterate over -`all_days = virt_days + real_days` instead of just `day_numbers`. Several -constraints (rest, transitions, day_blocks) already follow this pattern. - -Hard constraints use `model.add(...)` to assert the rule. Soft constraints -introduce reified boolean indicator variables (e.g. "is this day a violation?") -and add weighted terms to `objective_terms` inside `solve()`. The S4 -implementation in `model.py` (search for `W_SHORT_WORK_BLOCK`) is a clean -reference for soft constraints with cross-month behaviour. - -Wire your constraint into `model.solve()` by importing it and calling `apply()` -after the existing constraints. Constraint order does not matter for correctness -(CP-SAT is declarative), but grouping related rules together keeps the file -readable. - -If the constraint contributes to `softPenalty`, also accumulate its breakdown -for the output. The pattern is to keep a list of `(indicator_var, weight)` -tuples while building the model and read back `solver.value(var)` after -`solver.solve()` to populate `stats.softPenaltyBreakdown[]` — same key -string as in `soft-weights.ts`. - -**Step 4 — Add a fixture to the corpus.** - -Create `backend/tests/scheduling-corpus/fixtures/Fnn-.json` -following the schema in `_schema.ts`. The fixture has three sections: - -1. `input` — month, employees, days, config, lockedCells, previousMonthHistory, - and a fully-formed `assignments` matrix. The matrix should exercise the rule: - for a hard violation, include a schedule that breaks it. For a soft test, - include a schedule where the rule's penalty is calculable manually. -2. `expected.isValid` — true if the schedule has no hard errors, false - otherwise. -3. `expected.violations` — list of matchers (type, severity, employeeId, day) - that the validator must emit. Plus `expected.softPenalty` and - `expected.softPenaltyBreakdown` for soft contributions. - -If the fixture is intended to also pass through the Python solver (i.e. the -schedule is solver-reachable), add its ID to `SOLVABLE_FIXTURES` in -`scheduling-solver/tests/test_corpus.py` and to `PARITY_FIXTURES` in -`tests/scheduling/solver-parity.test.ts`. Coverage-disabled fixtures -(`minMorningStaff: 0`, etc.) are usually safe additions to both lists. - -**Step 5 — Run the four test suites.** - -```bash -cd backend -pnpm vitest run tests/scheduling/corpus.test.ts # TS validator vs fixtures -pnpm vitest run tests/scheduling/solver-parity.test.ts # Solver output → TS validator (no hard errors) -cd scheduling-solver -venv/Scripts/python -m pytest tests/test_corpus.py # Python solver no crashes -``` - -All four must be green. If parity fails, the validator and solver disagree -somewhere; if `test_corpus.py` errors, the Python constraint has a bug; if -`corpus.test.ts` fails, the TS validator does. The error message points at the -divergence — fix the side that disagrees with the spec in -`docs/scheduling/constraints.md` (the spec is the source of truth, not the -code). - -**Step 6 — Document the decision.** - -Append a brief entry to `docs/scheduling/decisions.md` with the date, the rule, -why it's hard or soft, and the chosen weight if applicable. This is the audit -trail that lets a future agent (or you, six months later) understand why a -constraint exists without spelunking through git history. - -## Important Development Notes - -### Backend Notes - -1. **Import Extensions**: Always use `.js` extension in imports despite - TypeScript: - - ```typescript - import { something } from './file.js' // Correct - import { something } from './file' // Wrong - ``` - -2. **No Build Step**: Backend uses `tsx` directly, no compilation needed - -3. **Environment Files**: - - `.env` for backend configuration - - Two database modes: `DB_ENVIRONMENT=local` or `DB_ENVIRONMENT=aiven` - -4. **Auth is JWT** (HttpOnly cookies). The repo briefly contained plans to - switch to `express-session`; those plans were dropped. Do not reintroduce - `req.session` or look for a `sessions` table — it does not exist. - -5. **Layering**: `routes → controllers → services → repositories → models`. - Validations (Zod) sit between controller and service. - -6. **Cron Jobs**: `services/cron/cron-service.ts` runs scheduled tasks, started - in `index.ts` - -7. **Error Handling**: Global error handler in `index.ts` catches all unhandled - errors; module-specific codes in `config/error-codes.ts` - -8. **Testing**: Tests use Vitest and are located in `tests/` directory - -### Frontend Notes - -1. **Server Component First**: Default to server components, only add - `'use client'` when needed - -2. **Paths**: Everything lives under `frontend/app/` — there is **no - `frontend/lib/`**. The utilities directory is `frontend/app/lib/`. - -3. **Data Fetching**: - - Server Components: Use `serverFetch` or direct fetch - - Client Components: Use React Query with `apiClient` - - Always include `credentials: 'include'` in fetch options - -4. **Authentication Flow**: - - Login → Backend sets `access_token` and `refresh_token` HttpOnly cookies - - Frontend `AuthProvider` calls `GET /api/auth/me` on mount; on 401 it - retries via `POST /api/auth/refresh-token` - - `useAuthContext()` provides auth state globally - -5. **Environment Variables**: - - Client-accessible: `NEXT_PUBLIC_API_URL` - - Server-only: Can use non-prefixed vars in Server Components - -6. **Proxy vs Direct**: Frontend calls backend directly at `localhost:4000`, no - Next.js API routes proxy - -7. **Theme System**: Uses `next-themes` for dark/light mode switching - -8. **Form Validation**: Use React Hook Form + Zod schemas from `lib/schemas/` - -### Common Pitfalls to Avoid - -1. **Don't mix server and client patterns**: Can't use `useState` in server - components or `async` component functions in client components - -2. **Don't forget credentials**: All API calls must include - `credentials: 'include'` for auth cookies - -3. **Don't modify code without permission**: Always ask before making code - changes - -4. **Database changes**: Coordinate SQL migrations between local and Aiven - databases - -5. **Module resolution**: Backend imports must use `.js` extension even for - `.ts` files - -## Module-Specific Guidance - -### Logbook System - -- Tracks hotel operational notes and tasks -- Supports read/unread status (stored in `logbook_history` table) -- Has comment system for entries -- Frontend: `app/dashboard/logbooks/` (plural) -- Backend: `controllers/logbook/`, `services/logbook/` (singular — the plural is - only the frontend route) - -### Parking System - -- Manages hotel parking spaces and bookings -- Multiple sub-routes: stats, bookings -- Frontend: `app/dashboard/parking/` -- Backend: Multiple route files in `routes/parking/` - -### Authentication System - -- JWT (`jsonwebtoken`), signed with `SECRET_JWT_KEY`. Tokens are stateless (no - DB lookup per request). -- Cookies: `access_token` (15 min) and `refresh_token` (7 d), both HttpOnly + - SameSite=lax. -- Production cookie domain: `.four-points.stackbp.es`. -- Roles defined in `roles` table (admin, recepcionista, mantenimiento, - group-admin, demo-admin). Enforcement in `middlewares/roleCheck.ts`. -- Demo user (`username: demo`) is **disabled** (`is_active=0`) since 2026-05-12. - See `aiven/15_demo_user.sql` for context. - -### Maintenance System - -- Tracks maintenance requests and tasks -- Status workflow: pending → in-progress → completed -- Frontend: `app/dashboard/maintenance/` - -### Messaging System - -- Internal messaging between staff -- Real-time notifications -- ⚠️ **There is no `app/dashboard/messages/` route.** The UI is a panel inside - the profile page: `app/components/profile/MessagesPanel.tsx`, with hooks and - queries in `app/lib/messaging/` -- Backend: `controllers/messages/`, `repositories/messages/`, - `routes/messages/messages-routes.ts` - -## Module Documentation Index - -Each entry below points to an `AGENTS.md` with module-specific context. Open the -agent from the module's directory to load only the relevant files. - -- **Scheduling** (CP-SAT solver, grid UI): `backend/services/scheduling/` · - `backend/scheduling-solver/` · `frontend/app/components/scheduling/` -- **Checklist** (daily ops checklists, JSON sync rule): - `backend/services/checklist/` -- **Logbook** (incident log, read/unread, audit trail): - `backend/services/logbook/` -- **Parking** (bookings lifecycle, responsive reference): - `backend/services/parking/` · `frontend/app/dashboard/parking/` -- **Maintenance** (7-state workflow, Cloudinary images): +The same level line is repeated in `backend/AGENTS.md` and `frontend/AGENTS.md`; +a re-estimate changes all three. + +**Areas:** each subproject lists its own in its `## Security` section, with +paths relative to it — [`backend/AGENTS.md`](backend/AGENTS.md#security) and +[`frontend/AGENTS.md`](frontend/AGENTS.md#security). A review that spans both +reads both lists; one that spans neither (root config, `.githooks/`, `.github/`) +deduces its areas from the code. + +## Module Map + +Each entry points to the module's `AGENTS.md`. Some modules have one doc per +side; the smaller ones keep a single doc covering both, where most of their +logic lives. + +- **Scheduling** (CP-SAT solver, grid UI) — backend TS: + `backend/services/scheduling/` · Python solver: `backend/scheduling-solver/` · + UI: `frontend/app/components/scheduling/` +- **Parking** (bookings lifecycle, responsive reference) — backend: + `backend/services/parking/` · UI: `frontend/app/dashboard/parking/` +- **Cashier** (shifts, denominations, vouchers, PDF export) — backend: + `backend/services/cashier/` · UI: `frontend/app/components/cashier/` +- **Group Tracking** (bookings, payments, 4-status tracks) — backend: + `backend/services/group/` · UI: `frontend/app/components/groups/` +- **Maintenance** (7-state workflow, Cloudinary images) — both sides: `frontend/app/components/maintenance/` -- **F&B / Restaurant** (Opera PDF parser, daily revenue): +- **Checklist** (daily ops checklists, JSON sync rule) — both sides: + `backend/services/checklist/` +- **Logbook** (incident log, read/unread, audit trail) — both sides: + `backend/services/logbook/`. The frontend route is plural + (`app/dashboard/logbooks/`), the backend folders singular +- **F&B / Restaurant** (Opera PDF parser, daily revenue) — both sides: `backend/services/fnb/` -- **Cashier** (shifts, denominations, vouchers, PDF export): - `backend/services/cashier/` · `frontend/app/components/cashier/` -- **Group Tracking** (bookings, payments, 4-status tracks): - `backend/services/group/` · `frontend/app/components/groups/` -- **Backoffice** (suppliers, invoices, assets, batch-pay): +- **Backoffice** (suppliers, invoices, assets, batch-pay) — both sides: `backend/services/backoffice/` -- **Blacklist** (banned guests, Cloudinary photos, audit trail): +- **Blacklist** (banned guests, Cloudinary photos, audit trail) — both sides: `backend/services/blacklist/` -- **DB / Migrations** (policy, schema, idempotent scripts): `backend/db-mysql/` +- **DB / Migrations** (policy, schema, idempotent scripts) — `backend/db-mysql/` +- **Messaging** — no module doc yet: its paths are in `backend/AGENTS.md` and + `frontend/AGENTS.md` ## Cross-Module Navigation When working inside a module and encountering a concept, endpoint, or behaviour that belongs to another module: -1. **Don't guess** from the 1-line entry in the Module Documentation Index above - — that entry only tells you where the doc is, not what's in it. -2. **Read the target module's `AGENTS.md`** (path shown in the index) before +1. **Don't guess** from the 1-line entry in the Module Map above — that entry + only tells you where the doc is, not what's in it. +2. **Read the target module's `AGENTS.md`** (path shown in the map) before making any decision that touches that module. 3. Then return to the original task with full context. -The Module Documentation Index is a discovery map, not a summary. If something -in the codebase seems undocumented, check whether a sibling module's `AGENTS.md` -covers it before assuming it isn't documented. +The Module Map is a discovery map, not a summary. If something in the codebase +seems undocumented, check whether a sibling module's `AGENTS.md` covers it +before assuming it isn't documented. ## Adding a New Module @@ -927,51 +317,19 @@ When a new module grows complex enough to warrant its own doc: 1. Create `AGENTS.md` in the most specific relevant directory (`backend/services//` or `frontend/app/components//`). If no - `services/` dir exists, create it as a documentation anchor. + `services/` dir exists, create it as a documentation anchor. Add a one-line + `CLAUDE.md` next to it with `@AGENTS.md`. 2. Write it in idiomatic technical English. Make it self-contained — a reader in that directory should not need the root to understand the module. -3. Add an entry to the **Module Documentation Index** list above. -4. Commit as `docs(agents): split module context into module file`. - -## Code Search Commands - -For searching code in Windows PowerShell: - -```powershell -# Search in TypeScript/JavaScript files -Get-ChildItem -Recurse -Include *.ts,*.tsx -Path app | Select-String "pattern" - -# Exclude node_modules and dist -Get-ChildItem -Recurse -Include *.js,*.ts -Exclude node_modules,dist | Select-String "pattern" -``` - -## Database Information - -- **Database Name**: `hotel_db` -- **Key Tables**: users, logbook, parking, scheduling_months, - scheduling_assignments, scheduling_solver_runs, scheduling_employee_requests, - checklist_runs, demo_activity_log -- **Migrations**: Manual SQL scripts in `backend/db-mysql/` -- **Backups**: Store in `backend/db-mysql/backup/` - -## External Services - -- **Cloudinary**: Image upload and storage -- **Email**: none. The unused `email-service.ts` and the `nodemailer` - dependency were removed on 2026-10-02 - -## Development Workflow - -1. Start backend: `cd backend && pnpm dev:local` -2. Start frontend: `cd frontend && pnpm dev` -3. Access app at `http://localhost:3000` -4. API available at `http://localhost:4000` - -## Notes - -- The system uses Spanish for UI and some code comments -- ⚠️ **There is no dev-mode auth bypass.** Older docs mentioned a `DEV_MODE` - flag in a `DashboardLayout` component: neither the flag nor that component - exists anywhere in the repo. Auth is enforced in every environment by - `app/dashboard/layout.tsx`, which calls `useAuth()` and redirects when there - is no user. Do not write code that assumes it can be skipped locally. +3. Open it with the up link the other module docs carry (subproject `AGENTS.md` + and this file), and link its counterpart on the other side, both ways. +4. Add it to the **Module Map** above and to the module list of its subproject's + `AGENTS.md` (both, when it covers both sides). +5. Commit as `docs(agents): split module context into module file`. + +## Common Pitfalls to Avoid + +1. **Don't modify code without permission**: always ask before making code + changes. +2. **Database changes**: coordinate SQL migrations between local and Aiven + databases — see `backend/AGENTS.md` § Database. diff --git a/CLAUDE.md b/CLAUDE.md index 34eed31c..024ef69e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,3 +1,4 @@ -# CLAUDE.md +@AGENTS.md -This file has been migrated to `AGENTS.md`. Please refer to `AGENTS.md` for all project documentation. +This file has been migrated to `AGENTS.md`. Please refer to `AGENTS.md` for all +project documentation. diff --git a/README.md b/README.md index a4fe20c8..197fcf64 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,11 @@ scheduler that solves the monthly roster with constraint programming. ![Four-Points dashboard](docs/images/dashboard.png) It complements the hotel's commercial PMS (Opera): logbook, parking, -maintenance, groups, cashier, blacklist, staff scheduling, shift checklists, -F&B revenue and invoice backoffice. +maintenance, groups, cashier, blacklist, staff scheduling, shift checklists, F&B +revenue and invoice backoffice. -| Staff scheduling (OR-Tools) | Parking | -| --- | --- | +| Staff scheduling (OR-Tools) | Parking | +| --------------------------------------------------------------------- | -------------------------------------------- | | ![Monthly roster generated by the solver](docs/images/scheduling.png) | ![Parking overview](docs/images/parking.png) | - **Frontend**: Next.js 16 (App Router), React 19, Tailwind CSS, NextUI. @@ -36,19 +36,20 @@ Full documentation lives in [`docs/`](docs/general/README.md): - [Backoffice](docs/backoffice/README.md) - [Blacklist](docs/blacklist/README.md) - [Conciliation](docs/conciliation/README.md) -- [Public demo](docs/demo/README.md) — how the demo account works and when it resets +- [Public demo](docs/demo/README.md) — how the demo account works and when it + resets ## Try it -A public demo runs on the project site: **Try our live demo** on the login -page signs you in as an administrator, with fictitious data that resets every -day. User management and settings are locked there. To see everything, run it +A public demo runs on the project site: **Try our live demo** on the login page +signs you in as an administrator, with fictitious data that resets every day. +User management and settings are locked there. To see everything, run it locally. ## Run it locally -You need Node ≥ 22.16, pnpm, Python 3.11+ (for the scheduling solver) and -MySQL 8, either installed or through Docker. `frontend/` and `backend/` are +You need Node ≥ 22.16, pnpm, Python 3.11+ (for the scheduling solver) and MySQL +8, either installed or through Docker. `frontend/` and `backend/` are independent pnpm projects, each with its own lockfile. ```bash @@ -76,15 +77,15 @@ pnpm dev:local # backend/: API on :4000 cd frontend && pnpm install && pnpm dev # http://localhost:3000 ``` -Sign in as **`admin`** / **`fourpoints-local`**: a full administrator, -users, roles and settings included. The password is public on purpose: -`setup:local` only runs against a local database, so this account never -exists anywhere else. Change it in Profile if you like. +Sign in as **`admin`** / **`fourpoints-local`**: a full administrator, users, +roles and settings included. The password is public on purpose: `setup:local` +only runs against a local database, so this account never exists anywhere else. +Change it in Profile if you like. -Uploading photos and PDFs needs a Cloudinary account in `backend/.env`. -Without it everything else works, and an upload just says storage is not -configured. +Uploading photos and PDFs needs a Cloudinary account in `backend/.env`. Without +it everything else works, and an upload just says storage is not configured. ## License -This project is licensed under the MIT License — see the [LICENSE](LICENSE) file for details. +This project is licensed under the MIT License — see the [LICENSE](LICENSE) file +for details. diff --git a/.nvmrc b/backend/.nvmrc similarity index 100% rename from .nvmrc rename to backend/.nvmrc diff --git a/backend/.prettierrc b/backend/.prettierrc index e91c0509..bf20e9a8 100644 --- a/backend/.prettierrc +++ b/backend/.prettierrc @@ -10,6 +10,7 @@ "options": { "printWidth": 80, "proseWrap": "always", + "embeddedLanguageFormatting": "off", "tabWidth": 2 } } diff --git a/backend/AGENTS.md b/backend/AGENTS.md new file mode 100644 index 00000000..70916b0f --- /dev/null +++ b/backend/AGENTS.md @@ -0,0 +1,322 @@ +# backend + + + +## Verification + +1. **Verify before asserting, and with whatever consumes it.** Never take a + file's word about the environment: **run the tool that reads it and trigger + what it controls.** What cannot be checked stays `❓ pending`, said out loud. + + _Asking is not enough: `pnpm config get` answered `42` for a key that does + not exist, and `true` for one that changed nothing._ + +2. **"Done" means verified by CI, not asserted.** What has not passed the gate + stays pending, and that is said. + + _A green on your machine only proves your machine._ + +## What the AI does not decide + +3. **`.env`, credentials and real data dumps: neither read without explicit + permission, nor committed.** What the agent reads **enters the + conversation**, and from there it does not come back. `.env.example` is what + travels. + +4. **The AI installs and configures nothing on its own**: it proposes, the owner + decides. + + _An install or a config change lands in everyone's lockfile and CI, and + neither shows in the diff the agent presents._ + +5. **Nothing destructive without a `--dry-run` first**, showing what would be + deleted. + + _A `git clean -fd` took three unrecoverable directories._ + +6. **The AI commits; the person decides the push.** Local commits pile up until + the go-ahead, **and one go-ahead covers one push**, not the next ones. + +7. **No AI trace in commits or output**: no `Co-Authored-By`, no "Generated + with…". + + _Tools add it by default. History says what changed and why, not with which + tool._ + +8. **Conventional Commits, in English**: `type(scope): summary`, with `!` for a + breaking change. The types: `feat` · `fix` · `docs` · `style` · `refactor` · + `perf` · `test` · `build` · `ci` · `chore` · `revert`. The body carries + **only** the list of changes and the reason for each — the how is already in + the diff. + +## The environment + +9. **An agent session has no Node auto-switch.** The hook lives in the shell + profile and a non-interactive session never loads it: the session runs on + whatever Node it **inherited when it was launched**, and `cd` into another + project does not change it. + + **Start the session from a terminal already on the repo's version** — enter + the directory, let fnm/nvm switch, then launch the agent. **One session per + project.** + + _If that was not done, activate it on every invocation: `fnm use ` + then the command. `fnm exec --using=N pnpm` does not work on Windows — `pnpm` + is a `.cmd` and `fnm exec` only launches real executables._ + +10. **pnpm, never npm.** + +_`npm install` leaves a `package-lock.json` next to `pnpm-lock.yaml`, and two +installers disagree in silence._ + +11. **pnpm settings live in `pnpm-workspace.yaml`**; `.npmrc` carries registry + and authentication only — from pnpm 11 anything else there is ignored, and + pnpm 10 still reads it, so it works today and stops on upgrade. Every repo + with dependencies sets `minimumReleaseAge` there. + +## Continuity + +12. **State lives in `docs/SESSION.md`**, written **when a task closes** — at + session close there is no context left — and **overwritten**: git keeps the + history. Nothing the code or `git log` already tells goes in. + +13. **The reasoning lives in `docs/DECISIONS.md`**, append-only. A decision that + changes **is not edited**: another is appended revising it, and the old one + is **marked, never deleted**. Knowing what was believed is part of the + record. + +14. **An exception is declared** in the project's `AGENTS.md`, with its reason + and its scope; failing silently is not an option. Does not apply to + security, AI traces or pushing. + +## Naming + +15. **File names, configuration keys and identifiers in English.** + +## Knowledge that is not in this file + +16. **Skills are per project.** `npx autoskills` installs them into + `/.claude/skills/`, which is **git-ignored and rebuilt** like + `node_modules`; **`skills-lock.json` is committed** — it carries each + skill's source and hash, and it is what makes three machines rebuild the + same set. **The developer runs `autoskills`, not the agent** — `npx` + downloads a package, and installing is not the agent's call. + +_Global skills in `~/.claude/skills/` are the machine's, not the project's._ + +17. **Official docs and MCPs are consulted and never silenced**, at the + **installed** version — from the lockfile, not the `^` range. **Context7 is + the one every project uses**: `resolve-library-id`, then `query-docs` at + that version. If the MCP does not answer, say so instead of pretending it + was consulted. + +## Review + +18. **When work is reviewed, this is the shape.** `architect` before building; + `reviewer` and `security` over what was built, **read-only**; `tester` to + verify. Run them whenever you want, together or on their own. + + + +`backend/` is one of two independent pnpm projects inside the `Four-Points` repo +(the other is `frontend/`). The root [`AGENTS.md`](../AGENTS.md) holds what both +share — Context7 policy, Markdown formatting, commits and hooks, the security +level, the frontend ↔ backend contract and the module map — and is loaded +through the root `CLAUDE.md`. + +This file also covers the stack, commands, architecture and module docs of +`backend/`. Paths are relative to `backend/` unless they start with `frontend/`. + +## Exceptions + +- **Rules 12 and 13 — state and decisions live at the repo root.** They are + `docs/SESSION.md` and `docs/DECISIONS.md` in the root, shared with + `frontend/`. `backend/docs/` is git-ignored (`backend/.gitignore:127`) and is + not used for them. +- **Rule 9 — the session starts at the repo root, not here.** One session works + on both subprojects, and they run different Node majors (22 here, 24 in + `frontend/`), so no inherited Node fits both and the root has no `.nvmrc`. + Every Node or `pnpm` command activates this project's version first, from its + `.nvmrc`: `eval "$(fnm env --shell bash)" && fnm use && pnpm …` in Bash, or + `fnm env | Out-String | Invoke-Expression; fnm use` in PowerShell, both run + inside `backend/`. + +## Security + +**L3** — estimated on 2026-09-28: there is login and user accounts, personal +data (users, guest blacklist with photos, cashier) and own backend; the L3 level +is declared by the owner as the target (`docs/DECISIONS.md` ADR-017), not the +current state. **Re-estimate when:** Aiven starts storing real guest or staff +data. + +The level line is the same in the root `AGENTS.md`; a re-estimate changes both. +Paths below are relative to `backend/`. + +**Areas:** + +- Authentication, Session, JWT tokens → `controllers/auth/`, `services/auth/`, + `middlewares/authenticateToken.ts` +- Authorization → `middlewares/roleCheck.ts`, `middlewares/demoRestriction.ts`; + object-level authorization lives in controllers (e.g. `controllers/messages/`) + and in `WHERE` clauses of `repositories/` +- API, Validation / errors → `routes/`, `controllers/`, `validations/`, + `index.ts` +- File upload → `middlewares/imageUpload.ts` and + `middlewares/backofficeUpload.ts` (routes `auth`, `backoffice`, `checklist`, + `maintenance`); multer directly in `routes/blacklist/` and `routes/fnb/` +- Data protection / privacy → `blacklist`, `cashier`, users + (`repositories/auth/`), parking vehicles (`parking_vehicles`) and messaging + (`repositories/messages/`) +- Configuration / DevOps → `config/`, `.env*` +- Logging / monitoring → `config/logger.ts`; business audit trail in + `services/logbook/` and `repositories/logbook/logbookHistory-repository.ts` +- Validation / errors (data access) → `repositories/` and the triggers and + procedures in `db-mysql/` (parking availability, booking code) + +## Where the model goes wrong + +- **A prompt that asks for several tasks at once** → work them **one at a + time**, each with the context specific to it, when the work touches code. + +## Stack + +- **Express 5.1.0** on Node 22 (`.nvmrc`, `engines: 22.x`), pnpm 10.28.0. +- **TypeScript 5.9.3**, ES modules (`"type": "module"`), run directly by **tsx** + — no compile step. `pnpm build` only creates the Python venv for the + scheduling solver; it is Render's build command. +- **MySQL** through `mysql2`, against a local DB or Aiven (cloud). +- **Zod 4** for validation, **Vitest** for tests, **pino** for logs. +- **Cloudinary** for images and PDFs. No email sending: `email-service.ts` and + `nodemailer` were removed on 2026-10-02. + +## Installed Versions + +From `pnpm-lock.yaml`, checked on 2026-10-13. Query Context7 at these versions +(root `AGENTS.md` § Up-to-date Documentation). + +- **`express`** 5.1.0 — v5: async errors automatic, path-to-regexp 8, + `req.query` getter +- **`zod`** 4.0.5 — ⚠️ v4, a different API from the v3 in `frontend/` +- **`cloudinary`** 2.8.0 — SDK v2, API different from v1 in most examples +- **`multer`** 2.0.2 — v2 breaks from the 1.x in common examples +- **`pdf-parse`** 1.1.1 — **pinned to v1** (v2 → OOM on Render). See + `services/fnb/AGENTS.md` +- **`vitest`** 4.0.16 — mock API and `environment` different from v1/v2 +- **`typescript`** 5.9.3 — `frontend/` runs 5.7.3 + +Lower risk, but verify when touched: `node-cron` 4.2.1, `express-rate-limit` +8.2.1, `helmet` 8.1.0, `pino` 10.3.1, `archiver` 7.0.1, `mysql2` 3.14.2, +`jsonwebtoken` 9.0.2. + +## Commands + +```bash +pnpm dev # tsx watch; DB from DB_ENVIRONMENT (default local) +pnpm dev:local # local MySQL +pnpm dev:aiven # Aiven cloud DB +pnpm start # without watch +pnpm setup:local # fresh local install: tables, first admin, departments, mock data +pnpm typecheck # tsc --noEmit +pnpm lint # ESLint +pnpm format # Prettier (format:check to only check) +pnpm test # all tests with Vitest (test:watch, test:coverage) +pnpm test:corpus # scheduling corpus only +pnpm verify # lint + format:check + typecheck + test — what CI runs +``` + +The port comes from `PORT` (4000 in `.env.example`; the code falls back to +3000). `demo:user`, `demo:user:aiven` and `seed:scheduling:export` are one-off +scripts in `scripts/`. + +## Architecture + +``` +index.ts → routes → controllers → services → repositories → models + ↓ + validations (Zod) +``` + +- **Imports use the `.js` extension** even for `.ts` files: + `import { x } from './file.js'`, never `'./file'`. +- **One folder per domain in each layer**: `routes//`, + `controllers//`, `services//`, `repositories//`, + `validations/`, `models//`. +- **Database**: the pool is in `config/db.ts`; `config/db-config.ts` picks the + connection by `DB_ENVIRONMENT` (`local` by default, or `aiven`). Transactions + through `config/transaction.ts`. +- **Errors**: Express 5 forwards async errors to the global handler in + `index.ts`; module-specific codes in `config/error-codes.ts`. +- **CORS** (`config/cors.ts`): a fixed list of origins — `localhost:3000` + outside production, the production and Vercel domains — plus `FRONTEND_URL`. + Credentials enabled. It is also the CSRF defence: any other origin gets a 403 + before reaching a route. +- **Cron jobs**: `services/cron/cron-service.ts`, started from `index.ts`. +- **Tests**: Vitest, in `tests/`, one folder per domain. + +## Authentication + +The contract with the frontend is in the root `AGENTS.md`; here, where it lives. + +- **JWT** (`jsonwebtoken`), signed with `SECRET_JWT_KEY`, stateless — no DB + lookup per request. The cookies are set in + `controllers/auth/auth-controllers.ts`; `COOKIE_DOMAIN` applies only in + production. +- **`middlewares/authenticateToken.ts`** reads the `access_token` cookie + (`Authorization: Bearer` as fallback), verifies it and then runs + `demoRestriction`. +- **Roles** are enforced in `middlewares/roleCheck.ts` (`canManageGroups`, + `canAccessFnb`…). Object-level checks live in controllers and in the `WHERE` + clauses of `repositories/`. +- **Demo** (ADR-038): only with `DEMO_MODE=true`. The account is an admin with + `users.is_demo = 1`, carried in the token; `middlewares/demoRestriction.ts` + blocks what the demo must not touch, and a route that writes outside what the + daily reset restores needs its own `denyDemo` / `denyDemoWrites` guard. There + is no `demo-admin` role any more. +- **Never reintroduce** `express-session`, `req.session` or a `sessions` table: + they were planned once and dropped. + +## Database + +- Database `hotel_db`, local and on Aiven — **both have data**. +- ⚠️ **Never** run `MASTER_INSTALL*.sql` or `db-mysql/aiven/NN_*.sql` against + them: first-install scripts only. A fresh install is `pnpm setup:local`. +- Any schema change goes in a new idempotent `db-mysql/scripts/YYYYMMDD_*.sql`, + applied to local and Aiven, plus a row in the "Migraciones incrementales" + table of `db-mysql/INDEX.md`. +- Policy, schema overview, roles and backups: + [`db-mysql/AGENTS.md`](db-mysql/AGENTS.md). Read it before touching any table. + +## Messaging + +No module doc yet. Internal messages between staff: `controllers/messages/`, +`repositories/messages/` (object-level authorization in the queries) and +`routes/messages/messages-routes.ts`. The UI side is in `frontend/AGENTS.md` § +Messaging. + +## Module Docs + +Read the module's `AGENTS.md` before touching it. Where the UI has its own doc, +it is listed after the `·`. + +- **Scheduling** (TS validator, constraints, endpoints) — + [`services/scheduling/`](services/scheduling/AGENTS.md) · UI: + `frontend/app/components/scheduling/` +- **Scheduling solver** (Python, CP-SAT daemon) — + [`scheduling-solver/`](scheduling-solver/AGENTS.md) +- **Parking** — [`services/parking/`](services/parking/AGENTS.md) · UI: + `frontend/app/dashboard/parking/` +- **Cashier** — [`services/cashier/`](services/cashier/AGENTS.md) · UI: + `frontend/app/components/cashier/` +- **Group Tracking** — [`services/group/`](services/group/AGENTS.md) · UI: + `frontend/app/components/groups/` +- **Checklist** (both sides, JSON sync rule) — + [`services/checklist/`](services/checklist/AGENTS.md) +- **Logbook** (both sides) — [`services/logbook/`](services/logbook/AGENTS.md) +- **F&B / Restaurant** (both sides) — [`services/fnb/`](services/fnb/AGENTS.md) +- **Backoffice** (both sides) — + [`services/backoffice/`](services/backoffice/AGENTS.md) +- **Blacklist** (both sides) — + [`services/blacklist/`](services/blacklist/AGENTS.md) +- **DB / Migrations** — [`db-mysql/`](db-mysql/AGENTS.md) +- **Maintenance** — its backend side is documented with the UI, in + `frontend/app/components/maintenance/AGENTS.md` diff --git a/backend/CLAUDE.md b/backend/CLAUDE.md new file mode 100644 index 00000000..43c994c2 --- /dev/null +++ b/backend/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/backend/README.md b/backend/README.md index 6c6554af..0c9197e8 100644 --- a/backend/README.md +++ b/backend/README.md @@ -18,13 +18,13 @@ see the [root README](../README.md). ## Commands -| Command | Does | -| ---------------------------------------------------- | ------------------------------------------------------------------ | -| `pnpm setup:local` | Builds a local `hotel_db` with fictitious data and the local admin | -| `pnpm dev:local` | API on :4000 against the local database | -| `pnpm test` | Unit and integration tests | -| `pnpm test:corpus` | Solver corpus (needs the Python venv) | -| `pnpm typecheck` · `pnpm lint` · `pnpm format:check` | What CI runs | +| Command | Does | +| ---------------------- | ------------------------------------------------------------------- | +| `pnpm run setup:local` | Builds a local `hotel_db` with fictitious data and the local admin | +| `pnpm run dev:local` | API on :4000 against the local database | +| `pnpm run test` | Unit and integration tests | +| `pnpm run test:corpus` | Solver corpus (needs the Python venv) | +| `pnpm run verify` | What CI runs: lint (no warnings), format check, typecheck and tests | Each module has an `AGENTS.md` under `services/` with its rules; the product documentation lives in [`docs/`](../docs/general/README.md). diff --git a/backend/db-mysql/AGENTS.md b/backend/db-mysql/AGENTS.md index 47b3ac1c..4d29254d 100644 --- a/backend/db-mysql/AGENTS.md +++ b/backend/db-mysql/AGENTS.md @@ -1,5 +1,8 @@ # AGENTS.md — DB / Migrations +**Up:** [`backend/AGENTS.md`](../AGENTS.md) — `backend/` rules · +[root `AGENTS.md`](../../AGENTS.md) — shared rules and module map. + > Single doc for `backend/db-mysql/`. Covers the migration policy, the frozen > baseline, how to write a new incremental script, and how to reconstruct the > schema from scratch. **Read this before touching any table.** @@ -14,20 +17,28 @@ idempotent, registered in `INDEX.md`. Nothing else. ``` backend/db-mysql/ ├── aiven/ # FROZEN baseline snapshot (2026-05-20). Never edit. -│ ├── 01_create_database.sql … 19_scheduling.sql +│ ├── 01_create_database.sql … 20_checklist.sql │ └── 99_verification.sql ├── scripts/ # Incremental migrations — the single source of truth │ ├── 20251220_add_backoffice.sql │ ├── 20251222_add_user_avatar_columns.sql │ ├── … (chronological order) -│ └── apply-migration.sh # Applies one migration to local or Aiven -├── backup/ # DB dumps (local + Aiven) +│ ├── apply-migration.sh # Applies one migration to local or Aiven +│ ├── local-env.sh # Local credentials from backend/.env, sourced by the others +│ ├── backup-local.sh # Dumps the local DB into backup/ +│ ├── check-collation.sh # Shows the local DB collation +│ └── recreate-local.sh # ⚠️ Drops the local DB and runs MASTER_INSTALL.sql only +├── backup/ # DB dumps (local + Aiven), git-ignored; backup.ps1 ├── MASTER_INSTALL.sql # Runs all aiven/ files in order. Empty DB only. ├── MIGRATIONS_POLICY.md ├── INDEX.md # Full table index + incremental migration log -└── mock-data.sql # Clears module data and loads fictitious data (keeps users and scheduling) +├── mock-data.sql # Clears module data and loads fictitious data (keeps users and scheduling) +└── scheduling-seed.sql # Demo scheduling base: employees, contracts, months ``` +`recreate-local.sh` predates `pnpm setup:local` and does less: no baseline +fixes, no incremental scripts, no admin. Use `pnpm setup:local` instead. + ## Migration policy (since 2026-05-20) **Incremental-only.** Rails/Django/Flyway style. @@ -76,13 +87,20 @@ Reference implementation: ## Reconstruct from scratch ```bash -# 1. Frozen baseline (snapshot 2026-05-20) -mysql -u root -p < MASTER_INSTALL.sql - -# 2. All incrementals in chronological order -for f in scripts/*.sql; do mysql -u root -p hotel_db < "$f"; done +cd backend +pnpm setup:local # refuses a DB that is not local or already has tables +pnpm setup:local -- --force # drops an existing local hotel_db first ``` +`scripts/setup-local.ts` runs `MASTER_INSTALL.sql`, then only the incremental +scripts from `20260521` on (the earlier ones are already in the baseline), then +the departments, a local admin (`admin` / `fourpoints-local`), +`scheduling-seed.sql` and `mock-data.sql`. Running `MASTER_INSTALL.sql` by hand +fails: the frozen baseline has a duplicated key name in `cashier_history` and +creates `scheduling_assignments` before the table it references. The script +patches both in memory; `20261005_complete_fresh_install.sql` creates what the +baseline leaves out. + Idempotency guarantees applying all scripts twice leaves the DB intact. **Never run `MASTER_INSTALL.sql` or `aiven/NN_*.sql` against a DB that already @@ -92,9 +110,9 @@ has data.** They will overwrite or duplicate rows. `scripts/apply-migration.sh` runs one `scripts/YYYYMMDD_*.sql` against local or Aiven. It reads the credentials from `backend/.env` (`LOCAL_DB_*`, or -`AIVEN_DB_*` and `AIVEN_PASSWORD`), passes the password through `MYSQL_PWD` -so it never shows in the command line, and verifies Aiven's TLS certificate -with `config/certs/ca-certificate.pem`. It refuses any other file. +`AIVEN_DB_*` and `AIVEN_PASSWORD`), passes the password through `MYSQL_PWD` so +it never shows in the command line, and verifies Aiven's TLS certificate with +`config/certs/ca-certificate.pem`. It refuses any other file. ```bash cd backend/db-mysql/scripts @@ -105,8 +123,8 @@ cd backend/db-mysql/scripts ``` - `--dry-run` only checks the connection and prints the target; nothing runs. -- Each script prints its own result (a skip message or the new definition), - so the output shows whether it changed anything. +- Each script prints its own result (a skip message or the new definition), so + the output shows whether it changed anything. - Running it twice is the idempotency check: the second run must skip. - Then set the row's status in `INDEX.md` (`✅ local · ✅ Aiven`). @@ -116,25 +134,27 @@ cd backend/db-mysql/scripts mysql -h HOST -P PORT -u USER -p --ssl-ca=../config/certs/ca-certificate.pem hotel_db ``` -See `aiven/aiven-conexion.md` for connection details. SSL required. +Host, port, user and password are the `AIVEN_DB_*` and `AIVEN_PASSWORD` +variables of `backend/.env`. SSL required. ## DB at a glance - **Name:** `hotel_db` - **Engine:** MySQL 8.0+ (`utf8mb4_0900_ai_ci`) -- **~50 tables + 3 views** across core, logbook, parking, conciliation, groups, - cashier, blacklist, maintenance, messages, notifications, backoffice, - scheduling, checklist, F&B. +- **67 tables + 3 views** declared between the baseline and `scripts/`, across + core, logbook, parking, conciliation, groups, cashier, blacklist, maintenance, + messages, notifications, backoffice, scheduling, checklist, F&B and demo. The + demo also creates `demo_snapshot_scheduling_*` tables at runtime. - Full table list in `INDEX.md`. ### Roles -| ID | Name | Description | -| --- | --------------- | ------------------------------------- | -| 1 | `recepcionista` | Standard user | -| 2 | `admin` | Full admin | -| 3 | `mantenimiento` | Maintenance staff | -| 6 | `group-admin` | Group administrator | +| ID | Name | Description | +| --- | --------------- | ------------------- | +| 1 | `recepcionista` | Standard user | +| 2 | `admin` | Full admin | +| 3 | `mantenimiento` | Maintenance staff | +| 6 | `group-admin` | Group administrator | ## Backups @@ -158,17 +178,19 @@ password. missing from the table, it's not tracked. Keep it up to date. 4. **No one-off data scripts.** One-time data fixers (the old `add-libre-number.ts` and `backfill-libre-numbers.ts`) and quick query - scripts were removed on 2026-10-04: they carried the Aiven password and - had already run. Write a new one only if it reads `backend/.env`, and drop - it once applied. + scripts were removed on 2026-10-04: they carried the Aiven password and had + already run. Write a new one only if it reads `backend/.env`, and drop it + once applied. 5. **The frozen baseline is not the live schema.** `aiven/NN_*.sql` may list things the live databases never got: `aiven/17_notifications.sql` had - `'messages'` in `notifications.module`, Aiven did not, and the urgent - message notices failed silently until `20261004_add_messages_to_notifications_module.sql`. - Before relying on a column, check `information_schema` on the target DB. + `'messages'` in `notifications.module`, Aiven did not, and the urgent message + notices failed silently until + `20261004_add_messages_to_notifications_module.sql`. Before relying on a + column, check `information_schema` on the target DB. ## Cross references - `MIGRATIONS_POLICY.md` — full policy rationale and when to change the model. - `INDEX.md` — module-by-module table list + full incremental migration log. -- Fresh install: `pnpm setup:local` (`scripts/setup-local.ts`), never `MASTER_INSTALL.sql` by hand. +- Fresh install: `pnpm setup:local` (`scripts/setup-local.ts`), never + `MASTER_INSTALL.sql` by hand. diff --git a/backend/db-mysql/CLAUDE.md b/backend/db-mysql/CLAUDE.md new file mode 100644 index 00000000..43c994c2 --- /dev/null +++ b/backend/db-mysql/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/backend/db-mysql/INDEX.md b/backend/db-mysql/INDEX.md index 3ba27b5d..95c9652e 100644 --- a/backend/db-mysql/INDEX.md +++ b/backend/db-mysql/INDEX.md @@ -42,7 +42,9 @@ db-mysql/ ## Módulos del Sistema -> ⚠️ **Los archivos `aiven/NN_*.sql` están congelados desde 2026-05-20** como snapshot del install base. Cualquier cambio posterior vive en `scripts/`. Ver `MIGRATIONS_POLICY.md`. +> ⚠️ **Los archivos `aiven/NN_*.sql` están congelados desde 2026-05-20** como +> snapshot del install base. Cualquier cambio posterior vive en `scripts/`. Ver +> `MIGRATIONS_POLICY.md`. ### Core (01-03) @@ -80,36 +82,40 @@ db-mysql/ ### Migraciones incrementales (`scripts/`) -> Scripts con etiqueta **[RETROACTIVE]** documentan cambios de schema que entraron históricamente editando directo `aiven/NN_*.sql` antes de que existiera la política de incrementales. Reconstruidos a posteriori desde git para trazabilidad completa. Todos son idempotentes (no-op contra BD ya actualizada). - -| Fecha | Archivo | Descripción | Estado | -| ---------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------- | -| 2025-12-20 | `20251220_add_backoffice.sql` | **[RETROACTIVE]** Módulo Backoffice: 5 tablas (`bo_categories`, `bo_suppliers`, `bo_invoices`, `bo_invoice_history`, `bo_assets`) + 3 vistas (`v_bo_invoices_detail`, `v_bo_monthly_summary`, `v_bo_suppliers_stats`). Commit `4bc0543`. | ✅ local · ✅ Aiven | -| 2025-12-22 | `20251222_add_user_avatar_columns.sql` | **[RETROACTIVE]** Columnas `avatar_url` (VARCHAR 500) y `avatar_public_id` (VARCHAR 255) en `users` para almacenamiento Cloudinary. Commit `9f2b443`. | ✅ local · ✅ Aiven | -| 2025-12-24 | `20251224_add_scheduling.sql` | **[RETROACTIVE]** Módulo Scheduling completo: 10 tablas (`scheduling_config`, `scheduling_shifts`, `scheduling_employees`, `scheduling_employee_contracts`, `scheduling_employee_rules`, `scheduling_months`, `scheduling_days`, `scheduling_assignments`, `scheduling_constraints`, `scheduling_history`). Commits `86ea812` + `f438d81`. | ✅ local · ✅ Aiven | -| 2025-12-30 | `20251230_add_demo_activity_log.sql` | **[RETROACTIVE]** Tabla `demo_activity_log` para registrar intentos de escritura bloqueados de usuarios demo. Commit `68a4589`. | ✅ local · ✅ Aiven | -| 2026-02-26 | `20260226_unify_collation.sql` | **[RETROACTIVE — marker]** Unificación de collation a `utf8mb4_0900_ai_ci` en todo `hotel_db` + fusión de `MASTER_INSTALL_AIVEN.sql` + `MASTER_INSTALL_LOCAL.sql` en uno único. Commit `f5d47d6`. Script es no-op informativo. | ✅ local · ✅ Aiven | -| 2026-02-28 | `20260228_add_libre_number_column.sql` | **[RETROACTIVE]** Columna `libre_number` (INT) en `scheduling_assignments` para agrupar pares de libre semanales (1-45). Commit `142372d`. Companion TS: `add-libre-number.ts`, `backfill-libre-numbers.ts`. | ✅ local · ✅ Aiven | -| 2026-04-25 | `20260425_create_scheduling_employee_requests.sql` | Nueva tabla `scheduling_employee_requests` — peticiones de turno de empleados (Fase 1 del solver). Referencia FK a `users(id)`. | ✅ local · ✅ Aiven | -| 2026-05-12 | `20260512_disable_demo_user.sql` | **[RETROACTIVE]** Desactivación del usuario demo (`is_active=0`) tras hallazgo H1-12. Commit `5644aab`. | ✅ local · ✅ Aiven | -| 2026-05-12 | `20260512_add_checklist_tables.sql` | Tablas del módulo checklist: `checklist_runs`, `checklist_step_state`, `checklist_event_log`, `checklist_config`. Daemon de reset diario. Commit `a9024a3`. | ✅ local · ✅ Aiven | -| 2026-05-12 | `20260512_add_scheduling_solver_runs_and_requests.sql` | Tabla `scheduling_solver_runs` (logging estructurado de cada ejecución del solver CP-SAT) + ajustes a `scheduling_employee_requests`. Commit `a9024a3`. | ✅ local · ✅ Aiven | -| 2026-05-19 | `20260519_add_scheduling_employee_display_order.sql` | Columna `display_order` en `scheduling_employees` para reordenar manualmente la lista en las UIs de scheduling. NULL = fallback alfabético. Idempotente. | ✅ local · ✅ Aiven | -| 2026-05-20 | `20260520_add_scheduling_employee_dates.sql` | Columnas `start_date` y `end_date` en `scheduling_employees` para modelar tenencias parciales (altas y bajas a mitad de año). NULL = activo sin restricción. Idempotente. | ✅ local · ✅ Aiven | -| 2026-05-20 | `20260520_add_shift_LI.sql` | Nuevo código `LI` (Libre Disposición) en `scheduling_shifts`. Día libre extraordinario fuera de la rotación semanal. Idempotente (`INSERT IGNORE`). | ✅ local · ✅ Aiven | -| 2026-05-20 | `20260520_insert_user_*.sql` (retirado) | Alta de una recepcionista con su periodo en `scheduling_employees` (Ene-Feb 2026). El fichero se quitó el 2026-10-04 porque era una persona real; el alta ya estaba aplicada. | ✅ local · ✅ Aiven | -| 2026-05-21 | `20260521_add_fnb_revenue.sql` | Módulo F&B Daily Revenue: `fnb_category` (seed 7 categorías Breakfast/Lunch/Dinner) + `fnb_daily_revenue` (valores diarios por código Opera). Idempotente. | ✅ local · ✅ Aiven | -| 2026-09-29 | `20260929_add_cashier_voucher_status_dates.sql` | `justified_at` y `cancelled_at` (DATETIME NULL) en `cashier_vouchers`. El backend las escribía y los informes muestran la fecha de justificación, pero nunca existieron: justificar y cancelar vales fallaban siempre. Idempotente (`information_schema`). | ✅ local · ✅ Aiven | -| 2026-09-29 | `20260929_fix_cashier_daily_trigger.sql` | Recrea `trg_cashier_shift_update_daily`: separa el cálculo de `income` (suma directa de `cashier_shifts`) del de pagos electrónicos (subquery agregado), eliminando la inflación por LEFT JOIN. No toca esquema; solo DDL del trigger. Aplicado manualmente en Aiven + recálculo de las 10 filas de `cashier_daily` desde origen. | ✅ local · ✅ Aiven | -| 2026-10-04 | `20261004_add_messages_to_notifications_module.sql` | Añade `messages` al ENUM `notifications.module`. Desde `9be16b1` el aviso de mensaje urgente se guarda con ese módulo y el INSERT fallaba en silencio («Data truncated»). Solo cambia la columna si tiene la definición anterior exacta. En local ya estaba (BD montada desde el baseline); en Aiven se aplicó el 2026-10-04. | ✅ local · ✅ Aiven | -| 2026-10-04 | `20261004_fix_parking_availability_triggers.sql` | Recrea `trg_free_availability_on_status_change` (solo libera los días de la propia reserva) y `trg_update_availability_on_date_change` (solo ocupa días libres o propios). Antes completar, cancelar o mover una reserva podía liberar u ocupar días de otra. La comprobación de solapes está en `bookings.repository.ts`. Local tenía otro cuerpo de `date_change`; se parte del de Aiven. | ⏳ local · ✅ Aiven | -| 2026-10-05 | `20261005_add_is_demo_to_users.sql` | Añade `users.is_demo` (0 por defecto): marca la cuenta demo pública, que el backend lleva en el token y `demoRestriction` limita (ADR-038). Hay que aplicarla **antes** de desplegar el código: el login lee la columna. Solo añade la columna si no existe. En Aiven se aplicó el 2026-10-05, antes del código. | ✅ local · ✅ Aiven | -| 2026-10-05 | `20261005_add_demo_reset_log.sql` | Crea `demo_reset_log`: un registro por cada reinicio diario de la demo y por cada base de horarios guardada (ADR-038). La base vive en tablas `demo_snapshot_scheduling_*` que crea el backend al guardarla. Hay que aplicarla antes de desplegar el código. `CREATE TABLE IF NOT EXISTS`. | ✅ local · ✅ Aiven | -| 2026-10-05 | `20261005_complete_fresh_install.sql` | Completa una instalación desde cero: el baseline congelado no crea `cashier_history` (nombre de clave duplicado), `scheduling_assignments` (referencia a una tabla creada después) ni `demo_activity_log`, y deja `roles.name` en `varchar(50)`. Definiciones copiadas de Aiven. En Aiven no cambia nada; en local creó `demo_activity_log`, que faltaba. `pnpm setup:local` además corrige esos dos fallos del baseline en memoria. | ✅ local · ✅ Aiven (sin cambios) | -| 2026-10-05 | `20261005_fix_bo_monthly_summary_view.sql` | Recrea `v_bo_monthly_summary` con `period` en el `GROUP BY`. Con `ONLY_FULL_GROUP_BY` (activo en Aiven y local) toda consulta a la vista fallaba con el error 1055 y `GET /api/backoffice/stats/monthly` respondía siempre 500. Mismas columnas y mismos grupos; comprobada contra un cálculo directo sobre `bo_invoices`. | ✅ local · ✅ Aiven | -| 2026-10-05 | `20261005_drop_demo_admin_role.sql` | Borra el rol `demo-admin` si ningún usuario lo tiene (ADR-038): la cuenta demo es un admin con `is_demo` y el código ya no conoce ese rol. En local no existía. | ✅ local (sin cambios) · ✅ Aiven | -| 2026-10-11 | `20261011_keep_group_history_on_delete.sql` | Quita la clave foránea `group_history_ibfk_1` (`ON DELETE CASCADE` a `hotel_groups`): borrar un grupo ya no borra su historial, y el borrado deja una fila `deleted` con la foto del grupo. **Aplicar en Aiven antes de desplegar el código**: con la clave dentro, borrar un grupo falla. | ✅ local · ✅ Aiven | -| 2026-10-12 | `20261012_unique_invoice_supplier_number.sql` | Añade `UNIQUE KEY uk_invoice_supplier_number (supplier_id, invoice_number)` a `bo_invoices`: la misma factura de un proveedor se podía registrar dos veces y se pagaban las dos en el lote mensual. El backend responde 409 `BACKOFFICE_INVOICE_DUPLICATE`. Antes del `ALTER` lista las duplicadas; si hay alguna, el `ALTER` falla y hay que corregirlas. Idempotente. El código funciona sin ella (no rechaza nada hasta que exista). | ✅ local · ✅ Aiven | +> Scripts con etiqueta **[RETROACTIVE]** documentan cambios de schema que +> entraron históricamente editando directo `aiven/NN_*.sql` antes de que +> existiera la política de incrementales. Reconstruidos a posteriori desde git +> para trazabilidad completa. Todos son idempotentes (no-op contra BD ya +> actualizada). + +| Fecha | Archivo | Descripción | Estado | +| ---------- | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | +| 2025-12-20 | `20251220_add_backoffice.sql` | **[RETROACTIVE]** Módulo Backoffice: 5 tablas (`bo_categories`, `bo_suppliers`, `bo_invoices`, `bo_invoice_history`, `bo_assets`) + 3 vistas (`v_bo_invoices_detail`, `v_bo_monthly_summary`, `v_bo_suppliers_stats`). Commit `4bc0543`. | ✅ local · ✅ Aiven | +| 2025-12-22 | `20251222_add_user_avatar_columns.sql` | **[RETROACTIVE]** Columnas `avatar_url` (VARCHAR 500) y `avatar_public_id` (VARCHAR 255) en `users` para almacenamiento Cloudinary. Commit `9f2b443`. | ✅ local · ✅ Aiven | +| 2025-12-24 | `20251224_add_scheduling.sql` | **[RETROACTIVE]** Módulo Scheduling completo: 10 tablas (`scheduling_config`, `scheduling_shifts`, `scheduling_employees`, `scheduling_employee_contracts`, `scheduling_employee_rules`, `scheduling_months`, `scheduling_days`, `scheduling_assignments`, `scheduling_constraints`, `scheduling_history`). Commits `86ea812` + `f438d81`. | ✅ local · ✅ Aiven | +| 2025-12-30 | `20251230_add_demo_activity_log.sql` | **[RETROACTIVE]** Tabla `demo_activity_log` para registrar intentos de escritura bloqueados de usuarios demo. Commit `68a4589`. | ✅ local · ✅ Aiven | +| 2026-02-26 | `20260226_unify_collation.sql` | **[RETROACTIVE — marker]** Unificación de collation a `utf8mb4_0900_ai_ci` en todo `hotel_db` + fusión de `MASTER_INSTALL_AIVEN.sql` + `MASTER_INSTALL_LOCAL.sql` en uno único. Commit `f5d47d6`. Script es no-op informativo. | ✅ local · ✅ Aiven | +| 2026-02-28 | `20260228_add_libre_number_column.sql` | **[RETROACTIVE]** Columna `libre_number` (INT) en `scheduling_assignments` para agrupar pares de libre semanales (1-45). Commit `142372d`. Companion TS: `add-libre-number.ts`, `backfill-libre-numbers.ts`. | ✅ local · ✅ Aiven | +| 2026-04-25 | `20260425_create_scheduling_employee_requests.sql` | Nueva tabla `scheduling_employee_requests` — peticiones de turno de empleados (Fase 1 del solver). Referencia FK a `users(id)`. | ✅ local · ✅ Aiven | +| 2026-05-12 | `20260512_disable_demo_user.sql` | **[RETROACTIVE]** Desactivación del usuario demo (`is_active=0`) tras hallazgo H1-12. Commit `5644aab`. | ✅ local · ✅ Aiven | +| 2026-05-12 | `20260512_add_checklist_tables.sql` | Tablas del módulo checklist: `checklist_runs`, `checklist_step_state`, `checklist_event_log`, `checklist_config`. Daemon de reset diario. Commit `a9024a3`. | ✅ local · ✅ Aiven | +| 2026-05-12 | `20260512_add_scheduling_solver_runs_and_requests.sql` | Tabla `scheduling_solver_runs` (logging estructurado de cada ejecución del solver CP-SAT) + ajustes a `scheduling_employee_requests`. Commit `a9024a3`. | ✅ local · ✅ Aiven | +| 2026-05-19 | `20260519_add_scheduling_employee_display_order.sql` | Columna `display_order` en `scheduling_employees` para reordenar manualmente la lista en las UIs de scheduling. NULL = fallback alfabético. Idempotente. | ✅ local · ✅ Aiven | +| 2026-05-20 | `20260520_add_scheduling_employee_dates.sql` | Columnas `start_date` y `end_date` en `scheduling_employees` para modelar tenencias parciales (altas y bajas a mitad de año). NULL = activo sin restricción. Idempotente. | ✅ local · ✅ Aiven | +| 2026-05-20 | `20260520_add_shift_LI.sql` | Nuevo código `LI` (Libre Disposición) en `scheduling_shifts`. Día libre extraordinario fuera de la rotación semanal. Idempotente (`INSERT IGNORE`). | ✅ local · ✅ Aiven | +| 2026-05-20 | `20260520_insert_user_*.sql` (retirado) | Alta de una recepcionista con su periodo en `scheduling_employees` (Ene-Feb 2026). El fichero se quitó el 2026-10-04 porque era una persona real; el alta ya estaba aplicada. | ✅ local · ✅ Aiven | +| 2026-05-21 | `20260521_add_fnb_revenue.sql` | Módulo F&B Daily Revenue: `fnb_category` (seed 7 categorías Breakfast/Lunch/Dinner) + `fnb_daily_revenue` (valores diarios por código Opera). Idempotente. | ✅ local · ✅ Aiven | +| 2026-09-29 | `20260929_add_cashier_voucher_status_dates.sql` | `justified_at` y `cancelled_at` (DATETIME NULL) en `cashier_vouchers`. El backend las escribía y los informes muestran la fecha de justificación, pero nunca existieron: justificar y cancelar vales fallaban siempre. Idempotente (`information_schema`). | ✅ local · ✅ Aiven | +| 2026-09-29 | `20260929_fix_cashier_daily_trigger.sql` | Recrea `trg_cashier_shift_update_daily`: separa el cálculo de `income` (suma directa de `cashier_shifts`) del de pagos electrónicos (subquery agregado), eliminando la inflación por LEFT JOIN. No toca esquema; solo DDL del trigger. Aplicado manualmente en Aiven + recálculo de las 10 filas de `cashier_daily` desde origen. | ✅ local · ✅ Aiven | +| 2026-10-04 | `20261004_add_messages_to_notifications_module.sql` | Añade `messages` al ENUM `notifications.module`. Desde `9be16b1` el aviso de mensaje urgente se guarda con ese módulo y el INSERT fallaba en silencio («Data truncated»). Solo cambia la columna si tiene la definición anterior exacta. En local ya estaba (BD montada desde el baseline); en Aiven se aplicó el 2026-10-04. | ✅ local · ✅ Aiven | +| 2026-10-04 | `20261004_fix_parking_availability_triggers.sql` | Recrea `trg_free_availability_on_status_change` (solo libera los días de la propia reserva) y `trg_update_availability_on_date_change` (solo ocupa días libres o propios). Antes completar, cancelar o mover una reserva podía liberar u ocupar días de otra. La comprobación de solapes está en `bookings.repository.ts`. Local tenía otro cuerpo de `date_change`; se parte del de Aiven. | ⏳ local · ✅ Aiven | +| 2026-10-05 | `20261005_add_is_demo_to_users.sql` | Añade `users.is_demo` (0 por defecto): marca la cuenta demo pública, que el backend lleva en el token y `demoRestriction` limita (ADR-038). Hay que aplicarla **antes** de desplegar el código: el login lee la columna. Solo añade la columna si no existe. En Aiven se aplicó el 2026-10-05, antes del código. | ✅ local · ✅ Aiven | +| 2026-10-05 | `20261005_add_demo_reset_log.sql` | Crea `demo_reset_log`: un registro por cada reinicio diario de la demo y por cada base de horarios guardada (ADR-038). La base vive en tablas `demo_snapshot_scheduling_*` que crea el backend al guardarla. Hay que aplicarla antes de desplegar el código. `CREATE TABLE IF NOT EXISTS`. | ✅ local · ✅ Aiven | +| 2026-10-05 | `20261005_complete_fresh_install.sql` | Completa una instalación desde cero: el baseline congelado no crea `cashier_history` (nombre de clave duplicado), `scheduling_assignments` (referencia a una tabla creada después) ni `demo_activity_log`, y deja `roles.name` en `varchar(50)`. Definiciones copiadas de Aiven. En Aiven no cambia nada; en local creó `demo_activity_log`, que faltaba. `pnpm setup:local` además corrige esos dos fallos del baseline en memoria. | ✅ local · ✅ Aiven (sin cambios) | +| 2026-10-05 | `20261005_fix_bo_monthly_summary_view.sql` | Recrea `v_bo_monthly_summary` con `period` en el `GROUP BY`. Con `ONLY_FULL_GROUP_BY` (activo en Aiven y local) toda consulta a la vista fallaba con el error 1055 y `GET /api/backoffice/stats/monthly` respondía siempre 500. Mismas columnas y mismos grupos; comprobada contra un cálculo directo sobre `bo_invoices`. | ✅ local · ✅ Aiven | +| 2026-10-05 | `20261005_drop_demo_admin_role.sql` | Borra el rol `demo-admin` si ningún usuario lo tiene (ADR-038): la cuenta demo es un admin con `is_demo` y el código ya no conoce ese rol. En local no existía. | ✅ local (sin cambios) · ✅ Aiven | +| 2026-10-11 | `20261011_keep_group_history_on_delete.sql` | Quita la clave foránea `group_history_ibfk_1` (`ON DELETE CASCADE` a `hotel_groups`): borrar un grupo ya no borra su historial, y el borrado deja una fila `deleted` con la foto del grupo. **Aplicar en Aiven antes de desplegar el código**: con la clave dentro, borrar un grupo falla. | ✅ local · ✅ Aiven | +| 2026-10-12 | `20261012_unique_invoice_supplier_number.sql` | Añade `UNIQUE KEY uk_invoice_supplier_number (supplier_id, invoice_number)` a `bo_invoices`: la misma factura de un proveedor se podía registrar dos veces y se pagaban las dos en el lote mensual. El backend responde 409 `BACKOFFICE_INVOICE_DUPLICATE`. Antes del `ALTER` lista las duplicadas; si hay alguna, el `ALTER` falla y hay que corregirlas. Idempotente. El código funciona sin ella (no rechaza nada hasta que exista). | ✅ local · ✅ Aiven | ### Verificación (99) @@ -140,7 +146,12 @@ mysql -h HOST -P PORT -u USER -p --ssl-ca=../config/certs/ca-certificate.pem < M - **Collation**: `utf8mb4_0900_ai_ci` (MySQL 8.0+) - SSL requerido para Aiven (ca-certificate.pem) -- **Política de migraciones**: ver [`MIGRATIONS_POLICY.md`](./MIGRATIONS_POLICY.md). Resumen: cambios de schema → script incremental idempotente en `scripts/` + registro en la tabla "Migraciones incrementales" de este archivo. **NO** editar archivos `aiven/NN_*.sql` (congelados desde 2026-05-20). **NO** ejecutar `MASTER_INSTALL.sql` ni los scripts numerados contra una BD con datos. +- **Política de migraciones**: ver + [`MIGRATIONS_POLICY.md`](./MIGRATIONS_POLICY.md). Resumen: cambios de schema → + script incremental idempotente en `scripts/` + registro en la tabla + "Migraciones incrementales" de este archivo. **NO** editar archivos + `aiven/NN_*.sql` (congelados desde 2026-05-20). **NO** ejecutar + `MASTER_INSTALL.sql` ni los scripts numerados contra una BD con datos. --- diff --git a/backend/db-mysql/MIGRATIONS_POLICY.md b/backend/db-mysql/MIGRATIONS_POLICY.md index 6a805718..4b0a4e30 100644 --- a/backend/db-mysql/MIGRATIONS_POLICY.md +++ b/backend/db-mysql/MIGRATIONS_POLICY.md @@ -2,22 +2,37 @@ > Última revisión: 2026-05-20 (transición a "solo incrementales") -Este documento recoge **cómo se gestionan los cambios de schema** en el proyecto. Si vas a tocar tablas, leelo entero antes. +Este documento recoge **cómo se gestionan los cambios de schema** en el +proyecto. Si vas a tocar tablas, leelo entero antes. --- ## Estado actual -Patrón **solo incrementales**, estilo Rails/Django/Flyway. Una sola fuente de verdad: la secuencia ordenada de scripts en `backend/db-mysql/scripts/`. +Patrón **solo incrementales**, estilo Rails/Django/Flyway. Una sola fuente de +verdad: la secuencia ordenada de scripts en `backend/db-mysql/scripts/`. ### Reglas -1. **`aiven/NN_*.sql` está congelado** desde el 2026-05-20. Cabecera `⚠️ FROZEN` en cada archivo. Son snapshot histórico del install base, **no se editan nunca**. -2. **Cada cambio de schema** = un nuevo script en `backend/db-mysql/scripts/AAAAMMDD_.sql`. Idempotente. Ejecutable sobre BD viva. -3. **Registro obligatorio en `INDEX.md`** (tabla "Migraciones incrementales") con fecha, archivo, descripción, estado por entorno (`✅ local · ✅ Aiven`, `⏳ pendiente`, etc). -4. **Nunca modificar un script commiteado**. Si algo salió mal, crear nuevo script `AAAAMMDD_fix_…sql` que lo enmiende. -5. Las migraciones **deben ser idempotentes**: protegerse con `information_schema.COLUMNS`, `INSERT IGNORE`, `CREATE TABLE IF NOT EXISTS`, etc. Plantilla en `scripts/20260519_add_scheduling_employee_display_order.sql`. -6. Aplicar primero en **local**, luego en **Aiven**, con `scripts/apply-migration.sh local|aiven [--dry-run]` (lee las credenciales de `backend/.env`; detalle en `AGENTS.md`, «Applying a migration»). Actualizar columna "Estado" de `INDEX.md`. +1. **`aiven/NN_*.sql` está congelado** desde el 2026-05-20. Cabecera `⚠️ FROZEN` + en cada archivo. Son snapshot histórico del install base, **no se editan + nunca**. +2. **Cada cambio de schema** = un nuevo script en + `backend/db-mysql/scripts/AAAAMMDD_.sql`. Idempotente. + Ejecutable sobre BD viva. +3. **Registro obligatorio en `INDEX.md`** (tabla "Migraciones incrementales") + con fecha, archivo, descripción, estado por entorno (`✅ local · ✅ Aiven`, + `⏳ pendiente`, etc). +4. **Nunca modificar un script commiteado**. Si algo salió mal, crear nuevo + script `AAAAMMDD_fix_…sql` que lo enmiende. +5. Las migraciones **deben ser idempotentes**: protegerse con + `information_schema.COLUMNS`, `INSERT IGNORE`, `CREATE TABLE IF NOT EXISTS`, + etc. Plantilla en + `scripts/20260519_add_scheduling_employee_display_order.sql`. +6. Aplicar primero en **local**, luego en **Aiven**, con + `scripts/apply-migration.sh local|aiven [--dry-run]` (lee las + credenciales de `backend/.env`; detalle en `AGENTS.md`, «Applying a + migration»). Actualizar columna "Estado" de `INDEX.md`. 7. Charset/collation siempre: `utf8mb4` / `utf8mb4_0900_ai_ci`. ### Reconstrucción desde cero @@ -36,35 +51,52 @@ Idempotencia garantiza que aplicar todos los scripts dos veces no rompe nada. ## Por qué este modelo -**Antes (híbrido, hasta 2026-05-19):** doble escritura. Cada cambio se replicaba en `aiven/NN_*.sql` (espejo manual) y en `scripts/` (ejecución real). Coste asumido: divergencia silenciosa entre el archivo "documental" y la BD real cuando se olvidaba el espejo. +**Antes (híbrido, hasta 2026-05-19):** doble escritura. Cada cambio se replicaba +en `aiven/NN_*.sql` (espejo manual) y en `scripts/` (ejecución real). Coste +asumido: divergencia silenciosa entre el archivo "documental" y la BD real +cuando se olvidaba el espejo. -**Ahora (solo incrementales, desde 2026-05-20):** los `aiven/NN_*.sql` quedan como snapshot del schema en el momento del congelamiento. Todo cambio posterior vive en `scripts/`. Beneficios: +**Ahora (solo incrementales, desde 2026-05-20):** los `aiven/NN_*.sql` quedan +como snapshot del schema en el momento del congelamiento. Todo cambio posterior +vive en `scripts/`. Beneficios: - Una sola fuente de verdad. Sin riesgo de divergencia. -- Trazabilidad histórica máxima (cada cambio tiene fecha, autor, razón en cabecera). -- Sin ejecución accidental del install completo sobre BD con datos (porque `MASTER_INSTALL.sql` ya no representa el estado actual — solo el del 2026-05-20). +- Trazabilidad histórica máxima (cada cambio tiene fecha, autor, razón en + cabecera). +- Sin ejecución accidental del install completo sobre BD con datos (porque + `MASTER_INSTALL.sql` ya no representa el estado actual — solo el del + 2026-05-20). - Estándar de la industria. -**Coste asumido:** el snapshot envejece. Para entender el schema actual hay que leer base congelada + N scripts. Compensado parcialmente por: +**Coste asumido:** el snapshot envejece. Para entender el schema actual hay que +leer base congelada + N scripts. Compensado parcialmente por: - Lista cronológica completa en `INDEX.md` (tabla "Migraciones incrementales"). -- Scripts retroactivos `[RETROACTIVE]` documentan cambios pre-2026-04-25 que entraron editando `aiven/` directamente. Reconstrucción histórica desde commits. +- Scripts retroactivos `[RETROACTIVE]` documentan cambios pre-2026-04-25 que + entraron editando `aiven/` directamente. Reconstrucción histórica desde + commits. --- ## Reconstrucción retroactiva (histórico pre-2026-04-25) -`scripts/` empieza el 2026-04-25 (`20260425_create_scheduling_employee_requests.sql`). Cambios anteriores entraron editando directo `aiven/NN_*.sql` sin script incremental. +`scripts/` empieza el 2026-04-25 +(`20260425_create_scheduling_employee_requests.sql`). Cambios anteriores +entraron editando directo `aiven/NN_*.sql` sin script incremental. -Esos cambios se han reconstruido a posteriori como scripts marcados `[RETROACTIVE]` con fecha extraída del commit original. Ver `INDEX.md` para la lista completa. Todos idempotentes y no-op contra la BD viva actual. +Esos cambios se han reconstruido a posteriori como scripts marcados +`[RETROACTIVE]` con fecha extraída del commit original. Ver `INDEX.md` para la +lista completa. Todos idempotentes y no-op contra la BD viva actual. --- ## Cuándo cambiar este modelo -- Si el equipo crece > 1-2 devs y necesita sincronización formal → considerar herramienta (dbmate / flyway / prisma migrate). +- Si el equipo crece > 1-2 devs y necesita sincronización formal → considerar + herramienta (dbmate / flyway / prisma migrate). - Si llega CI que valide schema → automatizar aplicación de scripts. -- Si el snapshot base resulta confuso → renombrar `aiven/NN_*.sql` a `aiven/baseline-YYYYMMDD/` para enfatizar que es congelado. +- Si el snapshot base resulta confuso → renombrar `aiven/NN_*.sql` a + `aiven/baseline-YYYYMMDD/` para enfatizar que es congelado. --- @@ -73,5 +105,6 @@ Esos cambios se han reconstruido a posteriori como scripts marcados `[RETROACTIV - `INDEX.md` — índice general, tabla "Migraciones incrementales" - `pnpm setup:local` — instalación local desde cero (`scripts/setup-local.ts`) - `README.md` — overview de la carpeta -- `scripts/` — scripts incrementales, ordenados por fecha. Única fuente de verdad para cambios desde 2026-05-20. +- `scripts/` — scripts incrementales, ordenados por fecha. Única fuente de + verdad para cambios desde 2026-05-20. - `aiven/NN_*.sql` — snapshot congelado del install base. No editar. diff --git a/backend/db-mysql/README.md b/backend/db-mysql/README.md index d13f342d..0ec4ad46 100644 --- a/backend/db-mysql/README.md +++ b/backend/db-mysql/README.md @@ -1,8 +1,12 @@ # 🏨 Hotel DB - Sistema de Gestión -Base de datos completa para gestión hotelera con múltiples módulos: parking, logbook, scheduling, messaging, etc. +Base de datos completa para gestión hotelera con múltiples módulos: parking, +logbook, scheduling, messaging, etc. -> ⚠️ **Política de migraciones (desde 2026-05-20):** sólo incrementales. Archivos `aiven/NN_*.sql` **congelados** como snapshot del install base. Todo cambio nuevo va a `scripts/AAAAMMDD_*.sql`. Ver [`MIGRATIONS_POLICY.md`](./MIGRATIONS_POLICY.md). +> ⚠️ **Política de migraciones (desde 2026-05-20):** sólo incrementales. +> Archivos `aiven/NN_*.sql` **congelados** como snapshot del install base. Todo +> cambio nuevo va a `scripts/AAAAMMDD_*.sql`. Ver +> [`MIGRATIONS_POLICY.md`](./MIGRATIONS_POLICY.md). ## 📁 Estructura de Archivos @@ -49,8 +53,8 @@ pnpm setup:local ``` No ejecutes `MASTER_INSTALL.sql` a mano: borra `hotel_db`, no aplica las -migraciones de `scripts/` y tiene dos fallos del baseline que solo -`setup:local` corrige (ADR-039). +migraciones de `scripts/` y tiene dos fallos del baseline que solo `setup:local` +corrige (ADR-039). ### Verificación @@ -97,9 +101,11 @@ mysql -u root -p < aiven/99_verification.sql ### Integridad referencial -- **spot_id**: `ON DELETE RESTRICT` - No se puede borrar plaza con reservas activas +- **spot_id**: `ON DELETE RESTRICT` - No se puede borrar plaza con reservas + activas - **vehicle_id**: `ON DELETE SET NULL` - Preserva reserva si se borra vehículo -- **operator_id, created_by, updated_by**: `ON DELETE SET NULL` - Preserva historial +- **operator_id, created_by, updated_by**: `ON DELETE SET NULL` - Preserva + historial ### Validaciones automáticas (Parking) @@ -140,7 +146,8 @@ WHERE status = 'completed' ## 🆘 Troubleshooting -Una instalación que falla: vuelve a lanzar `pnpm setup:local -- --force` (ver «Instalación completa»). +Una instalación que falla: vuelve a lanzar `pnpm setup:local -- --force` (ver +«Instalación completa»). --- diff --git a/backend/package.json b/backend/package.json index b4a00ff4..e00d48d6 100644 --- a/backend/package.json +++ b/backend/package.json @@ -21,14 +21,15 @@ "test:corpus": "vitest run tests/scheduling/corpus.test.ts", "lint": "eslint .", "format": "prettier --write \"**/*.{ts,js}\"", - "format:check": "prettier --check \"**/*.{ts,js}\"" + "format:check": "prettier --check \"**/*.{ts,js}\"", + "verify": "pnpm run lint --max-warnings=0 && pnpm run format:check && pnpm run typecheck && pnpm run test" }, "keywords": [], "author": "", "license": "ISC", - "packageManager": "pnpm@10.13.1", + "packageManager": "pnpm@10.28.0", "engines": { - "node": ">=22.16.0" + "node": "22.x" }, "dependencies": { "@types/pdf-parse": "^1.1.5", diff --git a/backend/pnpm-workspace.yaml b/backend/pnpm-workspace.yaml new file mode 100644 index 00000000..a8fca7b3 --- /dev/null +++ b/backend/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +# Do not install a version published less than 7 days ago (minutes). +# Needs pnpm >= 10.16; ignored silently below that. +minimumReleaseAge: 10080 diff --git a/backend/scheduling-solver/AGENTS.md b/backend/scheduling-solver/AGENTS.md index 2f336d25..efd4db04 100644 --- a/backend/scheduling-solver/AGENTS.md +++ b/backend/scheduling-solver/AGENTS.md @@ -1,8 +1,13 @@ # AGENTS.md — Scheduling Solver (Python / CP-SAT) -> Context specific to this directory. The root `CLAUDE.md` covers the scheduling -> system end-to-end (UI + backend TS + solver). This file focuses on the Python -> side: daemon, CP-SAT model, constraints, tests and deploy. +**Up:** [`backend/AGENTS.md`](../AGENTS.md) — `backend/` rules · +[root `AGENTS.md`](../../AGENTS.md) — shared rules and module map. + +> Context specific to this directory. The rest of the scheduling system is in +> `backend/services/scheduling/AGENTS.md` (TS validator, endpoints, the +> cross-language guide) and `frontend/app/components/scheduling/AGENTS.md` (UI). +> This file focuses on the Python side: daemon, CP-SAT model, constraints, tests +> and deploy. ## Purpose @@ -23,7 +28,7 @@ daemon.py ← persistent entry point; stdin/stdout JSON line-delimited lo main.py ← one-shot CLI entry point (legacy, keep for occasional debug) schemas.py ← Pydantic: SolverInput / SolverOutput (contract with Node) model.py ← Builds the CP-SAT model, applies constraints, optimizes, decodes -constraints/ ← One file per hard constraint, same signature apply(model, x, input, ...) +constraints/ ← One file per hard constraint, each with an apply(model, x, input, ...) coverage.py — H1: minimum M/T/N coverage per day night_block.py — H2: consecutive night blocks [min, max] transitions.py — H3: forbidden transitions (N→M/T/PI/P, T→M) @@ -54,8 +59,9 @@ it receives EOF on stdin, it shuts down cleanly. The Node client (`backend/services/scheduling/solver-client.ts`) handles: -- Daemon state: `idle` → `starting` → `ready` → `busy` -- Semaphore: one request at a time (CP-SAT is not thread-safe per process) +- Daemon phase: `idle` → `starting` → `ready` +- Semaphore: one request at a time (CP-SAT is not thread-safe per process); it + is what marks the daemon busy, there is no `busy` phase - Abort-safe: the semaphore is not released until the daemon answers - Transparent respawn if the process dies (broken pipe → relaunch) @@ -90,18 +96,19 @@ is created. ## Hard constraints -Each file in `constraints/` exposes a function: +Each file in `constraints/` exposes an `apply()`. The cross-month ones (`rest`, +`night_block`, `transitions`, `day_blocks`) take the full signature and iterate +over `all_days_e = virt_days + day_numbers`: ```python def apply(model, x, input, employees, days, virtual_days_by_emp=None): ... ``` -`employees` and `days` are passed for convenience (avoids re-extracting them -from `input`). `virtual_days_by_emp` is optional; constraints that need -cross-month continuity (`rest`, `night_block`, `transitions`, `day_blocks`) -iterate over `all_days_e = virt_days + day_numbers`. The ones that do not -(`coverage`, `libres`, `employee_rules`, `locked_cells`) do not. +`coverage`, `libres` and `employee_rules` take +`(model, x, input, employees, days)`, with no virtual days. `locked_cells` is +the odd one: `(model, x, input, emp_idx_by_id)`. `employees` and `days` are +passed for convenience (avoids re-extracting them from `input`). **Fine notes per constraint:** @@ -125,8 +132,10 @@ iterate over `all_days_e = virt_days + day_numbers`. The ones that do not and would produce an artificial INFEASIBLE when generating the new month. `rest.py` **explicitly skips** these "doomed" windows — validating the previous month is the previous month's responsibility. Full reasoning in - `docs/scheduling/decisions.md` (entry 2026-05-20). Outstanding debt: the TS - validator does not apply this same exemption yet → potential drift. See + `docs/scheduling/decisions.md` (entry 2026-05-20). Outstanding debt, drift + with the TS validator: the live one checks H4 and H5 only inside the month + (and H5 as a warning), so cross-month windows are enforced here and not there. + Details in `backend/services/scheduling/AGENTS.md` § "Cross-month gotcha". See `docs/scheduling/constraints.md §H5`. - **H6 libres.** The monthly minimum is reduced by the number of locked @@ -137,8 +146,9 @@ iterate over `all_days_e = virt_days + day_numbers`. The ones that do not (V/B/IT/E/FO/A) appear only where there is a locked cell; the solver never assigns them freely to other employees. -- **day_blocks.** Minimum consecutive M and T blocks (`day_block_min` from - config). Forbids the M→T transition without a libre in between. +- **day_blocks.** Minimum consecutive M and T blocks: `MIN_SHIFT_BLOCK = 3`, a + constant in `day_blocks.py`, not a config value. Forbids the M→T transition + without a libre in between. - **employee_rules.** `noWeekends` forces L on Saturday/Sunday for the affected employee. `fixedShift ∈ {M,T,N}` forbids other work shifts (only the fixed one @@ -206,17 +216,18 @@ correct, specific message beats a thousand generic ones. ```bash cd backend/scheduling-solver -python -m venv venv # create venv -venv/Scripts/pip install ortools pydantic pytest # Windows -# venv/bin/pip install ortools pydantic pytest # Linux/Mac +python -m venv venv # Python 3.11 (.python-version) +venv/Scripts/pip install -r requirements.txt pytest # Windows +# venv/bin/pip install -r requirements.txt pytest # Linux/Mac ``` -The venv lives here and is `.gitignore`d. On Render (deploy), the build command -recreates the venv — see `TODO.md` § "Deploy en Render" if you need to touch the -deploy. +The venv lives here and is `.gitignore`d. On Render, the build command is +`pnpm build` from `backend/`, which creates the venv with `python3` and installs +`requirements.txt`. -`requirements.txt` and `pyproject.toml` are alternative sources of truth; in -practice we use the inline pins above for local dev. +`requirements.txt` pins the versions production runs (`ortools`, `pydantic`); +`pyproject.toml` only sets minimums. Installing from `requirements.txt` keeps +local on the same versions as Render. ## Tests @@ -240,10 +251,10 @@ test are the two lists to keep in sync when you add a solver-reachable fixture. ## Adding a new constraint (Python side) > Full cross-language guide (TS validator + Python solver + fixtures) lives in -> the root `CLAUDE.md`, § "Adding a new constraint". This section covers only -> the Python side. +> `backend/services/scheduling/AGENTS.md`, § "Adding a new constraint (full +> cross-language guide)". This section covers only the Python side. -1. Create `constraints/.py` with the standard signature +1. Create `constraints/.py` with the full signature `apply(model, x, input, employees, days, virtual_days_by_emp=None)`. Look at `coverage.py` for a simple template, `rest.py` for a cross-month template, `night_block.py` for a trailing template. diff --git a/backend/scheduling-solver/CLAUDE.md b/backend/scheduling-solver/CLAUDE.md new file mode 100644 index 00000000..43c994c2 --- /dev/null +++ b/backend/scheduling-solver/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/backend/services/backoffice/AGENTS.md b/backend/services/backoffice/AGENTS.md index 9967091b..a16eb434 100644 --- a/backend/services/backoffice/AGENTS.md +++ b/backend/services/backoffice/AGENTS.md @@ -1,5 +1,10 @@ # AGENTS.md — Backoffice (bo) +**Up:** [`backend/AGENTS.md`](../../AGENTS.md) — `backend/` rules · +[`frontend/AGENTS.md`](../../../frontend/AGENTS.md) — `frontend/` rules, since +this doc covers both sides · [root `AGENTS.md`](../../../AGENTS.md) — shared +rules and module map. + > Combined doc for the backoffice module (backend + frontend). Covers the data > model, invoice lifecycle, PDF handling via Cloudinary, batch payment, and the > server-component-first frontend pattern. @@ -9,9 +14,9 @@ Internal expense management for the hotel: suppliers are organised by category, each supplier has invoices (with PDF attachments) that flow through a validation → payment lifecycle, and the hotel's stamp and signature images (assets) are -stored to mark validated invoices. The module is **admin-only**. Stats and monthly summaries provide spending -visibility. A batch-pay endpoint marks multiple validated invoices as paid in a -single operation. +stored to mark validated invoices. The module is **admin-only**. Stats and +monthly summaries provide spending visibility. A batch-pay endpoint marks +multiple validated invoices as paid in a single operation. ## DB tables @@ -38,7 +43,7 @@ single operation. ``` InvoiceStatus: 'pending' | 'validated' | 'rejected' | 'paid' -AssetType: (string — image type/mime) +AssetType: 'stamp' | 'signature' ``` `INVOICE_STATUS` constant maps each status to a display label. @@ -47,11 +52,14 @@ AssetType: (string — image type/mime) ``` backend/controllers/backoffice/ -└── backoffice-controller.ts (1863 lines — all domains: categories, suppliers, - invoices, assets, stats, batch-pay, PDF upload/download/zip) +├── backoffice-controller.ts (~1960 lines — all domains: categories, suppliers, +│ invoices, assets, stats, batch-pay, PDF upload/download/zip) +└── backoffice-files.ts (swaps the signed Cloudinary URLs for the API paths + that serve them before any invoice/asset leaves the API) backend/repositories/backoffice/ -└── backoffice-repository.ts (960 lines) +├── backoffice-repository.ts (~990 lines) +└── backoffice-columns.ts (column lists) backend/routes/backoffice/backoffice-routes.ts backend/services/backoffice/ (this dir — documentation anchor, no service files yet) @@ -66,33 +74,39 @@ PDF and asset uploads. ``` created → pending -pending → validated (isRealAdmin) validated invoice appears in batch-pay previews - → rejected (isRealAdmin) adds rejection note to bo_invoice_history -validated → paid (isRealAdmin) via markAsPaid or executeBatchPayment -validated → unvalidated (isRealAdmin) back to pending for correction +pending → validated (isAdmin) validated invoice appears in batch-pay previews + → rejected (isAdmin) adds rejection note to bo_invoice_history +validated → paid (isAdmin) via markAsPaid or executeBatchPayment +validated → pending (isAdmin) unvalidate, for correction +paid → validated (isAdmin) unpay (revertPayment) or the batch revert ``` -⚠️ This is the intended flow, **not enforced**: the backend does not check the -current status, so a pending or rejected invoice can be paid and a paid one -validated (see `docs/TODO.md`). +**Enforced in the repository:** each `UPDATE` carries the status it starts from +(`WHERE id = ? AND status = 'pending'`, `'validated'`, `'paid'`), so a +transition from any other status changes nothing. Editing an invoice is also +only possible while `pending`. Rejected invoices have no UI yet +(`docs/TODO.md`). Every status change creates a row in `bo_invoice_history` with `changed_by`, `old_status`, `new_status`, and `notes`. ## PDF handling -PDFs are uploaded to **Cloudinary** (not the server filesystem). -`uploadInvoicePdf` uses `multer` (`upload.single('pdf')`) as middleware to -receive the file, then streams it to Cloudinary via `CloudinaryService`. The -`?type=original|validated` chooses the pair of columns: `original_pdf_url` / -`original_pdf_public_id` or `validated_pdf_url` / `validated_pdf_public_id`. -Replacing a PDF deletes the previous file (only ids inside -`backoffice/invoices/`). - -`downloadInvoicePdf` fetches the PDF from Cloudinary via `axios` and streams it -to the response with `Content-Type: application/pdf`. This avoids storing the -PDF on the server. Only URLs of our own cloud are fetched -(`isOwnCloudinaryUrl`); any other stored URL answers 422. +PDFs are uploaded to **Cloudinary** (not the server filesystem) as **private** +files. `POST /invoices/:id/pdf` runs `requireStorage` and +`singleInvoicePdf('pdf')` from `middlewares/backofficeUpload.ts` (10 MB, +`application/pdf` and real PDF bytes), then `uploadInvoicePdf` sends it to +Cloudinary via `CloudinaryService`. The `?type=original|validated` chooses the +pair of columns: `original_pdf_url` / `original_pdf_public_id` or +`validated_pdf_url` / `validated_pdf_public_id`. Replacing a PDF deletes the +previous file (only ids inside `backoffice/invoices/`). + +`downloadInvoicePdf` fetches the private PDF with `fetchStoredFile` +(`services/uploads/private-files.ts`, `axios` inside) and sends it with +`Content-Type: application/pdf`; the signed URL never leaves the server. Only +URLs of our own cloud are fetched (`isOwnCloudinaryUrl`); any other stored URL +answers 422. Stamps and signatures are served the same way by +`GET /assets/:id/file`. **Deleting an invoice is a hard delete**: it removes its PDFs from Cloudinary, its `bo_invoice_history` rows and the invoice row. The `deleted_at` columns are @@ -106,8 +120,9 @@ the response. `previewBatchPayment` returns the list of validated invoices that would be affected (grouped by supplier). `executeBatchPayment` marks them all as paid, -logging each one to `bo_invoice_history`. Both endpoints are `isRealAdmin` only. -A `revertBatchPayment` endpoint exists for undo (also with a preview). +logging each one to `bo_invoice_history`; it needs `isAdmin` (the preview only +the module access, which is admin anyway). A `revertBatchPayment` endpoint +exists for undo (also with a preview). ## Role boundaries @@ -123,27 +138,31 @@ admin: it cannot create categories or upload PDFs/assets (`denyDemo`, ## Frontend layout The frontend is **server-component-first** — initial data is fetched on the -server in `page.tsx` and passed to client components as props. There are no -React Query hooks; mutations go through `backofficeApi.ts` directly. +server in `page.tsx` (one `Promise.all`) and passed to client components as +props. The tabs then take over with React Query (`useQuery` with their own keys, +invalidated after each mutation); mutations go through `backofficeApi.ts`. ``` app/dashboard/bo/ └── page.tsx ← server component (fetches stats, categories, invoices, suppliers, assets) ├── StatsCards ← summary KPI cards (114 lines) - ├── TabsNavigation ← tab switcher: Pending / Paid / Suppliers / Assets (69 lines) + ├── TabsNavigation ← tab switcher: pending / paid / suppliers / settings (69 lines) └── TabContent ← renders the active tab content (105 lines) app/components/bo/ -├── StatsCards.tsx -├── StatsCardsSkeleton.tsx -├── TabsNavigation.tsx -└── TabContent.tsx +├── StatsCards.tsx, StatsCardsSkeleton.tsx, TabsNavigation.tsx, TabContent.tsx +├── tabs/ ← PendingInvoicesTabLazy (~1140), PaidInvoicesTabLazy (~1010), +│ SuppliersTabLazy (~670), SettingsTabLazy (stamps/signatures) +└── modals/ ← InvoiceFormModal, SupplierFormModal, SupplierInvoicesModal, + PdfUploadModal, PdfViewerModal, PdfEditorModal (~1235 lines: + places the stamp/signature on the PDF in the browser + with pdf-lib, preview with pdfjs-dist) app/lib/backoffice/ -├── backofficeApi.ts ← all HTTP calls via apiClient (528 lines) -├── data.ts ← server-side fetch helpers (used in page.tsx, 351 lines) -├── types.ts ← TypeScript types (352 lines) -├── export-utils.ts ← client-side export helpers (335 lines) +├── backofficeApi.ts ← all HTTP calls via apiClient (~530 lines) +├── data.ts ← server-side fetch helpers (used in page.tsx, ~340 lines) +├── types.ts ← TypeScript types (~325 lines) +├── export-utils.ts ← client-side export helpers (~335 lines) └── index.ts ← barrel export ``` @@ -154,15 +173,15 @@ them. ## Known gotchas -1. **`CloudinaryService` is shared with Blacklist.** - `backend/services/blacklist/cloudinary-service.ts` is imported by both the - blacklist module and the backoffice module. If you refactor or move that - service, update both importers. -2. **PDF download proxied through the backend.** The frontend never talks to - Cloudinary directly — it calls - `GET /api/backoffice/invoices/:id/pdf-download` which proxies through the - backend. This hides Cloudinary credentials from the browser and allows auth - on the endpoint. +1. **`CloudinaryService` is shared.** + `backend/services/blacklist/cloudinary-service.ts` is imported by blacklist, + backoffice, auth, maintenance and checklist. If you refactor or move that + service, update every importer. +2. **Files proxied through the backend.** The frontend never talks to Cloudinary + directly — it calls `GET /api/backoffice/invoices/:id/pdf-download` or + `GET /assets/:id/file`. The files are private: the signed URL stays on the + server, and `backoffice-files.ts` replaces it with the API path in every + response. 3. **ZIP download is streaming.** `downloadValidatedInvoicesZip` pipes to the response as it builds. Do not buffer the entire ZIP in memory — the current implementation uses `archiver`'s pipe correctly. Avoid large refactors that @@ -183,10 +202,11 @@ them. - `backend/controllers/backoffice/backoffice-controller.ts` — all 35+ controller methods. +- `backend/middlewares/backofficeUpload.ts` — PDF and stamp/signature uploads. - `backend/repositories/backoffice/backoffice-repository.ts` — DB access layer. - `backend/services/blacklist/cloudinary-service.ts` — shared Cloudinary upload/delete helper. -- `backend/middlewares/roleCheck.ts:194` — `canAccessBackoffice` definition. +- `backend/middlewares/roleCheck.ts` → `canAccessBackoffice` (admin). - `backend/db-mysql/aiven/16_backoffice.sql` — frozen schema (5 tables + 3 views). - `frontend/app/lib/backoffice/backofficeApi.ts` — client-side API calls. diff --git a/backend/services/backoffice/CLAUDE.md b/backend/services/backoffice/CLAUDE.md new file mode 100644 index 00000000..43c994c2 --- /dev/null +++ b/backend/services/backoffice/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/backend/services/blacklist/AGENTS.md b/backend/services/blacklist/AGENTS.md index b8edbd2c..2bcf14ce 100644 --- a/backend/services/blacklist/AGENTS.md +++ b/backend/services/blacklist/AGENTS.md @@ -1,5 +1,10 @@ # AGENTS.md — Blacklist +**Up:** [`backend/AGENTS.md`](../../AGENTS.md) — `backend/` rules · +[`frontend/AGENTS.md`](../../../frontend/AGENTS.md) — `frontend/` rules, since +this doc covers both sides · [root `AGENTS.md`](../../../AGENTS.md) — shared +rules and module map. + > Combined doc for the blacklist module (backend + frontend). Covers the data > model, audit trail pattern, image handling via Cloudinary, soft > delete/restore, and the frontend component map. @@ -8,19 +13,19 @@ Registry of individuals banned from the hotel. Each entry records the person's name, document type and number (DNI/Passport/NIE/Other), the stay dates, the -reason for the ban, a severity, up to five photos (Cloudinary URLs in a JSON -array), free-text comments, and a full audit trail of all changes. Entries can be soft-deleted and later -restored. All users except `mantenimiento` can read the list; creating, editing, -and deleting requires authentication. +reason for the ban, a severity, up to five photos (API paths in a JSON array), +free-text comments, and a full audit trail of all changes. Entries can be +soft-deleted and later restored. All users except `mantenimiento` can read the +list; creating, editing, and deleting requires authentication. ## DB table — `blacklist_entries` Single table (`backend/db-mysql/aiven/12_blacklist.sql`): `id`, `guest_name`, `document_type` (enum: DNI/PASSPORT/NIE/OTHER), `document_number`, `check_in_date`, `check_out_date`, `reason`, `severity` (enum: -LOW/MEDIUM/HIGH/CRITICAL), `comments`, `images` (JSON array of Cloudinary URLs; -since 2026-10-02 only URLs of our cloud inside `blacklist/` are accepted), -`status` (enum: ACTIVE/DELETED — the soft delete), `deleted_at`, `deleted_by`, +LOW/MEDIUM/HIGH/CRITICAL), `comments`, `images` (JSON array of API paths, +`/api/blacklist/images/`; the validation accepts nothing else), `status` +(enum: ACTIVE/DELETED — the soft delete), `deleted_at`, `deleted_by`, `created_by`, `created_at`, `updated_at`, `audit_trail` (JSON array of `AuditEntry` objects). @@ -32,13 +37,18 @@ the array. There is no separate history table. ``` backend/controllers/blacklist/ -└── blacklist-controller.ts (517 lines) +└── blacklist-controller.ts (~520 lines) backend/repositories/blacklist/ -└── blacklist-repository.ts (565 lines — includes audit trail management) +└── blacklist-repository.ts (~560 lines — includes audit trail management) backend/services/blacklist/ -└── cloudinary-service.ts (shared with backoffice — Cloudinary upload/delete) +└── cloudinary-service.ts (shared Cloudinary upload/delete/private delivery) + +backend/services/uploads/ +├── blacklist-images.ts (photo file name ↔ API path) +├── image-signature.ts (checks the file's bytes are an image) +└── private-files.ts (fetch and send private files) backend/routes/blacklist/blacklist-routes.ts backend/validations/blacklist/schemas.ts (Zod — createBlacklistSchema, updateBlacklistSchema, blacklistFiltersSchema) @@ -72,29 +82,35 @@ export, consider streaming rows rather than loading all at once. Images are uploaded in two steps: -1. `POST /api/blacklist/upload` — uploads the file to Cloudinary via - `CloudinaryService`. Returns `{ url, publicId }`. Frontend calls this before - saving the entry. -2. The frontend collects the returned URLs/publicIds and includes them in the - `images` array when calling `POST /` or `PATCH /:id`. +1. `POST /api/blacklist/upload` — checks the bytes are an image (5 MB max) and + uploads it to Cloudinary as a **private** file via `CloudinaryService`. + Returns `{ url, secure_url, public_id, ... }`, where `url` is the API path + `/api/blacklist/images/`, never a Cloudinary URL. Frontend calls this + before saving the entry. +2. The frontend collects the returned paths and includes them in the `images` + array when calling `POST /` or `PATCH /:id`. +3. `GET /api/blacklist/images/:file` builds the signed URL on the server and + sends the file, so the signature never reaches the browser. Photos uploaded + before 2026-10-04 were migrated by `scripts/migrate-private-files.ts`. Removing a photo before saving only drops it from the form: the file stays in Cloudinary (there is no delete endpoint, see "Route order" below). **Important:** `CloudinaryService` is in -`backend/services/blacklist/cloudinary-service.ts` and is also imported by the -backoffice module. Do not move or rename it without updating both importers. +`backend/services/blacklist/cloudinary-service.ts` and is also imported by auth +(avatars), backoffice, maintenance and checklist. Do not move or rename it +without updating every importer. ## Soft delete / restore -- `DELETE /:id` — sets `status = 'DELETED'`, records `deleted_by` and `deleted_at`, - appends `{ action: 'deleted' }` to audit trail. +- `DELETE /:id` — sets `status = 'DELETED'`, records `deleted_by` and + `deleted_at`, appends `{ action: 'deleted' }` to audit trail. - `PATCH /:id/restore` — sets `status = 'ACTIVE'`, clears `deleted_by`/`deleted_at`, appends `{ action: 'restored' }` to audit trail. - `getAll` by default lists `status = 'ACTIVE'` only. Pass `status=DELETED` for the deleted ones or `status=ALL` for both. -- No screen calls the restore endpoint yet; the unused `restoreBlacklist` - server action was removed on 2026-10-02. +- No screen calls the restore endpoint yet; the unused `restoreBlacklist` server + action was removed on 2026-10-02. ## Endpoints @@ -106,40 +122,43 @@ backoffice module. Do not move or rename it without updating both importers. - **PATCH** `/api/blacklist/:id` — all · Update entry - **DELETE** `/api/blacklist/:id` — all · Soft delete - **PATCH** `/api/blacklist/:id/restore` — all · Restore soft-deleted entry -- **POST** `/api/blacklist/upload` — all · Upload image to Cloudinary +- **POST** `/api/blacklist/upload` — all · Upload image (private) to Cloudinary +- **GET** `/api/blacklist/images/:file` — all · Serve one photo All routes sit behind `authenticateToken` + `excludeMantenimiento`. No admin-only mutations — any authenticated non-maintenance user can create, edit, and delete entries. -**Route order:** `POST /upload` is declared **before** the `/:id` routes so -Express does not match `upload` as an ID parameter. There is no endpoint to -delete an uploaded image: `DELETE /upload/:publicId` had no caller and was -removed on 2026-10-02. +**Route order:** `POST /upload`, `GET /images/:file` and `GET /stats` are +declared **before** the `/:id` routes so Express does not match them as an ID +parameter. There is no endpoint to delete an uploaded image: +`DELETE /upload/:publicId` had no caller and was removed on 2026-10-02. ## Frontend layout ``` app/dashboard/blacklist/ -├── page.tsx ← list page +├── page.tsx ← list page (+ error, loading, not-found) ├── new/page.tsx ← create form ├── [id]/page.tsx ← detail page -└── [id]/edit/page.tsx ← edit form (with not-found.tsx) +├── [id]/edit/page.tsx ← edit form (with not-found.tsx) +└── actions/ ← server actions: create, update, delete, getById app/components/blacklist/ -├── BlacklistDetailClient.tsx ← detail orchestrator (323 lines) +├── BlacklistDetailClient.tsx ← detail orchestrator (335 lines) ├── mains/ -│ ├── BlacklistForm.tsx ← shared create/edit form (435 lines) -│ ├── ImageGallery.tsx ← photo display + delete (339 lines) +│ ├── BlacklistForm.tsx ← shared create/edit form (436 lines) +│ ├── ImageGallery.tsx ← photo display + delete (344 lines) │ ├── AuditTrail.tsx ← renders the JSON audit trail (308 lines) │ └── DeleteButton.tsx (56 lines) ├── panels/ │ ├── CreateBlacklistPanel.tsx ← slide-in create panel (418 lines) -│ └── EditBlacklistPanel.tsx ← slide-in edit panel (563 lines — largest) +│ └── EditBlacklistPanel.tsx ← slide-in edit panel (564 lines — largest) ├── layout/ │ └── BlacklistDetailSummaryPanel.tsx (157 lines) └── ui/ - └── ImageUploader.tsx ← handles Cloudinary upload flow (287 lines) + ├── ImageUploader.tsx ← handles the upload flow (287 lines) + └── Highlight.tsx ← highlights the search term in results app/lib/blacklist/ ├── blacklistApi.ts ← all HTTP calls @@ -151,7 +170,7 @@ app/lib/blacklist/ ## Known gotchas 1. **Two-step image upload.** The image is uploaded to Cloudinary first - (`POST /upload`), then the returned URL/publicId is included in the entry + (`POST /upload`), then the returned API path is included in the entry payload. If the user cancels after uploading, the image stays in Cloudinary orphaned. There is no cleanup job for orphaned images — they accumulate until manually purged from the Cloudinary dashboard. @@ -159,7 +178,7 @@ app/lib/blacklist/ entry. For long-lived entries with many edits, this column can grow large. Currently there is no truncation. If you notice slow queries on heavily-edited entries, consider offloading history to a separate table. -3. **`EditBlacklistPanel` is 563 lines.** It handles the full edit form +3. **`EditBlacklistPanel` is 564 lines.** It handles the full edit form including image management and audit trail display. If adding non-trivial features, extract subcomponents first. 4. **`CloudinaryService` shared with backoffice.** Lives in @@ -174,7 +193,8 @@ app/lib/blacklist/ - `backend/repositories/blacklist/blacklist-repository.ts` — audit trail append logic, soft delete, restore. - `backend/services/blacklist/cloudinary-service.ts` — Cloudinary upload/delete - (shared with backoffice). + (shared with auth, backoffice, maintenance and checklist). +- `backend/services/uploads/blacklist-images.ts` — photo name ↔ API path. - `backend/db-mysql/aiven/12_blacklist.sql` — frozen schema. - `frontend/app/lib/blacklist/blacklistApi.ts` — client-side API calls. - `frontend/app/components/blacklist/ui/ImageUploader.tsx` — two-step Cloudinary diff --git a/backend/services/blacklist/CLAUDE.md b/backend/services/blacklist/CLAUDE.md new file mode 100644 index 00000000..43c994c2 --- /dev/null +++ b/backend/services/blacklist/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/backend/services/cashier/AGENTS.md b/backend/services/cashier/AGENTS.md index dffd9711..66cb3cea 100644 --- a/backend/services/cashier/AGENTS.md +++ b/backend/services/cashier/AGENTS.md @@ -1,8 +1,11 @@ # AGENTS.md — Cashier (backend) +**Up:** [`backend/AGENTS.md`](../../AGENTS.md) — `backend/` rules · +[root `AGENTS.md`](../../../AGENTS.md) — shared rules and module map. + > Backend doc for the cashier module. Covers the data model, shift/day > lifecycle, role boundaries, and all endpoints. For the React UI see -> `frontend/app/components/cashier/CLAUDE.md`. +> `frontend/app/components/cashier/AGENTS.md`. ## What it does @@ -111,11 +114,13 @@ patches. ## Role boundaries -- **Read daily, shifts, vouchers** — all authenticated (excl. `mantenimiento`) +- **Read a day, shifts, vouchers, payments, denominations** — all authenticated + (excl. `mantenimiento`) - **Create/update/close shifts, payments, denominations, vouchers** — `admin`, `recepcionista`, `group-admin` (`canManageCashier`) - **Reopen shifts or days, delete any resource** — `admin` only (`isAdmin`) -- **Reports, history, stats** — `admin` (`canViewReports`) +- **Reports, history, stats, the list of days, a shift's history** — `admin` + (`canViewReports`) `mantenimiento` role is excluded at the router level (`excludeMantenimiento`). @@ -127,7 +132,7 @@ patches. - **PATCH** `/api/cashier/daily/:date/close` — canManageCashier · Close the day - **PATCH** `/api/cashier/daily/:date/reopen` — isAdmin · Reopen closed day - **GET** `/api/cashier/daily/:date/summary` — all · Day summary -- **GET** `/api/cashier/daily` — all · Paginated list of days +- **GET** `/api/cashier/daily` — canViewReports · Paginated list of days - **GET** `/api/cashier/reports/monthly/:year/:month` — canViewReports · Monthly aggregate - **GET** `/api/cashier/shifts` — all · Filtered list @@ -137,18 +142,28 @@ patches. - **PATCH** `/api/cashier/shifts/:id/close` — canManageCashier · Close shift - **PATCH** `/api/cashier/shifts/:id/reopen` — isAdmin · Reopen shift - **PUT** `/api/cashier/shifts/:id/users` — isAdmin · Replace shift users -- **DELETE** `/api/cashier/shifts/:id` — isAdmin · Delete (only if open) -- **GET** `/api/cashier/shifts/:id/history` — all · Shift audit log +- **DELETE** `/api/cashier/shifts/:id` — isAdmin · Delete (refused if closed) +- **GET** `/api/cashier/shifts/:id/history` — canViewReports · Shift audit log - **GET/POST** `/api/cashier/shifts/:shiftId/payments` — all / canManageCashier · List / create payment +- **GET** `/api/cashier/shifts/:shiftId/payments/summary` — all · Totals by + method - **PUT** `/api/cashier/shifts/:shiftId/payments` — canManageCashier · Bulk replace payments +- **PATCH** `/api/cashier/payments/:id` — canManageCashier · Update payment +- **DELETE** `/api/cashier/payments/:id` — isAdmin · Delete payment - **GET/POST** `/api/cashier/shifts/:shiftId/denominations` — all / canManageCashier · List / create denomination - **PUT** `/api/cashier/shifts/:shiftId/denominations` — canManageCashier · Bulk replace denominations -- **GET/POST** `/api/cashier/vouchers` — all / canManageCashier · List / - (implicit via shift) +- **PATCH** `/api/cashier/denominations/:id` — canManageCashier · Update + denomination +- **DELETE** `/api/cashier/denominations/:id` — isAdmin · Delete denomination +- **GET** `/api/cashier/vouchers` — all · List (vouchers are created on a shift, + there is no `POST /vouchers`) +- **GET** `/api/cashier/vouchers/active` — all · Pending and justified vouchers +- **GET** `/api/cashier/vouchers/stats` — canViewReports · Voucher stats +- **GET** `/api/cashier/vouchers/:id` — all · Voucher detail - **POST** `/api/cashier/shifts/:shiftId/vouchers` — canManageCashier · Create voucher on shift - **PATCH** `/api/cashier/vouchers/:id` — canManageCashier · Update voucher @@ -175,18 +190,20 @@ patches. 1. **DB trigger handles daily totals.** Don't manually update `cashier_daily` totals — the trigger `trg_cashier_shift_update_daily` does it automatically after a shift UPDATE that changes those columns. **Three competing update - paths exist:** (a) the DB trigger, (b) `updateDailyTotals()` in - `cashier-shift-repository.ts` (called after saving payments/denominations), - (c) monthly reports calculated live. The trigger was fixed 2026-09-29 to - remove a LEFT JOIN row-multiplication bug. + paths exist:** (a) the DB trigger, (b) `recalculateTotals()` → + `updateDailyTotals()` in `cashier-shift-repository.ts` (called after saving + payments/denominations and on shift updates), (c) monthly reports calculated + live. The trigger was fixed 2026-09-29 to remove a LEFT JOIN + row-multiplication bug. 2. **`can_close` is a computed field.** `getDetailsByDate` in the daily - repository computes whether all 4 shifts are closed and injects - `can_close: boolean` into the response. The controller trusts this field — it - does not re-query. + repository computes whether all 4 shifts are closed and the day is still + `open`, and injects `can_close: boolean` into the response. The controller + trusts this field — it does not re-query. 3. **Voucher lifecycle:** A voucher created in a shift starts as `pending`. It - can be `justified` (money recovered / documented) or `cancelled` (annulled). - Once cancelled, it cannot be unjustified. Only `admin` can cancel or delete - vouchers. + can be `justified` (money recovered / documented) or `cancelled` (annulled), + and only from `pending`: both are final. Deleting also requires `pending`. + Only `admin` can cancel or delete vouchers. `justified_at` and `cancelled_at` + record when (`20260929_add_cashier_voucher_status_dates.sql`). 4. **Payments `replaceAll` deletes first.** The bulk replace does a `DELETE` then `INSERT`. If the frontend sends an empty array, all payments for the shift are wiped. This is intentional (the cashier cleared the form). @@ -197,7 +214,7 @@ patches. ## Cross references - `backend/models/cashier/index.ts` — all TypeScript types and enums. -- `backend/middlewares/roleCheck.ts:106` — `canManageCashier` definition. -- `backend/middlewares/roleCheck.ts:128` — `canViewReports` definition. +- `backend/middlewares/roleCheck.ts` → `canManageCashier` (admin, recepcionista, + group-admin) and `canViewReports` (admin). - `backend/db-mysql/aiven/11_cashier.sql` — frozen schema baseline. -- `frontend/app/components/cashier/CLAUDE.md` — UI, PDF export, React Query. +- `frontend/app/components/cashier/AGENTS.md` — UI, PDF export, React Query. diff --git a/backend/services/cashier/CLAUDE.md b/backend/services/cashier/CLAUDE.md new file mode 100644 index 00000000..43c994c2 --- /dev/null +++ b/backend/services/cashier/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/backend/services/checklist/AGENTS.md b/backend/services/checklist/AGENTS.md index dc0df9ba..13dec1e8 100644 --- a/backend/services/checklist/AGENTS.md +++ b/backend/services/checklist/AGENTS.md @@ -1,5 +1,10 @@ # AGENTS.md — Checklist +**Up:** [`backend/AGENTS.md`](../../AGENTS.md) — `backend/` rules · +[`frontend/AGENTS.md`](../../../frontend/AGENTS.md) — `frontend/` rules, since +this doc covers both sides · [root `AGENTS.md`](../../../AGENTS.md) — shared +rules and module map. + > Single doc for the checklist module. Covers backend, frontend and the **JSON > sync rule** that is the module's main gotcha. No separate frontend file > because the state logic lives here. @@ -17,7 +22,8 @@ that day's `run`. `reset_at`). - `checklist_step_state` — done/undone state per step of a run. - `checklist_step_comments` — comments per step. -- `checklist_step_attachments` — per-step attachments (uploaded via Cloudinary). +- `checklist_step_attachments` — per-step attachments (private files in + Cloudinary). - `checklist_event_log` — operational audit (check, uncheck, reset). **Purged weekly**, not long-term history. @@ -97,9 +103,11 @@ There are also Markdown items (`.md` with YAML frontmatter) loaded via ``` backend/services/checklist/ - ├── checklist.service.ts (92 lines — runs, steps, daily reset, purge) - ├── checklist-comments.service.ts (84 lines — comments + attachments) - └── checklist-content.ts (54 lines — getValidStepIds + JSON cache) + ├── checklist.service.ts (94 lines — runs, steps, daily reset, purge) + ├── checklist-comments.service.ts (121 lines — comments + attachments) + ├── checklist-content.ts (64 lines — getValidStepIds + JSON cache) + └── attachment-response.ts (16 lines — the API path the client gets + instead of the signed URL) backend/controllers/checklist/ ├── checklist-controllers.ts (run state, toggle, reset, history) @@ -109,7 +117,7 @@ backend/repositories/checklist/ ├── checklist-repository.ts (runs, step_state, event_log, history) └── checklist-comments.repository.ts (comments + attachments + counts) -backend/routes/checklist/checklist-routes.ts (61 lines — all endpoints) +backend/routes/checklist/checklist-routes.ts (75 lines — all endpoints) ``` ### Run lifecycle @@ -183,7 +191,9 @@ checked by their bytes). `canResetChecklist`) - **`GET/POST/DELETE /:id/steps/:stepId/comments[/:commentId]`**: Comment CRUD - **`GET/POST/DELETE /:id/steps/:stepId/attachments[/:attachmentId]`**: - Attachment CRUD (multer + Cloudinary) + Attachment CRUD (`requireStorage` + `singleImage('file')` + Cloudinary) +- **`GET /api/checklists/attachments/:attachmentId/file`**: Serve one attachment + (private file, through the backend) The whole subroute sits behind `authenticateToken` + `excludeMantenimiento`. Mantenimiento doesn't enter. Reset additionally requires `canResetChecklist` @@ -201,7 +211,7 @@ frontend/app/dashboard/checklist/ frontend/app/components/checklist/ ├── ChecklistTOC.tsx (282 lines — side index of the catalog) ├── ChecklistTasksContent.tsx (261 lines — checkable checklist render + steps + comments) - ├── StepDetailsPanel.tsx (292 lines — side detail panel of a step: comments, attachments) + ├── StepDetailsPanel.tsx (299 lines — side detail panel of a step: comments, attachments) ├── ChecklistGuideContent.tsx (renders items of type=guide) ├── ChecklistReferenceContent.tsx (renders items of type=reference) ├── ChecklistHeader.tsx @@ -242,14 +252,14 @@ components. `ChecklistTasksContent` drives the interaction: `GET /run`, renders sections+steps with checkboxes, mutations to the backend for toggle / reset / -add comment / upload attachment. Optimistic updates with invalidation after -success. +add comment / upload attachment. The toggle is optimistic (`onMutate` snapshot, +rollback on error, invalidation when settled). `ChecklistTOC.tsx` is the sidebar — collapsible categories, flat item list, link to the detail. On mobile the whole sidebar collapses (local state in `ChecklistClientWrapper`). -`StepDetailsPanel.tsx` (292 lines) opens to the side when a step is selected: +`StepDetailsPanel.tsx` (299 lines) opens to the side when a step is selected: shows comments + attachments for the step and lets you add/remove. ## Conventions and patterns @@ -265,8 +275,11 @@ shows comments + attachments for the step and lets you add/remove. - **Auth:** every backend route sits behind `authenticateToken` + `excludeMantenimiento`. Mantenimiento doesn't enter the module. Reset requires `canResetChecklist` (admin / recepcionista). -- **Attachment upload:** multer in-memory → Cloudinary. Wiring in - `addAttachmentController`. +- **Attachments:** `requireStorage` + `singleImage('file')` (multer in-memory) → + Cloudinary as a **private** file, wired in `addAttachmentController`. The + client only gets the API path (`attachment-response.ts`) and reads the file + through `/attachments/:attachmentId/file`; the signed URL and the `public_id` + stay on the server. ## Known gotchas diff --git a/backend/services/checklist/CLAUDE.md b/backend/services/checklist/CLAUDE.md new file mode 100644 index 00000000..43c994c2 --- /dev/null +++ b/backend/services/checklist/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/backend/services/fnb/AGENTS.md b/backend/services/fnb/AGENTS.md index 00cac415..695af869 100644 --- a/backend/services/fnb/AGENTS.md +++ b/backend/services/fnb/AGENTS.md @@ -1,5 +1,10 @@ # AGENTS.md — F&B / Restaurant +**Up:** [`backend/AGENTS.md`](../../AGENTS.md) — `backend/` rules · +[`frontend/AGENTS.md`](../../../frontend/AGENTS.md) — `frontend/` rules, since +this doc covers both sides · [root `AGENTS.md`](../../../AGENTS.md) — shared +rules and module map. + > Food & Beverage module. Recent (2026-05) and evolving. Main tab in production: > **Daily Revenue** (Opera PDF parsing + manual entry). The Inventory / Orders / > Stats tabs are **mocked** placeholders. **Single file** because the module is @@ -21,24 +26,24 @@ header (`summaryStats` in `page.tsx`) are hardcoded mock data. ``` backend/services/fnb/ - ├── pdf-parser.service.ts (89 lines — parseOperaPdf: extracts date, + ├── pdf-parser.service.ts (~130 lines — parseOperaPdf: extracts date, │ per-code entries, grand total) - └── fnb-categories.cache.ts (39 lines — caches `trackedCodesSet`, the + └── fnb-categories.cache.ts (~35 lines — caches `trackedCodesSet`, the active codes used to filter the PDF) backend/controllers/fnb/ - ├── fnb-upload.controller.ts (42 lines — POST /upload: orchestrates parser + ├── fnb-upload.controller.ts (~50 lines — POST /upload: orchestrates parser │ + validation + persistence) ├── fnb-revenue.controller.ts (40 lines — GET /monthly, /daily; DELETE /day/:date) └── fnb-manual.controller.ts (39 lines — POST /entries — manual entry when no PDF is available) backend/repositories/fnb/ - └── fnb.repository.ts (155 lines — monthly pivot with CASE WHEN + └── fnb.repository.ts (~185 lines — monthly pivot with CASE WHEN per code, totals computed in SQL) backend/routes/fnb/ - └── fnb-routes.ts (34 lines) + └── fnb-routes.ts (44 lines) frontend/app/dashboard/restaurant/ ├── page.tsx (184 lines — tab switcher + mocked summary stats) @@ -46,9 +51,9 @@ frontend/app/dashboard/restaurant/ └── error.tsx frontend/app/components/restaurant/tabs/ - ├── DailyRevenueTab.tsx (1113 lines — the real tab: PDF upload, manual + ├── DailyRevenueTab.tsx (~1120 lines — the real tab: PDF upload, manual │ edit, monthly/daily charts) - ├── InventoryTab.tsx (413 lines — MOCK) + ├── InventoryTab.tsx (416 lines — MOCK) ├── OrdersTab.tsx (416 lines — MOCK) └── StatsTab.tsx (294 lines — MOCK) ``` @@ -76,15 +81,20 @@ The system assumes **fixed Opera codes** mapped to 7 stable columns. Defined as 4. If the new code should affect the parser filter, add it to `fnb_category` (table) and restart the backend — `fnb-categories.cache.ts` reads it once. -**`trackedCodesSet` code cache:** reads `fnb_category` on first use and keeps -it in process memory for the life of the process. There is no TTL: -`invalidateCategories()` is only called by the tests, so a code added via -direct SQL is not seen until the backend restarts. +**`trackedCodesSet` code cache:** reads `fnb_category` on first use and keeps it +in process memory for the life of the process. There is no TTL: +`invalidateCategories()` is only called by the tests, so a code added via direct +SQL is not seen until the backend restarts. ## PDF parser — `parseOperaPdf(buffer)` Input: `Buffer` of the uploaded PDF (multer in-memory). Output: -`{ date, entries[], grandTotal }`. +`{ date, entries[], grandTotal }`. Failures throw `FnbPdfError`. + +**Guards before parsing:** the bytes must be a PDF (`isPdfBuffer`), only the +first 20 pages are read (`MAX_PAGES`) and the parse gives up after 15 s +(`PARSE_TIMEOUT_MS`, `withTimeout`). pdf.js cannot be aborted, so the page limit +is what bounds the work left running after a timeout. **Algorithm:** @@ -93,7 +103,8 @@ Input: `Buffer` of the uploaded PDF (multer in-memory). Output: that appears in Opera's filter line (`Calendar/Month to Date`). Returns `YYYY-MM-DD`. **If no match, returns null** and the controller responds 422 — **there's no fallback** because the PDF header date is +1 day off from the - hotel date and would silently corrupt records. + hotel date and would silently corrupt records. No tracked code in the PDF is + also a 422. 3. **Entry extraction (`extractEntries`):** 3-phase state machine: - `codes` — collects lines matching `/^\d{5}$/`. - `descriptions` — skipped (descriptive text between codes and values). @@ -105,8 +116,10 @@ Input: `Buffer` of the uploaded PDF (multer in-memory). Output: - The parser assumes Opera's exact PDF layout. If Opera changes the format, the heuristics need tuning. Not brittle, but not robust against Opera refactors. -- 10 MB hard cap in multer. PDFs that big never come out of Opera in normal - conditions, so the cap is defensive, not functional. +- 10 MB hard cap in multer (`routes/fnb/fnb-routes.ts`, PDF mimetype or `.pdf` + name; the bytes are checked by the parser). PDFs that big never come out of + Opera in normal conditions, so the cap is defensive, not functional. F&B only + reads the PDF: it is not stored, so the route has no `requireStorage`. ## Performance — pdf-parse v1 @@ -115,6 +128,15 @@ OOMs on Render free tier. v2 loads full PDF.js into memory; v1 is light. If at some point Node is upgraded or the host changes, **test v2 before upgrading** — it might become viable again. Don't upgrade without measuring. +**Import from `lib/`, never the package entry.** v1's `index.js` ships a debug +block: imported from ESM, `module.parent` is undefined, so it reads +`./test/data/05-versions-space.pdf` from the working directory, writes a `.txt` +next to it, and crashes with ENOENT if the file is missing. That is why +`backend/test/data/` used to exist. `pdf-parser.service.ts` imports +`pdf-parse/lib/pdf-parse.js` (the same function without the block, typed in +`types/pdf-parse.d.ts`), so no `test/` folder is needed. The real fixture is +`tests/fnb/fixtures/opera-revenue-sample.pdf`. + ## Endpoints - **`GET /api/fnb/categories`**: list of active categories (drives @@ -128,8 +150,8 @@ it might become viable again. Don't upgrade without measuring. - **`DELETE /api/fnb/day/:date`**: delete all records for a day The whole subroute sits behind `authenticateToken` + `canAccessFnb`. Allowed -roles: `admin`, `recepcionista`, **`group-admin`** (the last one -added in `00bab83`). +roles: `admin`, `recepcionista`, **`group-admin`** (the last one added in +`00bab83`). **About `/entries` (manual):** the manual path exists for when Opera doesn't generate the PDF, or when the day has F&B revenue without an Opera record @@ -154,7 +176,7 @@ have visual gaps. ## Frontend — Daily Revenue tab -`DailyRevenueTab.tsx` (1113 lines) is the only real tab. Internal structure: +`DailyRevenueTab.tsx` (~1120 lines) is the only real tab. Internal structure: - **Upload PDF section**: drag & drop + apiClient call to `/upload`. Toast with the result. @@ -166,7 +188,7 @@ have visual gaps. distribution (breakfast / lunch / dinner) across the month. - **Daily section**: drill-down into a day with all detail. -The size (1113 lines) is high. If you're about to touch non-trivial UI here, +The size (~1120 lines) is high. If you're about to touch non-trivial UI here, consider splitting by section. Not urgent. ## Mock tabs (Inventory / Orders / Stats) @@ -188,26 +210,27 @@ monthlyExpenses) is also hardcoded — part of the mocked phase. ## Auth -`canAccessFnb` allows: `admin`, `group-admin`, `recepcionista`. -**Mantenimiento blocked.** No admin-only restrictions on endpoints — reception -can upload PDFs and edit manually. +`canAccessFnb` allows: `admin`, `group-admin`, `recepcionista`. **Mantenimiento +blocked.** No admin-only restrictions on endpoints — reception can upload PDFs +and edit manually. ## Known gotchas 1. **pdf-parse v1 pinned.** Don't upgrade without testing OOM on Render. - Documented in commit `954043c`. + Documented in commit `954043c`. Import it from `pdf-parse/lib/pdf-parse.js`: + the package entry needs a `./test/data/` PDF in the working directory. 2. **PDF date: filter line vs header.** The parser uses the filter (`Date DD/MM/YY`) **not the header**. The header is +1 day off and would corrupt data. -3. **`trackedCodesSet` cache without TTL.** Changes to `fnb_category` reach - the parser only after a backend restart. +3. **`trackedCodesSet` cache without TTL.** Changes to `fnb_category` reach the + parser only after a backend restart. 4. **`CATEGORY_CODES` is hardcoded** in the repo — specific Opera codes for the hotel. If the hotel changes or Opera renumbers, touch here. 5. **Floats: use `r2()`.** Any amount sum must pass through `r2()` before going to the frontend. 6. **Mock tabs are not features.** If asked to "fix the inventory", confirm the scope — the UI exists but there's no real data behind it. -7. **`DailyRevenueTab.tsx` at 1113 lines.** Watch if it grows; consider +7. **`DailyRevenueTab.tsx` at ~1120 lines.** Watch if it grows; consider splitting by section. ## Cross references @@ -215,4 +238,4 @@ can upload PDFs and edit manually. - `backend/middlewares/roleCheck.ts → canAccessFnb` — allowed roles. - `backend/models/fnb/fnb.models.ts` — module types. - `backend/scripts/import-fnb-2026.ts` — one-off script to import historical - data (if present — used in initial setup). + data (used in initial setup). diff --git a/backend/services/fnb/CLAUDE.md b/backend/services/fnb/CLAUDE.md new file mode 100644 index 00000000..43c994c2 --- /dev/null +++ b/backend/services/fnb/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/backend/services/fnb/pdf-parser.service.ts b/backend/services/fnb/pdf-parser.service.ts index b673abff..7bba12ae 100644 --- a/backend/services/fnb/pdf-parser.service.ts +++ b/backend/services/fnb/pdf-parser.service.ts @@ -1,4 +1,7 @@ -import pdfParse from 'pdf-parse' +// The package entry (index.js) runs a debug block when imported from ESM: it +// reads ./test/data/05-versions-space.pdf from the working directory and +// crashes with ENOENT if it is missing. lib/ is the same function without it +import pdfParse from 'pdf-parse/lib/pdf-parse.js' import { trackedCodesSet } from './fnb-categories.cache.js' import { isPdfBuffer } from '../uploads/pdf-signature.js' diff --git a/backend/services/group/AGENTS.md b/backend/services/group/AGENTS.md index bf91c63b..1e387ea1 100644 --- a/backend/services/group/AGENTS.md +++ b/backend/services/group/AGENTS.md @@ -1,8 +1,11 @@ # AGENTS.md — Group Tracking (backend) +**Up:** [`backend/AGENTS.md`](../../AGENTS.md) — `backend/` rules · +[root `AGENTS.md`](../../../AGENTS.md) — shared rules and module map. + > Backend doc for the group tracking module. Covers the data model, lifecycle, > payment calculator, services, role boundaries, and all endpoints. For the -> React UI see `frontend/app/components/groups/CLAUDE.md`. +> React UI see `frontend/app/components/groups/AGENTS.md`. ## What it does @@ -12,8 +15,10 @@ amount, and a list of room types requested. Throughout its lifecycle, staff update four parallel status tracks — booking confirmation, contract, rooming list, and balance — as well as a list of scheduled payments (each with a percentage and due date) and a list of assigned rooms. All mutations are audited -in `group_history`. An email notification system (nodemailer) can alert -stakeholders when key milestones are reached. +in `group_history`, which outlives the group: since +`20261011_keep_group_history_on_delete.sql` deleting a group keeps its history. +There is no email sending: `email-service.ts` and `nodemailer` were removed on +2026-10-02; group notifications are in-app only. ## DB tables @@ -28,7 +33,7 @@ stakeholders when key milestones are reached. - **`group_payments`**: scheduled payments: amount, percentage, due date, status, method - **`group_history`**: audit log of all mutations with `table_affected`, - `old_value`/`new_value` JSON + `old_value`/`new_value` JSON. No foreign key to `hotel_groups` any more **Known design debt:** `hotel_groups.status` (`GroupStatus` enum) and `group_status.booking_confirmed` both represent booking-level status — they are @@ -68,9 +73,9 @@ backend/repositories/group/ └── group-history-repository.ts (79 lines) backend/services/group/ -├── payment-calculator-service.ts — recalculate payment amounts when group total changes -├── group-history-service.ts — helpers to log CRUD history with old/new JSON -└── email-service.ts — nodemailer wrapper for notification emails +├── payment-calculator-service.ts — recalculate payment amounts when group total changes; balance +├── payment-rules.ts — coherence rules for a payment (2026-10-11) +└── group-history-service.ts — helpers to log CRUD history with old/new JSON backend/routes/group/group-routes.ts (176 lines) backend/models/group/index.ts (all types + enums) @@ -83,7 +88,8 @@ backend/models/group/index.ts (all types + enums) When a group's `total_amount` changes, all payments with a `percentage` defined need their `amount` recalculated: `amount = round(total * percentage / 100, 2)`. `PaymentCalculatorService.recalculatePayments(groupId, newTotal)` is called from -`group-payment-controller.ts` after any update that touches the total. +`group-controller.ts` when `PUT /:id` changes `total_amount`. +`calculateBalance(groupId)` feeds the group detail and the payment list. Payments with `percentage = null` are fixed-amount and are not touched by the recalculation. @@ -95,38 +101,53 @@ Wraps `GroupHistoryRepository.create()` with typed helper methods `old_value`/`new_value` JSON and set `table_affected` automatically. All controllers should use this service rather than calling the repository directly. -### EmailService +### Payment rules (`payment-rules.ts`) + +Decided on 2026-10-11: what was paid never exceeds what is owed, marking a +payment paid by hand fills in what was paid, the status follows the amounts +(`statusFor`, `reconcilePayment`; `requested` stays until money arrives), and +the percentages of a group's payments add up to 100 at most +(`checkPercentages`). A broken rule throws `PaymentRuleError` with the +`ERROR_CODES` entry to answer with. + +### Manual notifications -Sends notification emails via nodemailer (SMTP configured via -`SMTP_HOST/PORT/USER/PASS` env vars). Called by -`NotificationController.createManualNotification` when a manual notification is -created for a group. Email sending is **optional** — if SMTP is not configured, -the notification is saved but the email is skipped silently. +`POST /:id/notifications` goes to +`NotificationController.createManualNotification` (the notifications module): it +stores an in-app notification. No email. ## Role boundaries -- **Read groups, contacts, rooms, payments, status, history**: all authenticated - (`canViewGroups` — includes `mantenimiento`) +- **Read groups, rooms, payments, status, history**: all authenticated + (`canViewGroups` — includes `mantenimiento`); the history leaves out + `group_contacts` entries for `mantenimiento` +- **Read contacts** (`/contacts`, `/contacts/primary`): `admin`, + `recepcionista`, `group-admin` (`canViewGroupContacts`) - **Create/update groups, contacts, rooms, payments, status**: `admin`, `group-admin` (`canManageGroups`) - **Delete group**: `admin` only (`canManageGroups` + `isAdmin`) `mantenimiento` can **view** groups (needed for notification access from the -profile page) but cannot create or modify anything. +profile page) but not their contacts, and cannot create or modify anything +(decided 2026-10-12). + +⚠️ **Gap:** `GET /api/groups/:id` still returns the group's `contacts` (email, +phone) to every `canViewGroups` role, `mantenimiento` included, so it bypasses +`canViewGroupContacts` (checked 2026-10-13). ## Endpoints summary - **GET** `/api/groups/dashboard/overview` — canViewGroups · Dashboard stats - **GET** `/api/groups/dashboard/timeline` — canViewGroups · Timeline view -- **GET** `/api/groups` — canViewGroups · List with filters (status, dates, - agency, pagination) -- **GET** `/api/groups/:id` — canViewGroups · Group detail (joins contacts + - rooms + payments + status) +- **GET** `/api/groups` — canViewGroups · List with filters (status, arrival and + departure ranges, agency, sort, limit/offset) +- **GET** `/api/groups/:id` — canViewGroups · Group detail (status + contacts + + rooms + payments + rooms summary + balance; see the gap above) - **POST** `/api/groups` — canManageGroups · Create group - **PUT** `/api/groups/:id` — canManageGroups · Update group (triggers payment recalculation if total changes) - **DELETE** `/api/groups/:id` — canManageGroups + isAdmin · Delete group - (cascades to all sub-tables) + (cascades to the sub-tables; `group_history` is kept) - **GET** `/api/groups/payments/upcoming` — canViewGroups · Upcoming payments across all groups - **GET** `/api/groups/payments/overdue` — canViewGroups · Overdue payments @@ -153,8 +174,9 @@ profile page) but cannot create or modify anything. - **POST** `/api/groups/:id/rooms` — canManageGroups · Create or upsert room - **PUT** `/api/groups/:id/rooms/:roomId` — canManageGroups · Update room - **DELETE** `/api/groups/:id/rooms/:roomId` — canManageGroups · Delete room -- **GET** `/api/groups/:id/contacts` — canViewGroups · Contact list -- **GET** `/api/groups/:id/contacts/primary` — canViewGroups · Primary contact +- **GET** `/api/groups/:id/contacts` — canViewGroupContacts · Contact list +- **GET** `/api/groups/:id/contacts/primary` — canViewGroupContacts · Primary + contact - **POST** `/api/groups/:id/contacts` — canManageGroups · Create contact - **PUT** `/api/groups/:id/contacts/:contactId` — canManageGroups · Update contact @@ -169,9 +191,10 @@ profile page) but cannot create or modify anything. ## Known gotchas 1. **Status duplication.** `hotel_groups.status` and - `group_status.booking_confirmed` must be kept in sync. When updating booking - status via `PUT /:id/status/booking`, the controller also updates - `hotel_groups.status`. If you add new status-mutation paths, replicate the + `group_status.booking_confirmed` must be kept in sync. The repositories do it + both ways: `GroupStatusRepository` updates `hotel_groups` when the booking + track changes, and `GroupRepository.update` syncs `booking_confirmed` when + the group status changes. If you add new status-mutation paths, replicate the update to both fields until the schema is unified. 2. **Payment recalculation on total change.** `PUT /:id` calls `PaymentCalculatorService.recalculatePayments()` if `total_amount` changes. @@ -181,13 +204,12 @@ profile page) but cannot create or modify anything. upsert (`INSERT … ON DUPLICATE KEY UPDATE`) keyed on `(group_id, room_type)`. Calling it with a room type that already exists updates the count and notes rather than creating a duplicate. -4. **Notification emails are silent-fail.** If SMTP is not configured, - `EmailService.sendNotification()` logs a warning and returns without - throwing. The notification row is saved in the DB regardless. -5. **`/payments/upcoming` and `/payments/overdue` are global routes.** They must - be declared **before** `/:id` in the router to avoid Express capturing - `upcoming`/`overdue` as IDs. They are already in the correct order in - `group-routes.ts` — keep it that way. +4. **Payment coherence lives in `payment-rules.ts`.** A new payment path must go + through it, or paid amounts and statuses drift apart again. +5. **`/payments/upcoming` and `/payments/overdue` are global routes.** They are + declared after `GET /:id`, which is safe only because `/:id` matches a single + segment. Do not add a one-segment global route (`/upcoming`) after `/:id`: + Express would read it as an ID. 6. **`group-admin` role.** This role is specific to the groups module. It can manage groups, contacts, rooms, payments, and status but cannot delete groups (admin only) and cannot access other modules' management functions. @@ -195,9 +217,10 @@ profile page) but cannot create or modify anything. ## Cross references - `backend/models/group/index.ts` — all TypeScript types and enums. -- `backend/middlewares/roleCheck.ts:62` — `canManageGroups` definition. -- `backend/middlewares/roleCheck.ts:84` — `canViewGroups` definition. +- `backend/middlewares/roleCheck.ts` → `canManageGroups` (admin, group-admin), + `canViewGroups` (adds recepcionista and mantenimiento) and + `canViewGroupContacts` (admin, recepcionista, group-admin). - `backend/db-mysql/aiven/10_group-tracking.sql` — frozen schema baseline (6 tables). -- `frontend/app/components/groups/CLAUDE.md` — UI, tab navigation, panels, React +- `frontend/app/components/groups/AGENTS.md` — UI, tab navigation, panels, React Query. diff --git a/backend/services/group/CLAUDE.md b/backend/services/group/CLAUDE.md new file mode 100644 index 00000000..43c994c2 --- /dev/null +++ b/backend/services/group/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/backend/services/logbook/AGENTS.md b/backend/services/logbook/AGENTS.md index 25d26403..26241e09 100644 --- a/backend/services/logbook/AGENTS.md +++ b/backend/services/logbook/AGENTS.md @@ -1,5 +1,10 @@ # AGENTS.md — Logbook +**Up:** [`backend/AGENTS.md`](../../AGENTS.md) — `backend/` rules · +[`frontend/AGENTS.md`](../../../frontend/AGENTS.md) — `frontend/` rules, since +this doc covers both sides · [root `AGENTS.md`](../../../AGENTS.md) — shared +rules and module map. + > Single doc for the logbook module (hotel incident log / operational notes). > Covers backend and frontend. The state logic (per-user read/unread, > solved/pending, soft delete + trashed, history audit) is the densest part of @@ -34,35 +39,38 @@ not exist. Solving/reopening updates `logbooks.is_solved`, `solved_at`, **Importance:** backend values `baja` / `media` / `alta` / `urgente`. **Frontend uses `low` / `medium` / `high` / `critical`** and maps via -`mapPriorityToBackend()` in `LogbooksList.tsx`. **Gotcha:** if you add a new -level, update the mapping on both sides — there's no shared enum. +`mapPriorityToBackend()`, defined twice: in `LogbooksList.tsx` and in +`hooks/useLogbooks.ts`. **Gotcha:** if you add a new level, update the mapping +on both sides — there's no shared enum. ## Backend — layout ``` backend/services/logbook/ - └── logbookHistory-service.ts (104 lines — single service; helpers to write + └── logbookHistory-service.ts (114 lines — single service; helpers to write into logbook_history around the repo's create/update/delete) backend/controllers/logbook/ - ├── logbook-controllers.ts (334 lines — CRUD + filters: all, + ├── logbook-controllers.ts (~350 lines — CRUD + filters: all, │ byDepartment, byAuthor, byImportance, │ byDay, soft delete, history) - ├── logbookComments-controllers.ts (298 lines — comment CRUD + history) - └── logbookReads-controllers.ts (186 lines — read/unread, solve/reopen, - list readers and solvers) + ├── logbookComments-controllers.ts (~320 lines — comment CRUD + history) + ├── logbookReads-controllers.ts (~190 lines — read/unread, solve/reopen, + │ list readers and solvers) + └── logbook-errors.ts (error codes, e.g. LOGBOOK_ONLY_AUTHOR_UPDATE) backend/repositories/logbook/ - ├── logbook-repository.ts (315 lines — includes getAllTrashedLogbooks) - ├── logbookHistory-repository.ts (150 lines — generic addHistory for + ├── logbook-repository.ts (~315 lines — includes getAllTrashedLogbooks) + ├── logbookHistory-repository.ts (~150 lines — generic addHistory for │ logbooks and comments) ├── logbookComments-repository.ts (141 lines) ├── logbookCommentsHistory-repository.ts (55 lines — writes to logbook_history │ with type = 'comment') - └── logbookReads-repository.ts (248 lines — reads, solved, pending) + └── logbookReads-repository.ts (~255 lines — reads, solved, pending; + writes their history rows) -backend/routes/logbook/logbook-routes.ts (143 lines) +backend/routes/logbook/logbook-routes.ts (150 lines; GET /trashed is handled inline) ``` ### Main pattern — service + repository + controller @@ -99,8 +107,8 @@ These are the flags that flip most often from the UI: `solved_at = NULL`, `solved_by = NULL`. Reads/solves also log to `logbook_history` (actions `read`, `unread`, `solve`, -`reopen`). This gives the full audit: who read what note and when, who solved -it, who reopened it. +`reopen`), from `logbookReads-repository.ts`. This gives the full audit: who +read what note and when, who solved it, who reopened it. ### Endpoints @@ -109,12 +117,15 @@ Route prefix: `/api/logbooks` (mounted in `backend/index.ts`). - `POST /api/logbooks/` — Create logbook - `PUT /api/logbooks/:id` — Update (author only) - `DELETE /api/logbooks/:id` — Soft delete (author only) -- `GET /api/logbooks/all` — All logbooks (filters via query) +- `GET /api/logbooks/all` — All logbooks (filters via query: `date_from`, + `date_to`, `importance_level`, `include_trashed`, `limit`, `offset`) - `GET /api/logbooks/department/:departmentId` — Filter by department - `GET /api/logbooks/author/:authorId` — Filter by author - `GET /api/logbooks/priority/:importance` — Filter by importance - `GET /api/logbooks/day/:day` — Filter by day (YYYY-MM-DD) -- `GET /api/logbooks/trashed` — List of deleted entries (soft delete recovery) +- `GET /api/logbooks/trashed` — List of deleted entries (soft delete recovery). + ⚠️ Like `include_trashed`, the history and the comments of deleted entries, it + answers any role with access: only the screen hides them (`docs/TODO.md`) - `GET /api/logbooks/:logbookId/history` — Audit of the entry - `POST/GET/PUT/DELETE /api/logbooks/:logbookId/comments[/:id]` — Comment CRUD - `GET /api/logbooks/:logbookId/comments/:commentId/history` — Audit of a @@ -167,15 +178,17 @@ frontend/app/lib/logbooks/ └── hooks/useDepartments.ts ``` -### `useLogbooks(date, messages)` — the key hook +### `useLogbooks({ date, enabled, messages })` — the key hook `useLogbooks` is where most of the feed logic lives. Takes the active date and a `messages` object with the i18n toast strings (injected from the container so the hook is i18n-agnostic). Returns: -- `entries` — the day's logbooks. +- `entries` (plus `isLoading`, `error`, `refetch`) — the day's logbooks. +- `useReaders` — the readers query of one entry. - Mutations: `createLogbook`, `updateLogbook`, `deleteLogbook`, `toggleStatus`, `toggleRead`, `createComment`, `updateComment`, `deleteComment`. +- `mapPriorityFromBackend` / `mapPriorityToBackend`. - Every mutation invalidates the day's query after the server responds + emits success/error toast. **No optimistic updates.** @@ -194,15 +207,16 @@ Frontend Backend 'low' ↔ 'baja' ``` -`mapPriorityToBackend()` in `LogbooksList.tsx`. Per-priority styling in -`getPriorityBackground()` — red borders for `critical`, orange for `high`, -neutral for the rest. +`mapPriorityToBackend()` in `LogbooksList.tsx` and `useLogbooks.ts`. +Per-priority styling in `getPriorityBackground()` — red borders for `critical`, +orange for `high`, neutral for the rest. ### Day picker `HorizontalDatePicker` from `@/app/ui/calendar/` shows a horizontal strip of days for the selected month. Active state is `(currentDate, selectedDay)`. The -backend query uses `?day=YYYY-MM-DD`. +day is fetched from `GET /api/logbooks/day/:day` (`YYYY-MM-DD`); date ranges use +`/all?date_from=&date_to=` paged by `limit`/`offset`. ## Conventions and patterns @@ -221,9 +235,10 @@ backend query uses `?day=YYYY-MM-DD`. ## Known gotchas -1. **ES↔EN importance mapping:** two places define the strings (backend uses - ES, frontend uses EN). If you add a level, update both ends and the mapping - in `LogbooksList.tsx` + `getPriorityBackground()`. +1. **ES↔EN importance mapping:** two places define the strings (backend uses ES, + frontend uses EN). If you add a level, update both ends and both copies of + the mapping (`LogbooksList.tsx`, `useLogbooks.ts`) + + `getPriorityBackground()`. 2. **Only the author edits/deletes:** no admin override. If you need one in the future, put it in `services` with its own audit trail (`action: 'admin_override'`). diff --git a/backend/services/logbook/CLAUDE.md b/backend/services/logbook/CLAUDE.md new file mode 100644 index 00000000..43c994c2 --- /dev/null +++ b/backend/services/logbook/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/backend/services/parking/AGENTS.md b/backend/services/parking/AGENTS.md index 5a8fdc86..42105d4a 100644 --- a/backend/services/parking/AGENTS.md +++ b/backend/services/parking/AGENTS.md @@ -1,8 +1,11 @@ # AGENTS.md — Parking (backend) -> Backend for the hotel parking module: bookings, vehicles, spots and stats. The UI has its own file at -> `frontend/app/dashboard/parking/CLAUDE.md`. This doc covers the HTTP contract, -> booking logic, states and server-side gotchas. +**Up:** [`backend/AGENTS.md`](../../AGENTS.md) — `backend/` rules · +[root `AGENTS.md`](../../../AGENTS.md) — shared rules and module map. + +> Backend for the hotel parking module: bookings, vehicles, spots and stats. The +> UI has its own file at `frontend/app/dashboard/parking/AGENTS.md`. This doc +> covers the HTTP contract, booking logic, states and server-side gotchas. ## What it does @@ -15,18 +18,19 @@ dashboard and historical stats. (A trend analytics API under ``` backend/services/parking/ - └── (only this AGENTS.md: controllers call the repositories directly) + └── target-spot.ts (the spot a booking update points to; the + controllers call the repositories directly) backend/controllers/parking/ - ├── parking.controller.ts (457 lines — spots + vehicles CRUD + search) - ├── bookings.controller.ts (843 lines — the core: list, create, edit, + ├── parking.controller.ts (~440 lines — spots + vehicles CRUD + search) + ├── bookings.controller.ts (~890 lines — the core: list, create, edit, │ check-in, check-out, cancel, no-show, overdue) └── stats.controller.ts (629 lines — dashboard stats with single-date vs range modes) backend/repositories/parking/ - ├── parking.repository.ts (328 lines — spots, vehicles, invoice metadata) - ├── bookings.repository.ts (1102 lines — bookings with joins; the densest one) + ├── parking.repository.ts (~280 lines — spots, vehicles, availability) + ├── bookings.repository.ts (~1150 lines — bookings with joins; the densest one) └── stats.repository.ts (718 lines — aggregations for stats) backend/routes/parking/ @@ -47,7 +51,9 @@ backend/routes/parking/ days there is no rate and 15 €/day is used - **`parking_availability`**: precomputed calendar: one row per spot and day, kept by the triggers in - `backend/db-mysql/aiven/05_parking_functions_triggers.sql` + `backend/db-mysql/aiven/05_parking_functions_triggers.sql`; a job in + `services/cron/cron-service.ts` (`extendAvailability`, at startup and daily at + 03:00) keeps it covering the next 365 days **Note:** there is no `parking_invoices` table. @@ -58,9 +64,14 @@ frontend never exposes `id` to the user. ## States and enums (backend ↔ frontend sync) -The following values are duplicated in -`frontend/app/components/parking/helpers/constants.ts`. If you add one, **update -both sides**. +Booking status and payment method are duplicated in +`frontend/app/components/parking/helpers/constants.ts`; the rest live only in +the backend (`models/parking/index.ts`) and the DB ENUMs. If you add a status or +a method, **update both sides**. + +⚠️ `SpotType` in `models/parking/index.ts` (`standard`, `large`, `handicapped`, +`electric`) does not match the `spot_type` ENUM of the DB, which is the list +below (checked 2026-10-13). ### Booking status @@ -149,8 +160,8 @@ those comments — they're well-maintained. **Bookings (`/api/parking/bookings`)** -- `GET /` — List (filters: status, date, spot_id, vehicle_id, plate_number, - owner_name, booking_source) +- `GET /` — List (filters: id, status, date, spot_id, vehicle_id, plate_number, + owner_name, booking_source; or `quickFilter`, or `startDate`/`endDate`) - `POST /` — Create booking - `GET /:code` — Detail by booking_code - `PUT /:code` — Edit booking @@ -182,12 +193,11 @@ order when modifying. ### Class-based controllers Unlike the rest of the backend (free-standing exported functions), -`bookings.controller.ts` and `stats.controller.ts` -export as **classes with static methods** -(`ParkingBookingsController.getBookings`, etc.). This is inherited from the -first scaffold of the module. **Not a bug and not urgent debt**: it works, it's -internally consistent. If you add a new controller here, follow the class -pattern to keep internal coherence. +`bookings.controller.ts` and `stats.controller.ts` export as **classes with +static methods** (`ParkingBookingsController.getBookings`, etc.). This is +inherited from the first scaffold of the module. **Not a bug and not urgent +debt**: it works, it's internally consistent. If you add a new controller here, +follow the class pattern to keep internal coherence. ### Date handling @@ -197,9 +207,14 @@ Booking dates are stored as MySQL `DATETIME` in Madrid local time. ### Filters and pagination -`GET /bookings` accepts multiple combinable filters (AND). Pagination via -`limit` + `offset`, returns `PaginationInfo` with `total`, `page`, `totalPages`. -Coordinated with the frontend grid that paginates them. +`GET /bookings` has three modes, checked in this order: `quickFilter` (the +dashboard's composite filters: `arrivals_pending`, `arrivals_inside`, +`arrivals_total`, `departures_pending`, `departures_completed`, +`departures_total`), a date range (`startDate`, optional `endDate` and +`status`), or the combinable filters (AND) listed under Endpoints. Pagination +via `page` + `limit`; the response carries +`pagination: { page, limit, total, totalPages }`. Coordinated with the frontend +grid that paginates them. ## PDF invoicing — **does not exist** @@ -222,12 +237,12 @@ in the git history if the feature is ever built. with curl. 5. **Madrid time hardcoded:** everything assumes Madrid time. Multi-tenant across different timezones would need a refactor (not on the near horizon). -6. **Frontend ↔ backend enum duplication:** the list lives in - `helpers/constants.ts` on the frontend. Keep them in sync. +6. **Frontend ↔ backend enum duplication:** status and payment method live in + `helpers/constants.ts` on the frontend too. Keep them in sync. ## Cross references -- `frontend/app/dashboard/parking/CLAUDE.md` — the module's UI side. +- `frontend/app/dashboard/parking/AGENTS.md` — the module's UI side. - `frontend/app/components/parking/helpers/constants.ts` — the frontend copy of the enums. - `backend/models/parking/index.ts` — every TS type for the module. diff --git a/backend/services/parking/CLAUDE.md b/backend/services/parking/CLAUDE.md new file mode 100644 index 00000000..43c994c2 --- /dev/null +++ b/backend/services/parking/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/backend/services/scheduling/AGENTS.md b/backend/services/scheduling/AGENTS.md index 6c4065f4..63ebb7c7 100644 --- a/backend/services/scheduling/AGENTS.md +++ b/backend/services/scheduling/AGENTS.md @@ -1,9 +1,12 @@ # AGENTS.md — Scheduling (TS backend) +**Up:** [`backend/AGENTS.md`](../../AGENTS.md) — `backend/` rules · +[root `AGENTS.md`](../../../AGENTS.md) — shared rules and module map. + > Context for the backend TS piece of the scheduling module: validator, TS > constraints, registry, soft weights, solver-input builder, Python daemon > client, controllers and repository. The Python solver has its own file at -> `backend/scheduling-solver/CLAUDE.md` — read that alongside this one when the +> `backend/scheduling-solver/AGENTS.md` — read that alongside this one when the > task crosses the boundary. ## What the module does @@ -12,8 +15,12 @@ Manages monthly staff schedules through **manual cell-by-cell editing with real-time validation**, plus optional **auto-generation** via a CP-SAT solver (Python). +**Admin only.** The whole router runs `authenticateToken` + `isAdmin`; no other +role reads the schedule (a read-only view for them is in `TODO.md`). + **Month states:** `draft` (editable, validation surfaces warnings) ↔ `published` -(read-only for staff). +(locked: any edit of its cells, days or constraints answers 409 until +`POST /months/:id/unpublish`). **Cell locking.** Assignments with `source_constraint_id` are **locked**: they come from approvals (vacations, IT, etc.) and can't be overwritten from the UI @@ -24,8 +31,8 @@ anything, and the validator distinguishes "user" cells from "system" cells. **Constraint flow.** Manager creates a constraint (e.g. vacation) → admin approves → the matching shift gets written as an assignment **with `source_constraint_id`** (locked). If rejected, nothing happens to the grid. If -an approved constraint is deleted, its derived assignments get unlocked / -removed. +an approved constraint is deleted, its assignments are unlocked by the foreign +key (`ON DELETE SET NULL`) and keep their shift. **Retroactive constraints.** Admins can add constraints for past dates (e.g. log an IT absence after the fact). This is by design — the module doesn't assume @@ -51,17 +58,19 @@ services/scheduling/ ├── schedule-validator.ts (~1000 lines — validates an existing month; uses │ constraints/ + per-employee logic in runFinalValidation) ├── build-solver-input.ts (builds SolverInput from DB; lockedCells from 3 sources) - ├── solver-client.ts (manages the Python daemon: state machine + semaphore) - ├── soft-weights.ts (soft-catalog numeric weights, kept in sync with Python) + ├── solver-client.ts (manages the Python daemon: phases + semaphore) + ├── generated-rows.ts (rows to insert after a generation) + ├── solver-errors.ts (client-facing message per solver errorCode) + ├── soft-weights.ts (soft-catalog numeric weights) ├── constraints/ - │ ├── base-constraint.ts (abstract class with warn/softWarn/failure/success helpers) + │ ├── base-constraint.ts (abstract class with warn/error/softWarn/failure/success) │ ├── registry.ts (ConstraintRegistry: register, getEnabled, checkAll) - │ ├── coverage.constraint.ts (H1: minimum M/T/N coverage per day) - │ ├── consecutive-rest.constraint.ts (H5: ≥2 rest in window of 7) - │ ├── max-consecutive-work.constraint.ts (H4: max consecutive work days) - │ ├── monthly-libre.constraint.ts (H6: monthly libres [min, max]) - │ ├── night-block.constraint.ts (H2: night blocks [min, max]) - │ └── employee-rules.constraint.ts (noWeekends, fixedShift, fixedDays) + │ ├── coverage.constraint.ts (H1: minimum M/T/N coverage per day) ← used by validate() + │ ├── employee-rules.constraint.ts (noWeekends, fixedShift, fixedDays) ← used by validate() + │ ├── consecutive-rest.constraint.ts (H5) ┐ + │ ├── max-consecutive-work.constraint.ts (H4) │ only exercised by + │ ├── monthly-libre.constraint.ts (H6 libres) │ tests/scheduling/constraints.test.ts; + │ └── night-block.constraint.ts (H2) ┘ validate() runs these rules inline ├── utils/ │ ├── matrix.ts (isWorkShift, isLibreShift, getEmployeeShiftCounts...) │ └── day-helpers.ts (getWeeksInMonth, areConsecutive) @@ -99,15 +108,26 @@ interface ValidationResult { 1. **Constructor:** receives `monthId`, `year`, `month`, config, shifts, days, employees, assignments, `previousMonthHistory`. Builds the `GeneratorContext`. -2. **`validate()`:** instantiates a `ConstraintRegistry`, registers the relevant - constraints (`CoverageConstraint`, `EmployeeRulesConstraint`, etc.), runs - `checkAll(context)` → aggregates violations and soft penalties. -3. **`runFinalValidation()`:** per-employee logic that doesn't fit a discrete - constraint (accumulated libre counts, weekend-off missing, M/T variety, - etc.). Emits warnings directly with `severity: 'error'` or `'warning'` and - accumulates soft penalty with the same convention +2. **`validate()`:** runs `runFinalValidation()` first, then a + `ConstraintRegistry` with **only** `CoverageConstraint` and + `EmployeeRulesConstraint`, and merges violations and soft penalties. +3. **`runFinalValidation()`:** the per-employee rules, inline, for every + employee without `fixedDays`. In order: monthly libres (min/max → warning, + distance to the preferred count → soft `libre_*_pref`); H4 max consecutive + work days (error, **inside the month only**); S4 short work blocks (warning + + soft, extended across the month boundary); H5 two consecutive rests in every + 7-day window (**warning**, inside the month only); `T` on the last day of the + previous month → `M` on day 1 (error); `M`↔`T` rotation change at the month + boundary (error + soft `rotation_continuity_break`); `maxShiftPerMonth`; + weekly shifts; H2 night blocks (scattered or under 3 → error, with the + previous month's incomplete block counted). Accumulates soft penalty as `softPenaltyBreakdown[key] += ...`. +The classes in `constraints/` for H2, H4, H5 and H6 are **not** what the live +validator runs: they differ (H4 adds the previous month's streak, H5 is an error +there) and only the unit tests use them. Change a rule in +`runFinalValidation()`, or register the class there first. + **Decision:** some rules live inside `runFinalValidation` because they need **per-employee accumulated state** that would be costly (or ugly) to thread through the `BaseConstraint` API. This isn't tech debt, it's pragmatism — if the @@ -158,9 +178,11 @@ of its own (`W_NIGHT_BALANCE`, `W_ISOLATED_L`, `W_SHIFT_PRIORITY`, pointing at the file that currently emits that soft penalty. If you add a new soft weight, declare `@emitter` pointing at your constraint. If a weight is declared but no file emits it yet, mark it `@deferred ` — this is how we -reserve a key without implementing it yet (S2 weekly_shifts_off, -weekend_imbalance, shift_variety_low, request_preference_unmet are in this state -as of 2026-05-23). +reserve a key without implementing it yet (`pref_weekly_shifts_off`, +`weekend_imbalance`, `shift_variety_low`, `request_preference_unmet` are in this +state). ⚠️ The `@emitter` of `libre_below_pref`, `libre_above_pref` and +`pref_night_block_size_off` points at `monthly-libre` and `night-block` classes, +but the live emitter is `runFinalValidation()` (checked 2026-10-13). The keys returned by the TS validator's `softPenaltyBreakdown` must **match exactly** the keys returned by the Python solver's `stats.softPenaltyBreakdown`. @@ -212,16 +234,11 @@ the Python schema. ## `solver-client.ts` Wraps the persistent Python daemon. Details on the daemon side in -`backend/scheduling-solver/CLAUDE.md`; here's what matters on the Node side. +`backend/scheduling-solver/AGENTS.md`; here's what matters on the Node side. -**State machine:** - -``` -idle → starting → ready → busy → ready → busy → ... - ↑ ↓ - └──────────┘ - (per-request semaphore) -``` +**Phases:** `idle` → `starting` → `ready`, back to `idle` if the process dies. +There is no `busy` phase: a per-request semaphore lets one request at a time +reach the `ready` daemon. **Guarantees:** @@ -251,17 +268,22 @@ together. **`schedule-generate.controller.ts`** (~200 lines): does **one thing** — orchestrate generation. Flow: -1. Validate that the month exists, is `draft`, has generated days, has - employees. -2. `buildSolverInput()` → SolverInput JSON. -3. `runSolver(input, abortSignal)` → SolverOutput. -4. If `status === 'ok'`: apply `matrix` to `scheduling_assignments` **inside a - transaction** (negative keys — virtual days — are ignored). +1. Validate the month: 404 if missing, 409 if `published`, 422 with no days. +2. `buildSolverInput()` → SolverInput JSON; 422 if it has no employees. +3. `runSolver(input, abortSignal)` → SolverOutput (`req.on('close')` aborts). +4. If `status === 'ok'`: `applyGeneratedSchedule()` deletes, **in one + transaction**, every assignment of the month without `source_constraint_id` + and inserts the matrix plus the other locked cells (approved requests, + `fixedDays`) from `generatedRows()`. Those rows carry no + `source_constraint_id`, so they are editable after a generation. Negative + keys (virtual days) are ignored. Then `recalculateLibreNumbers()` per + employee (`L` → `L1`, `L2`…); a failure there is logged, not returned. 5. If `status === 'infeasible'`: return 422 with `conflictingConstraints` and `suggestedRelaxations`. -6. If `status === 'error'`: return 500 with the `errorCode`. -7. Persist the full run in `scheduling_solver_runs` (input + output + stats + - time). +6. If `status === 'error'`: return 500 with `errorCode` and the message from + `solver-errors.ts` (the solver's own message is only logged). +7. Every outcome persists the run in `scheduling_solver_runs` (input + output + + stats + time). **Run log persistence is non-fatal:** if the insert into `scheduling_solver_runs` fails, the response to the user is still OK. Internal @@ -286,15 +308,14 @@ When you touch this file: of another month, published or not. - `getSchedulableEmployeesForMonth(year, month)` applies `start_date`/`end_date` filtering. For raw employee lists (Totals tab, etc.) use - `getAllEmployeesWithStatus`. + `getAllEmployeesWithSchedulableStatus`. - `getPreviousMonthEndAssignments(year, month, N)` reads the previous month even when it's still `draft` — this is intentional so that Jan→Feb→Mar chains work without having to publish month by month. -**Known bug (TODO.md):** `setSchedulableEmployees` does mass `DELETE` + `INSERT` -and loses `start_date`/`end_date` in the process. Proposed fix: selective diff -(only DELETE rows that leave, INSERT rows that arrive). See `TODO.md` § "Bug: -setSchedulableEmployees". +- `setSchedulableEmployees` deletes only the employees that leave and inserts + the new ones, so those that stay keep `start_date`, `end_date`, + `display_order` and `added_by`. It used to clear the table and lose them. ## Main endpoints @@ -305,16 +326,18 @@ setSchedulableEmployees". - `POST /months/:id/reset` — Wipe all assignments, re-seed from approved constraints, in one transaction (`resetMonthAssignments`) - `POST /months/:id/unpublish` — Back to draft (controlled transition) -- `PATCH /assignments/:id` — Update single cell (409 if locked) -- `PUT /months/:id/assignments` — Bulk update; respects locks, 400 for a day - of another month -- `POST /constraints/:id/approve` — Approve/reject + auto-sync assignments -- `GET /employee-rules` — Rules list in camelCase for the panel -- `PUT /scheduling/employees` — Set schedulable employees list (⚠️ see bug - above) - -Full routing in `routes/scheduling/scheduling-routes.ts` with -`authenticateToken` + `isAdmin` / `excludeMantenimiento` per endpoint. +- `PUT /assignments/:assignmentId` — Update single cell (409 if locked) +- `PUT /months/:id/assignments` — Bulk update; respects locks, 400 for a day of + another month +- `PUT /constraints/:constraintId/approve` — Approve/reject + auto-sync + assignments +- `GET /rules` — Rules list in camelCase for the panel +- `PUT /employees` — Set schedulable employees list + +All under `/api/scheduling`. Full routing in +`routes/scheduling/scheduling-routes.ts`: `authenticateToken` + `isAdmin` for +the whole router, `denyDemoWrites` on `/config`, `/shifts`, `/rules`, +`/employees` and `/contracts`, and `demoGenerateLimiter` on `generate`. ## Shift types @@ -416,7 +439,7 @@ per-employee state (libre counts, weekend off check, M/T variety…), inline it ### Step 3 — Implement in the Python solver -→ Full detail in `backend/scheduling-solver/CLAUDE.md` § "Adding a new +→ Full detail in `backend/scheduling-solver/AGENTS.md` § "Adding a new constraint (Python side)". Summary: 1. Create `backend/scheduling-solver/constraints/.py` with signature @@ -487,21 +510,22 @@ previous month already breaks the constraint mathematically (e.g. 6 M shifts in a row at last month's close), the window is unsatisfiable and would produce an artificial INFEASIBLE. `rest.py` **skips** these "doomed" windows. -**Debt:** the TS validator does NOT apply the same exemption yet. Possible -drift: a month generated by the solver may surface spurious warnings when -re-validated by the TS validator. When it bites, replicate the cross-month skip -logic in `consecutive-rest.constraint.ts` and -`max-consecutive-work.constraint.ts`. See `docs/scheduling/decisions.md` (entry -2026-05-20) and `docs/scheduling/constraints.md §H5`. +**Debt:** the live TS validator (`runFinalValidation()`) checks H4 and H5 only +inside the month, so it neither needs that exemption nor sees a violation that +crosses the boundary; H5 is a warning there. The unregistered classes +(`consecutive-rest`, `max-consecutive-work`) do count the previous month for H4, +without the exemption. Aligning them is pending: see +`docs/scheduling/decisions.md` (entry 2026-05-20) and +`docs/scheduling/constraints.md §H5`. ## Cross references -- `backend/scheduling-solver/CLAUDE.md` — the Python solver (daemon, CP-SAT +- `backend/scheduling-solver/AGENTS.md` — the Python solver (daemon, CP-SAT model, constraints, tests). - `docs/scheduling/constraints.md` — full textual specification. Source of truth. - `docs/scheduling/decisions.md` — decision log (why hard/soft, why the weights, what was tried); the 2026-04-22 entry keeps the original phases. -- `docs/TODO.md` — known module bugs (setSchedulableEmployees, H5 cross-month - debt, etc.). +- `docs/TODO.md` — open module items (read-only view for other roles, health + data kept in `scheduling_solver_runs`, solver phase 3). - `frontend/app/components/scheduling/AGENTS.md` — the UI side of the module. diff --git a/backend/services/scheduling/CLAUDE.md b/backend/services/scheduling/CLAUDE.md new file mode 100644 index 00000000..43c994c2 --- /dev/null +++ b/backend/services/scheduling/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/backend/skills-lock.json b/backend/skills-lock.json new file mode 100644 index 00000000..5d79cc38 --- /dev/null +++ b/backend/skills-lock.json @@ -0,0 +1,40 @@ +{ + "version": 1, + "skills": { + "bash-defensive-patterns": { + "source": "wshobson/agents", + "sourceType": "autoskills-registry", + "computedHash": "1f566c2d9c3b50a784b05d08f6b08ebfeb31f038e4d4173f2d488484edb058a6" + }, + "nodejs-backend-patterns": { + "source": "wshobson/agents", + "sourceType": "autoskills-registry", + "computedHash": "710a5e6f83c46e8f6c43356df55c143a7375c4414559a581654ce51709138c55" + }, + "nodejs-best-practices": { + "source": "sickn33/antigravity-awesome-skills", + "sourceType": "autoskills-registry", + "computedHash": "7361ab02fb6b09913e3bdd9cf61c629ed6c17de9485e6a781054e5d437ccfc29" + }, + "nodejs-express-server": { + "source": "aj-geddes/useful-ai-prompts", + "sourceType": "autoskills-registry", + "computedHash": "b15b3e6dda23d3f3c230285a122de9df3aabae6408dc2bd52038445e60ba745e" + }, + "typescript-advanced-types": { + "source": "wshobson/agents", + "sourceType": "autoskills-registry", + "computedHash": "5ca0e177c6aaaba1889255691224daafdb7d71f317cc70bede1590d3907ded42" + }, + "vitest": { + "source": "antfu/skills", + "sourceType": "autoskills-registry", + "computedHash": "be7329a28a492b802061206c2d75603ab294f25f541bc5421d5de8d41abe68b1" + }, + "zod": { + "source": "pproenca/dot-skills", + "sourceType": "autoskills-registry", + "computedHash": "aa8a97195b07763d37494b511f48be90979fd6826c031956c831cfc58f6a24b8" + } + } +} diff --git a/backend/test/data/05-versions-space.pdf b/backend/test/data/05-versions-space.pdf deleted file mode 100644 index 9248082f..00000000 Binary files a/backend/test/data/05-versions-space.pdf and /dev/null differ diff --git a/backend/test/data/05-versions-space.pdf.txt b/backend/test/data/05-versions-space.pdf.txt deleted file mode 100644 index 063889bb..00000000 --- a/backend/test/data/05-versions-space.pdf.txt +++ /dev/null @@ -1,344 +0,0 @@ - - -10/05/26 -04:53 -Revenue by Transaction Codes Net -Revenue Gross/Net Net -Last Year Y/N N Calendar/ Month to Date (Date 09/05/26) -Tax Columns N Transaction Code All -Filter -Demo Hotel City Centre -findeptcodes - Page 1 of 3 -7070I -7080I -10000 -10120 -10130 -10150 -10170 -10174 -10175 -10180 -10195 -10205 -10990 -10991 -12100 -12110 -12120 -12121 -12122 -12130 -12990 -12991 -14100 -14990 -15200 -15201 -15202 -16101 -16191 -16901 -16990 -16991 -16992 -16993 -16994 -16995 -16996 -16997 -16998 -17000 -17001 -17002 -17005 -17006 -17007 -Owner Revenue Posting -Travel Agent Commission -Room Charge(Manual) -Retail -Special Corporate -Discount Rate -Wholesale Rate -Gov. / Military -Associate Leisure -Package Rate -AAA Rate -Prepay -Rebate Transient -Rebate Wholesaler -Corporate Group Rate -Association Group -Other Group -Wholesale Group -Government Group -Delayed Flight -Rebate Group Rate -Rebate Delayed Flights -Contract Rate -Rebate Contract Rate -Internal - Deposit IVA 0% -Internal - Deposit IVA 10% -Internal - Deposit IVA 21% -Guaranteed No Show (VAT) -Rebate Guaranteed No Show (VAT) -Rooms - Miscellaneous Income1 -Rebate - Miscellaneous Income -Loyalty Program - Paid Upgrade Charge -Rebate - MR Paid Upgrade Charge -Partner Rewards Redemption -Rebate Partner Rewards Redemption -Rewards Redemptions -Rebate Loyalty Program -Loyalty Program Redemption Occupancy Pre -Rebate Loyalty Program Redemption Occupa -Early Check Out Fee -Early Arrival Fee -Late Check Out Fee -Rebate Early Check Out Fee -Rebate Early Arrival Fee -Rebate Late Check Out Fee -DAYMONTHYEAR -ActualActualActual -Description -202620262026 -Trn. - 0.00 0.00 0.00 - 0.00 0.00 0.00 - 259.37 1,965.00 23,272.66 - 3,686.78 25,119.57 304,246.68 - 154.71 3,168.57 8,552.29 - 68.73 2,158.10 14,231.78 - 1,147.07 12,837.05 81,333.08 - 0.00 0.00 0.00 - 95.02 441.29 9,577.64 - 179.62 4,455.46 34,176.80 - 0.00 0.00 0.00 - 3,545.62 28,165.84 269,979.64 - 0.00 0.00 0.00 - 0.00 0.00- 175.05 - 0.00 0.00 19,747.64 - 0.00 0.00 0.00 - 0.00 0.00 0.00 - 0.00 0.00 0.00 - 0.00 0.00 0.00 - 0.00 0.00 0.00 - 0.00 0.00 0.00 - 0.00 0.00 0.00 - 0.00 0.00 0.00 - 0.00 0.00- 26.46 - 0.00 0.00 0.00 - 0.00 275.62 2,014.30 - 0.00 0.00 0.00 - 141.61 314.06 520.53 - 0.00 0.00 5,993.46 - 0.00 0.00 23.45 - 0.00 0.00 0.00 - 0.00 0.00 0.00 - 0.00 0.00 0.00 - 0.00 0.00 0.00 - 0.00 0.00 0.00 - 0.00 0.00 393.17 - 0.00 0.00 0.00 - 0.00 0.00 0.00 - 0.00 0.00 0.00 - 0.00 0.00 0.00 - 0.00 0.00 89.45 - 19.91 26.18 638.18 - 0.00 0.00 0.00 - 0.00 0.00 0.00 - 0.00 0.00 0.00 -Code - -10/05/26 -04:53 -Revenue by Transaction Codes Net -Revenue Gross/Net Net -Last Year Y/N N Calendar/ Month to Date (Date 09/05/26) -Tax Columns N Transaction Code All -Filter -Demo Hotel City Centre -findeptcodes - Page 2 of 3 -17010 -17015 -21100 -21110 -21111 -21112 -21113 -21120 -21124 -21125 -21190 -21191 -21192 -21193 -21194 -21195 -21227 -21235 -21237 -21267 -21307 -21810 -29100 -29101 -52121 -52122 -52123 -52126 -52191 -52192 -52193 -52196 -67120 -70200 -70900 -70990 -71010 -71031 -71032 -71033 -71045 -71200 -71300 -71400 -71550 -Guestroom Cancellation -Rebate - Guestroom Cancellations -F&B Miscellaneous charge -Breakfast Buffet included -Lunch Food -Dinner Food -Breakfast Complimentary -Outlet01 Manual Breakfast Food -Breakfast buffet excluded -Breakast Take away -Rebate F&B Miscellaneous charge -Rebate Breakfast Buffet -Rebate Lunch Food -Rebate Dinner Food -Rebate Breakfast buffet excluded -Rebate Breakfast Take Away -Breakfast Beverage -Rebate Breakfast Beverage -Rebate Outlet01 Breakfast Manual Food -Outlet01 Lunch Manual Beverage -Outlet01 Dinner Manual Beverage -Platinum Welcome Gift Complimentary Break -Package Loss Breakfast -Package Profit Breakfast -Outside Breakfast -OutsideLunch -Outside Dinner -Outside Coffee Break -Rebate Outside Breakfast -Rebate Outside Lunch -Rebate Outside Dinner -Rebate Outside Coffee Break -Meeting Room -Laundry - Cleaning Services -Laundry - Other Income -Rebate - Laundry -Guest Deposits (Adaptor etc) -Lockers / Taquillas -Luggage -Damage / smoking fee -Vending machine -Garage-Monthly Parking -Garage - Guest Parking -Garage - Parking Partner -Garage - Daily Parking -DAYMONTHYEAR -ActualActualActual -Description -202620262026 -Trn. - 0.00 0.00 40.41 - 0.00 0.00 0.00 - 0.00 0.00 0.00 - 274.66 4,390.22 25,194.59 - 51.35 527.42 2,472.85 - 80.80 921.48 7,347.59 - 0.00 0.00 0.00 - 198.03 1,317.44 4,686.71 - 13.40 159.79 3,411.42 - 0.00 0.00 166.21 - 0.00 0.00 0.00 - 0.00 0.00- 9.10 - 0.00 0.00 0.00 - 0.00 0.00 0.00 - 0.00 0.00 141.44 - 0.00 0.00 21.99 - 0.00 0.00 0.00 - 0.00 0.00 0.00 - 0.00 0.00 0.00 - 77.67 755.97 5,651.07 - 87.77 940.72 5,753.22 - 0.00 0.00- 46.09 - 0.00 0.00 0.00 - 0.00 0.00 0.00 - 0.00 0.00 0.00 - 0.00 0.00 0.00 - 0.00 0.00 0.00 - 0.00 0.00 0.00 - 0.00 0.00 0.00 - 0.00 0.00 0.00 - 0.00 0.00 0.00 - 0.00 0.00 0.00 - 0.00 0.00 0.00 - 0.00 0.00 0.00 - 0.00 0.00 0.00 - 0.00 0.00 0.00 - 0.00 0.00 0.00 - 0.00 0.00 0.00 - 0.00 0.00 0.00 - 0.00 0.00 71.82 - 0.00 0.00 0.00 - 0.00 0.00 0.00 - 57.27 366.00 6,851.59 - 0.00 0.00 94.04 - 0.00 0.00 1,905.46 -Code - -10/05/26 -04:53 -Revenue by Transaction Codes Net -Revenue Gross/Net Net -Last Year Y/N N Calendar/ Month to Date (Date 09/05/26) -Tax Columns N Transaction Code All -Filter -Demo Hotel City Centre -findeptcodes - Page 3 of 3 -71755 -71790 -71900 -71910 -75000 -75002 -75003 -Rebate Parking Partner -Rebate - Parking -Pet sanitazion fee -Rebate -Pet Fee -Miscellaneous 10% -Miscellaneous 0% -Miscellaneous 21% -DAYMONTHYEAR -ActualActualActual -Description - 9,985.19 84,104.35 861,464.19Grand Total -202620262026 -Trn. - 0.00 0.00 8.31 - 0.00 0.00 0.00 - 0.00 0.00 248.15 - 0.00 0.00 110.62 - 0.00 0.00 0.00 - 0.00 0.00 0.00 - 0.00 0.00 0.00 -Code \ No newline at end of file diff --git a/backend/tests/README.md b/backend/tests/README.md index 45ea97f3..22673260 100644 --- a/backend/tests/README.md +++ b/backend/tests/README.md @@ -1,14 +1,33 @@ # Backend tests -How to run them is in `docs/general/README.md` (`pnpm test` in `backend/`). - -Two kinds live here: - -- **Self-contained** (most of them): Zod schemas, middlewares and pure - helpers, some against a real Express app on a random port. They need no - `.env` and no database. -- **Need `.env`** (8 files: `auth/`, `checklist/checklist.test.ts`, `fnb/`, - `scheduling/corpus.test.ts`, `scheduling/employee-requests-repository.test.ts`, - `scheduling/solver-parity.test.ts`): they load `SECRET_JWT_KEY` or - `config/db.ts`, and those using the database run against the configured one - (today the shared Aiven database). Without `.env` the file fails to load. +How to run them is in `docs/general/README.md` (`pnpm test` in `backend/`). Run +them from `backend/`: `auth/role-access.test.ts` reads the route files relative +to the working directory. + +Three kinds live here: + +- **Self-contained** (most of them): Zod schemas, middlewares and pure helpers, + some against a real Express app on a random port. They need no `.env` and no + database. +- **Need `SECRET_JWT_KEY`** (4 files in `auth/`: `demo-password-login`, + `login-timing`, `role-access`, `token-service`). CI passes a dummy key. +- **Need the database** (7 files, listed in `NEEDS_DATABASE` in + `vitest.config.ts`): they import `config/db.ts`, which needs the database + credentials in `.env`, and run against the database `DB_ENVIRONMENT` selects + (`local` by default). `SKIP_DB_TESTS=1` leaves them out, as CI does. + +Measured on 2026-10-13 by running the suite without `.env`, with and without the +key. + +Besides the per-module folders: + +- `frontend/` tests dependency-free frontend helpers: the frontend has no test + runner of its own. +- `scheduling-corpus/` holds the scheduling fixtures (`fixtures/*.json`, schema + in `_schema.ts`) and their loader; `scheduling/corpus.test.ts` and + `scheduling/solver-parity.test.ts` run them. See its own `README.md`. +- `fnb/fixtures/opera-revenue-sample.pdf` is a synthetic Opera report used by + the F&B parser tests. + +There is no `backend/test/` folder any more: it only fed a debug block of +`pdf-parse` v1, which the F&B parser now skips (`services/fnb/AGENTS.md`). diff --git a/backend/tests/frontend/error-code-message.test.ts b/backend/tests/frontend/error-code-message.test.ts index e97c2224..95a58b74 100644 --- a/backend/tests/frontend/error-code-message.test.ts +++ b/backend/tests/frontend/error-code-message.test.ts @@ -9,11 +9,11 @@ import { errorCodeMessage, } from '../../../frontend/app/lib/helpers/errorCodeMessage.js' -// Every ERROR_CODES.X the backend source mentions (tests and the catalogue aside), +// Every ERROR_CODES.X the backend source mentions (tests, CLI scripts and the catalogue aside), // plus any code written as a bare string (`code: 'X'`) function backendErrorCodes(): string[] { const root = resolve(__dirname, '../..') - const skip = new Set(['node_modules', 'tests', 'scheduling-solver', 'db-mysql']) + const skip = new Set(['node_modules', 'tests', 'scheduling-solver', 'db-mysql', 'scripts']) const codes = new Set() const walk = (dir: string) => { for (const entry of readdirSync(dir, { withFileTypes: true })) { diff --git a/backend/tests/scheduling-corpus/README.md b/backend/tests/scheduling-corpus/README.md index 5213291c..6b13dc9f 100644 --- a/backend/tests/scheduling-corpus/README.md +++ b/backend/tests/scheduling-corpus/README.md @@ -1,6 +1,7 @@ # Scheduling Corpus -Test fixtures for the `ScheduleValidator`. Each fixture is a JSON file that describes: +Test fixtures for the `ScheduleValidator`. Each fixture is a JSON file that +describes: - A scheduling scenario (employees, days, assignments, config) - Expected validation output (which violations must/must not be present) @@ -8,7 +9,8 @@ Test fixtures for the `ScheduleValidator`. Each fixture is a JSON file that desc Used in two places: 1. **Today**: run against the TypeScript `ScheduleValidator` via Vitest -2. **Fase 2**: run against the Python CP-SAT solver for parity checks (same JSON, different runner) +2. **Fase 2**: run against the Python CP-SAT solver for parity checks (same + JSON, different runner) ## Running @@ -53,12 +55,15 @@ scheduling-corpus/ | F25 | Edge: fully broken | 1 | | **Total** | | **25** | -Cross-month fixtures (F11–F13) are marked `"todo": true` — they will pass as `it.todo` until step B.0 wires `PreviousMonthHistory` into the validator. +Cross-month fixtures (F11–F13) are marked `"todo": true` — they will pass as +`it.todo` until step B.0 wires `PreviousMonthHistory` into the validator. ## Adding a fixture -1. Edit `generate-fixtures.mjs` — add a `write('F##-slug', { ... })` call at the end. -2. Run `node generate-fixtures.mjs` from this directory to regenerate `fixtures/`. +1. Edit `generate-fixtures.mjs` — add a `write('F##-slug', { ... })` call at the + end. +2. Run `node generate-fixtures.mjs` from this directory to regenerate + `fixtures/`. 3. Run `pnpm test:corpus` to verify the new fixture passes. ### Fixture format @@ -104,22 +109,27 @@ Cross-month fixtures (F11–F13) are marked `"todo": true` — they will pass as Matchers match by **shape**, NOT by message text. Fields: -- `type`: `"coverage" | "night_block" | "rest" | "hours" | "constraint" | "validation"` +- `type`: + `"coverage" | "night_block" | "rest" | "hours" | "constraint" | "validation"` - `severity`: `"error" | "warning" | "info"` - `employeeId`: optional — specific employee UUID or omit for "any" - `day`: optional — specific day number or omit for "any" ### Employee IDs -Use deterministic IDs: `E0000000-0000-0000-0000-000000000001` (employee 1), `E0000000-0000-0000-0000-000000000002` (employee 2), etc. +Use deterministic IDs: `E0000000-0000-0000-0000-000000000001` (employee 1), +`E0000000-0000-0000-0000-000000000002` (employee 2), etc. ### Constraint: don't fix fixtures to match bugs -If a fixture reveals a bug in the validator, document it in `docs/scheduling/decisions.md` and fix the validator in step B. Do NOT adjust the expected output to match wrong validator behavior. +If a fixture reveals a bug in the validator, document it in +`docs/scheduling/decisions.md` and fix the validator in step B. Do NOT adjust +the expected output to match wrong validator behavior. ## Config defaults -The `defaultConfig()` function (in `factory.ts`) returns values from `docs/scheduling/constraints.md §4`: +The `defaultConfig()` function (in `factory.ts`) returns values from +`docs/scheduling/constraints.md §4`: | Key | Value | | ---------------------- | ----- | diff --git a/backend/types/pdf-parse.d.ts b/backend/types/pdf-parse.d.ts new file mode 100644 index 00000000..7c162631 --- /dev/null +++ b/backend/types/pdf-parse.d.ts @@ -0,0 +1,8 @@ +// types/pdf-parse.d.ts + +// services/fnb/pdf-parser.service.ts imports the function from lib/ to skip +// the package's debug block; @types/pdf-parse only types the entry point +declare module 'pdf-parse/lib/pdf-parse.js' { + import pdfParse from 'pdf-parse' + export default pdfParse +} diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 4a70615c..c2a1d659 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -170,9 +170,9 @@ `frontend/docs/fnb/FNB-FRONTEND.md`, ya estaba ignorado). **El contenido sigue en el historial**: lo privado se limpia igualmente en la fase 2. - **Dos Excel archivados parecen datos reales del personal** - (`docs/checklists/PLANNING 2026.xlsx` y - el Excel de presencias del personal en `docs/frontend/schedule/`). No se han - abierto. Son candidatos a privados para la fase 2. + (`docs/checklists/PLANNING 2026.xlsx` y el Excel de presencias del personal + en `docs/frontend/schedule/`). No se han abierto. Son candidatos a privados + para la fase 2. - **Rechazado:** aplanar el archivo en una sola carpeta (se pierde el origen de cada fichero) y usar `git mv` (versionaría el destino, que debe ser local). @@ -286,7 +286,9 @@ ## ADR-017 — El nivel de seguridad objetivo es L3 (OWASP ASVS 5.0) -- **Estado:** ✅ aceptada (2026-09-28) +- **Estado:** ✅ aceptada (2026-09-28) · 📌 implantación del harness revisada + por + [ADR-042](#adr-042--el-harness-se-implanta-por-subproyecto-empezando-por-backend) - **Fecha:** 2026-09-28 - **Decisión:** el proyecto se revisa contra el **nivel L3** de OWASP ASVS 5.0. Es el **objetivo**, no el estado actual: los hallazgos de seguridad de @@ -552,15 +554,16 @@ `callback(null, false)` las peticiones simples llegarían a las rutas. - No se añade token CSRF ni cabecera obligatoria. - **Ninguna ruta GET puede modificar datos**: una navegación normal envía las - cookies `Lax` sin `Origin`. Excepción revisada: `GET /api/checklists/:id/run` - abre el run del día y cierra los atrasados; es idempotente. + cookies `Lax` sin `Origin`. Excepción revisada: + `GET /api/checklists/:id/run` abre el run del día y cierra los atrasados; es + idempotente. - `http://localhost:3000` solo está en la lista fuera de producción. - **Motivo:** los navegadores envían `Origin` en toda petición POST, PUT, PATCH y DELETE, así que comprobarlo es la defensa de «verificar el origen» de OWASP. Una cabecera obligatoria no añade nada y obligaría a tocar más de 25 llamadas - `fetch` del frontend. Comprobado en producción el 2026-09-29 con una sesión - de `admin` válida: los orígenes ajenos, del mismo sitio y `null` reciben 403; - el propio pasa. + `fetch` del frontend. Comprobado en producción el 2026-09-29 con una sesión de + `admin` válida: los orígenes ajenos, del mismo sitio y `null` reciben 403; el + propio pasa. ## ADR-030 — Entorno preview completo en subdominios de `four-points.stackbp.es` @@ -599,14 +602,14 @@ - **Estado:** ✅ aceptada (2026-10-04) - **Fecha:** 2026-10-04 - **Decisión:** - - `claude/compassionate-planck-gh6aof` entra en `main` con un **merge - normal** («Create a merge commit»), **no con squash**. Los commits llegan a - `main` uno a uno. + - `claude/compassionate-planck-gh6aof` entra en `main` con un **merge normal** + («Create a merge commit»), **no con squash**. Los commits llegan a `main` + uno a uno. - Los **5 commits con autor `Claude `** (`c4719b2`, `8af5641`, `9c583eb`, `1d5b3ca`, `d432c30`) **no se rehacen ahora**: su autor pasa a `bpstack` en la reescritura de la fase 2, con - `git-filter-repo --mailmap`, la misma pasada que quita los ficheros - privados (`GITCLEAN.md`). + `git-filter-repo --mailmap`, la misma pasada que quita los ficheros privados + (`GITCLEAN.md`). - Los pares fnb y revert se quedan: se anulan entre sí y no cambian código. - La lista de arreglos vive en `git log` y en `docs/VERIFY.md`, versionado. - **Motivo:** `TODO.md` borra lo terminado porque `git log` ya lo registra. Un @@ -637,22 +640,22 @@ entrada. - **F&B**: quien tiene acceso sube el PDF, mete cifras a mano y borra el día que sea, de cualquier fecha. - - **Mantenimiento**: quien tiene acceso crea, edita, cambia de estado, - asigna, borra y restaura cualquier parte. + - **Mantenimiento**: quien tiene acceso crea, edita, cambia de estado, asigna, + borra y restaura cualquier parte. - **Caja**: los turnos y los días cerrados siguen editables por los roles que los gestionan. - **Parking** (añadido el 2026-10-04): el importe cobrado, el método y la referencia de pago se pueden editar en cualquier estado de la reserva, también `completed`. - - **Mensajería** (añadido el 2026-10-04): cualquier rol ve el nombre, el - email y el rol de todos los usuarios activos al empezar una conversación, - y la lista de participantes devuelve sus emails y `last_read_at`. + - **Mensajería** (añadido el 2026-10-04): cualquier rol ve el nombre, el email + y el rol de todos los usuarios activos al empezar una conversación, y la + lista de participantes devuelve sus emails y `last_read_at`. - **Motivo:** decisión del propietario. El hotel trabaja con pocas personas que necesitan acceso completo, y mantenimiento ya guarda historial de cada cambio. -- **Rechazado:** separar acciones de `admin` en F&B (borrar días, cifras a - mano) y en mantenimiento (borrar, restaurar, cambiar estado); bloquear la - edición de turnos y días cerrados; bloquear el cobro de parking según el - estado de la reserva; ocultar emails y roles en el directorio de mensajería. +- **Rechazado:** separar acciones de `admin` en F&B (borrar días, cifras a mano) + y en mantenimiento (borrar, restaurar, cambiar estado); bloquear la edición de + turnos y días cerrados; bloquear el cobro de parking según el estado de la + reserva; ocultar emails y roles en el directorio de mensajería. - **Consecuencias:** salen de `TODO.md` los cinco puntos que pedían esos permisos. Siguen abiertos los que no son de permisos: validar los importes manuales de F&B, el contenido del PDF y el rastro de cambios de dinero en caja @@ -676,14 +679,14 @@ plazo. - Las prioridades 🔴🟡🟢 se mantienen dentro de cada sección. - **Motivo:** decisión del propietario. El objetivo es cerrar las brechas y - publicar; las mejoras se pueden hacer en cualquier momento, y esperar a - todas retrasaría la publicación sin reducir el riesgo. + publicar; las mejoras se pueden hacer en cualquier momento, y esperar a todas + retrasaría la publicación sin reducir el riesgo. - **Rechazado:** resolver todo `TODO.md` antes de la fase 2 (ADR-027 tal como estaba). -- **Consecuencias:** `TODO.md` se reorganiza en las dos secciones y - `ROADMAP.md` cambia la definición de la fase 1c. Los dudosos se clasificaron - como mejora: datos sensibles en los logs (no son públicos) y los informes de - caja sin `canViewReports` (un rol interno viendo totales). +- **Consecuencias:** `TODO.md` se reorganiza en las dos secciones y `ROADMAP.md` + cambia la definición de la fase 1c. Los dudosos se clasificaron como mejora: + datos sensibles en los logs (no son públicos) y los informes de caja sin + `canViewReports` (un rol interno viendo totales). - **Revisa:** ADR-027 en el alcance de la fase 1c. ## ADR-034 — La limpieza del historial es híbrida: eliminar, reemplazar y cambiar autoría @@ -696,16 +699,16 @@ 1. **Eliminar** del historial los ficheros o directorios enteramente privados (volcados, scripts con la contraseña de Aiven, Excel del personal). - 2. **Reemplazar contenido** (`--replace-text`) en los ficheros legítimos - que llevaron credenciales o nombres reales escritos: el fichero se - conserva y solo cambia el valor. + 2. **Reemplazar contenido** (`--replace-text`) en los ficheros legítimos que + llevaron credenciales o nombres reales escritos: el fichero se conserva y + solo cambia el valor. 3. **Cambiar autoría** (`--mailmap`), como ya decía ADR-031. - - Se aplica poco a poco: cada operación se prueba y se audita en el clon - antes de la siguiente. + - Se aplica poco a poco: cada operación se prueba y se audita en el clon antes + de la siguiente. - Antes, en la rama, se quita del árbol lo que no hace falta en el repo público y los scripts legítimos leen las credenciales de `backend/.env`. -- **Motivo:** decisión del propietario. Borrar del historial un script útil - solo porque llevó una contraseña pierde su historia; reemplazar el valor la +- **Motivo:** decisión del propietario. Borrar del historial un script útil solo + porque llevó una contraseña pierde su historia; reemplazar el valor la conserva. Lo enteramente privado no tiene nada que conservar. - **Rechazado:** solo eliminar ficheros (la regla anterior de `GITCLEAN.md`). - **Consecuencias:** `GITCLEAN.md` clasifica los candidatos por operación. Los @@ -725,22 +728,22 @@ `claude/compassionate-planck-gh6aof`**. `chore/audit-prep-sprint-0`, `feature/ai-schedule-generator`, `feature/auth-hardening` y `feature/observability-pino` no tienen ningún commit fuera de `main`. - - El repositorio actual se **renombra a `four-points-archive`**, sigue - privado y se marca como archivado (solo lectura). El nuevo, con el - historial limpio, se crea como **`four-points`**: primero privado, se - revisa y se pasa a público, con secret scanning y push protection. - - Orden para no subir historial viejo al repositorio nuevo: renombrar; - apuntar `origin` de los tres equipos a `four-points-archive` (o - sustituir esos clones); archivar; crear el nuevo; reconectar Vercel y - Render (se enlazan por ID y seguirían en el archivado). -- **Motivo:** decisión del propietario. Las cuatro ramas no aportan nada que - no esté en `main` y cada rama más es historial que limpiar y auditar. - Mantener el nombre `four-points` conserva enlaces y README. -- **Rechazado:** publicar todas las ramas (ADR-026 tal como estaba); un - nombre nuevo para el repositorio público. -- **Consecuencias:** `GITCLEAN.md` cambia la lista de ramas. Tras crear el - nuevo repositorio, GitHub deja de redirigir `bpstack/four-points` al - archivado: un clon viejo sin el `origin` cambiado subiría al público. + - El repositorio actual se **renombra a `four-points-archive`**, sigue privado + y se marca como archivado (solo lectura). El nuevo, con el historial limpio, + se crea como **`four-points`**: primero privado, se revisa y se pasa a + público, con secret scanning y push protection. + - Orden para no subir historial viejo al repositorio nuevo: renombrar; apuntar + `origin` de los tres equipos a `four-points-archive` (o sustituir esos + clones); archivar; crear el nuevo; reconectar Vercel y Render (se enlazan + por ID y seguirían en el archivado). +- **Motivo:** decisión del propietario. Las cuatro ramas no aportan nada que no + esté en `main` y cada rama más es historial que limpiar y auditar. Mantener el + nombre `four-points` conserva enlaces y README. +- **Rechazado:** publicar todas las ramas (ADR-026 tal como estaba); un nombre + nuevo para el repositorio público. +- **Consecuencias:** `GITCLEAN.md` cambia la lista de ramas. Tras crear el nuevo + repositorio, GitHub deja de redirigir `bpstack/four-points` al archivado: un + clon viejo sin el `origin` cambiado subiría al público. - **Revisa:** ADR-026 en las ramas que se publican. ## ADR-036 — El original se archiva al final y la rotación va después de publicar @@ -748,27 +751,25 @@ - **Estado:** ✅ aceptada (2026-10-05) - **Fecha:** 2026-10-05 - **Decisión:** - - **Orden de la publicación:** renombrar el original a - `four-points-archive`; crear `four-points` privado con el historial - limpio; sustituir los clones locales por clones nuevos (no se cambia el - `origin`); reconectar Vercel y Render, producción y preview; y solo - entonces archivar el original y pasar el nuevo a público. - - **La rotación de credenciales no bloquea la publicación:** se hace - después, en «Después de publicar» de `TODO.md`. + - **Orden de la publicación:** renombrar el original a `four-points-archive`; + crear `four-points` privado con el historial limpio; sustituir los clones + locales por clones nuevos (no se cambia el `origin`); reconectar Vercel y + Render, producción y preview; y solo entonces archivar el original y pasar + el nuevo a público. + - **La rotación de credenciales no bloquea la publicación:** se hace después, + en «Después de publicar» de `TODO.md`. - **El procedimiento de rotación queda fuera del repositorio**, con el propietario. -- **Motivo:** decisión del propietario. Un repositorio archivado no admite - push: mientras producción despliegue desde el original, tiene que poder - recibir un arreglo urgente. El historial - publicado no contiene credenciales y la BD de producción solo tiene datos de - prueba. El procedimiento describe paneles, equipos y claves concretas que no - sirven a quien lea el repositorio. +- **Motivo:** decisión del propietario. Un repositorio archivado no admite push: + mientras producción despliegue desde el original, tiene que poder recibir un + arreglo urgente. El historial publicado no contiene credenciales y la BD de + producción solo tiene datos de prueba. El procedimiento describe paneles, + equipos y claves concretas que no sirven a quien lea el repositorio. - **Revisa:** ADR-035 en el orden; ADR-007 en el momento de la rotación. ## ADR-037 — La demo va en la propia web y el repositorio se publica después -- **Estado:** ✅ aceptada (2026-10-05) · 📌 usuarios demo y acceso revisados - por +- **Estado:** ✅ aceptada (2026-10-05) · 📌 usuarios demo y acceso revisados por [ADR-038](#adr-038--un-solo-admin-demo-restringido-y-la-instalación-local-con-mysql-en-docker) - **Fecha:** 2026-10-05 - **Decisión:** @@ -793,10 +794,10 @@ - **Estado:** ✅ aceptada (2026-10-05) - **Fecha:** 2026-10-05 - **Decisión:** - - **Un solo usuario demo, con rol admin**, en lugar de uno por rol: como - admin ve todos los módulos. Se entra con un botón «Probar la demo» en el - login, sin contraseña: el usuario tiene una aleatoria que nadie conoce, y el - botón y su ruta solo existen con `DEMO_MODE=true` en el backend. + - **Un solo usuario demo, con rol admin**, en lugar de uno por rol: como admin + ve todos los módulos. Se entra con un botón «Probar la demo» en el login, + sin contraseña: el usuario tiene una aleatoria que nadie conoce, y el botón + y su ruta solo existen con `DEMO_MODE=true` en el backend. - **Marca `is_demo`** en el usuario, que viaja en el token. Un middleware bloquea solo lo prohibido: gestión de usuarios y contraseñas, el propio perfil, departamentos, toda subida de ficheros (cualquier `multipart`), el @@ -811,19 +812,19 @@ pnpm como ahora. `pnpm setup:local` crea las tablas, el primer admin y los departamentos, y carga el mock. - **Motivo:** simplicidad, decisión del propietario. Un admin demo prueba toda - la app de una vez, y un botón no obliga a escribir nada. Con roles reales, - los permisos funcionan igual que para cualquier usuario y la restricción vive - en un solo sitio. Render gratuito se duerme, así que un cron a hora fija - puede no ejecutarse. Docker ahorra instalar MySQL; meter en él backend, - frontend y solver sería un segundo sistema que producción no usa. + la app de una vez, y un botón no obliga a escribir nada. Con roles reales, los + permisos funcionan igual que para cualquier usuario y la restricción vive en + un solo sitio. Render gratuito se duerme, así que un cron a hora fija puede no + ejecutarse. Docker ahorra instalar MySQL; meter en él backend, frontend y + solver sería un segundo sistema que producción no usa. - **Rechazado:** usuarios demo por rol y contraseña pública en el login (ADR-037); lista blanca de escrituras con el rol `demo-admin`; reinicio por GitHub Actions (credenciales de Aiven en GitHub y tareas programadas que se desactivan tras 60 días sin actividad); la app entera en Docker. - **Consecuencias:** CSRF sigue cubierto por `SameSite=Lax` y el rechazo de orígenes ajenos; se añade un test. Para XSS se comprueba que un enlace - `javascript:` en las notas del checklist no se ejecuta. Límites nuevos para - el solver y las escrituras del usuario demo. El README pasa a explicar la + `javascript:` en las notas del checklist no se ejecuta. Límites nuevos para el + solver y las escrituras del usuario demo. El README pasa a explicar la instalación local. - **Revisa:** ADR-037 en los usuarios demo y su acceso. @@ -832,23 +833,22 @@ - **Estado:** ✅ aceptada (2026-10-05) - **Fecha:** 2026-10-05 - **Decisión:** - - **Aiven** sigue siendo la única BD del proyecto que se comparte: - producción, preview y la demo pública. + - **Aiven** sigue siendo la única BD del proyecto que se comparte: producción, + preview y la demo pública. - **La BD local** vuelve a ser parte documentada del proyecto: quien clona el - repositorio la monta con `pnpm setup:local` (con MySQL propio o en Docker) - y el propietario puede seguir usando la suya con `pnpm dev:local`. Se + repositorio la monta con `pnpm setup:local` (con MySQL propio o en Docker) y + el propietario puede seguir usando la suya con `pnpm dev:local`. Se mantienen `DB_ENVIRONMENT`, el preset `local` y `LOCAL_DB_*`. -- **Motivo:** ADR-038: quien quiera ver la aplicación entera la instala en - local con un admin completo. Y lo que ADR-015 rechazaba ya existe: una - instalación desde los scripts del repositorio, comprobada el 2026-10-05 en - un MySQL limpio, deja la misma estructura que Aiven (1161 de 1161 columnas, - índices, claves, triggers y procedimientos) y los mismos catálogos. -- **Rechazado:** quitar el soporte de BD local (la tarea pendiente de - ADR-015). +- **Motivo:** ADR-038: quien quiera ver la aplicación entera la instala en local + con un admin completo. Y lo que ADR-015 rechazaba ya existe: una instalación + desde los scripts del repositorio, comprobada el 2026-10-05 en un MySQL + limpio, deja la misma estructura que Aiven (1161 de 1161 columnas, índices, + claves, triggers y procedimientos) y los mismos catálogos. +- **Rechazado:** quitar el soporte de BD local (la tarea pendiente de ADR-015). - **Consecuencias:** las migraciones se siguen aplicando en local y en Aiven (`INDEX.md`). Dos fallos del baseline congelado, que impedían instalar desde - cero, se corrigen en `setup:local` y con - `20261005_complete_fresh_install.sql` sin tocar `aiven/` (ADR-021). + cero, se corrigen en `setup:local` y con `20261005_complete_fresh_install.sql` + sin tocar `aiven/` (ADR-021). - **Revisa:** ADR-015 en la BD local. ## ADR-040 — La documentación obsoleta se archiva fuera del repositorio @@ -861,20 +861,20 @@ que lo contiene: `git show :` lo recupera. - **Archivados el 2026-10-05** (último commit: `da7c643`): - | Ruta | Por qué | - | --- | --- | - | `backend/backend.md` | Duplicaba `backend/README.md` en inglés; decía que el backend era privado y describía la demo antigua (lista blanca de rutas) | - | `backend/README.md` | 442 líneas de arquitectura repetidas en `docs/`; sustituido por un README corto | - | `frontend/README.md` | 877 líneas con Next 14 / React 18 y la demo antigua; sustituido por un README corto | - | `backend/db-mysql/MIGRATION_GUIDE.md` | Mandaba instalar con `MASTER_INSTALL.sql` a mano, que falla desde cero; lo sustituye `pnpm setup:local` (ADR-039) | - | `frontend/messages/cmd.md` | Una línea con un comando de búsqueda | - | `frontend/app/dashboard/conciliation/nota.md` | Idea sin hacer: una descripción obligatoria en cada entrada de la conciliación (habitaciones OOO que restan) | + | Ruta | Por qué | + | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | + | `backend/backend.md` | Duplicaba `backend/README.md` en inglés; decía que el backend era privado y describía la demo antigua (lista blanca de rutas) | + | `backend/README.md` | 442 líneas de arquitectura repetidas en `docs/`; sustituido por un README corto | + | `frontend/README.md` | 877 líneas con Next 14 / React 18 y la demo antigua; sustituido por un README corto | + | `backend/db-mysql/MIGRATION_GUIDE.md` | Mandaba instalar con `MASTER_INSTALL.sql` a mano, que falla desde cero; lo sustituye `pnpm setup:local` (ADR-039) | + | `frontend/messages/cmd.md` | Una línea con un comando de búsqueda | + | `frontend/app/dashboard/conciliation/nota.md` | Idea sin hacer: una descripción obligatoria en cada entrada de la conciliación (habitaciones OOO que restan) | - **Motivo:** el repositorio va a ser público; un documento con datos falsos confunde más que su ausencia, y los README largos repetían `docs/` sin mantenerse. -- **Rechazado:** actualizarlos en el sitio. Era más trabajo y seguían - duplicando `docs/general`. +- **Rechazado:** actualizarlos en el sitio. Era más trabajo y seguían duplicando + `docs/general`. - **Consecuencias:** los documentos del proceso de publicación (`GITCLEAN.md`, `VERIFY.md`, `ROADMAP.md`, `SESSION.md`) se archivan igual cuando el repositorio sea público. @@ -884,20 +884,111 @@ - **Estado:** ✅ aceptada (2026-10-05) - **Fecha:** 2026-10-05 - **Decisión:** `ROADMAP.md`, `GITCLEAN.md` y `VERIFY.md` pasan a - `docs/_archive/open-source-release/`, con un README que dice qué es cada - uno. Se recuperan del historial público: + `docs/_archive/open-source-release/`, con un README que dice qué es cada uno. + Se recuperan del historial público: - | Fichero | Comando | - | --- | --- | - | `ROADMAP.md` | `git show 5f0daa2:docs/ROADMAP.md` | + | Fichero | Comando | + | ------------- | ----------------------------------- | + | `ROADMAP.md` | `git show 5f0daa2:docs/ROADMAP.md` | | `GITCLEAN.md` | `git show 025e76c:docs/GITCLEAN.md` | - | `VERIFY.md` | `git show 025e76c:docs/VERIFY.md` | + | `VERIFY.md` | `git show 025e76c:docs/VERIFY.md` | + + Lo pendiente vive solo en `TODO.md`, y si un punto depende de otro se dice en + el propio punto. - Lo pendiente vive solo en `TODO.md`, y si - un punto depende de otro se dice en el propio punto. - **Motivo:** describían el paso de privado a público, que terminó el 2026-10-05: todas las fases cerradas, el historial reescrito y auditado, ninguna fila de `VERIFY.md` sin probar o fallida (194 filas). Juntos cuentan el proceso entero; sueltos en `docs/` parecían trabajo vivo. -- **Consecuencias:** las ADR anteriores que los citan no se tocan; el comando - de arriba los recupera. `SESSION.md` sigue en `docs/`. Aplica ADR-040. +- **Consecuencias:** las ADR anteriores que los citan no se tocan; el comando de + arriba los recupera. `SESSION.md` sigue en `docs/`. Aplica ADR-040. + +## ADR-042 — El harness se implanta por subproyecto, empezando por `backend/` + +- **Estado:** ✅ aceptada (2026-10-12) +- **Fecha:** 2026-10-12 +- **Decisión:** `backend/` se trata como proyecto independiente para el harness: + capa 1 en `backend/AGENTS.md`, `backend/CLAUDE.md` con `@AGENTS.md`. El + `AGENTS.md` raíz se queda con lo común a los dos subproyectos y el `CLAUDE.md` + raíz lo importa. Cada `AGENTS.md` de módulo lleva al lado un `CLAUDE.md` de + una línea (`@AGENTS.md`). El estado y las decisiones siguen en el `docs/` raíz + (excepción declarada en `backend/AGENTS.md`). `frontend/` queda para después. +- **Motivo:** ADR-017 aplazaba la implantación hasta adaptar el harness a repos + sin `package.json` en la raíz. Apuntado a `backend/`, el generador ya detecta + pnpm por `packageManager`, y el otro bloqueo que citaba (17 `CLAUDE.md` + anidados) ya no existía: se habían borrado. Sin los punteros, Claude Code no + cargaba los `AGENTS.md` de módulo: comprobado el 2026-10-12 leyendo + `backend/services/parking/` (no cargó) y `backend/services/fnb/` con puntero + (cargó). +- **Consecuencias:** + - El nivel de seguridad (ADR-017) sigue siendo uno para toda la aplicación; + las áreas se repartirán entre `backend/AGENTS.md` y `frontend/AGENTS.md` + cuando se reestructure el `AGENTS.md` raíz. + - El generador, apuntado a `backend/`, avisa de que faltan `.gitattributes` y + CI, y no es así: los dos están en la raíz y cubren `backend/`. + - Al aceptarla, ADR-017 se marca como revisada por esta. +- **Rechazado:** implantar la capa 1 solo en el `AGENTS.md` raíz. El generador + no ve ahí gestor de paquetes (`manager none`), y las reglas de cada + subproyecto acabarían mezcladas en un solo fichero. + +## ADR-043 — Los `AGENTS.md` se organizan en tres niveles enlazados + +- **Estado:** ✅ aceptada (2026-10-13) +- **Fecha:** 2026-10-13 +- **Decisión:** + - El `AGENTS.md` raíz se queda con lo común: Context7, formato Markdown, + commits y hooks, el nivel de seguridad, el contrato entre frontend y backend + y el mapa de módulos. Stack, versiones instaladas, comandos y arquitectura + de cada lado pasan a `backend/AGENTS.md` y `frontend/AGENTS.md`, que ya + tenían sus áreas de seguridad. + - La sección de Scheduling del raíz se borra sin moverla: estaba entera en los + tres `AGENTS.md` del módulo. + - Los enlaces son explícitos en los tres sentidos: el raíz enlaza a los + subproyectos y a los módulos; cada subproyecto lista sus módulos; cada + módulo abre con una línea **Up** a su subproyecto (a los dos, si cubre ambos + lados) y al raíz, y enlaza a su pareja del otro lado. + - Los enlaces entre documentos apuntan a `AGENTS.md`, no al `CLAUDE.md` de una + línea que lo importa. + - Clientes soportados: Claude Code y OpenCode. Codex sale del `AGENTS.md` raíz + porque el propietario no lo usa. +- **Motivo:** los dos clientes cargan solos el `AGENTS.md` del directorio de + arranque y los de encima, y el de un módulo al leer un fichero suyo (OpenCode + comprobado en su código, versión 1.18.12, el 2026-10-13; Claude Code, en + ADR-042). Ninguno carga el del otro lado: un documento de módulo del backend + no trae `frontend/AGENTS.md`. Los enlaces dicen qué leer cuando una tarea + cruza de un lado a otro, y desde el raíz, qué hay debajo. +- **Consecuencias:** + - Un módulo nuevo se añade en dos listas: el mapa del raíz y la de su + subproyecto (o las dos, si cubre ambos lados). Lo dice «Adding a New Module» + del raíz. + - Al repartir se corrigió lo que había caducado en el raíz: el rol + `demo-admin` y el usuario demo desactivado (ADR-038), el pool en + `config/config.ts` (está en `config/db.ts`), el patrón de previews de Vercel + en CORS (no existe), `mysql2` en el frontend y los `debug-*.js` de + scheduling, que no existen. +- **Rechazado:** + - Declarar `instructions` en `opencode.json` para que OpenCode cargue el raíz: + ya lo carga. + - Copiar en cada nivel lo de los niveles de arriba. Se desincroniza, y la capa + 1 del harness ya repite en cada subproyecto lo que no puede faltar (commits, + push, Context7). + +## ADR-044 — La sesión se abre en la raíz y activa el Node de cada lado + +- **Estado:** ✅ aceptada (2026-10-14) +- **Fecha:** 2026-10-14 +- **Decisión:** + - La regla 9 de la capa 1 («una sesión por proyecto») tiene una excepción + declarada en `backend/AGENTS.md` y `frontend/AGENTS.md`: la sesión se abre + en la raíz del repo y trabaja en los dos lados. Cada comando de Node o + `pnpm` activa antes la versión del subproyecto con `fnm use`, que lee su + `.nvmrc`. + - Se borra el `.nvmrc` raíz (22). +- **Motivo:** los dos lados no comparten major (22 y 24) y una sesión arrancada + en la raíz heredaba el 22 del `.nvmrc` raíz también para `frontend/`; sin + `engine-strict`, pnpm solo avisa. Ese `.nvmrc` ya no lo leía nada: Render y CI + usan el de cada subproyecto desde f934b4d. `fnm use` por comando se comprobó + el 2026-10-14 en Bash y en PowerShell (24 en `frontend/`, 22 en `backend/`). +- **Rechazado:** abrir una sesión por subproyecto. Las tareas que cruzan el + contrato entre los dos lados (tipos, esquemas, JSON duplicado) piden verlos a + la vez, y la excepción basta. diff --git a/docs/SESSION.md b/docs/SESSION.md index 1b921f8b..b327566a 100644 --- a/docs/SESSION.md +++ b/docs/SESSION.md @@ -7,7 +7,7 @@ Lo que no encaje aquí es una decisión (a `DECISIONS.md`), un hallazgo (a su propio fichero) o ruido (se borra). -**Última actualización:** 2026-10-12 +**Última actualización:** 2026-10-14 --- @@ -28,7 +28,12 @@ y auditado, publicación y demo. Sus documentos (`ROADMAP.md`, `GITCLEAN.md`, - **Local:** `pnpm setup:local` instala desde cero con admin completo, datos ficticios y la base de horarios de la demo (`scheduling-seed.sql`). - **Repositorio original:** `four-points-archive`, archivado y privado. -- **Decisiones:** `DECISIONS.md`, hasta ADR-041. +- **Decisiones:** `DECISIONS.md`, hasta ADR-044. +- **Harness:** capa 1 en `backend/AGENTS.md` y `frontend/AGENTS.md`; el + `AGENTS.md` raíz guarda lo común y los tres niveles se enlazan entre sí + (ADR-042, ADR-043). La sesión se abre en la raíz y activa el Node de cada lado + antes de cada comando (ADR-044). CI comprueba el formato de todo el Markdown + versionado. - **Revisión de OpenCode, cerrada el 2026-10-12:** sus 20 ramas `opencode/*` están fusionadas en esta rama (PR #7–#26) y probadas en preview; las migraciones `20261011_keep_group_history_on_delete.sql` y @@ -60,10 +65,10 @@ y auditado, publicación y demo. Sus documentos (`ROADMAP.md`, `GITCLEAN.md`, Luego, «Después de publicar» de `TODO.md`, por prioridad: 2. **Rotar las credenciales de producción** (BD, clave JWT, Cloudinary, claves - de IA). Ninguna de ellas está en el historial, pero sí la contraseña en - claro de un usuario de la aplicación (`test-login.js`, commit `8595ec0`): - cambiarla también. Se hace en los paneles y se comprueba que producción y - preview siguen funcionando. + de IA). Ninguna de ellas está en el historial, pero sí la contraseña en claro + de un usuario de la aplicación (`test-login.js`, commit `8595ec0`): cambiarla + también. Se hace en los paneles y se comprueba que producción y preview + siguen funcionando. 3. **Rastro de cambios en dinero:** ingresos de F&B, pagos y recuentos de caja, cobros de parking. 4. **Fechas y «hoy» según la zona horaria de cada pieza**, como tarea propia @@ -78,20 +83,19 @@ Nada ahora mismo. ## Cuidado con esto -- **`main` está protegida** (ruleset `protect-main`): solo entra por PR, con - los checks `backend` y `frontend` en verde y como merge commit (sin squash ni +- **`main` está protegida** (ruleset `protect-main`): solo entra por PR, con los + checks `backend` y `frontend` en verde y como merge commit (sin squash ni rebase); no admite force push ni borrado. La rama de trabajo no tiene reglas. -- **Preview y producción comparten la BD de Aiven** y las cookies de sesión: - lo que se prueba en preview cambia datos de producción, y entrar en preview +- **Preview y producción comparten la BD de Aiven** y las cookies de sesión: lo + que se prueba en preview cambia datos de producción, y entrar en preview sustituye la sesión de producción en ese navegador. -- **La cuenta demo es un admin** (`users.is_demo = 1`): lo que no deba tocar - se bloquea en la ruta con `denyDemo`/`denyDemoWrites` o en - `demoRestriction`. Una ruta nueva que escriba fuera de lo que reinicia el - mock (usuarios, catálogos, configuración) necesita su guarda. Si cambia la - base de horarios de la demo, se regenera el seed local con - `pnpm seed:scheduling:export`. -- **Los `AGENTS.md` de módulo** ya se corrigieron, pero no son fuente fiable: - se contrastan siempre con el código y la BD. +- **La cuenta demo es un admin** (`users.is_demo = 1`): lo que no deba tocar se + bloquea en la ruta con `denyDemo`/`denyDemoWrites` o en `demoRestriction`. Una + ruta nueva que escriba fuera de lo que reinicia el mock (usuarios, catálogos, + configuración) necesita su guarda. Si cambia la base de horarios de la demo, + se regenera el seed local con `pnpm seed:scheduling:export`. +- **Los `AGENTS.md` de módulo** ya se corrigieron, pero no son fuente fiable: se + contrastan siempre con el código y la BD. - **Nada de `docs/_archive/` se publica** (git lo ignora). Si algo de ahí debe ser público, se copia fuera. - **Enlaces rotos conocidos:** 3 ficheros fuera de `docs/` citan todavía diff --git a/docs/TODO.md b/docs/TODO.md index 9a669b9f..38442da7 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -18,9 +18,8 @@ hace que alguien reimplemente lo que ya existe. ## Rama `claude/compassionate-planck-gh6aof` -Última fusión en `main`: 2026-10-05 (PR #4, commit de merge `15a8531`). La -rama sigue siendo donde se arregla este fichero y se prueba en el entorno -preview. +Última fusión en `main`: 2026-10-05 (PR #4, commit de merge `15a8531`). La rama +sigue siendo donde se arregla este fichero y se prueba en el entorno preview. ## Después de publicar @@ -30,15 +29,15 @@ preview. ### 🔴 Alta -- [ ] **Rotar las credenciales de producción** — contraseña de la BD y claves - de Cloudinary, y revocar las claves de servicios que ningún código usa. - El historial publicado no las contiene, pero siguen en copias privadas - (ADR-007). El procedimiento está fuera del repositorio. - `SECRET_JWT_KEY` ya se cambió el 2026-09-29. Además, el historial - publicado sí trae la contraseña en claro de un usuario de la aplicación - (`test-login.js`, commit `8595ec0`, con su hash): cambiarla en toda - cuenta que la use, en Aiven y fuera. Decisión del dueño (2026-10-12): - se rota y el historial no se reescribe. +- [ ] **Rotar las credenciales de producción** — contraseña de la BD y claves de + Cloudinary, y revocar las claves de servicios que ningún código usa. El + historial publicado no las contiene, pero siguen en copias privadas + (ADR-007). El procedimiento está fuera del repositorio. `SECRET_JWT_KEY` + ya se cambió el 2026-09-29. Además, el historial publicado sí trae la + contraseña en claro de un usuario de la aplicación (`test-login.js`, + commit `8595ec0`, con su hash): cambiarla en toda cuenta que la use, en + Aiven y fuera. Decisión del dueño (2026-10-12): se rota y el historial no + se reescribe. - [ ] **Fnb: ninguna mutación de ingresos deja rastro de quién la hizo** — `upsertMany` sobrescribe el importe anterior (`ON DUPLICATE KEY UPDATE`) y `deleteDay` borra filas sin dejar ningún registro de usuario, valor @@ -54,28 +53,31 @@ preview. autoría ya no sale del cuerpo (`shift_id` del pago, `created_by` del vale, `opened_by` del día; _comprobado en producción el 2026-09-29_). - [ ] **Parking: cambios del cobro sin historial** — editar `payment_amount`, - método o referencia no deja rastro del valor anterior. Que se pueda - editar en cualquier estado es intencionado (ADR-032, 2026-10-04). El - signo, el `ENUM` del método y las longitudes ya se validan - (2026-10-02). _Comprobado por mí el 2026-09-28._ + método o referencia no deja rastro del valor anterior. Que se pueda editar + en cualquier estado es intencionado (ADR-032, 2026-10-04). El signo, el + `ENUM` del método y las longitudes ya se validan (2026-10-02). _Comprobado + por mí el 2026-09-28._ + +- [ ] **Grupos: el detalle del grupo da los contactos a `mantenimiento`** — + `GET /api/groups/:id` (`controllers/group/group-controller.ts`, detalle) + devuelve `contacts` con email y teléfono a cualquier rol de + `canViewGroups`. El commit `e7a5443` (decisión del 2026-10-12) protegió + `/contacts`, `/contacts/primary` y el historial con + `canViewGroupContacts`, pero este endpoint se quedó fuera; el frontend + oculta la pestaña, pero `useGroup` recibe los datos igual. Quitar los + contactos del detalle para esos roles y añadir el test. _Comprobado por mí + el 2026-10-13 leyendo el código._ ### 🟡 Media -- [ ] **Borrar los datos de prueba de Aiven** — la BD de producción solo - tiene datos de prueba (incluidos usuarios de horarios con nombre de - pila). Si el borrado se retrasa o se da acceso a gente de fuera, antes - renombrarlos (`Empleado 01`…). -- [ ] **Render: comprobar qué versión de Node usa** — `engines` pide - `>=22.16.0` sin tope y, según la documentación de Render, un rango - abierto se resuelve a la última versión; el log de producción muestra el - aviso `DEP0205`, que podría ser de una versión posterior. Mirarlo en el - log de build y, si no es la 22, acotar `engines` (`>=22.16.0 <23`) o - fijar `NODE_VERSION`. - _No comprobado._ +- [ ] **Borrar los datos de prueba de Aiven** — la BD de producción solo tiene + datos de prueba (incluidos usuarios de horarios con nombre de pila). Si el + borrado se retrasa o se da acceso a gente de fuera, antes renombrarlos + (`Empleado 01`…). - [ ] **Maintenance: las fotos no se borran con el parte** — ya son privadas (solo la API las sirve), pero el borrado lógico no las toca, - `auto_delete_on_close` se guarda y nadie lo usa, y subir o borrar fotos - no queda en el historial. _Comprobado por mí el 2026-09-28._ + `auto_delete_on_close` se guarda y nadie lo usa, y subir o borrar fotos no + queda en el historial. _Comprobado por mí el 2026-09-28._ - [ ] **Scheduling: datos de salud guardados sin plazo** — `scheduling_solver_runs` guarda para siempre la entrada completa del solver (con las bajas `IT`/`E` de cada usuario) y su matriz. Decidir el @@ -83,15 +85,15 @@ preview. publicar el 2026-10-04. La lectura ya es solo de `admin` y el log de un INFEASIBLE ya no las incluye (2026-10-04). -- [ ] **Parking: dos reservas creadas a la vez chocan en `booking_code`** — - el trigger `trg_generate_booking_code` calcula el siguiente número con +- [ ] **Parking: dos reservas creadas a la vez chocan en `booking_code`** — el + trigger `trg_generate_booking_code` calcula el siguiente número con `MAX()` sobre una lectura sin bloqueo; dos altas simultáneas (aunque sean de plazas distintas) sacan el mismo código y una falla con 500. No hay doble reserva, solo el error. _Visto en preview el 2026-10-04._ - [ ] **Probar una restauración del backup de Aiven** antes de meter datos reales — Aiven hace copias automáticas y existe - `backend/db-mysql/backup/backup.ps1`, pero no consta que se haya - probado a restaurar ninguna. Del plan archivado (`Global-Plan.md`, H3-8). + `backend/db-mysql/backup/backup.ps1`, pero no consta que se haya probado a + restaurar ninguna. Del plan archivado (`Global-Plan.md`, H3-8). - [ ] **Sentry para los errores** — no está instalado (_comprobado el 2026-09-28_). Esperaba un DSN. Cuando lo haya: `@sentry/node` en el backend (`Sentry.init` al principio de `index.ts` y envolver el manejador @@ -120,38 +122,25 @@ preview. aún sin estrenar. _Comprobado el 2026-09-28 en local y Aiven: `event_scheduler=ON` y el evento está `ENABLED` (última ejecución en Aiven: 2026-09-27)._ -- [ ] **Adaptar el harness a repos con varios proyectos e implantarlo** — en el - repo `harness`, privado y local: - soportar repos sin `package.json` ni lockfile en la raíz (aquí `frontend/` - y `backend/` son dos proyectos pnpm independientes) y `AGENTS.md` - anidados. Después, implantar la capa 1 en este repo. _Comprobado el - 2026-09-28 en `lib/detect.mjs` y `commands/init-project.md` del harness - (ADR-017)._ -- [ ] **Comprobar que Claude Code carga los `AGENTS.md` de las subcarpetas** — - los `CLAUDE.md` de módulo se borraron en vez de quedar como punteros; si - Claude Code solo lee `CLAUDE.md` por directorio, ese contexto se pierde. - _Sin comprobar._ - [ ] **Fechas y «hoy» dependen de la zona horaria de cada pieza** — navegador en Madrid, Node en la del proceso (Madrid en local; Render sin comprobar, UTC si no tiene `TZ`) y MySQL en la suya (Madrid en local; Aiven en UTC, `time_zone=SYSTEM`, comprobado el 2026-09-28). No son relojes - desincronizados: - - El pool (`config/db.ts`) no fija `timezone` ni `dateStrings`: una - columna `DATE` llega como instante a medianoche de Node. Con Node en - Madrid, el 30/09 viaja como `2026-09-29T22:00:00Z`, y quien lo corta por - la `T` se queda con el 29. Así, en local, cada edición de una factura le - resta un día a sus fechas (`InvoiceFormModal.tsx:83-87`). Con Node en - UTC sale bien. - - Si Node y MySQL no están en la misma zona, los `DATETIME` se desplazan: - con Node en UTC contra la MySQL local, `NOW()` sale 2 h movido (p. ej. - `pnpm dev:aiven` desde un equipo en Madrid). - - «Hoy» en UTC está mal entre las 00:00 y las 02:00 de Madrid: - `toISOString()` en 26 sitios del frontend y 6 del backend, - `CURDATE()`/`NOW()` en Aiven (19 consultas con `CURDATE()`: filtros de - parking, código `PK-AAAAMMDD` del trigger, `CURRENT_TIMESTAMP`) y el - identificador `DDMMAA-NNN` de Maintenance (fecha de Node). - - 15 sitios del frontend cortan por la `T` fechas del servidor (grupos, - lista negra, parking, horarios, checklist, caja, backoffice). + desincronizados: - El pool (`config/db.ts`) no fija `timezone` ni + `dateStrings`: una columna `DATE` llega como instante a medianoche de + Node. Con Node en Madrid, el 30/09 viaja como `2026-09-29T22:00:00Z`, y + quien lo corta por la `T` se queda con el 29. Así, en local, cada edición + de una factura le resta un día a sus fechas + (`InvoiceFormModal.tsx:83-87`). Con Node en UTC sale bien. - Si Node y + MySQL no están en la misma zona, los `DATETIME` se desplazan: con Node en + UTC contra la MySQL local, `NOW()` sale 2 h movido (p. ej. + `pnpm dev:aiven` desde un equipo en Madrid). - «Hoy» en UTC está mal entre + las 00:00 y las 02:00 de Madrid: `toISOString()` en 26 sitios del frontend + y 6 del backend, `CURDATE()`/`NOW()` en Aiven (19 consultas con + `CURDATE()`: filtros de parking, código `PK-AAAAMMDD` del trigger, + `CURRENT_TIMESTAMP`) y el identificador `DDMMAA-NNN` de Maintenance (fecha + de Node). - 15 sitios del frontend cortan por la `T` fechas del servidor + (grupos, lista negra, parking, horarios, checklist, caja, backoffice). Plan: `DATE` como texto `AAAA-MM-DD` (`dateStrings`), «hoy» siempre en Madrid (un helper en el frontend, `getTodayMadrid` en el backend) y zona @@ -159,6 +148,7 @@ preview. fecha de Aiven se ha movido ya por ediciones. Tarea propia, decidida el 2026-10-12. _Medido el 2026-10-12 contra la BD local con Node en Madrid y en UTC._ + - [ ] **Logbook: papelera e historial solo ocultos en la pantalla** — el backend da `/trashed`, `include_trashed`, el historial y los comentarios de entradas borradas a cualquier rol con acceso. _Según la revisión @@ -170,8 +160,8 @@ preview. - [ ] **Parking: `GET /vehicles` vuelca todos los vehículos** (matrícula y titular) sin paginar ni filtrar. _Según la revisión `security` L3 del 2026-09-28 (fichero y línea en el informe); no repasado por mí._ -- [ ] **Mensajería: crear una conversación no va en transacción** — el formato - y los repetidos de `participant_ids` y `user_ids` ya se validan, pero un +- [ ] **Mensajería: crear una conversación no va en transacción** — el formato y + los repetidos de `participant_ids` y `user_ids` ya se validan, pero un usuario inexistente o inactivo deja la conversación creada a medias. _Según la revisión `security` L3 del 2026-09-28 (fichero y línea en el informe); no repasado por mí._ @@ -219,15 +209,15 @@ preview. - [ ] **Groups: las mutaciones no van en transacción con su historial** — el historial se escribe en el controlador, fuera de las transacciones del repositorio: un fallo entre ambos deja cambios sin registrar. El borrado - del grupo ya va junto a su fila de historial (2026-10-11). _Comprobado - el 2026-10-11._ -- [ ] **Groups: el historial registra campos que no cambiaron** — editar solo - el importe de un grupo dejó 7 filas `updated` (`arrival_date`, - `created_at`, `updated_by`…). La comparación de `logGroupUpdated` trata - como distintos valores iguales (fechas, nulos). _Visto el 2026-10-11._ + del grupo ya va junto a su fila de historial (2026-10-11). _Comprobado el + 2026-10-11._ +- [ ] **Groups: el historial registra campos que no cambiaron** — editar solo el + importe de un grupo dejó 7 filas `updated` (`arrival_date`, `created_at`, + `updated_by`…). La comparación de `logGroupUpdated` trata como distintos + valores iguales (fechas, nulos). _Visto el 2026-10-11._ - [ ] **Checklist: el reinicio manual no funciona** — `resetRun` cierra el run y crea otro del mismo día, pero la clave única - `uk_run (hotel_id, checklist_id, hotel_date)` lo impide: 500. Después + `uk_run (hotel_id, checklist_id, hotel_date)` lo impide: 500. Después `getOrCreateRun` devuelve el run cerrado y los cambios del día se escriben en él. Sin transacción ni test. En Aiven nadie lo ha usado (0 cierres manuales) y la pantalla no lo llama: decidido el 2026-10-11 dejar el @@ -239,18 +229,36 @@ preview. van en transacción (2026-10-11). _Según la revisión `security` L3 del 2026-09-28; no repasado._ +- [ ] **Scheduling: las reglas H2, H4, H5 y libres están duplicadas y no + coinciden** — `validate()` solo registra `CoverageConstraint` y + `EmployeeRulesConstraint`; H2, H4, H5 y los libres se aplican en línea en + `runFinalValidation()`. Las clases `night-block`, `max-consecutive-work`, + `consecutive-rest` y `monthly-libre` solo las usa + `tests/scheduling/constraints.test.ts` y se comportan distinto (H4 suma la + racha del mes anterior; H5 es error, en línea es aviso). + `docs/scheduling/constraints.md` y los `@emitter` de `soft-weights.ts` + apuntan a esas clases. Decidir cuál es la buena, dejar una sola y alinear + la especificación. _Comprobado por mí el 2026-10-13._ +- [ ] **Scheduling: tras generar, las celdas de peticiones aprobadas y de + `fixedDays` quedan editables** — `applyGeneratedSchedule()` las inserta + desde `generatedRows()` sin `source_constraint_id`, así que no salen + bloqueadas en la parrilla ni devuelven 409 al editarlas. Decidir si es lo + querido o si deben bloquearse como las de restricciones aprobadas. + _Comprobado por mí el 2026-10-13 leyendo el código._ + ### 🟢 Baja -- [ ] **Artículo en el blog del portfolio sobre el motor de horarios** — cómo - se modelaron los cuadrantes de recepción con OR-Tools (CP-SAT): reglas - duras y blandas, bloques de noches, continuidad entre meses y el daemon - de Python. Enlazar la demo y el repo. Es el gancho para promocionar el +- [ ] **Artículo en el blog del portfolio sobre el motor de horarios** — cómo se + modelaron los cuadrantes de recepción con OR-Tools (CP-SAT): reglas duras + y blandas, bloques de noches, continuidad entre meses y el daemon de + Python. Enlazar la demo y el repo. Es el gancho para promocionar el proyecto. Se escribe en el repo `portfolio-bp`, no aquí. _Decidido el 2026-10-05._ - [ ] **`20260512_disable_demo_user.sql` desactivaría la cuenta demo** si se - volviera a ejecutar (`UPDATE users SET is_active = 0 WHERE username = - 'demo'`). `setup:local` no la ejecuta (es anterior al baseline); no - aplicarla a mano. _Comprobado el 2026-10-05._ + volviera a ejecutar + (`UPDATE users SET is_active = 0 WHERE username = 'demo'`). `setup:local` + no la ejecuta (es anterior al baseline); no aplicarla a mano. _Comprobado + el 2026-10-05._ - [ ] **Errores que aún llegan sin traducir** — los códigos del backend ya se traducen en el cliente (`app/lib/helpers/errorCodeMessage.ts`; un test exige texto en es y en para cada `ERROR_CODES.X` y cada `code: 'X'`). @@ -258,14 +266,14 @@ preview. (`app/dashboard/*/actions/`, `app/lib/parking/actions.ts`) lanzan `errorData.error`, que es el código en crudo, y en el servidor no hay `document` para leer el idioma; unas 380 respuestas 4xx/5xx del backend - llevan un texto fijo (casi siempre en español) y ningún `code`, así que - en inglés se ven en español: scheduling ~135, maintenance ~56, parking - ~59 (`parking.controller.ts`, `stats.controller.ts` y los 400 de - check-out, cancelar y no-show), middlewares ~39 (`roleCheck.ts`, - `rateLimiter.ts`…), cashier ~39, checklist ~20, auth ~13 y algunas de - fnb, demo y search; el 409 del reinicio de la demo y el subtítulo del - login (`login/page.tsx`) también van fijos en español. Ir módulo a - módulo (B5.n). _Medido el 2026-10-12._ + llevan un texto fijo (casi siempre en español) y ningún `code`, así que en + inglés se ven en español: scheduling ~135, maintenance ~56, parking ~59 + (`parking.controller.ts`, `stats.controller.ts` y los 400 de check-out, + cancelar y no-show), middlewares ~39 (`roleCheck.ts`, `rateLimiter.ts`…), + cashier ~39, checklist ~20, auth ~13 y algunas de fnb, demo y search; el + 409 del reinicio de la demo y el subtítulo del login (`login/page.tsx`) + también van fijos en español. Ir módulo a módulo (B5.n). _Medido el + 2026-10-12._ - [ ] **Scheduling: vista de solo lectura para los demás roles** — hoy `/dashboard/scheduling` es solo de `admin` y el resto no ve el cuadrante. El dueño quiere que los demás (salvo `mantenimiento`) vean el cuadrante @@ -277,14 +285,14 @@ preview. pestaña muestra las rechazadas. Una factura incorrecta hoy se borra. El dueño decidió el 2026-10-04 no añadirlo porque casi nunca pasa; si se añade, hace falta también reabrir (`rejected` → `pending`). -- [ ] **Conciliation: el cierre mensual no tiene pantalla ni congela los - días** — `PATCH /monthly-summary/:year/:month/status` solo existe en la - API (`useUpdateMonthlySummaryStatus` no lo usa ningún componente), y con - el mes cerrado un admin puede reabrir o borrar cualquiera de sus días. La - comprobación de que todos los días están cerrados va sin bloqueo. El - dueño decidió el 2026-10-11 dejarlo así; si se le da pantalla, cerrar el - mes debe bloquear sus días y comprobar con el mes bloqueado. _Comprobado - el 2026-10-11._ +- [ ] **Conciliation: el cierre mensual no tiene pantalla ni congela los días** + — `PATCH /monthly-summary/:year/:month/status` solo existe en la API + (`useUpdateMonthlySummaryStatus` no lo usa ningún componente), y con el + mes cerrado un admin puede reabrir o borrar cualquiera de sus días. La + comprobación de que todos los días están cerrados va sin bloqueo. El dueño + decidió el 2026-10-11 dejarlo así; si se le da pantalla, cerrar el mes + debe bloquear sus días y comprobar con el mes bloqueado. _Comprobado el + 2026-10-11._ - [ ] **Maintenance: comprobar si Cloudinary quita los metadatos EXIF/GPS** de las fotos. _Sin comprobar._ - [ ] **Parking: salto de precio a partir de 31 días** — las tarifas llegan a 30 @@ -311,9 +319,27 @@ preview. solver y retocar pesos cada 2-3 meses; método en `decisions.md`, entrada 2026-05-09. - [ ] **Checklist: contenido que falta** (lo escribe el manager, no desarrollo) - — `tasks/housekeeping-daily.json` y las guías `fidelizacion-postcheckin.md` y - `balancing-opera.md` en `frontend/content/checklist/`. + — `tasks/housekeeping-daily.json` y las guías + `fidelizacion-postcheckin.md` y `balancing-opera.md` en + `frontend/content/checklist/`. - [ ] **Seguridad cuando haya datos reales** (ideas del plan antiguo, no fallos) — registro de auditoría de seguridad, 2FA opcional para `admin`, log estructurado de los eventos de login y errores genéricos que no dejen averiguar si un usuario existe. +- [ ] **Subir el backend a Node 24** — producción va en 22 (Render, 22.23.3) y + el frontend ya en 24 (Vercel). Desde f934b4d Render lee `backend/.nvmrc`; + el `.nvmrc` raíz ya no existe (ADR-044). Cambiar `backend/.nvmrc` y + `engines` a `24.x`, y probarlo antes en preview (pdf-parse v1, solver, + cron). _Comprobado en los logs de Render y Vercel el 2026-10-12, y en el + deploy preview de 72b5d3c el 2026-10-13._ +- [ ] **Parking: `SpotType` no coincide con la BD** — en + `backend/models/parking/index.ts` es + `standard | large | handicapped | electric`; el `ENUM` de + `parking_spots.spot_type` es + `normal | ancha | mas_ancha | esquina | accesible | estrecha_bicis`. El + tipo no protege nada hoy. _Comprobado por mí el 2026-10-13._ +- [ ] **Solver: el venv local no coincide con producción** — en el equipo + principal corre Python 3.13.5 y `.python-version` fija 3.11, y no tiene + `pytest`, así que la suite de Python no se puede lanzar. Recrearlo con + 3.11 e instalar `requirements.txt` + `pytest` (lo hace el dueño). + _Comprobado el 2026-10-13._ diff --git a/docs/cashier/README.md b/docs/cashier/README.md index a03380b3..8c8474ba 100644 --- a/docs/cashier/README.md +++ b/docs/cashier/README.md @@ -19,8 +19,7 @@ Every role **except `mantenimiento`**: - **Everyone with access** sees days, shifts and vouchers. - **`admin`, `recepcionista` and `group-admin`** start a day, fill in and close - shifts, record payments and counts, create vouchers and close the - day. + shifts, record payments and counts, create vouchers and close the day. - **Only `admin`** reopens a shift or a day, cancels or deletes a voucher, deletes a shift and changes who worked a shift. - **Reports, history and statistics** are for `admin`. diff --git a/docs/checklist/README.md b/docs/checklist/README.md index 139b8552..550c077c 100644 --- a/docs/checklist/README.md +++ b/docs/checklist/README.md @@ -15,9 +15,9 @@ away. Every morning the lists start fresh. ## Who uses it Every role **except `mantenimiento`**. Anyone with access ticks and unticks -steps and adds comments or images. Restarting a checklist by hand is for -`admin` and `recepcionista`. A comment or image can be deleted by -its author or by an `admin`. +steps and adds comments or images. Restarting a checklist by hand is for `admin` +and `recepcionista`. A comment or image can be deleted by its author or by an +`admin`. ## What it can do diff --git a/docs/conciliation/README.md b/docs/conciliation/README.md index b7043115..633de667 100644 --- a/docs/conciliation/README.md +++ b/docs/conciliation/README.md @@ -37,8 +37,8 @@ reconciliation or closes a month. — it locks the day. `confirmed` behaves the same as `draft`. - **Monthly summary**: totals for the month, which days are missing, and whether the month can be closed. -- **Close the month**, only once every day of the month exists and is closed. - It exists only in the API (`PATCH /monthly-summary/:year/:month/status`): no +- **Close the month**, only once every day of the month exists and is closed. It + exists only in the API (`PATCH /monthly-summary/:year/:month/status`): no screen calls it. ## What data it handles @@ -66,11 +66,11 @@ Four tables (`backend/db-mysql/aiven/09_conciliation.sql`): except by an `admin`. `confirmed` is not a locking state. - **A month closes only when every one of its days exists and is closed.** Missing or still-open days block it and are listed by day number. A closed - month does not lock its days: an `admin` can still reopen or delete them - (left as is on 2026-10-11, see `docs/TODO.md`). + month does not lock its days: an `admin` can still reopen or delete them (left + as is on 2026-10-11, see `docs/TODO.md`). - **Deleting a day's reconciliation is only for `admin`.** -- **`GET /api/conciliations` returns every day, without paging or filters**, - to any role with access. No screen uses it; left as is on 2026-10-12. +- **`GET /api/conciliations` returns every day, without paging or filters**, to + any role with access. No screen uses it; left as is on 2026-10-12. ## How information flows diff --git a/docs/fnb/README.md b/docs/fnb/README.md index e2e9d82f..6e072d09 100644 --- a/docs/fnb/README.md +++ b/docs/fnb/README.md @@ -21,9 +21,9 @@ phase (product inventory, supplier orders) that has not started. ## Who uses it Every role **except `mantenimiento`**: `admin`, `recepcionista` and -`group-admin`. There are no admin-only actions — any of these roles can -upload a PDF, edit a day by hand, or delete a day's revenue, of any date. This -is deliberate for now (ADR-032). +`group-admin`. There are no admin-only actions — any of these roles can upload a +PDF, edit a day by hand, or delete a day's revenue, of any date. This is +deliberate for now (ADR-032). ## What it can do diff --git a/docs/general/README.md b/docs/general/README.md index 73bf4ead..4c7d851f 100644 --- a/docs/general/README.md +++ b/docs/general/README.md @@ -100,9 +100,9 @@ if it does not match. conversations or messages they are not in (`admin_override`). - Passwords and tokens are hidden at the first three levels of each entry, and so is the SQL of a failed query (`err.sql`), which carries its values. -- Failed logins keep the username that was typed (decided on 2026-10-12, left - as is): it is what reveals a brute-force attack, although someone may type - their password in that field. +- Failed logins keep the username that was typed (decided on 2026-10-12, left as + is): it is what reveals a brute-force attack, although someone may type their + password in that field. **Database** @@ -208,7 +208,8 @@ like this is in ADR-030. `SameSite=Lax` cookies are not sent and the CORS list rejects it. Under `four-points.stackbp.es` both work without touching code. -**Test users** (created on 2026-10-04 to verify the pre-release fixes role by role) +**Test users** (created on 2026-10-04 to verify the pre-release fixes role by +role) | User | Role | Email | | ----------- | --------------- | ---------------------- | diff --git a/docs/general/database/README.md b/docs/general/database/README.md index 43d44687..568cb879 100644 --- a/docs/general/database/README.md +++ b/docs/general/database/README.md @@ -58,8 +58,8 @@ Tables by module: another one is written that fixes it. - **`aiven/` is the frozen base schema** as of 2026-05-20. The name is misleading: it is not "the Aiven database", but the initial installation that - runs `MASTER_INSTALL.sql`. It is never run against a database with data. - Two of its files fail on an empty server (a duplicate foreign key name in + runs `MASTER_INSTALL.sql`. It is never run against a database with data. Two + of its files fail on an empty server (a duplicate foreign key name in `11_cashier.sql`, a foreign key to a later table in `19_scheduling.sql`): `pnpm setup:local` fixes both in memory, and `20261005_complete_fresh_install.sql` adds what an install made by hand @@ -70,13 +70,12 @@ Tables by module: `NOW()` or `CURDATE()` runs 1–2 hours behind Madrid; the backend calculates "today" in Madrid time on its own. - **Encrypted connection**: TLS with the Aiven certificate in - `backend/config/certs/`. It is the project's public CA certificate (no - private key) and names the Aiven project by its id; it stays in the - repository because the backend and `apply-migration.sh` need it (decided on - 2026-10-12). + `backend/config/certs/`. It is the project's public CA certificate (no private + key) and names the Aiven project by its id; it stays in the repository because + the backend and `apply-migration.sh` need it (decided on 2026-10-12). -**Migration status**: the 26 recorded in `INDEX.md` are applied on Aiven -(the last two, `20261011_keep_group_history_on_delete.sql` and +**Migration status**: the 26 recorded in `INDEX.md` are applied on Aiven (the +last two, `20261011_keep_group_history_on_delete.sql` and `20261012_unique_invoice_supplier_number.sql`, applied and verified on 2026-10-12). @@ -101,9 +100,8 @@ backend (repositories) ─► mysql2 pool ─► TLS ─► MySQL on Aiven (hote verification) and a copy of the Aiven certificate. - **`scripts/`**: the incremental migrations and utilities: applying a migration, local backup, local database recreation and collation check. All - read the credentials from `backend/.env`. The one-off data fixes and the - quick query scripts were removed on 2026-10-04 (they carried the Aiven - password). + read the credentials from `backend/.env`. The one-off data fixes and the quick + query scripts were removed on 2026-10-04 (they carried the Aiven password). - **`backup/`**: database dumps, never committed. - **Four documents** that overlap: `CLAUDE.md`, `README.md`, `INDEX.md` and `MIGRATIONS_POLICY.md`. Several data they give are outdated. diff --git a/docs/groups/README.md b/docs/groups/README.md index adf5799e..80bc8369 100644 --- a/docs/groups/README.md +++ b/docs/groups/README.md @@ -18,8 +18,8 @@ lines in parallel, and sends in-app reminders before the important dates. (the latter needs it to open group notifications from the profile). `mantenimiento` does not see the contacts (email and phone), neither in their tab nor in the history (decided on 2026-10-12). -- **`admin` and `group-admin`** create and edit groups, payments, - contacts, rooms and statuses, and send manual notifications. +- **`admin` and `group-admin`** create and edit groups, payments, contacts, + rooms and statuses, and send manual notifications. - **Only `admin`** deletes a group. The screens do not hide the editing controls by role: a user who cannot edit @@ -85,9 +85,9 @@ Six tables (`backend/db-mysql/aiven/10_group-tracking.sql`): - The configuration also lists reminders for unsigned contracts and pending balances, but **they are never generated**. - Duplicates are only checked within 48 hours: a payment due in 2 days gets - both the "15 days" and the "7 days" reminder every two days, and an - overdue payment is reminded every two days until it is paid; the rooming - list behaves the same. Left as is on 2026-10-12. + both the "15 days" and the "7 days" reminder every two days, and an overdue + payment is reminded every two days until it is paid; the rooming list + behaves the same. Left as is on 2026-10-12. - **No emails are sent**: the module includes an email service, but nothing calls it. All notifications are in-app. diff --git a/docs/logbook/README.md b/docs/logbook/README.md index 5b05f959..f68d604d 100644 --- a/docs/logbook/README.md +++ b/docs/logbook/README.md @@ -61,12 +61,12 @@ Departments are the application's general ones (table `departments`). - **Everything is recorded in the history**: creating, editing and deleting entries and comments, and also reading, unmarking, resolving and reopening. - Each change and its history rows are written in one transaction: either - both are saved or neither is. + Each change and its history rows are written in one transaction: either both + are saved or neither is. - **A comment leaves two history rows** (one of the comment, one of the entry), and **each read or unmark adds one**, with no limit; all of them show in the - entry's history in Reports and in the recent activity. Left as is (decided - on 2026-10-12). + entry's history in Reports and in the recent activity. Left as is (decided on + 2026-10-12). - **Nothing is truly deleted**: entries and comments are only marked with `deleted_at`. There is no way to restore them from the interface. - **Only the author edits or deletes** their entry or comment. diff --git a/docs/scheduling/constraints.md b/docs/scheduling/constraints.md index 85bdfa64..01721d62 100644 --- a/docs/scheduling/constraints.md +++ b/docs/scheduling/constraints.md @@ -506,8 +506,8 @@ valid. boundary use the context of N-1); - **rest between shifts** (`consecutive-rest.constraint`): rest on day 1 vs the last shift of N-1; - - **rotation continuity / T→M**: `schedule-validator.ts` ("VALIDATION - 4c") compares day 1 with the last shift type of N-1. + - **rotation continuity / T→M**: `schedule-validator.ts` ("VALIDATION 4c") + compares day 1 with the last shift type of N-1. **Validator:** `PreviousMonthHistory` is wired and read by the four constraints (completed 2026-04-26). @@ -588,5 +588,5 @@ per rule in `backend/services/scheduling/constraints/` (`coverage`, `night-block`, `consecutive-rest`, `max-consecutive-work`, `monthly-libre`, `employee-rules`). `schedule-validator.ts` registers only `coverage` and `employee-rules`; the other four are exercised by -`tests/scheduling/constraints.test.ts` only (checked 2026-10-02). Soft weights in -`soft-weights.ts`. +`tests/scheduling/constraints.test.ts` only (checked 2026-10-02). Soft weights +in `soft-weights.ts`. diff --git a/docs/scheduling/decisions.md b/docs/scheduling/decisions.md index 5abb1862..1a3d535b 100644 --- a/docs/scheduling/decisions.md +++ b/docs/scheduling/decisions.md @@ -1312,27 +1312,27 @@ parallel, so generating the same month twice can give two different rosters. Measured on the local October 2026 month (10 employees, 31 days) and on the synthetic 30×31 benchmark, four runs per setting on 12 cores: -| Setting | 10 employees | 30×31 | -| --------------------------------------- | --------------------------------------------- | ----------------------------------------- | -| Current (no seed, all threads) | 4 different rosters, all OPTIMAL, ~0.8 s | different rosters, FEASIBLE at 30 s | -| Seed 0, all threads | 3 different rosters out of 4 | — | -| Seed 0, 1 thread | always the same, but no solution in 30 s | — | -| Seed 0, `interleave_search`, 8 threads | always the same, OPTIMAL, ~3.5 s | the same roster, FEASIBLE at 30 s | +| Setting | 10 employees | 30×31 | +| -------------------------------------- | ---------------------------------------- | ----------------------------------- | +| Current (no seed, all threads) | 4 different rosters, all OPTIMAL, ~0.8 s | different rosters, FEASIBLE at 30 s | +| Seed 0, all threads | 3 different rosters out of 4 | — | +| Seed 0, 1 thread | always the same, but no solution in 30 s | — | +| Seed 0, `interleave_search`, 8 threads | always the same, OPTIMAL, ~3.5 s | the same roster, FEASIBLE at 30 s | Every roster in a row had the same soft penalty: they are equally good, only -different. With `interleave_search` the result also depends on `num_workers` -(8 and 12 give different rosters), and when the 30 s limit is hit the cut -point depends on the speed of the machine, so local and production could -still differ on large months. +different. With `interleave_search` the result also depends on `num_workers` (8 +and 12 give different rosters), and when the 30 s limit is hit the cut point +depends on the speed of the machine, so local and production could still differ +on large months. **Decision.** Keep the current behaviour: no seed, all threads, a wall-clock limit. Two generations of the same month may differ; both satisfy every hard rule and have the same penalty. -**Why.** Reproducibility would cost about 4× the generation time for the -current hotel and would still not hold across machines for months that hit -the time limit. A roster is generated a few times a month and then edited by -hand; the owner chose speed over reproducibility. +**Why.** Reproducibility would cost about 4× the generation time for the current +hotel and would still not hold across machines for months that hit the time +limit. A roster is generated a few times a month and then edited by hand; the +owner chose speed over reproducibility. **If it is revisited.** Set `random_seed`, `interleave_search = True` and a fixed `num_workers` in `model.py`, and replace `max_time_in_seconds` with @@ -1343,30 +1343,29 @@ fixed `num_workers` in `model.py`, and replace `max_time_in_seconds` with **Context.** `LI` (Libre Disposición) is not in `REST_SHIFTS` of `constraints/rest.py` nor in `_SPECIAL_REST` of `constraints/libres.py`: a locked `LI` neither counts towards the two free days of any 7-day window nor -lowers the monthly minimum of `L`. Whoever has an `LI` also gets every -regular free day. +lowers the monthly minimum of `L`. Whoever has an `LI` also gets every regular +free day. -**Decision.** Keep it. It matches the catalogue (`README.md`: "extra free -day"; `services/scheduling/AGENTS.md`: "outside the weekly rotation"). +**Decision.** Keep it. It matches the catalogue (`README.md`: "extra free day"; +`services/scheduling/AGENTS.md`: "outside the weekly rotation"). -**Why.** The owner's priority is a solver that keeps working: counting `LI` -as rest would change two hard rules and the rosters of every month with an -`LI`, with no reported problem behind it. +**Why.** The owner's priority is a solver that keeps working: counting `LI` as +rest would change two hard rules and the rosters of every month with an `LI`, +with no reported problem behind it. ## 2026-10-12 — Solver daemon: startup limit and queue left as they are **Context.** `services/scheduling/solver-client.ts` takes the semaphore before -waiting for the daemon, waits up to 30 minutes for it to start on every -platform (the limit was set for Windows with Defender) and queues requests -without a limit. +waiting for the daemon, waits up to 30 minutes for it to start on every platform +(the limit was set for Windows with Defender) and queues requests without a +limit. **Decision.** Keep it. -**Why.** The risk is low: only `admin` generates, the demo account is limited -to 5 generations per IP and 30 in total per hour, the daemon starts in 1–3 s -when it is warm, and a request cancelled while it waits returns before -calling the solver. - -**If it is revisited.** A shorter startup limit outside Windows and a cap on -the queue that answers "solver busy". +**Why.** The risk is low: only `admin` generates, the demo account is limited to +5 generations per IP and 30 in total per hour, the daemon starts in 1–3 s when +it is warm, and a request cancelled while it waits returns before calling the +solver. +**If it is revisited.** A shorter startup limit outside Windows and a cap on the +queue that answers "solver busy". diff --git a/frontend/.nvmrc b/frontend/.nvmrc new file mode 100644 index 00000000..a45fd52c --- /dev/null +++ b/frontend/.nvmrc @@ -0,0 +1 @@ +24 diff --git a/frontend/.prettierignore b/frontend/.prettierignore index 015aa270..28423d9b 100644 --- a/frontend/.prettierignore +++ b/frontend/.prettierignore @@ -2,6 +2,7 @@ node_modules .next dist *.log +pnpm-lock.yaml # Checklist guides and references: Markdown rendered by the app; keep their # line layout until they are edited on purpose. content/checklist/**/*.md diff --git a/frontend/.prettierrc b/frontend/.prettierrc index 0b85faf5..4881fd61 100644 --- a/frontend/.prettierrc +++ b/frontend/.prettierrc @@ -11,6 +11,7 @@ "options": { "printWidth": 80, "proseWrap": "always", + "embeddedLanguageFormatting": "off", "tabWidth": 2 } } diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md new file mode 100644 index 00000000..aac867bd --- /dev/null +++ b/frontend/AGENTS.md @@ -0,0 +1,339 @@ +# frontend + + + +## Verification + +1. **Verify before asserting, and with whatever consumes it.** Never take a + file's word about the environment: **run the tool that reads it and trigger + what it controls.** What cannot be checked stays `❓ pending`, said out loud. + + _Asking is not enough: `pnpm config get` answered `42` for a key that does + not exist, and `true` for one that changed nothing._ + +2. **"Done" means verified by CI, not asserted.** What has not passed the gate + stays pending, and that is said. + + _A green on your machine only proves your machine._ + +## What the AI does not decide + +3. **`.env`, credentials and real data dumps: neither read without explicit + permission, nor committed.** What the agent reads **enters the + conversation**, and from there it does not come back. `.env.example` is what + travels. + +4. **The AI installs and configures nothing on its own**: it proposes, the owner + decides. + + _An install or a config change lands in everyone's lockfile and CI, and + neither shows in the diff the agent presents._ + +5. **Nothing destructive without a `--dry-run` first**, showing what would be + deleted. + + _A `git clean -fd` took three unrecoverable directories._ + +6. **The AI commits; the person decides the push.** Local commits pile up until + the go-ahead, **and one go-ahead covers one push**, not the next ones. + +7. **No AI trace in commits or output**: no `Co-Authored-By`, no "Generated + with…". + + _Tools add it by default. History says what changed and why, not with which + tool._ + +8. **Conventional Commits, in English**: `type(scope): summary`, with `!` for a + breaking change. The types: `feat` · `fix` · `docs` · `style` · `refactor` · + `perf` · `test` · `build` · `ci` · `chore` · `revert`. The body carries + **only** the list of changes and the reason for each — the how is already in + the diff. + +## The environment + +9. **An agent session has no Node auto-switch.** The hook lives in the shell + profile and a non-interactive session never loads it: the session runs on + whatever Node it **inherited when it was launched**, and `cd` into another + project does not change it. + + **Start the session from a terminal already on the repo's version** — enter + the directory, let fnm/nvm switch, then launch the agent. **One session per + project.** + + _If that was not done, activate it on every invocation: `fnm use ` + then the command. `fnm exec --using=N pnpm` does not work on Windows — `pnpm` + is a `.cmd` and `fnm exec` only launches real executables._ + +10. **pnpm, never npm.** + +_`npm install` leaves a `package-lock.json` next to `pnpm-lock.yaml`, and two +installers disagree in silence._ + +11. **pnpm settings live in `pnpm-workspace.yaml`**; `.npmrc` carries registry + and authentication only — from pnpm 11 anything else there is ignored, and + pnpm 10 still reads it, so it works today and stops on upgrade. Every repo + with dependencies sets `minimumReleaseAge` there. + +## Continuity + +12. **State lives in `docs/SESSION.md`**, written **when a task closes** — at + session close there is no context left — and **overwritten**: git keeps the + history. Nothing the code or `git log` already tells goes in. + +13. **The reasoning lives in `docs/DECISIONS.md`**, append-only. A decision that + changes **is not edited**: another is appended revising it, and the old one + is **marked, never deleted**. Knowing what was believed is part of the + record. + +14. **An exception is declared** in the project's `AGENTS.md`, with its reason + and its scope; failing silently is not an option. Does not apply to + security, AI traces or pushing. + +## Naming + +15. **File names, configuration keys and identifiers in English.** + +## Knowledge that is not in this file + +16. **Skills are per project.** `npx autoskills` installs them into + `/.claude/skills/`, which is **git-ignored and rebuilt** like + `node_modules`; **`skills-lock.json` is committed** — it carries each + skill's source and hash, and it is what makes three machines rebuild the + same set. **The developer runs `autoskills`, not the agent** — `npx` + downloads a package, and installing is not the agent's call. + +_Global skills in `~/.claude/skills/` are the machine's, not the project's._ + +17. **Official docs and MCPs are consulted and never silenced**, at the + **installed** version — from the lockfile, not the `^` range. **Context7 is + the one every project uses**: `resolve-library-id`, then `query-docs` at + that version. If the MCP does not answer, say so instead of pretending it + was consulted. + +## Review + +18. **When work is reviewed, this is the shape.** `architect` before building; + `reviewer` and `security` over what was built, **read-only**; `tester` to + verify. Run them whenever you want, together or on their own. + + + +`frontend/` is one of two independent pnpm projects inside the `Four-Points` +repo (the other is `backend/`). The root [`AGENTS.md`](../AGENTS.md) holds what +both share — Context7 policy, Markdown formatting, commits and hooks, the +security level, the frontend ↔ backend contract and the module map — and is +loaded through the root `CLAUDE.md`. + +This file also covers the stack, commands, architecture and module docs of +`frontend/`. Paths are relative to `frontend/` unless they start with +`backend/`. + +## Exceptions + +- **Rules 12 and 13 — state and decisions live at the repo root.** They are + `docs/SESSION.md` and `docs/DECISIONS.md` in the root, shared with `backend/`. + `frontend/docs/` is git-ignored (`frontend/.gitignore:85`) and is not used for + them. +- **Rule 9 — the session starts at the repo root, not here.** One session works + on both subprojects, and they run different Node majors (24 here, 22 in + `backend/`), so no inherited Node fits both and the root has no `.nvmrc`. + Every Node or `pnpm` command activates this project's version first, from its + `.nvmrc`: `eval "$(fnm env --shell bash)" && fnm use && pnpm …` in Bash, or + `fnm env | Out-String | Invoke-Expression; fnm use` in PowerShell, both run + inside `frontend/`. + +## Security + +**L3** — estimated on 2026-09-28: there is login and user accounts, personal +data (users, guest blacklist with photos, cashier) and own backend; the L3 level +is declared by the owner as the target (`docs/DECISIONS.md` ADR-017), not the +current state. **Re-estimate when:** Aiven starts storing real guest or staff +data. + +The level line is the same in the root `AGENTS.md` and in `backend/AGENTS.md`; a +re-estimate changes all three. Paths below are relative to `frontend/`. + +**Areas:** + +- Authentication, Session → `app/lib/auth/` (`useAuth.tsx`, `authService.ts`, + `passwordPolicy.ts`), `app/(auth)/login/`, `proxy.ts` (route protection; reads + the JWT `exp` without verifying it), `app/dashboard/layout.tsx` (redirect) +- Authorization → UI gating only: `app/ui/dashboard/nav-links.tsx`, + `app/ui/dashboard/sidenav.tsx`, `app/dashboard/scheduling/layout.tsx`; + enforcement lives in the backend +- API → `app/lib/apiClient.ts`, `app/lib/serverFetch.ts`, per-module calls in + `app/lib//`, server actions in `app/dashboard/blacklist/actions/` +- File upload → client side, under `app/components/`: + `blacklist/ui/ImageUploader.tsx`, `bo/modals/PdfUploadModal.tsx`, the + maintenance and groups panels, `restaurant/tabs/DailyRevenueTab.tsx` +- Data protection / privacy → blacklist, cashier, users + (`app/components/auth/NewUserModal.tsx`, profile) and messaging + (`app/components/profile/MessagesPanel.tsx`, `app/lib/messaging/`) +- Frontend → `next.config.ts` (CSP and security headers), `react-markdown` in + `app/components/checklist/` over `content/checklist/`, + `app/components/blacklist/ui/Highlight.tsx` +- Configuration / DevOps → `next.config.ts`, `vercel.json`, `app/lib/env.ts`, + `.env*` + +## Where the model goes wrong + +- **A prompt that asks for several tasks at once** → work them **one at a + time**, each with the context specific to it, when the work touches code. + +## Stack + +- **Next.js 16** (App Router, Turbopack in dev) with **React 19**, on Node 24 + (`.nvmrc`, `engines: 24.x`), pnpm 10.28.0. TypeScript 5.7.3. +- **Styling**: Tailwind CSS **v3**; UI from NextUI (`@nextui-org`), Headless UI, + Heroicons and `react-icons`. +- **State**: Zustand stores in `app/stores/`; server data through React Query. +- **Forms**: React Hook Form + Zod **v3**. +- **i18n**: `next-intl`. **Theme**: `next-themes`. PDFs with `pdf-lib` and + `pdfjs-dist`, charts with `recharts`, spreadsheets with `xlsx`. +- No auth library: `next-auth`, `bcrypt`, `mysql2`, `postgres` and `uuid` were + removed on 2026-10-02 as unused. + +## Installed Versions + +From `pnpm-lock.yaml`, checked on 2026-10-13. Query Context7 at these versions +(root `AGENTS.md` § Up-to-date Documentation). + +- **`next`** 16.0.8 — App Router + Turbopack. React 19.1.1 +- **`@tanstack/react-query`** 5.90.11 — v5: `isPending`, single object in + `useQuery({...})` +- **`zod`** 3.25.76 — ⚠️ v3, a different API from the v4 in `backend/`. The + range is `^3.25.17`: check the lockfile +- **`next-intl`** 4.6.1 — v4 (see § i18n below) +- **`tailwindcss`** 3.4.17 — **v3, not v4**: no `@import "tailwindcss"` or + `@theme` +- **`zustand`** 5.0.8 — v5 without default selectors +- **`react-hook-form`** 7.66.0 — with `@hookform/resolvers` 5.2.2 (breaking vs + 3.x) +- **`react-day-picker`** 9.11.1 — v9 renamed props and CSS vars +- **`framer-motion`** 12.23.22 — v12 still publishes as `framer-motion`, but the + current upstream package is `motion`. Import from `framer-motion`, **never** + from `motion/react` +- **`date-fns`** 4.1.0 — v4 adds the timezone system, changes imports from v2/v3 +- **`@nextui-org/react`** 2.6.11 — package `@nextui-org`, **not** the `@heroui` + rebrand +- **`recharts`** 3.5.1 — v3 changed types and some defaults from v2 +- **`pdf-lib`** 1.17.1 — verify font/image embedding before use +- **`pdfjs-dist`** 5.4.449 — worker configuration differs by major; + bundler-sensitive +- **`xlsx`** 0.18.5 — outside the public npm registry; do not assume a newer API +- **`typescript`** 5.7.3 — `backend/` runs 5.9.3 + +## Commands + +```bash +pnpm dev # Next.js dev server with Turbopack (port 3000) +pnpm build # production bundle +pnpm start # production server +pnpm lint # ESLint +pnpm typecheck # tsc --noEmit +pnpm format # Prettier (format:check to only check) +pnpm verify # lint + format:check + typecheck — what CI runs +``` + +There is no test suite in `frontend/`. + +## Architecture + +``` +app/ +├── (auth)/ # login +├── dashboard/ # protected routes, one folder per module +├── components/ # UI per module (components//) +├── lib/ # apiClient, serverFetch, env, auth/, and lib// +├── stores/ # Zustand stores +├── i18n/ # next-intl config and locale resolution +└── ui/ # design system: dashboard shell, fonts, tokens, global.css +messages// # translations, one JSON per namespace +content/checklist/ # checklist guides rendered by the app +proxy.ts # route protection +``` + +- **Everything lives under `app/`** — there is **no `frontend/lib/`**; the + utilities directory is `app/lib/`. +- **Server components by default.** `'use client'` only when the component has + interactivity, uses React hooks or browser APIs. Never `useState` in a server + component, never an `async` client component. +- **Data fetching**: client components use React Query with + `app/lib/apiClient.ts`; server components use `app/lib/serverFetch.ts`. A few + modules also have server actions (`app/dashboard//actions/`, + `app/lib/parking/actions.ts`). +- **Every call carries `credentials: 'include'`** — `apiClient` adds it, and + retries once through `/api/auth/refresh-token` on a 401. +- **Environment**: all API URLs come from `app/lib/env.ts` — + `NEXT_PUBLIC_API_URL` (browser), `NEXT_SERVER_API_URL` (server, falls back to + the public one) and `NEXT_PUBLIC_DEMO_MODE`. Only `NEXT_PUBLIC_*` reaches the + browser. +- **Forms**: React Hook Form + Zod; schemas live with their module + (`app/lib//`, plus `app/lib/schemas/` for groups). +- **Theme**: `next-themes` (`app/lib/theme/ThemeProvider.tsx`) toggles the + `.dark` class on ``. +- **Fonts**: configured in `app/ui/fonts-design/`; the active set is + `ACTIVE_FONTS` in `fonts.helper.ts`, applied with `font-sans` and + `font-display`. + +### i18n + +`next-intl` v4 with locales `es` and `en`. The locale is resolved in +`app/i18n/request.ts`: cookie `NEXT_LOCALE`, then Vercel's +`x-vercel-ip-country`, then `Accept-Language`, default `es`. No `routing.ts` and +no locale in the URL. Messages in `messages//.json`. + +## Authentication and Route Protection + +The contract with the backend is in the root `AGENTS.md`; here, the frontend +side. + +- **`app/lib/auth/useAuth.tsx`** — the `AuthProvider` calls `GET /api/auth/me` + on mount; `useAuth()` (alias `useAuthContext`) gives `user`, `login`, `logout` + and `isAuthenticated`. +- **`proxy.ts`** redirects by the presence and `exp` of the JWT cookie, + **without verifying it**; `app/dashboard/layout.tsx` redirects when + `useAuth()` has no user. Real enforcement is the backend's. +- ⚠️ **There is no dev-mode auth bypass.** Older docs mentioned a `DEV_MODE` + flag and a `DashboardLayout` component: neither exists. Do not write code that + assumes auth can be skipped locally. +- Role checks in the UI only hide navigation and screens. + +## Messaging + +No module doc yet. ⚠️ **There is no `app/dashboard/messages/` route**: the UI is +a panel inside the profile page, `app/components/profile/MessagesPanel.tsx`, +with hooks and queries in `app/lib/messaging/`. The backend side is in +`backend/AGENTS.md` § Messaging. + +## Module Docs + +Read the module's `AGENTS.md` before touching it. Where the backend has its own +doc, it is listed after the `·`. + +- **Scheduling** — + [`app/components/scheduling/`](app/components/scheduling/AGENTS.md) · backend: + `backend/services/scheduling/`, solver: `backend/scheduling-solver/` +- **Parking** (the responsive reference) — + [`app/dashboard/parking/`](app/dashboard/parking/AGENTS.md) · backend: + `backend/services/parking/` +- **Cashier** — [`app/components/cashier/`](app/components/cashier/AGENTS.md) · + backend: `backend/services/cashier/` +- **Group Tracking** — + [`app/components/groups/`](app/components/groups/AGENTS.md) · backend: + `backend/services/group/` +- **Maintenance** (both sides) — + [`app/components/maintenance/`](app/components/maintenance/AGENTS.md) + +These modules keep a single doc in `backend/` that covers their UI too: + +- **Checklist** (UI in `app/components/checklist/`, guides in + `content/checklist/`) — `backend/services/checklist/AGENTS.md` +- **Logbook** (UI in `app/components/logbooks/`, route `app/dashboard/logbooks/` + — plural here, singular in the backend) — `backend/services/logbook/AGENTS.md` +- **F&B / Restaurant** (UI in `app/components/restaurant/`) — + `backend/services/fnb/AGENTS.md` +- **Backoffice** (UI in `app/components/bo/`) — + `backend/services/backoffice/AGENTS.md` +- **Blacklist** (UI in `app/components/blacklist/`) — + `backend/services/blacklist/AGENTS.md` diff --git a/frontend/CLAUDE.md b/frontend/CLAUDE.md new file mode 100644 index 00000000..43c994c2 --- /dev/null +++ b/frontend/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/frontend/README.md b/frontend/README.md index 61e9dc9b..ae2cadd8 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -16,10 +16,10 @@ install and run the whole project, see the [root README](../README.md). ## Commands -| Command | Does | -| ---------------------------------------------------- | ----------------------------------------------------- | -| `pnpm dev` | http://localhost:3000, API from `NEXT_PUBLIC_API_URL` | -| `pnpm build` · `pnpm start` | Production build | -| `pnpm typecheck` · `pnpm lint` · `pnpm format:check` | What CI runs | +| Command | Does | +| ----------------------------------- | ----------------------------------------------------- | +| `pnpm run dev` | http://localhost:3000, API from `NEXT_PUBLIC_API_URL` | +| `pnpm run build` · `pnpm run start` | Production build | +| `pnpm run verify` | What CI runs: lint (no warnings), format check, types | The product documentation lives in [`docs/`](../docs/general/README.md). diff --git a/frontend/app/components/cashier/AGENTS.md b/frontend/app/components/cashier/AGENTS.md index f89ea530..9ebfa586 100644 --- a/frontend/app/components/cashier/AGENTS.md +++ b/frontend/app/components/cashier/AGENTS.md @@ -1,17 +1,21 @@ # AGENTS.md — Cashier (frontend) +**Up:** [`frontend/AGENTS.md`](../../../AGENTS.md) — `frontend/` rules · +[root `AGENTS.md`](../../../../AGENTS.md) — shared rules and module map. + > Frontend doc for the cashier module. Covers the route structure, Zustand > store, React Query hooks, component roles, and the PDF export. For the data > model, enums, endpoints, and role rules see -> `backend/services/cashier/CLAUDE.md`. +> `backend/services/cashier/AGENTS.md`. ## Route structure ``` app/dashboard/cashier/ -├── layout.tsx # Cashier layout (navigation between sub-pages) -├── page.tsx # Redirects to /cashier/hotel +├── page.tsx # Cashier dashboard — overview cards (useDashboardOverview) +├── error.tsx # Error boundary ├── hotel/ +│ ├── layout.tsx # Hotel view layout (date nav + day sidebar, Zustand) │ └── page.tsx # Main cashier view — day + 4 shifts ├── logs/ │ └── page.tsx # History / audit log view @@ -30,10 +34,12 @@ Key slices: - **`useActiveTab()`** (`ShiftType`): Which shift tab is open (night/morning/afternoon/closing) - **`useActiveModal()`** (`ModalType | null`): Which modal is open - (initializeDay, closeDay, reopenDay) -- **`useLogsDate()`** (range): Active date range in the logs view -- **`useReportsTab()`** (`string`): Active tab in the reports view -- **`useChartViewMode()`** (`string`): Payment chart display mode + (initializeDay, closeDay, reopenDay, closeShift) +- **`useLogsFilters()`**: Filters of the logs view (action, user, limit, offset) +- **`useReportsDate()`**: Active month in the reports view +- **`useReportsTab()`** (`summary | payments | vouchers`): Active tab in the + reports view +- **`useChartViewMode()`** (`pie | bar`): Payment chart display mode Exported selectors are fine-grained (`useSelectedDate`, `useActiveTab`, etc.) to avoid unnecessary re-renders — always use the selector, not @@ -64,13 +70,12 @@ Modals at page level (controlled by Zustand activeModal): dashboard/cashier/logs/page.tsx ├── LogsSummarySidebar (layout/) ← date range + quick stats ├── HistoryFilters ← filter by user, shift type, action -├── HistoryStats ← counts/aggregates for the filtered range └── HistoryTable ← paginated audit log (277 lines) dashboard/cashier/reports/page.tsx ├── ReportsSummarySidebar (layout/) ← month picker + totals ├── MonthlyReport ← monthly breakdown by day (301 lines) -├── PaymentChart ← bar/area chart of payments by method (278 lines) +├── PaymentChart ← pie/bar chart of payments by method (278 lines) └── VouchersHistory ← full voucher history table (271 lines) ``` @@ -88,50 +93,53 @@ All data fetching and mutations go through named hooks in - **`useReopenShift()`**: Mutation for reopening (admin) - **`useInitializeDay()`**: Mutation that creates the 4 shifts - **`useCloseDay()` / `useReopenDay()`**: Day-level close/reopen -- **`useReplaceDenominations()`**: Bulk replace denomination list (PUT) -- **`useReplacePayments()`**: Bulk replace payment list (PUT) +- **`useUpdateDenominations()`**: Bulk replace denomination list (PUT) +- **`useUpdatePayments()`**: Bulk replace payment list (PUT) +- **`useUpdateShiftUsers()`**: Replace a shift's users (admin) - **`useCreateVoucher()`**: Create voucher on a shift - **`useJustifyVoucher()`**: Mark voucher as justified -- **`useDashboardOverview()`**: Reports dashboard data +- **`useDashboardOverview()`**: Cashier dashboard data (`/dashboard/cashier`) - **`useMonthlyReport(year, month)`**: Monthly aggregate +- **`useVouchersHistory()`**, **`useHistoryLogs()`**, **`useHistoryStats()`**: + Reports and logs views +- **`useUsers()`**: User list for the shift pickers Cache keys follow the pattern in `cashierKeys`: `['cashier', 'daily', date]`, -`['cashier', 'shift', id]`, etc. Mutations invalidate the relevant keys after -success. +`['cashier', 'shift', id]`, `['cashier', 'vouchers']`… (`users` is +`['users', 'all']`). Mutations invalidate the relevant keys after success. ## PDF export — `lib/cashier/exportDailyPdf.ts` -Client-side PDF generation using `pdf-lib` (no server involved). Called from -`CloseDayModal` and `DaySummarySidebar` when the day is closed. +Client-side PDF generation using `pdf-lib` (no server involved). Called only +from `DaySummarySidebar`. -The function `exportDailyPdf(daily, shifts, labels)` takes: +The function `exportDailyToPdf(daily, selectedDate, labels, shiftType?)` takes: -- `daily` — the `CashierDaily` object (totals, status) -- `shifts` — array of `CashierShift` with their denominations, payments, - vouchers pre-loaded +- `daily` — the `CashierDaily` object, with its `shifts` +- `selectedDate` — `YYYY-MM-DD`, also used in the file name - `labels` — i18n strings injected from the caller (keeps the exporter i18n-agnostic) +- `shiftType` — the shift to export; the sidebar passes the active tab -Generates a structured PDF: header → day summary → per-shift breakdown → voucher -list. Returns a `Uint8Array` which the caller converts to a blob URL and -triggers a download. +Generates a page for that shift plus a day-summary page, then downloads it +itself (`cashier--.pdf`); it returns nothing. -**Important:** the labels object is large (`ExportLabels` type in `types.ts`). -When calling `exportDailyPdf`, make sure to pass the full labels object — -missing keys produce `undefined` text in the PDF silently. +**Important:** the labels object is large (`ExportLabels`, declared in +`exportDailyPdf.ts`). When calling it, pass the full labels object — missing +keys produce `undefined` text in the PDF silently. ## i18n Namespace: `cashier`. Used throughout with `useTranslations('cashier')`. -Dictionary at `frontend/i18n//cashier.json`. +Dictionary at `frontend/messages//cashier.json`. ## Known gotchas 1. **`ShiftCard` is 479 lines.** It owns denomination editing, payment editing, voucher listing, shift close, and shift reopen all in one component. If adding non-trivial behaviour here, consider extracting a subcomponent first. -2. **`CloseDayModal` is 348 lines** — includes the PDF export trigger and a full - day summary render. PDF labels are built inline inside the modal with +2. **`CloseDayModal` is 348 lines** — a full day summary render before closing. + The PDF labels are built inline in `DaySummarySidebar` with `useTranslations`. 3. **Zustand modal control vs local state.** Day-level modals (initializeDay, closeDay, reopenDay) are controlled via the store's `activeModal`. @@ -141,18 +149,19 @@ Dictionary at `frontend/i18n//cashier.json`. 4. **Date format.** The store and all API calls use `YYYY-MM-DD`. `CashierCalendarNav` formats dates before setting `selectedDate` via `setSelectedDate`. Do not pass `Date` objects to the store — convert first. -5. **Reports and logs are admin-gated.** The `/reports` page is only accessible - to `admin` / `demo-admin` (enforced by `canViewReports` on the backend). The - frontend doesn't gate the route itself — it relies on the API returning 403. - Add a frontend guard if needed. +5. **Reports, logs and the dashboard are admin-gated on the backend.** + `/reports`, `/logs` and the `/dashboard/cashier` overview read endpoints + behind `canViewReports` (`admin`). The frontend doesn't gate those routes — + the sidenav shows all four links and the API answers 403. Add a frontend + guard if needed. ## Cross references -- `backend/services/cashier/CLAUDE.md` — data model, enums, endpoints, role +- `backend/services/cashier/AGENTS.md` — data model, enums, endpoints, role middleware. - `frontend/app/stores/useCashierStore.ts` — full Zustand store. - `frontend/app/lib/cashier/queries.ts` — all React Query hooks. -- `frontend/app/lib/cashier/exportDailyPdf.ts` — PDF export (687 lines, the +- `frontend/app/lib/cashier/exportDailyPdf.ts` — PDF export (690 lines, the largest lib file). - `frontend/app/lib/cashier/types.ts` — TypeScript types mirroring backend models. diff --git a/frontend/app/components/cashier/CLAUDE.md b/frontend/app/components/cashier/CLAUDE.md new file mode 100644 index 00000000..43c994c2 --- /dev/null +++ b/frontend/app/components/cashier/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/frontend/app/components/cashier/reports/MonthlyReport.tsx b/frontend/app/components/cashier/reports/MonthlyReport.tsx index 4532df9a..43e96cf5 100644 --- a/frontend/app/components/cashier/reports/MonthlyReport.tsx +++ b/frontend/app/components/cashier/reports/MonthlyReport.tsx @@ -135,15 +135,13 @@ export default function MonthlyReport({ year, month }: MonthlyReportProps) { percentage: parseFloat(String(method.percentage)) || 0, }) ), - daily_breakdown: (rawReport.daily_breakdown || []).map( - (day: RawDay): ParsedDay => ({ - date: day.date, - status: day.status, - total_cash: parseFloat(String(day.total_cash)) || 0, - grand_total: parseFloat(String(day.grand_total)) || 0, - has_discrepancy: day.has_discrepancia, - }) - ), + daily_breakdown: (rawReport.daily_breakdown || []).map((day: RawDay): ParsedDay => ({ + date: day.date, + status: day.status, + total_cash: parseFloat(String(day.total_cash)) || 0, + grand_total: parseFloat(String(day.grand_total)) || 0, + has_discrepancy: day.has_discrepancia, + })), validation_errors: rawReport.validation_errors || [], } diff --git a/frontend/app/components/groups/AGENTS.md b/frontend/app/components/groups/AGENTS.md index 469bad7f..10a7c9a8 100644 --- a/frontend/app/components/groups/AGENTS.md +++ b/frontend/app/components/groups/AGENTS.md @@ -1,9 +1,12 @@ # AGENTS.md — Group Tracking (frontend) +**Up:** [`frontend/AGENTS.md`](../../../AGENTS.md) — `frontend/` rules · +[root `AGENTS.md`](../../../../AGENTS.md) — shared rules and module map. + > Frontend doc for the group tracking module. Covers the route structure, > Zustand store, tab/panel navigation pattern, component map, and React Query > hooks. For the data model, enums, endpoints, and role rules see -> `backend/services/group/CLAUDE.md`. +> `backend/services/group/AGENTS.md`. ## Route structure @@ -11,9 +14,10 @@ app/dashboard/groups/ ├── page.tsx # Groups list (GroupsListClient) ├── error.tsx / loading.tsx +├── actions/ +│ └── getGroups.ts # Server action that pre-fetches the list └── [id]/ - ├── page.tsx # Group detail (GroupDetailClient — server component wrapper) - └── actions/ # Server actions (if any) + └── page.tsx # Group detail (GroupDetailClient — server component wrapper) ``` ## State — Zustand (`useGroupStore`) @@ -25,10 +29,13 @@ Key slices: - **`currentGroup`** (`GroupWithDetails | null`): The group being viewed in the detail page +- **`payments` / `contacts` / `rooms` / `status`**: the group's sub-resources, + copied from the React Query data - **`activeTab`** (`string`): Active tab in GroupDetailClient (overview/payments/contacts/rooms/status/history) -- **`activePanelId`** (`string | null`): Which side panel is open - (payment/contact/room/editGroup) +- **`activePanelId`** (`string | null`): Which side panel is open (`'new'`, + `'edit-5'`…) +- **`highlightId`** and per-resource `isLoading*` flags **Note:** The `GroupDetailClient.tsx` uses URL query params (`?tab=...&panel=...`) as the navigation source of truth — the store's @@ -44,18 +51,18 @@ dashboard/groups/page.tsx └── CreateGroupPanel ← slide-in panel to create a new group (277 lines) dashboard/groups/[id]/page.tsx -└── GroupDetailClient ← main orchestrator for detail view (192 lines) +└── GroupDetailClient ← main orchestrator for detail view (213 lines) ├── GroupHeader (layout/) ← group name, status badge, action buttons - ├── TabNavigation (layout/) ← tab switcher (128 lines) + ├── TabNavigation (layout/) ← tab switcher; hides Contacts for roles without contact access (133 lines) ├── GroupDetailSummaryPanel (layout/) ← right sidebar: dates, pax, total, contacts (166 lines) │ ├── OverviewTab ← summary cards + quick stats (302 lines) ├── PaymentsTab ← payment list + balance bar (544 lines — largest) ├── ContactsTab ← contact list ├── RoomsTab ← room type allocations (132 lines) - ├── StatusTab ← 4 status tracks as cards - │ ├── BookingCard (245 lines) - │ ├── ContractCard (235 lines) + ├── StatusTab ← 4 status tracks as cards + StatusTimeline + │ ├── BookingCard (244 lines) + │ ├── ContractCard (234 lines) │ ├── RoomingCard (329 lines) │ └── BalanceCard (186 lines) └── HistoryTab ← audit log (uses HistoryItem — 345 lines) @@ -63,38 +70,35 @@ dashboard/groups/[id]/page.tsx Side panels (slide-in, controlled by store activePanelId): ├── PaymentPanel ← create/edit payment (379 lines) - ├── ContactPanel ← create/edit contact (178 lines) + ├── ContactPanel ← create/edit contact (172 lines) ├── RoomPanel ← create/edit room allocation (213 lines) └── EditGroupPanel ← edit group master data (304 lines) Modal: - └── NotificationModal ← create manual notification + email preview (481 lines) + └── NotificationModal ← create a manual notification, now or scheduled (481 lines) + +List rows: cards/ (PaymentCard, ContactCard, RoomCard); shared/ (badges, empty and loading states) ``` ## React Query — lib/groups/queries.ts -All data fetching and mutations go through named hooks exported from -`frontend/app/lib/groups/`. - -Key hooks: - -- **`useGroups(filters)`**: Paginated group list -- **`useGroup(id)`**: Single group with full detail (joined contacts + rooms + - payments + status) -- **`useGroupPayments(id)`**: Payment list for a group -- **`useGroupContacts(id)`**: Contact list -- **`useGroupRooms(id)`**: Room allocations -- **`useGroupStatus(id)`**: 4 status tracks -- **`useGroupHistory(id)`**: Audit history -- **`useCreateGroup()`**: Mutation — create group -- **`useUpdateGroup()`**: Mutation — update group master data -- **`useCreatePayment()`**: Mutation — add payment to group -- **`useUpdatePayment()`**: Mutation — update payment fields -- **`useUpdatePaymentStatus()`**: Mutation — change payment status -- **`useUpdateBookingStatus()`**: Mutation — update booking track - -Cache keys follow the pattern `['groups', id]`, `['groups', id, 'payments']`, -etc. +`groupsApi` (in `queries.ts`) has one method per endpoint. On top of it, the +hooks: + +- **Queries:** `useGroup(id)` (detail with contacts + rooms + payments + + status), `useGroupPayments`, `useGroupContacts`, `useGroupRooms`, + `useGroupStatus` +- **Mutations:** `useCreatePayment`, `useUpdatePayment`, `useDeletePayment`, + `useCreateContact`, `useUpdateContact`, `useCreateOrUpdateRoom`, + `useUpdateRoom` + +Not everything goes through hooks: `GroupsListClient` runs its own `useQuery` +over `groupsApi.getAll`, and creating, editing or deleting a group, the status +cards and the history tab call `groupsApi` directly (then invalidate). + +Cache keys come from `groupsKeys`: `['groups', 'list', filters]`, +`['groups', id]`, `['groups', id, 'payments']`, `contacts`, `rooms`, `status`, +plus the dashboard and upcoming-payment keys. ## Tab and panel navigation pattern @@ -118,7 +122,7 @@ user's attention to the new item. card manages one of the four status tracks independently: - **BookingCard** — `booking_confirmed`, `booking_date` → maps to `GroupStatus` - on the master record too (design debt, see backend CLAUDE.md). + on the master record too (design debt, see the backend AGENTS.md). - **ContractCard** — `contract_status` + `contract_date`, `contract_notes`. - **RoomingCard** — `rooming_status` + rooming list upload/notes. - **BalanceCard** — `balance_status` + `balance_date`, `balance_notes`. @@ -138,7 +142,7 @@ The tab also shows an overdue warning banner when any payment has status ## i18n Namespace: `groups`. Used throughout with `useTranslations('groups')`. -Dictionary at `frontend/i18n//groups.json`. +Dictionary at `frontend/messages//groups.json`. ## Known gotchas @@ -151,25 +155,24 @@ Dictionary at `frontend/i18n//groups.json`. source of truth. 3. **Status duplication in the UI.** `GroupDetailClient.tsx` has an explicit TODO comment about `hotel_groups.status` vs `group_status.booking_confirmed` - duplication. If you touch booking status mutations, you must update both - fields (the backend handles this in - `group-status-controller.ts:updateBooking`). + duplication. If you touch booking status mutations, both fields must change + (the backend repositories sync them both ways). 4. **`HistoryItem` renders old/new JSON diffs.** It parses the `old_value`/`new_value` strings from the history record. If the backend changes the JSON structure of history entries, update the renderer accordingly. -5. **`NotificationModal` is 481 lines.** It includes a live email preview. If - SMTP is not configured on the backend, the modal will show success after - saving the notification but no email will be sent. -6. **`group-admin` role.** This role sees the full group management UI. - `recepcionista` and `mantenimiento` can view groups (read-only) but the - create/edit UI should be gated. Currently the frontend does not gate these - controls — it relies on the backend returning 403. Add a frontend role check - if the UX needs to hide the controls visually. +5. **`NotificationModal` is 481 lines.** It creates an in-app notification, sent + now or scheduled (`scheduled_for`). There is no email. +6. **Roles in the UI.** Only the contacts are gated: `TabNavigation` uses + `canViewGroupContacts` (`app/lib/groups/contact-access.ts`) to leave out the + Contacts tab, and `?tab=contacts` falls back to the summary. The create/edit + controls are not gated — `recepcionista` and `mantenimiento` see them and the + backend answers 403. ⚠️ The group detail (`useGroup`) still receives the + contacts for every role: see the backend AGENTS.md. ## Cross references -- `backend/services/group/CLAUDE.md` — data model, enums, endpoints, role +- `backend/services/group/AGENTS.md` — data model, enums, endpoints, role middleware. - `frontend/app/stores/useGroupStore.ts` — Zustand store. - `frontend/app/lib/groups/queries.ts` — all React Query hooks. diff --git a/frontend/app/components/groups/CLAUDE.md b/frontend/app/components/groups/CLAUDE.md new file mode 100644 index 00000000..43c994c2 --- /dev/null +++ b/frontend/app/components/groups/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/frontend/app/components/maintenance/AGENTS.md b/frontend/app/components/maintenance/AGENTS.md index 37b900b7..49c4b4e3 100644 --- a/frontend/app/components/maintenance/AGENTS.md +++ b/frontend/app/components/maintenance/AGENTS.md @@ -1,5 +1,10 @@ # AGENTS.md — Maintenance +**Up:** [`frontend/AGENTS.md`](../../../AGENTS.md) — `frontend/` rules · +[`backend/AGENTS.md`](../../../../backend/AGENTS.md) — `backend/` rules, since +this doc covers both sides · [root `AGENTS.md`](../../../../AGENTS.md) — shared +rules and module map. + > Hotel maintenance report module. **Single file** because the module isn't big > enough to justify a backend/frontend split; the file lives in > `frontend/app/components/maintenance/` where most of the code surface area is. @@ -52,15 +57,15 @@ Validated by the frontend Zod schema (`reportSchema.refine()` in ``` backend/controllers/maintenance/ - └── maintenance-controller.ts (753 lines — class with static methods + └── maintenance-controller.ts (~740 lines — class with static methods MaintenanceController.getAll, create, etc.) backend/repositories/maintenance/ - └── maintenance-repository.ts (970 lines — all module SQL; + └── maintenance-repository.ts (~1050 lines — all module SQL; includes images and history) backend/routes/maintenance/ - └── maintenance-routes.ts (155 lines — 14 routes) + └── maintenance-routes.ts (160 lines — 16 routes) backend/validations/maintenance/ └── schemas.ts (Zod schemas: create, update, status, priority, @@ -73,7 +78,7 @@ frontend/app/dashboard/maintenance/ └── actions/getMaintenance.ts (server action for SSR) frontend/app/components/maintenance/ - ├── MaintenanceListClient.tsx (766 lines — list with filters and modals) + ├── MaintenanceListClient.tsx (~780 lines — list with filters and modals) ├── ReportDetailClient.tsx (124 lines — detail wrapper) ├── hooks/useMaintenanceList.ts (185 lines — React Query with keys factory) ├── layout/ @@ -81,9 +86,9 @@ frontend/app/components/maintenance/ │ └── TabNavigation.tsx ├── panels/ │ ├── CreateReportPanel.tsx (359 lines — create form) - │ └── EditReportPanel.tsx (454 lines — edit form) + │ └── EditReportPanel.tsx (~460 lines — edit form) ├── tabs/ - │ ├── DetailTab.tsx (634 lines — main tab of the detail view) + │ ├── DetailTab.tsx (~660 lines — main tab of the detail view) │ └── HistoryTab.tsx (166 lines — change history) └── shared/ (EmptyState, LoadingSpinner) @@ -111,6 +116,8 @@ frontend/app/lib/maintenance/ - `DELETE /api/maintenance/:id` — Soft delete - `PATCH /api/maintenance/:id/restore` — Restore - `GET /api/maintenance/:id/images` — List images +- `GET /api/maintenance/:id/images/:imageId/file` — Serve one image (private + file, through the backend) - `POST /api/maintenance/:id/images` — Upload image (multipart, 5 MB max) - `DELETE /api/maintenance/:id/images/:imageId` — Delete image - `GET /api/maintenance/:id/history` — Change history @@ -119,9 +126,13 @@ frontend/app/lib/maintenance/ `canAccessMaintenance`. This middleware **lets the mantenimiento role in** — unlike most modules that exclude it. -**Images:** multer in-memory + Cloudinary. Hard cap of 5 MB per file in the -multer middleware. Accepted types: jpeg, png, webp, gif (by client-declared -mimetype). +**Images:** `requireStorage` (answers first when Cloudinary is not configured) + +- `singleImage('image')` from `middlewares/imageUpload.ts` (multer in-memory, 5 + MB, jpeg/png/webp/gif by client-declared mimetype) + Cloudinary. Uploads are + **private** (`authenticated`) since 2026-10-04: the browser never gets a + Cloudinary URL, it asks `/:id/images/:imageId/file` and the backend fetches + and sends the file (`services/uploads/private-files.ts`). ## Class-based controller pattern @@ -147,19 +158,19 @@ Every important change is logged with one of these `action` values: Visible in `HistoryTab.tsx`. **Rule:** any mutation that touches the report's status or assignment must go through the repo via a call that also writes to `maintenance_history`. If you add a new mutation, make sure it logs; if the -`action` doesn't fit the existing set, extend the enum on the TS side and on the -DB side (CHECK constraint). **Uploading or deleting images is NOT written to -`maintenance_history`.** +`action` doesn't fit the existing set, extend the type on the TS side and the +`action` ENUM column on the DB side (with a migration). **Uploading or deleting +images is NOT written to `maintenance_history`.** ## Frontend — patterns -### `useMaintenanceList(filters, page, limit, initialData, messages)` +### `useMaintenanceList({ filters, page, limit, initialData, messages })` -List orchestrator hook. Takes `initialData` from SSR (for first paint with no -spinner) and only uses it when no filters are active — once the user filters, -the server cache is discarded and a fresh fetch runs. Same `messages` injection -pattern as logbook: i18n toasts are passed in from the container so the hook -stays i18n-agnostic. +List orchestrator hook (`page` 1 and `limit` 100 by default). Takes +`initialData` from SSR (for first paint with no spinner) and only uses it when +no filters are active — once the user filters, the server cache is discarded and +a fresh fetch runs. Same `messages` injection pattern as logbook: i18n toasts +are passed in from the container so the hook stays i18n-agnostic. ### `maintenanceKeys` factory @@ -169,6 +180,9 @@ maintenanceKeys.detail(id) // ['maintenance', 'detail', id] maintenanceKeys.stats() // ['maintenance', 'stats'] ``` +It lives in `hooks/useMaintenanceList.ts`, with `lists()` and `details()` as the +prefixes to invalidate. + Unlike parking (which has no factory), there's one here because the invalidation hierarchy is more complex (editing a report has to invalidate both its detail and any lists containing it). @@ -179,7 +193,7 @@ and any lists containing it). `HistoryTab`). The URL preserves the active tab in the query string so F5 keeps the view. -`DetailTab.tsx` (634 lines) is the bulk of the detail UI: header, description, +`DetailTab.tsx` (~660 lines) is the bulk of the detail UI: header, description, location, assignment, image preview gallery, inline actions to change status/priority/assignment, resolution-notes form. @@ -194,7 +208,7 @@ any field within permissions. Both validate against the same `reportSchema` Zod `canAccessMaintenance` allows: -- `admin`, `group-admin`, `demo-admin` +- `admin`, `group-admin` - `recepcionista` - **`mantenimiento`** (this is the module they do enter) @@ -213,15 +227,18 @@ for mantenimiento is key. 3. **`closed` status is final in the UI, but not in the backend.** The UI doesn't expose a reopen from `closed`, but the API allows it. If you need a reopen in the UI, add it; check `maintenance_history.action` for coherence. -4. **`DetailTab.tsx` at 634 lines and `MaintenanceListClient.tsx` at 766 +4. **`DetailTab.tsx` at ~660 lines and `MaintenanceListClient.tsx` at ~780 lines.** On the radar for split if they grow more, not urgent. -5. **Image upload capped at 5 MB.** Hardcoded in the multer config in - `routes/maintenance/maintenance-routes.ts`. Accepted types: jpeg, png, webp, - gif. If the requirement changes, adjust there. +5. **Image upload capped at 5 MB.** `MAX_IMAGE_BYTES` and `IMAGE_MIME_TYPES` in + `middlewares/imageUpload.ts`, shared with the other image uploads. Accepted + types: jpeg, png, webp, gif. If the requirement changes, adjust there — it + changes every module that uses it. ## Cross references - `backend/middlewares/roleCheck.ts → canAccessMaintenance` — the allowed roles. - `backend/services/blacklist/cloudinary-service.ts` — the Cloudinary helper this module also uses for image uploads. +- `backend/services/uploads/private-files.ts` — fetching and sending the private + files. - `frontend/app/lib/maintenance/maintenance.ts` — all types and enums. diff --git a/frontend/app/components/maintenance/CLAUDE.md b/frontend/app/components/maintenance/CLAUDE.md new file mode 100644 index 00000000..43c994c2 --- /dev/null +++ b/frontend/app/components/maintenance/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/frontend/app/components/profile/reports/sections/CashierSection.tsx b/frontend/app/components/profile/reports/sections/CashierSection.tsx index fc56b214..3121781d 100644 --- a/frontend/app/components/profile/reports/sections/CashierSection.tsx +++ b/frontend/app/components/profile/reports/sections/CashierSection.tsx @@ -198,8 +198,7 @@ export default function CashierSection() { `${API_URL}/api/cashier/reports/vouchers-history?${params.toString()}` ) const data = response as - | { data?: { vouchers?: Voucher[] }; vouchers?: Voucher[] } - | Voucher[] + { data?: { vouchers?: Voucher[] }; vouchers?: Voucher[] } | Voucher[] if (Array.isArray(data)) { setVouchers(data) } else { diff --git a/frontend/app/components/profile/reports/sections/GroupsSection.tsx b/frontend/app/components/profile/reports/sections/GroupsSection.tsx index ea7acf4e..dc1b0806 100644 --- a/frontend/app/components/profile/reports/sections/GroupsSection.tsx +++ b/frontend/app/components/profile/reports/sections/GroupsSection.tsx @@ -105,8 +105,7 @@ export default function GroupsSection() { params.set('order', 'DESC') const groupsResponse = (await apiClient.get(`${API_URL}/api/groups?${params.toString()}`)) as - | { data?: Group[] } - | Group[] + { data?: Group[] } | Group[] const data = (groupsResponse as { data?: Group[] }).data || [] setGroups(Array.isArray(data) ? data.slice(0, DEFAULT_LIMIT) : []) } catch (err: unknown) { @@ -130,8 +129,7 @@ export default function GroupsSection() { setExpandedId(groupId) try { const response = (await apiClient.get(`${API_URL}/api/groups/${groupId}/history`)) as - | { data?: GroupHistoryEntry[] } - | GroupHistoryEntry[] + { data?: GroupHistoryEntry[] } | GroupHistoryEntry[] const data = (response as { data?: GroupHistoryEntry[] }).data || response setHistory(Array.isArray(data) ? data : []) } catch { @@ -149,8 +147,7 @@ export default function GroupsSection() { setError(null) try { const response = (await apiClient.get(`${API_URL}/api/groups/${searchId}`)) as - | { data?: Group } - | Group + { data?: Group } | Group const group = (response as { data?: Group }).data || (response as Group) if (group) setGroups([group]) } catch { diff --git a/frontend/app/components/profile/reports/types.ts b/frontend/app/components/profile/reports/types.ts index 9db3d3ed..667c9f95 100644 --- a/frontend/app/components/profile/reports/types.ts +++ b/frontend/app/components/profile/reports/types.ts @@ -132,9 +132,4 @@ export interface CashierHistoryEntry { // ═══════════════════════════════════════════════════════ export type ReportSection = - | 'overview' - | 'logbooks' - | 'maintenance' - | 'groups' - | 'cashier' - | 'checklist' + 'overview' | 'logbooks' | 'maintenance' | 'groups' | 'cashier' | 'checklist' diff --git a/frontend/app/components/restaurant/tabs/DailyRevenueTab.tsx b/frontend/app/components/restaurant/tabs/DailyRevenueTab.tsx index 4f3d2038..61169869 100644 --- a/frontend/app/components/restaurant/tabs/DailyRevenueTab.tsx +++ b/frontend/app/components/restaurant/tabs/DailyRevenueTab.tsx @@ -1,6 +1,6 @@ 'use client' -import { useState, useCallback, useRef } from 'react' +import { useState, useCallback, useMemo, useRef } from 'react' import { useTranslations } from 'next-intl' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import toast from 'react-hot-toast' @@ -448,7 +448,7 @@ export function DailyRevenueTab() { }, }) - const rows = data?.rows ?? [] + const rows = useMemo(() => data?.rows ?? [], [data?.rows]) const lookupDate = useCallback( (date: string): Record | undefined => { diff --git a/frontend/app/components/scheduling/AGENTS.md b/frontend/app/components/scheduling/AGENTS.md index d9073f44..b028b4bc 100644 --- a/frontend/app/components/scheduling/AGENTS.md +++ b/frontend/app/components/scheduling/AGENTS.md @@ -1,21 +1,24 @@ # AGENTS.md — Scheduling (frontend) +**Up:** [`frontend/AGENTS.md`](../../../AGENTS.md) — `frontend/` rules · +[root `AGENTS.md`](../../../../AGENTS.md) — shared rules and module map. + > UI for the scheduling module. Business logic (validator, constraints, solver) -> lives in the backend — read `backend/services/scheduling/CLAUDE.md` when the -> task crosses the boundary, and `backend/scheduling-solver/CLAUDE.md` when it +> lives in the backend — read `backend/services/scheduling/AGENTS.md` when the +> task crosses the boundary, and `backend/scheduling-solver/AGENTS.md` when it > touches the Python solver. ## Layout ``` frontend/app/dashboard/scheduling/ - ├── layout.tsx (auth gate: admin / group-admin only; redirects to /dashboard) + ├── layout.tsx (auth gate: admin only; redirects to /dashboard) ├── page.tsx (entry point — wraps SchedulingClient in Suspense with skeleton) └── config/ └── page.tsx (entry point for SchedulingConfigClient) frontend/app/components/scheduling/ - ├── SchedulingClient.tsx (877 lines — grid orchestrator + month-level actions) + ├── SchedulingClient.tsx (~900 lines — grid orchestrator + month-level actions) ├── ScheduleGrid.tsx (535 lines — interactive monthly table; cell locking visual; sticky columns) ├── ShiftSelector.tsx (shift-picker popover after a cell click; bulk mode for range selection) ├── ValidationWarnings.tsx (real-time errors/warnings/info banner; grouped by severity) @@ -33,15 +36,14 @@ frontend/app/components/scheduling/ ├── RulesTab.tsx (608 lines — per-employee rules: fixedShift, fixedDays, noWeekends, shiftPriority) ├── RequestsTab.tsx (704 lines — request management: pending → approved/rejected) ├── ShiftStatsTab.tsx (per-shift stats across the year) - ├── PresenciasTab.tsx (dedicated editor for employees with fixedShift='P' and fixedDays) - └── utils/ + ├── PresenciasTab.tsx (converts a schedule pasted from Excel into presencias) + └── utils/ (date.ts: formatLocalDate; presencias.ts: the converter) frontend/app/lib/scheduling/ ├── queries.ts (645 lines — schedulingApi: every call; schedulingKeys: React Query keys factory) - ├── types.ts (553 lines — all frontend DTOs; same shape as backend) - ├── server.ts (Server Component utilities — direct fetch to backend) + ├── types.ts (~500 lines — all frontend DTOs; same shape as backend) ├── shift-styles.ts (Tailwind classes per shift code; light + dark mode) - ├── export-pdf.ts (271 lines — month PDF export via jsPDF + autotable) + ├── export-pdf.ts (~270 lines — month PDF export with pdf-lib) └── index.ts (barrel exports) ``` @@ -53,18 +55,20 @@ entire `/dashboard/scheduling` subroute. Calls `useAuth()`, validates while `loading=true`. **Recepcionistas and mantenimiento don't enter this module.** -`useAuth()` comes from the global `AuthContext` (mounted in `DashboardLayout`). -No re-fetch; it reads from context. +`useAuth()` comes from the global `AuthProvider` (`app/lib/auth/useAuth.tsx`), +mounted inside `app/lib/theme/ThemeProvider.tsx`. No re-fetch; it reads from +context. The backend enforces the same rule: the whole scheduling router is +`isAdmin`. ## Main component — `SchedulingClient.tsx` -877 lines, marked `'use client'`. Orchestrates the monthly grid. +~900 lines, marked `'use client'`. Orchestrates the monthly grid. **Responsibilities:** - Reads `?month=` from the query string (URL is the source of truth for the active month). -- React Query: `schedulingApi.getMonthFull(monthId)`, `getAllShifts`, +- React Query: `schedulingApi.getMonthById(monthId)`, `getAllShifts`, `validateSchedule(monthId)`, `getHistory`, `getMonthInfo`, etc. - Manages local selection state (single cell + bulk selection via `BulkSelection`). @@ -72,7 +76,8 @@ No re-fetch; it reads from context. publish/unpublish, delete month, manage holidays. - Handles modals: `ManageHolidaysModal`, `ConfirmDialog` for destructive actions. -- Triggers PDF download (`downloadSchedulePdf` from `lib/scheduling`). +- Triggers PDF download (`downloadSchedulePdf(monthData)` from + `lib/scheduling`). - Toast notifications via `react-hot-toast` for mutations. **Editing flow:** @@ -91,16 +96,18 @@ No re-fetch; it reads from context. - "Generate schedule" button → calls `POST /months/:id/generate`. - While generating, shows loading state. Effective timeout is set by the backend (60 s from Node, 30 s from Python). -- If the response is `infeasible`, surfaces the `conflictingConstraints` and - `suggestedRelaxations` in the warnings banner (structured). +- If the response is a 422 with `conflictingConstraints` (infeasible), opens a + modal (`infeasibleModal`) listing them and the `suggestedRelaxations`. - If `ok`, invalidates the month → grid fills in. ## Grid — `ScheduleGrid.tsx` Monthly table of employees × days. Critical patterns: -- **Locked cells:** assignments with `source_constraint_id != null` show a - `` icon and aren't clickable; error toast if you try. +- **Locked cells:** assignments with `sourceConstraintId` show a `` + icon and aren't editable (`canEditCell` is false). Cells filled by a solver + run from approved requests or `fixedDays` carry no `sourceConstraintId`, so + they stay editable. - **Sticky columns:** employee + total libres are `position: sticky; left: 0`. Lets you scroll horizontally without losing context. - **Horizontal overflow:** wrapper with `overflow-x-auto` + table with @@ -118,8 +125,10 @@ Monthly table of employees × days. Critical patterns: ## Config panel — `SchedulingConfigClient.tsx` -Container for 7 tabs, navigated via `?tab=` in the query string. Each -tab is an independent client component that loads its own data: +Container for 7 tabs, navigated via `?tab=` in the query string +(`VALID_TABS`; `employees` when missing). Only the first five have a button in +the tab bar: `shift-stats` and `presencias` are reached by URL only. Each tab is +an independent client component that loads its own data: - **`employees`** (`EmployeesTab.tsx`): Schedulable employee list + add/remove checkbox + start_date/end_date @@ -132,8 +141,9 @@ tab is an independent client component that loads its own data: - **`requests`** (`RequestsTab.tsx`): Request management (vacations/IT/bonificables): approve / reject / edit - **`shift-stats`** (`ShiftStatsTab.tsx`): Per-shift stats across the year -- **`presencias`** (`PresenciasTab.tsx`): Editor specifically for employees with - `fixedShift='P'` and `fixedDays` +- **`presencias`** (`PresenciasTab.tsx`): Converts a monthly schedule pasted + from Excel (name + tab-separated days) into presencias; it does not read + `scheduling_assignments` (`TODO.md`: "Presencias a mano") `'react-day-picker/style.css'` is imported in `SchedulingConfigClient.tsx` (shared by several tabs that use DayPicker for dates). @@ -157,9 +167,8 @@ parent. (fetch wrapper with auth/credentials). Every call returns the matching TS type from `types.ts`. -There's a specific `downloadSchedulePdf(monthId)` wrapper that delegates to -`lib/scheduling/export-pdf.ts` to build the PDF in the browser (no backend PDF -endpoint). +`downloadSchedulePdf(monthData)` lives in `lib/scheduling/export-pdf.ts` and +builds the PDF in the browser (no backend PDF endpoint). ## Server fetching @@ -170,9 +179,9 @@ request cookies are forwarded. ## PDF export — `lib/scheduling/export-pdf.ts` -271 lines. Builds the month's schedule PDF using `jspdf` + `jspdf-autotable`. -Runs **in the browser** (no backend endpoint). Takes `SchedulingMonthFull` + -`shifts` and produces the blob for download. +~270 lines. Builds the month's schedule PDF with `pdf-lib` (text made encodable +by `app/lib/helpers/pdfText.ts`). Runs **in the browser** (no backend endpoint). +Takes `SchedulingMonthFull` and produces the blob for download. If you change shift codes or colors, **also update this file** — it duplicates the color maps locally so runtime doesn't depend on the DOM. @@ -181,10 +190,10 @@ the color maps locally so runtime doesn't depend on the DOM. ### Date formatting → `formatLocalDate(d: Date)` -`SchedulingConfigClient.tsx` defines a module-level helper -`formatLocalDate(d: Date)` that uses `getFullYear/getMonth/getDate` directly (no -`toLocaleDateString` with ISO string parsing, **timezone-safe**). Use it instead -of `new Date(isoString).toLocaleDateString()` to avoid the classic UTC-vs-local +`config/utils/date.ts` exports `formatLocalDate(d: Date)`, which uses +`getFullYear/getMonth/getDate` directly (no `toLocaleDateString` with ISO string +parsing, **timezone-safe**). Use it instead of +`new Date(isoString).toLocaleDateString()` to avoid the classic UTC-vs-local bug. ### DayPicker dark mode @@ -218,17 +227,18 @@ function components inside the clients. `useTranslations()` from `next-intl`. Main namespaces: -- `scheduling` — the main grid page. -- `scheduling.config` — the config panel and its tabs. +- `scheduling` and its sub-namespaces (`scheduling.actions`, + `scheduling.toasts`, `scheduling.messages`…) — the main grid page. +- `scheduling.config.*` — the config panel and its tabs. -Dictionaries in `frontend/i18n/` (es + en). When you add a new key, **add it to -both languages** in the same commit. +Dictionaries in `frontend/messages//scheduling.json` (es + en). When you +add a new key, **add it to both languages** in the same commit. ### Shift codes Defined exhaustively in `app/lib/scheduling/shift-styles.ts` (Tailwind color mapping). The semantics of each code (M, T, N, P, PI, L, V, B, E, IT, FO, A, LI) -is documented in `backend/services/scheduling/CLAUDE.md` § Shift types — that's +is documented in `backend/services/scheduling/AGENTS.md` § Shift types — that's the source of truth. If you add a new code: 1. Backend: add it to `scheduling_shifts` (DB) and to the validator / solver as @@ -238,21 +248,20 @@ the source of truth. If you add a new code: ## UI-visible known bugs / debt -- **`setSchedulableEmployees` wipes `start_date`/`end_date`** (TODO.md): saving - the list from `EmployeesTab` loses previously-set dates. The fix is on the - backend but the bug is felt in the Employees tab. If you touch it, note the - bug is still open. -- **DayPicker inside RequestsTab → iOS modal scroll**: known edge case on Safari - mobile — the modal overflow fights the DayPicker gestures. Workaround: - `touch-action: pan-y` on the modal body. +- **DayPicker inside RequestsTab → iOS modal scroll**: reported edge case on + Safari mobile — the modal overflow fights the DayPicker gestures. The + suggested workaround (`touch-action: pan-y` on the modal body) is **not** + applied in the code; not reproduced since. +- **Presencias by hand**: the `presencias` tab needs the schedule pasted as text + (`TODO.md`). ## Cross references -- `backend/services/scheduling/CLAUDE.md` — TS backend: validator, constraints, +- `backend/services/scheduling/AGENTS.md` — TS backend: validator, constraints, endpoints, shift type semantics, configuration tables, historical importer. -- `backend/scheduling-solver/CLAUDE.md` — Python solver: daemon, CP-SAT, hard +- `backend/scheduling-solver/AGENTS.md` — Python solver: daemon, CP-SAT, hard constraints, soft objective. - `frontend/app/lib/scheduling/types.ts` — DTOs shared with the backend; keep manually in sync when they change. -- `frontend/app/components/dashboard/parking/` — internal reference for the +- `frontend/app/dashboard/parking/AGENTS.md` — internal reference for the project's responsive patterns. diff --git a/frontend/app/components/scheduling/CLAUDE.md b/frontend/app/components/scheduling/CLAUDE.md new file mode 100644 index 00000000..43c994c2 --- /dev/null +++ b/frontend/app/components/scheduling/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/frontend/app/components/scheduling/SchedulingConfigClient.tsx b/frontend/app/components/scheduling/SchedulingConfigClient.tsx index 0722c788..2f6d5192 100644 --- a/frontend/app/components/scheduling/SchedulingConfigClient.tsx +++ b/frontend/app/components/scheduling/SchedulingConfigClient.tsx @@ -27,13 +27,7 @@ import { PresenciasTab } from './config/PresenciasTab' import 'react-day-picker/style.css' type TabType = - | 'employees' - | 'totals' - | 'general' - | 'rules' - | 'requests' - | 'shift-stats' - | 'presencias' + 'employees' | 'totals' | 'general' | 'rules' | 'requests' | 'shift-stats' | 'presencias' const VALID_TABS: TabType[] = [ 'employees', diff --git a/frontend/app/dashboard/parking/AGENTS.md b/frontend/app/dashboard/parking/AGENTS.md index 8582d073..a4d2ac48 100644 --- a/frontend/app/dashboard/parking/AGENTS.md +++ b/frontend/app/dashboard/parking/AGENTS.md @@ -1,7 +1,10 @@ # AGENTS.md — Parking (frontend) +**Up:** [`frontend/AGENTS.md`](../../../AGENTS.md) — `frontend/` rules · +[root `AGENTS.md`](../../../../AGENTS.md) — shared rules and module map. + > UI for the parking module. Backend documented at -> `backend/services/parking/CLAUDE.md`. **This module is the project's internal +> `backend/services/parking/AGENTS.md`. **This module is the project's internal > reference for responsive patterns** — when in doubt about how to do a wide > table or a mobile-friendly dashboard in another module, look here first. @@ -14,11 +17,11 @@ frontend/app/dashboard/parking/ ├── loading.tsx (skeleton) ├── error.tsx (error boundary) ├── actions/ - │ └── getParkingDashboardStats.ts (71 lines — server action for SSR) + │ └── getParkingDashboardStats.ts (~100 lines — server action for SSR) ├── components/ │ └── ParkingDashboardClient.tsx (client that receives the pre-fetched stats) ├── bookings/ - │ ├── page.tsx (booking list — SSR for page 0) + │ ├── page.tsx (booking list — SSR of ?page= (default 1), 50 per page) │ ├── new/page.tsx (new booking form) │ └── [code]/page.tsx (booking detail by booking_code) └── status/ @@ -31,7 +34,7 @@ frontend/app/dashboard/parking/ │ ├── StatusPanels.tsx (side KPI panels) │ └── modals/ (5 modals: CheckIn, CheckOut, Cancel, Overdue, BaseModal) ├── hooks/ - │ └── useParkingStatus.ts (React Query hook; no polling) + │ └── useParkingStatus.ts (React Query hook with local keys; no polling) └── utils/ └── statusBadges.tsx @@ -50,15 +53,15 @@ frontend/app/components/parking/ └── bookings/ ├── BookingDetailClient.tsx (688 lines — the detail page) ├── BookingHeader.tsx - ├── EditBookingModal.tsx (567 lines) + ├── EditBookingModal.tsx (564 lines) ├── CheckInModal.tsx ├── CheckOutModal.tsx ├── PaymentModal.tsx └── InfoCard.tsx frontend/app/lib/parking/ - ├── queries.ts (313 lines — apiClient calls, no React Query hooks here) - ├── types.ts (367 lines — every DTO) + ├── queries.ts (~320 lines — parkingApi: apiClient calls, no React Query hooks here) + ├── types.ts (~360 lines — every DTO) ├── actions.ts (71 lines — additional server actions) └── index.ts ``` @@ -88,7 +91,9 @@ Module entry screen. Server Component that pre-fetches the day's stats with - Day KPIs (occupancy, revenue, arrivals, departures). - Pending check-in and check-out lists. -- Trend charts (coming soon). + +There are no trend charts: the analytics API they would use was removed on +2026-10-02. SSR pre-fetch is **intentional**: the first paint with no spinner is what sets this module apart. If you add more pre-fetchable views, follow the `actions/` + @@ -105,7 +110,10 @@ Server Component pattern. `arrivals_total`, `departures_pending`, `departures_completed`, `departures_total`. Map to pre-armed combinations of `status` + `date` for the user. -- **Pagination:** server-side; `limit`/`offset` + `PaginationInfo`. +- **Pagination:** server-side; `page` + `limit` (50), and the response's + `pagination: { page, limit, total, totalPages }`. +- **No React Query here:** the list keeps its data in `useState` and calls + `parkingApi.getAllBookings()` directly. - **`ActionDropdown`** per row with contextual actions based on the booking's current status. - **New booking** via the `+ Nueva reserva` button → navigates to @@ -180,33 +188,35 @@ necessary — the module is tuned specifically for the `md` and `xl` breakpoints ## React Query — `lib/parking/queries.ts` `parkingApi` is a flat object with one method per endpoint -(`parkingApi.listSpots()`, `parkingApi.getBookings(...)`, etc.). **No pre-built -hooks** — each component calls `useQuery` directly with whatever key it wants. +(`parkingApi.getAllSpots()`, `parkingApi.getAllBookings(...)`, etc.). **No +pre-built hooks** — React Query is used in `useParkingStatus` and +`ParkingDashboardClient`; the bookings list and detail call `parkingApi` +directly. Typical client pattern: ```tsx const { data, isLoading } = useQuery({ - queryKey: ['parking', 'bookings', filters], - queryFn: () => parkingApi.getBookings(filters), + queryKey: ['parking', 'bookings', date], + queryFn: () => parkingApi.getAllBookings({ date }), }) ``` -**There's no `parkingKeys` factory** (unlike scheduling). Components hardcode -the keys. This is style, not a bug — it works because the module doesn't have a -complex invalidation hierarchy. If complexity grows, consider extracting a -factory equivalent to `schedulingKeys`. +**There's no shared `parkingKeys` factory** (unlike scheduling). +`useParkingStatus` defines its own (`statsKey`, `bookingsKey`, `overdueKey`). +This is style, not a bug — it works because the module doesn't have a complex +invalidation hierarchy. If complexity grows, consider extracting a factory +equivalent to `schedulingKeys`. ## Duplicated enums — sync rule `frontend/app/components/parking/helpers/constants.ts` holds the frontend copy -of the backend enums: +of two backend enums: + +- `BookingStatus` (5 values), with its label and badge tone (`STATUS_CONFIG`) +- Payment method (4 values, `PAYMENT_METHODS`), with its label -- `BookingStatus` (5 values) -- `BookingSource` (5 values) -- `PaymentMethod` (4 values) -- `SpotType` (6 values) -- Label and badge tone mappings +Booking source, spot type and level are not copied there. **If the backend adds a value, update both sides in the same commit.** If you leave the frontend out of sync, bookings with the new value will render with no @@ -215,9 +225,9 @@ badge or empty label. No visible error — just silently broken UI. ## Auth The whole `/dashboard/parking/*` subroute requires authentication + a role other -than mantenimiento (the backend blocks them and the dashboard layout assumes a -logged-in user). No admin-only restriction — recepcionistas use the module -daily. +than mantenimiento. The UI does not hide it from mantenimiento (the nav link +shows for every role); the backend answers 403. No admin-only restriction — +recepcionistas use the module daily. ## Known gotchas @@ -225,7 +235,7 @@ daily. you're about to add non-trivial features (new filters, sorting, exports), consider extracting subcomponents before adding more LOC. Not urgent but on the radar. -2. **`EditBookingModal.tsx` is 567 lines.** Same warning, smaller scale. +2. **`EditBookingModal.tsx` is 564 lines.** Same warning, smaller scale. 3. **`booking_code` vs `id` in URLs:** public URLs use `code`, never `id`. Keep that invariant when adding new parametric routes. 4. **Status view ≠ dashboard.** Don't merge the two UIs: dashboard is @@ -235,13 +245,13 @@ daily. Data refreshes when the user performs an action. If you ever add WebSocket / SSE for push-based invalidation, **respect the React Query invalidation pattern** — don't introduce parallel local state. -6. **No invoicing frontend yet.** PDF generation is off on the backend (TODO). - If you activate it, you'll need to build the invoice list/download UI — - nothing exists today. +6. **No invoicing.** There is no parking invoice on either side: the backend + placeholder was removed on 2026-10-02. Building it means a backend endpoint + and the invoice list/download UI. ## Cross references -- `backend/services/parking/CLAUDE.md` — full backend for the module. +- `backend/services/parking/AGENTS.md` — full backend for the module. - `frontend/app/components/parking/helpers/constants.ts` — frontend copy of the enums. - `frontend/app/components/parking/` — module's shared components (list, badges, diff --git a/frontend/app/dashboard/parking/CLAUDE.md b/frontend/app/dashboard/parking/CLAUDE.md new file mode 100644 index 00000000..43c994c2 --- /dev/null +++ b/frontend/app/dashboard/parking/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/frontend/app/lib/backoffice/types.ts b/frontend/app/lib/backoffice/types.ts index 6f4e24ce..b5056cc3 100644 --- a/frontend/app/lib/backoffice/types.ts +++ b/frontend/app/lib/backoffice/types.ts @@ -134,13 +134,7 @@ export interface InvoiceFormData { // ======================================== export type InvoiceAction = - | 'created' - | 'updated' - | 'validated' - | 'rejected' - | 'paid' - | 'deleted' - | 'restored' + 'created' | 'updated' | 'validated' | 'rejected' | 'paid' | 'deleted' | 'restored' export interface InvoiceHistory { id: number diff --git a/frontend/app/lib/maintenance/maintenance.ts b/frontend/app/lib/maintenance/maintenance.ts index 5e48722e..e53e8e57 100644 --- a/frontend/app/lib/maintenance/maintenance.ts +++ b/frontend/app/lib/maintenance/maintenance.ts @@ -2,13 +2,7 @@ export type LocationType = 'room' | 'common_area' | 'exterior' | 'facilities' | 'other' export type ReportStatus = - | 'reported' - | 'assigned' - | 'in_progress' - | 'waiting' - | 'completed' - | 'closed' - | 'canceled' + 'reported' | 'assigned' | 'in_progress' | 'waiting' | 'completed' | 'closed' | 'canceled' export type ReportPriority = 'low' | 'medium' | 'high' | 'urgent' export type AssignedType = 'internal' | 'external' export type HistoryAction = diff --git a/frontend/app/lib/notifications/types.ts b/frontend/app/lib/notifications/types.ts index 0f22303a..cce21455 100644 --- a/frontend/app/lib/notifications/types.ts +++ b/frontend/app/lib/notifications/types.ts @@ -3,13 +3,7 @@ export type NotificationPriority = 'low' | 'medium' | 'high' | 'urgent' export type NotificationModule = 'groups' | 'parking' | 'logbooks' | 'system' | 'messages' export type NotificationRelatedTo = - | 'payment' - | 'rooming' - | 'balance' - | 'contract' - | 'arrival' - | 'general' - | 'message' + 'payment' | 'rooming' | 'balance' | 'contract' | 'arrival' | 'general' | 'message' export interface Notification { id: number diff --git a/frontend/app/lib/scheduling/types.ts b/frontend/app/lib/scheduling/types.ts index 1d6148cc..5d1202b0 100644 --- a/frontend/app/lib/scheduling/types.ts +++ b/frontend/app/lib/scheduling/types.ts @@ -248,12 +248,7 @@ export type WarningSeverity = 'info' | 'warning' | 'error' * Synced from: backend/services/scheduling/types/index.ts */ export type WarningType = - | 'coverage' - | 'night_block' - | 'rest' - | 'hours' - | 'constraint' - | 'validation' + 'coverage' | 'night_block' | 'rest' | 'hours' | 'constraint' | 'validation' export interface GenerationWarning { type: WarningType diff --git a/frontend/package.json b/frontend/package.json index 740a666b..5d00319c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -2,7 +2,7 @@ "private": true, "packageManager": "pnpm@10.28.0", "engines": { - "node": ">=22.16.0" + "node": "24.x" }, "scripts": { "build": "next build", @@ -11,7 +11,8 @@ "lint": "eslint . --ext .js,.ts,.jsx,.tsx", "typecheck": "tsc --noEmit", "format": "prettier --write .", - "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx}\"" + "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx}\"", + "verify": "pnpm run lint --max-warnings=0 && pnpm run format:check && pnpm run typecheck" }, "dependencies": { "@headlessui/react": "^2.2.9", @@ -62,7 +63,7 @@ "eslint-config-next": "^15.5.3", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.5.4", - "prettier": "^3.6.2" + "prettier": "^3.9.9" }, "pnpm": { "onlyBuiltDependencies": [ diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 11589e3e..57d9c2d8 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -146,10 +146,10 @@ importers: version: 10.1.8(eslint@8.57.1) eslint-plugin-prettier: specifier: ^5.5.4 - version: 5.5.4(eslint-config-prettier@10.1.8(eslint@8.57.1))(eslint@8.57.1)(prettier@3.6.2) + version: 5.5.4(eslint-config-prettier@10.1.8(eslint@8.57.1))(eslint@8.57.1)(prettier@3.9.9) prettier: - specifier: ^3.6.2 - version: 3.6.2 + specifier: ^3.9.9 + version: 3.9.9 packages: @@ -290,78 +290,92 @@ packages: resolution: {integrity: sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.3': resolution: {integrity: sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.3': resolution: {integrity: sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.3': resolution: {integrity: sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.3': resolution: {integrity: sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.3': resolution: {integrity: sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.3': resolution: {integrity: sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linux-arm64@0.34.4': resolution: {integrity: sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.34.4': resolution: {integrity: sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-ppc64@0.34.4': resolution: {integrity: sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-linux-s390x@0.34.4': resolution: {integrity: sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.34.4': resolution: {integrity: sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.4': resolution: {integrity: sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.34.4': resolution: {integrity: sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-wasm32@0.34.4': resolution: {integrity: sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==} @@ -447,30 +461,35 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@napi-rs/canvas-linux-arm64-musl@0.1.84': resolution: {integrity: sha512-VyZq0EEw+OILnWk7G3ZgLLPaz1ERaPP++jLjeyLMbFOF+Tr4zHzWKiKDsEV/cT7btLPZbVoR3VX+T9/QubnURQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@napi-rs/canvas-linux-riscv64-gnu@0.1.84': resolution: {integrity: sha512-PSMTh8DiThvLRsbtc/a065I/ceZk17EXAATv9uNvHgkgo7wdEfTh2C3aveNkBMGByVO3tvnvD5v/YFtZL07cIg==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] + libc: [glibc] '@napi-rs/canvas-linux-x64-gnu@0.1.84': resolution: {integrity: sha512-N1GY3noO1oqgEo3rYQIwY44kfM11vA0lDbN0orTOHfCSUZTUyiYCY0nZ197QMahZBm1aR/vYgsWpV74MMMDuNA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@napi-rs/canvas-linux-x64-musl@0.1.84': resolution: {integrity: sha512-vUZmua6ADqTWyHyei81aXIt9wp0yjeNwTH0KdhdeoBb6azHmFR8uKTukZMXfLCC3bnsW0t4lW7K78KNMknmtjg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@napi-rs/canvas-win32-x64-msvc@0.1.84': resolution: {integrity: sha512-YSs8ncurc1xzegUMNnQUTYrdrAuaXdPMOa+iYYyAxydOtg0ppV386hyYMsy00Yip1NlTgLCseRG4sHSnjQx6og==} @@ -508,24 +527,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@next/swc-linux-arm64-musl@16.0.8': resolution: {integrity: sha512-JaXFAlqn8fJV+GhhA9lpg6da/NCN/v9ub98n3HoayoUSPOVdoxEEt86iT58jXqQCs/R3dv5ZnxGkW8aF4obMrQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@next/swc-linux-x64-gnu@16.0.8': resolution: {integrity: sha512-O7M9it6HyNhsJp3HNAsJoHk5BUsfj7hRshfptpGcVsPZ1u0KQ/oVy8oxF7tlwxA5tR43VUP0yRmAGm1us514ng==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@next/swc-linux-x64-musl@16.0.8': resolution: {integrity: sha512-8+KClEC/GLI2dLYcrWwHu5JyC5cZYCFnccVIvmxpo6K+XQt4qzqM5L4coofNDZYkct/VCCyJWGbZZDsg6w6LFA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@next/swc-win32-arm64-msvc@16.0.8': resolution: {integrity: sha512-rpQ/PgTEgH68SiXmhu/cJ2hk9aZ6YgFvspzQWe2I9HufY6g7V02DXRr/xrVqOaKm2lenBFPNQ+KAaeveywqV+A==} @@ -1149,36 +1172,42 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm-musl@2.5.1': resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [musl] '@parcel/watcher-linux-arm64-glibc@2.5.1': resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.5.1': resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [musl] '@parcel/watcher-linux-x64-glibc@2.5.1': resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-x64-musl@2.5.1': resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [musl] '@parcel/watcher-win32-arm64@2.5.1': resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} @@ -1844,24 +1873,28 @@ packages: engines: {node: '>=10'} cpu: [arm64] os: [linux] + libc: [glibc] '@swc/core-linux-arm64-musl@1.15.7': resolution: {integrity: sha512-/OOp9UZBg4v2q9+x/U21Jtld0Wb8ghzBScwhscI7YvoSh4E8RALaJ1msV8V8AKkBkZH7FUAFB7Vbv0oVzZsezA==} engines: {node: '>=10'} cpu: [arm64] os: [linux] + libc: [musl] '@swc/core-linux-x64-gnu@1.15.7': resolution: {integrity: sha512-VBbs4gtD4XQxrHuQ2/2+TDZpPQQgrOHYRnS6SyJW+dw0Nj/OomRqH+n5Z4e/TgKRRbieufipeIGvADYC/90PYQ==} engines: {node: '>=10'} cpu: [x64] os: [linux] + libc: [glibc] '@swc/core-linux-x64-musl@1.15.7': resolution: {integrity: sha512-kVuy2unodso6p0rMauS2zby8/bhzoGRYxBDyD6i2tls/fEYAE74oP0VPFzxIyHaIjK1SN6u5TgvV9MpyJ5xVug==} engines: {node: '>=10'} cpu: [x64] os: [linux] + libc: [musl] '@swc/core-win32-arm64-msvc@1.15.7': resolution: {integrity: sha512-uddYoo5Xmo1XKLhAnh4NBIyy5d0xk33x1sX3nIJboFySLNz878ksCFCZ3IBqrt1Za0gaoIWoOSSSk0eNhAc/sw==} @@ -2117,41 +2150,49 @@ packages: resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.11.1': resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} cpu: [arm64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} cpu: [riscv64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} cpu: [riscv64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} cpu: [s390x] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.11.1': resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} cpu: [x64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.11.1': resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} cpu: [x64] os: [linux] + libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.11.1': resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} @@ -3690,8 +3731,8 @@ packages: resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} engines: {node: '>=6.0.0'} - prettier@3.6.2: - resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + prettier@3.9.9: + resolution: {integrity: sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==} engines: {node: '>=14'} hasBin: true @@ -7738,10 +7779,10 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 - eslint-plugin-prettier@5.5.4(eslint-config-prettier@10.1.8(eslint@8.57.1))(eslint@8.57.1)(prettier@3.6.2): + eslint-plugin-prettier@5.5.4(eslint-config-prettier@10.1.8(eslint@8.57.1))(eslint@8.57.1)(prettier@3.9.9): dependencies: eslint: 8.57.1 - prettier: 3.6.2 + prettier: 3.9.9 prettier-linter-helpers: 1.0.0 synckit: 0.11.11 optionalDependencies: @@ -8963,7 +9004,7 @@ snapshots: dependencies: fast-diff: 1.3.0 - prettier@3.6.2: {} + prettier@3.9.9: {} prop-types@15.8.1: dependencies: diff --git a/frontend/pnpm-workspace.yaml b/frontend/pnpm-workspace.yaml new file mode 100644 index 00000000..a8fca7b3 --- /dev/null +++ b/frontend/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +# Do not install a version published less than 7 days ago (minutes). +# Needs pnpm >= 10.16; ignored silently below that. +minimumReleaseAge: 10080 diff --git a/frontend/skills-lock.json b/frontend/skills-lock.json new file mode 100644 index 00000000..dbd7af1c --- /dev/null +++ b/frontend/skills-lock.json @@ -0,0 +1,80 @@ +{ + "version": 1, + "skills": { + "accessibility": { + "source": "addyosmani/web-quality-skills", + "sourceType": "autoskills-registry", + "computedHash": "bffe3d08cfe92ebad63699f74ce29e35c19850ebfbf474c1463183cfe34d6a09" + }, + "composition-patterns": { + "source": "vercel-labs/agent-skills", + "sourceType": "autoskills-registry", + "computedHash": "c124390cc3441844903ada94cb5820b23d922f2d0ad34e01e029e410197c8a32" + }, + "deploy-to-vercel": { + "source": "vercel-labs/agent-skills", + "sourceType": "autoskills-registry", + "computedHash": "698dfb0ec0cf3e8d97c3f181e13b37b3b8fbc872a4b031d812adb4193f66035e" + }, + "frontend-design": { + "source": "anthropics/skills", + "sourceType": "autoskills-registry", + "computedHash": "82fb11a63fb1e35ee2469516ed02d54695f783115b1540c0e783197af4240a3a" + }, + "next-best-practices": { + "source": "vercel-labs/next-skills", + "sourceType": "autoskills-registry", + "computedHash": "22d9bba45864c4ae8acca9f6430290634343b7de91005d090e0ba201bdbbb5da" + }, + "next-cache-components": { + "source": "vercel-labs/next-skills", + "sourceType": "autoskills-registry", + "computedHash": "82a02f95879e9adf1a5a89f9ca2f51813e41ff3bab3c723cc8905f5a926bdd30" + }, + "next-upgrade": { + "source": "vercel-labs/next-skills", + "sourceType": "autoskills-registry", + "computedHash": "265833561c41038b4a626585b46186cff859b7f9b2254ae6b88d01648790a7b4" + }, + "nodejs-backend-patterns": { + "source": "wshobson/agents", + "sourceType": "autoskills-registry", + "computedHash": "710a5e6f83c46e8f6c43356df55c143a7375c4414559a581654ce51709138c55" + }, + "nodejs-best-practices": { + "source": "sickn33/antigravity-awesome-skills", + "sourceType": "autoskills-registry", + "computedHash": "7361ab02fb6b09913e3bdd9cf61c629ed6c17de9485e6a781054e5d437ccfc29" + }, + "react-best-practices": { + "source": "vercel-labs/agent-skills", + "sourceType": "autoskills-registry", + "computedHash": "502eb06700e2c3cb1bd7f710860528f72281320bd5171a2087b05a090b573dc1" + }, + "react-hook-form": { + "source": "pproenca/dot-skills", + "sourceType": "autoskills-registry", + "computedHash": "5cac0a77035b36714c95df1f0ccc9955713fbfc87734f189a16f5aac903eccda" + }, + "seo": { + "source": "addyosmani/web-quality-skills", + "sourceType": "autoskills-registry", + "computedHash": "c184da724d1c61ad077f27418ea8e7e88fd54bcdf98165e18be7e4681cbd5e20" + }, + "tailwind-css-patterns": { + "source": "giuseppe-trisciuoglio/developer-kit", + "sourceType": "autoskills-registry", + "computedHash": "8fd534b64b3f305ade80c207e2f9ba4338d5e409e355af9f724c36764b6709c1" + }, + "typescript-advanced-types": { + "source": "wshobson/agents", + "sourceType": "autoskills-registry", + "computedHash": "5ca0e177c6aaaba1889255691224daafdb7d71f317cc70bede1590d3907ded42" + }, + "zod": { + "source": "pproenca/dot-skills", + "sourceType": "autoskills-registry", + "computedHash": "aa8a97195b07763d37494b511f48be90979fd6826c031956c831cfc58f6a24b8" + } + } +}