CLAUDE.md: new "2026-06-09 audit" enforced-rules section (mandatory coercion helpers, deterministic id-tiebreak ordering, $queryRaw FOR-UPDATE locking discipline, uniqueness-in-transaction, guard reads, Rules-of-Hooks, seed prod guard), lint/typecheck/format commands, and the app_test/.env.test testing section with the hard-throw guard. README rewritten from the placeholder stub (setup/run/build/test/gates/migrations). REVIEW.md / REVIEW_FINDINGS.md / REVIEW_FIXES.md: the full audit record — 277 files reviewed, ~362 findings, every one fixed and verified. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1386 lines
95 KiB
Markdown
1386 lines
95 KiB
Markdown
# Codebase Audit — Fixes
|
||
|
||
> One checkbox per finding from REVIEW_FINDINGS.md, grouped by file with severity. Ticked when the fix is applied AND the gates (tsc -b --noEmit, npm run build, npx vitest run) pass. A finding resolved as intentional/by-design or deferred-needs-migration is annotated and ticked.
|
||
|
||
---
|
||
|
||
## PROJECT-LEVEL
|
||
|
||
- [x] [HIGH] No linter anywhere; formatter is unpinned and undeclared
|
||
- [x] [HIGH] Test sources are never type-checked
|
||
- [x] [MEDIUM] God-files — a few modules carry far too much
|
||
- [x] [MEDIUM] ~77 `any` escape hatches erode the strict-mode guarantee
|
||
- [x] [MEDIUM] Dependency drift + 8 known vulnerabilities (`npm audit`)
|
||
- [x] [MEDIUM] Dead dependencies (classic multi-AI residue)
|
||
- [x] [MEDIUM] No committed README; `.env.example` is stale
|
||
- [x] [LOW] `.gitignore` lets build/junk leak into the working tree
|
||
- [x] [LOW] Per-request auth does several uncached DB round-trips
|
||
|
||
## src/server.ts
|
||
|
||
- [x] [LOW] `Function('return import("vite")')()` indirect-eval hack (line 160) to load ESM Vite from the CJS bundle — intentional, brittle. Otherwise clean: plugin order, health check before rate-limit, graceful shutdown.
|
||
|
||
## src/config/env.ts
|
||
|
||
- [x] [LOW] Global `Date.prototype.toJSON` override (lines 14-22) — documented PHP-compat gotcha; app-wide. Strong boot-time secret/port validation. Clean.
|
||
|
||
## src/config/database.ts
|
||
|
||
|
||
## src/middleware/security.ts
|
||
|
||
- [x] [LOW] CSP/HSTS prod-only (line 18); `style-src 'unsafe-inline'` required by Emotion; no `report-uri`. Clean.
|
||
|
||
## src/middleware/auth.ts
|
||
|
||
|
||
## src/utils/response.ts
|
||
|
||
|
||
## src/services/auth.ts
|
||
|
||
- [x] [MEDIUM] Refresh-token rotation rejects replaced/expired tokens but does NOT revoke the token family on detected reuse (lines 273-280) — no breach containment.
|
||
- [x] [LOW] `loadAuthData` runs on every request (2-3 queries; admin full `permissions.findMany` scan). Strong otherwise (timing-safe, hashed tokens, FOR UPDATE, lockout).
|
||
|
||
## src/services/ai.service.ts
|
||
|
||
- [x] [LOW] `extractInvoice` `JSON.parse` of model output unguarded (line 351) — low risk with json_schema. Budget/cost ledger correct.
|
||
|
||
## src/routes/admin/ai.ts
|
||
|
||
- [x] [LOW] `extract-invoices` swallows per-file failure / budget-exhausted-mid-batch into partial success, no truncation signal (lines 130-131). Otherwise clean (guard, parseId/parseBody, per-user ownership, audit).
|
||
|
||
## src/routes/admin/attendance.ts
|
||
|
||
- [x] [MEDIUM] Many handlers use raw `reply.send` envelope instead of `success()`/`paginated()` (lines 32,108,118,128,142,165-239).
|
||
- [x] [LOW] `action=project_logs` (line 189) — no ownership check (location path does enforce it); `Number(query.year)` NaN fallbacks.
|
||
|
||
## src/routes/admin/audit-log.ts
|
||
|
||
- [x] [MEDIUM] `Number(query.user_id)` (line 33) → NaN → Prisma 500 instead of 400.
|
||
- [x] [LOW] `date_to`+`"T23:59:59"` (line 40) — malformed date → 500.
|
||
|
||
## src/routes/admin/auth.ts
|
||
|
||
|
||
## src/routes/admin/bank-accounts.ts
|
||
|
||
|
||
## src/routes/admin/company-settings.ts
|
||
|
||
- [x] [LOW] Extra `findFirst` for `has_logo` (302-307, deliberate); request-time `require(package.json)`; PUT `Number()` without NaN re-guard (481). Clean (magic-byte MIME sniff, race-safe create).
|
||
|
||
## src/routes/admin/customers.ts
|
||
|
||
- [x] [MEDIUM] GET `/` raw `reply.send` envelope (100-104) instead of `paginated()`. Otherwise clean (guards, FK-check before delete, allow-listed sort, parameterized search).
|
||
|
||
## src/routes/admin/dashboard.ts
|
||
|
||
- [x] [MEDIUM] N+1: `today_plan` per-cell `plan_categories.findUnique` (55-67); bounded but should be one `findMany`.
|
||
- [x] [LOW] `created_at ?? ""` dead fallback (314). Otherwise clean (gated, Promise.all, parameterized $queryRaw).
|
||
|
||
## src/routes/admin/leave-requests.ts
|
||
|
||
- [x] [MEDIUM] GET `/` raw `reply.send` envelope (57-61).
|
||
- [x] [LOW] Approval path drops `reviewer_note` (302-309) though rejection saves it. Otherwise clean (transactional approval, owner+pending cancel, audit).
|
||
|
||
## src/routes/admin/profile.ts
|
||
|
||
|
||
## src/routes/admin/invoices.ts
|
||
|
||
- [x] [LOW] GET `/` raw `reply.send` envelope (62-66); module-scoped `markOverdueInvoices` throttle can double-run under concurrency (30-39, self-correcting). Otherwise clean.
|
||
|
||
## src/routes/admin/invoices-pdf.ts
|
||
|
||
- [x] [LOW] `JSON.parse(settings.custom_fields)` (451) has no try/catch → malformed JSON makes whole PDF 500; `save=1` raw envelope (1073); loose typing. Note: `notes` IS DOMPurify+cleanQuillHtml sanitized (521).
|
||
|
||
## src/routes/admin/offers-pdf.ts
|
||
|
||
- [x] [LOW] `save=1` raw envelope (761); local `cleanQuillHtml` strips only `javascript:` (not data:/vbscript:) — defense-in-depth only (DOMPurify first, 357). Clean.
|
||
|
||
## src/routes/admin/orders.ts
|
||
|
||
- [x] [MEDIUM] From-quotation multipart parses `fields.quotationId` with raw `parseInt` (123) not schema/parseId.
|
||
- [x] [LOW] Raw `request.body` casts (203,313); unbounded multipart field/file count (only fileSize limited). Otherwise solid.
|
||
|
||
## src/routes/admin/orders-pdf.ts
|
||
|
||
- [x] [HIGH] POST renders an official "Order Confirmation" PDF entirely from client-supplied `body.items` (304-312) — `orders.view` holder can fabricate descriptions/prices not reflecting stored data (downloadable, not persisted → limited impact; trust/authz concern).
|
||
- [x] [LOW] Deprecated `.passthrough()` (30); loose untyped `body.lang`/`applyVat`; `(order as Record).payment_method` cast (387). `notes` sanitized (409).
|
||
|
||
## src/routes/admin/quotations.ts
|
||
|
||
- [x] [MEDIUM] GET `/:id` second `quotations.findUnique` for lock info right after `getOffer(id)` (123-129) — redundant query per detail view.
|
||
- [x] [LOW] `(result as any).status` cast (286); empty `catch {}` on NAS PDF delete commented "Non-fatal" but does NOT log (321-323). Lock/heartbeat/unlock scoped to `locked_by=userId` (correct).
|
||
|
||
## src/routes/admin/received-invoices.ts
|
||
|
||
- [x] [HIGH] JSON single-create computes `vat_amount=(amount*vat_rate)/100` (394-397) — treats GROSS as net, violating gross-VAT convention and INCONSISTENT with multipart (297) + update (467) paths using `vatFromGross`. Same invoice → different VAT by entry path.
|
||
- [x] [LOW] GET `/` raw envelope (89-93); `stats` `sumCzk` sequential + throws on unknown currency → 500 (135); multipart create loops per-file with no transaction (partial-success on failure).
|
||
|
||
## src/routes/admin/scope-templates.ts
|
||
|
||
- [x] [MEDIUM] No `logAudit` on ANY mutation (create/update/delete of scope + item templates) — audit-convention violation.
|
||
- [x] [MEDIUM] DELETE `?action=item&id=X` uses `Number(query.id)` no NaN/existence guard (134-139) → 500; no row-exists/`is_deleted` check.
|
||
- [x] [LOW] `?action=` dispatch mixes two entities in one handler; `Number(body.id)`/`Number(query.id)` instead of validated ids.
|
||
|
||
## src/routes/admin/plan.ts
|
||
|
||
- [x] [LOW] `(result.data as any).id`/`(result.oldData as any)` audit casts (256,310,334,361,392,416). Otherwise clean — guards on every route, parseId/parseBody, audit on every mutation, `isAdminLike` reads `roleName`, 92-day range cap.
|
||
|
||
## src/routes/admin/project-files.ts
|
||
|
||
- [x] [LOW] Folder-create (120) and move (236) read raw `request.body` bypassing parseBody (manually String-coerced). Path traversal delegated to `resolveProjectPath` (sound). Clean.
|
||
|
||
## src/routes/admin/projects.ts
|
||
|
||
- [x] [MEDIUM] GET `/` raw envelope (44-48).
|
||
- [x] [LOW] DELETE reads unvalidated body for `delete_files` (182-183); `(result as any).status` casts; POST `/:id/notes` does NOT `logAudit` though note-delete does (126-148) — inconsistent audit.
|
||
|
||
## src/routes/admin/roles.ts
|
||
|
||
- [x] [HIGH] POST `/` no uniqueness guard on `roles.name` and no admin-only gate — a non-admin `settings.roles` holder can create a role (even named `admin`) and attach ANY permission_ids → privilege escalation (59-98). PUT `/:id` can rewrite the `admin` role's permissions (no `name==="admin"` guard like DELETE) → escalation/lockout (102-138).
|
||
- [x] [MEDIUM] Role insert + permission assignment in separate transactions (67-73 vs 76-86) — partial failure leaves orphan role.
|
||
|
||
## src/routes/admin/sessions.ts
|
||
|
||
|
||
## src/routes/admin/totp.ts
|
||
|
||
- [x] [HIGH] `/enable` verifies the new secret with hardcoded SHA1/6/30 (87-94) not `config.totp.*` — config divergence → enrolled secrets verify under different params than login → lockout.
|
||
- [x] [MEDIUM] `/backup-verify` bcrypt-compares every code, no early-exit on match (307-312) — N bcrypt ops/attempt.
|
||
- [x] [LOW] `/enable` change-flow (2FA on) needs only a live code, not password (65-76); redundant dynamic re-imports shadow `config` (363-370).
|
||
|
||
## src/routes/admin/trips.ts
|
||
|
||
- [x] [MEDIUM] GET `/users` (83) guarded only by `requireAuth` — any authenticated user enumerates all active users. Information disclosure.
|
||
- [x] [MEDIUM] GET `/` raw envelope (73-77).
|
||
- [x] [LOW] In-handler permission checks run after parseBody; POST create unconditionally sets `vehicle.actual_km=end_km` (244-247) — can regress odometer (PUT/DELETE recompute via MAX). PUT/DELETE ownership checks correct.
|
||
|
||
## src/routes/admin/users.ts
|
||
|
||
- [x] [LOW] Non-admin escalation guard strips `role_id`/`is_active` (109-114, good); bounded `as Parameters<...>` cast (121); forced logout on pwd change not separately audited. Clean (parseId/parseBody/guard/audit, paginated used).
|
||
|
||
## src/routes/admin/vehicles.ts
|
||
|
||
- [x] [MEDIUM] GET `/` guarded only by `requireAuth` (15) — any authenticated user lists all vehicles + stats; mutations need `vehicles.manage`. Read should require a view permission. Otherwise clean (single groupBy, delete blocks linked trips).
|
||
|
||
## src/routes/admin/warehouse.ts
|
||
|
||
- [x] [HIGH] `confirmIssue` locks only the `sklad_issues` row, not item/batch (service:523) — two different draft issues selecting the same FIFO batch confirm concurrently, both read-modify-write `batch.quantity` -> lost update, negative stock, double-issue. Same gap in `confirmInventorySession`.
|
||
- [x] [HIGH] FIFO batch selection at issue create/update (1496-1499,1622-1625) runs with no lock, persists chosen batch on draft; availability re-checked only at confirm -> wide TOCTOU/oversell.
|
||
- [x] [HIGH] Reports stock-status/project-consumption/movement-log/below-minimum have NO pagination + per-item N+1 (3 sequential aggregates/item; 629-646,2174-2306).
|
||
- [x] [MEDIUM] PUT receipts/issues/inventory-sessions do deleteMany+createMany on lines with NO $transaction (1150-1164,1660-1673,2090-2098) -> failure leaves draft with no lines.
|
||
- [x] [MEDIUM] Attachment save: NAS write then DB insert, no compensating delete (orphan on DB failure); confirm does not verify batch.item_id===line.item_id (wrong item decremented); received_by overwritten at confirm (service:350).
|
||
- [x] [MEDIUM] 2399-line file bundles 8 sub-entities + inline mid-file multipart register (963) — split per sub-entity.
|
||
- [x] [LOW] Unvalidated query.status / Number(query.x) filters; pervasive Record-cast querystrings. Every handler guarded, tiers consistent, attachment DELETE ownership-checked — no missing-guard/IDOR.
|
||
|
||
## src/services/attendance.service.ts
|
||
|
||
- [x] [HIGH] `lockUserRow` uses `tx.$executeRaw` for a bare SELECT ... FOR UPDATE (85) — $executeRaw is for write statements; the punch/switch concurrency lock may not reliably hold. Should be `tx.$queryRaw`.
|
||
- [x] [HIGH] `getBalances` N+1: one `leave_balances.findFirst` per user in a loop (491), unbounded.
|
||
- [x] [MEDIUM] `createLeave` books a Dec->Jan range entirely against the start year (getFullYear, 1261); `getStatus` runs 5 independent queries serially.
|
||
- [x] [LOW] `deleteAttendance` (1694) has no ownership param (unlike updateAttendance); hardcoded 160 vacation default x3; ~1700-line file should be split.
|
||
|
||
## src/services/audit.ts
|
||
|
||
- [x] [LOW] `safeJsonReplacer` duck-types Prisma Decimal via method presence (14-19) — instanceof would be precise. Failure caught+logged. Clean.
|
||
|
||
## src/services/exchange-rates.ts
|
||
|
||
- [x] [HIGH] `fetch(url)` (33) has NO timeout/AbortController — a hung CNB endpoint blocks every awaiting caller indefinitely.
|
||
- [x] [MEDIUM] Global single `rateCacheTime` (14,22-24) — a hit on one key refreshes TTL for ALL keys; date-specific entries can outlive TTL; stale-fallback only checks today.
|
||
- [x] [LOW] No validation data.rates is an array before iterating (39).
|
||
|
||
## src/services/invoice-alerts.ts
|
||
|
||
- [x] [MEDIUM] Created-invoice alert amount uses only subtotal (net, 92-95) while received invoices show gross — inconsistent; per-invoice `invoice_alert_log.findUnique` N+1 (81,142).
|
||
- [x] [LOW] Dead/identical ternary branches (232-239); sequential unguarded create loop can re-fire alerts; escapeHtml omits the apostrophe char.
|
||
|
||
## src/services/invoices.service.ts
|
||
|
||
- [x] [MEDIUM] Three divergent money/VAT implementations: computeInvoiceTotals (47-74), invoiceTotalWithVat (164-196), stats VAT loop (264-270, no per-line rounding) — inconsistent totals risk.
|
||
- [x] [MEDIUM] `getInvoiceStats` serializes many toCzk awaits (247-295) — should Promise.all; createInvoice/updateInvoice take body: Record<string,any> with unchecked `as InvoiceItemInput[]` casts.
|
||
- [x] [LOW] `deleteInvoice` derives year from invoice_number.split("/")[1] (497) -> wrong sequence released for non-standard numbers; getInvoice returns null not {error,status}; paid_date settable on non-paid invoice (463).
|
||
|
||
## src/services/leave-notification.ts
|
||
|
||
- [x] [LOW] `formatDate` try/catch ineffective — new Date(invalid) returns Invalid Date, never throws (13-18). Header injection mitigated via sanitizeHeaderValue; send failure logged. Clean.
|
||
|
||
## src/services/mailer.ts
|
||
|
||
- [x] [MEDIUM] Transport hardcoded to local sendmail /usr/sbin/sendmail (5-9) — Linux-only, no SMTP fallback; on hosts without the binary every send silently fails (console.error only).
|
||
- [x] [LOW] `to` not validated/sanitized for CRLF (11) — nodemailer guards most.
|
||
|
||
## src/services/system-settings.ts
|
||
|
||
- [x] [LOW] JSON.parse of vat-rates/currencies (56,61) no shape validation; process-local cache won't propagate cross-instance within 60s TTL. Two empty catches are commented expected-condition guards (OK). Clean.
|
||
|
||
## src/services/nas-file-manager.ts
|
||
|
||
- [x] [MEDIUM] TOCTOU between resolveProjectPath validation (701-713) and the later fs op on the returned path — a symlink swapped into a path component could bypass the guard (low risk on a trusted share); ".." matched as literal substring (679, contained by prefix check).
|
||
- [x] [LOW] createProjectFolder sync/non-atomic (idempotent); MIME gate passes type-less files (SVG served as attachment w/ nosniff -> mitigated); collision suffix loop can yield name_1_2.
|
||
|
||
## src/services/nas-financials-manager.ts
|
||
|
||
- [x] [MEDIUM] Fully synchronous fs over a network share on the request path (39-73,81+) — slow/hung NAS blocks the event loop.
|
||
- [x] [LOW] isConfigured() does mkdirSync as a predicate side effect (24); saveIssuedInvoicePdf skips isConfigured() (asymmetry). resolveSafePath guard correct.
|
||
|
||
## src/services/nas-offers-manager.ts
|
||
|
||
- [x] [HIGH] Five fully-silent empty-catch blocks, no log/comment (24,46,59,76,111) — violates never-swallow rule; sibling financials manager logs all of them -> failed offer-PDF save/delete is invisible.
|
||
- [x] [LOW] deleteOfferPdf readdir/rmdir swallowed (covered above); isConfigured() mkdir side effect. resolveSafePath correct.
|
||
|
||
## src/services/numbering.service.ts
|
||
|
||
- [x] [MEDIUM] getNextSequence first-of-year INSERT race (61-75) -> @@unique([type,year]) P2002 as 500 instead of retry.
|
||
- [x] [MEDIUM] releaseSequence (116-158) SELECT-then-UPDATE with no transaction/lock/tx param — concurrent delete+create can lose a decrement or decrement an in-use number (gap/dup).
|
||
- [x] [LOW] releaseSequence reconstructs highest number from current settings (144-149); changed pattern -> no-op -> gap.
|
||
|
||
## src/services/offers.service.ts
|
||
|
||
- [x] [MEDIUM] createOffer TOCTOU: isOfferNumberTaken runs BEFORE the tx (162-167), create inside does not re-check -> concurrent dup numbers (orders.service fixed this).
|
||
- [x] [LOW] deleteOffer never calls deleteOfferPdf -> orphaned PDF on NAS; pervasive any in enrich/list money math.
|
||
|
||
## src/services/orders.service.ts
|
||
|
||
- [x] [LOW] enrichOrder/getOrder treat projects as array projects?.[0] (60,151) — confirm cardinality; status->project-status sync duplicated verbatim in both update branches (525-538 & 572-585); deleteOrder FK guard runs outside the tx (599,617-634) -> narrow re-intro window. Tx boundaries otherwise correct.
|
||
|
||
## src/services/plan.service.ts
|
||
|
||
- [x] [MEDIUM] N+1 per mutation: resolvePlanLabels(2q)+resolveCategoryLabel(1q) after each write for the audit description (483-489); bulkCreateEntries multiplies it.
|
||
- [x] [LOW] createEntry cap check outside any tx (464) — concurrent creates can exceed cap (intentional per comment). UTC date math deliberate.
|
||
|
||
## src/services/planCategory.service.ts
|
||
|
||
- [x] [LOW] slugifyCategory strips a literal combining-marks range (file-byte fragile); uniqueKey unbounded collision loop + race; sort_order=max+1 outside tx (dup possible). Soft-delete-aware checks correct.
|
||
|
||
## src/services/projects.service.ts
|
||
|
||
- [x] [LOW] updateProject renames NAS folder after DB commit outside any tx (DB/NAS divergence, failure unlogged at call site, 143); getProject does a synchronous full-base readdirSync on every GET (95); deleteProject removes folder BEFORE the DB delete (218) — inconsistent with orders.service DB-first ordering; Record<string,any> typing.
|
||
|
||
## src/services/users.service.ts
|
||
|
||
- [x] [MEDIUM] createUser username/email uniqueness via separate findFirst BEFORE a non-transactional create (116-142) — TOCTOU; duplicate -> unhandled 500 not 409 (updateUser already fixed this in a tx).
|
||
- [x] [LOW] createUser no in-service min password length (updateUser requires >=8); where: any. Last-admin + self-delete guards correct.
|
||
|
||
## src/services/warehouse.service.ts
|
||
|
||
- [x] [HIGH] cancelReservation (959-988) has NO row lock and NO transaction — a concurrent confirmIssue fulfilling the same reservation interleaves -> lost fulfillment, double-counted stock. Every other warehouse op locks the parent row; this one was missed.
|
||
- [x] [MEDIUM] selectFifoBatches orders only by received_at no id tie-break (170) + second-truncated precision -> nondeterministic FIFO; getBelowMinimumItems N+1 (232-261).
|
||
- [x] [LOW] validateIssueReservationRules N+1; confirmInventorySession negative-diff decrements item_locations independent of consumed batches (drift); surplus enters at unit_price:0 (valuation); dead itemQty/linePrice locals. Lock/FIFO/storno guards otherwise correct.
|
||
|
||
## src/schemas/common.ts
|
||
|
||
- [x] [HIGH] Contains only parseBody — NO shared coercion helpers despite the convention; the z.union number/string transform idiom is re-implemented ~150x across schemas with no shared home. Root cause of the NaN findings below.
|
||
- [x] [LOW] Deprecated ZodSchema import (1); Zod 4 prefers z.ZodType.
|
||
|
||
## src/schemas/ai.schema.ts
|
||
|
||
- [x] [LOW] Message objects plain z.object (not strict); AiBudgetSchema.budget_usd no upper bound (39, but Number.isFinite-guarded).
|
||
|
||
## src/schemas/attendance.schema.ts
|
||
|
||
- [x] [HIGH] Pervasive unguarded Number(v) transform with NO NaN check on user_id, year, vacation/sick totals, project_id, leave_hours, and all lat/lng/accuracy fields (13-145) — NaN flows to service/Prisma; project_id NaN -> bogus FK.
|
||
- [x] [MEDIUM] Time fields (arrival/departure/break) unbounded z.string() with no HH:MM regex (45-48 etc.).
|
||
- [x] [LOW] Plain z.object not strict; minutes not capped at 59.
|
||
|
||
## src/schemas/auth.schema.ts
|
||
|
||
- [x] [MEDIUM] username/password and TOTP password fields have no .max() (4-5) — unbounded string to bcrypt (file caps backup codes for this exact DoS reason but not password).
|
||
- [x] [LOW] totp_code .length(6) but not digits-only; plain objects.
|
||
|
||
## src/schemas/bank-accounts.schema.ts
|
||
|
||
- [x] [MEDIUM] iban/bic/account_number/currency/names all unbounded nullish strings (4-9) — no length caps, no IBAN/BIC/ISO format checks.
|
||
- [x] [LOW] position unguarded number-from-form (NaN); plain objects.
|
||
|
||
## src/schemas/customers.schema.ts
|
||
|
||
- [x] [MEDIUM] custom_fields: z.array(z.any()) (11,23) — fully over-permissive, arbitrary nested payload persisted as-is.
|
||
- [x] [LOW] All address/id strings unbounded, no format checks; plain objects.
|
||
|
||
## src/schemas/invoices.schema.ts
|
||
|
||
- [x] [LOW] quantity/unit_price throw on NaN (good) but vat_rate/position/order_id/customer_id use the UNGUARDED idiom; status/currency/language free strings (no enum); unbounded text; plain objects.
|
||
|
||
## src/schemas/leave-requests.schema.ts
|
||
|
||
- [x] [LOW] date_from/date_to min(1) no YYYY-MM-DD regex, no date_to>=date_from cross-check; unbounded notes; plain objects.
|
||
|
||
## src/schemas/offers.schema.ts
|
||
|
||
- [x] [LOW] Number coercions correctly refine(!isNaN) but re-implement the idiom ~7x; currency/language free strings; unbounded text; plain objects.
|
||
|
||
## src/schemas/orders.schema.ts
|
||
|
||
- [x] [LOW] Same as offers — refined but duplicated idiom; free currency/language; unbounded text; plain objects.
|
||
|
||
## src/schemas/plan.schema.ts
|
||
|
||
- [x] [LOW] intFromForm (7-10) is a proper guarded helper but defined locally (belongs in common.ts); project_id coercions (21-102) use the UNGUARDED idiom not intFromForm -> NaN FK; plain objects.
|
||
|
||
## src/schemas/planCategory.schema.ts
|
||
|
||
|
||
## src/schemas/profile.schema.ts
|
||
|
||
- [x] [MEDIUM] new_password .min(8) no .max() (8); current_password unbounded — unbounded to bcrypt.
|
||
- [x] [LOW] first_name/last_name no min/max; plain objects.
|
||
|
||
## src/schemas/projects.schema.ts
|
||
|
||
- [x] [LOW] Create FK fields refine-guard NaN, but Update + quotation_id/order_id use the UNGUARDED idiom (29-44) -> NaN FK on update; status free string (no enum); date strings no regex; plain objects.
|
||
|
||
## src/schemas/received-invoices.schema.ts
|
||
|
||
- [x] [HIGH] month/year coerce via Number(v) with NO NaN guard AND no range bounds (5-6,53-60) — month not 1-12, year unbounded; flows into the ledger + date logic.
|
||
- [x] [MEDIUM] amount/vat_rate/vat_amount unguarded idiom (NaN); vat_rate no range bound (negative/>100 accepted) — load-bearing for gross VAT back-calc.
|
||
- [x] [LOW] Unbounded text; plain objects.
|
||
|
||
## src/schemas/roles.schema.ts
|
||
|
||
- [x] [LOW] permission_ids: z.array(z.number()) not .int()/positive, no .max() length; name no slug/format; unbounded text; plain objects.
|
||
|
||
## src/schemas/scope-templates.schema.ts
|
||
|
||
- [x] [LOW] position/id/default_price unguarded number-from-form (NaN); name/title nullish/optional with no .min(1) (nameless template passes); unbounded text; plain objects.
|
||
|
||
## src/schemas/settings.schema.ts
|
||
|
||
- [x] [MEDIUM] Every numeric setting — incl. security-relevant max_login_attempts, lockout_minutes, max_requests_per_minute (15-62) — uses the unguarded idiom with NO NaN guard and NO range bounds; NaN/0/negative can disable lockout/rate-limit.
|
||
- [x] [MEDIUM] custom_fields/supplier_field_order: z.array(z.any()); available_vat_rates no int/range; available_currencies no enum.
|
||
- [x] [LOW] Email-ish fields no .email(); plain objects.
|
||
|
||
## src/schemas/trips.schema.ts
|
||
|
||
- [x] [HIGH] start_km/end_km/vehicle_id/user_id use the unguarded idiom — NO NaN check; confirmed routes/admin/trips.ts:226 end_km<start_km guard is bypassed when either is NaN (NaN compare = false) and Number() then writes NaN to Prisma. Real data-integrity bug.
|
||
- [x] [LOW] route_from/route_to unbounded; trip_date no regex; plain objects.
|
||
|
||
## src/schemas/users.schema.ts
|
||
|
||
- [x] [MEDIUM] password .min(8) no .max() (6,22-25) — unbounded to bcrypt.
|
||
- [x] [LOW] role_id unguarded NaN; username/names no .max(); plain objects.
|
||
|
||
## src/schemas/vehicles.schema.ts
|
||
|
||
- [x] [LOW] initial_km/actual_km unguarded NaN + no .min(0) (negative odometer); spz/name no .max()/plate regex; plain objects.
|
||
|
||
## src/schemas/warehouse.schema.ts
|
||
|
||
- [x] [HIGH] All movement quantities/prices (ReceiptItem/IssueItem/InventoryItem/Reservation, 124-219) use the unguarded idiom — NO NaN guard, NO .min/positivity; confirmed these feed stock math in warehouse.service -> NaN/negative corrupt inventory totals.
|
||
- [x] [MEDIUM] All FK coercions (item_id/category_id/supplier_id/project_id/location_id/batch_id/reservation_id) unguarded -> NaN FK to Prisma lookups.
|
||
- [x] [LOW] Supplier email/phone no format check; date strings no regex; unbounded text; plain objects; idiom duplicated dozens of times.
|
||
|
||
## src/utils/czech-holidays.ts
|
||
|
||
- [x] [LOW] Easter calc parses YYYY-MM-DD as UTC midnight then reads with local getters (50-57) — correct only because Prague is east of UTC; a negative-offset TZ would be a day early (safe today, fragile). Minor formatting duplication. holidayCache bounded by year.
|
||
|
||
## src/utils/date.ts
|
||
|
||
|
||
## src/utils/encryption.ts
|
||
|
||
- [x] [MEDIUM] Format detection via split(":").length===3 (27-28) — brittle (a malformed TS value falls through to base64 branch). Crypto sound: random IV per encrypt (no reuse), GCM tag verified, raw 32-byte key. A version/prefix tag would be more robust.
|
||
|
||
## src/utils/html-to-pdf.ts
|
||
|
||
- [x] [MEDIUM] process.env.CHROMIUM_PATH || "/usr/bin/chromium-browser" || "/usr/bin/chromium" (21-24) — third path is dead (second operand always truthy); if only chromium exists, launch fails. Browser lifecycle/finally sound (no leak). Injection safety handled upstream.
|
||
|
||
## src/utils/pagination.ts
|
||
|
||
- [x] [LOW] sort/search passed through unvalidated (19,22) — trust-boundary note for callers (orderBy field needs an allowlist); page/limit clamping correct.
|
||
|
||
## src/utils/planAuditDescription.ts
|
||
|
||
|
||
## src/utils/totp.ts
|
||
|
||
- [x] [LOW] console.error logs raw error on decrypt/verify failure (27) — no secret leak; swallow-to-{valid:false} intentional. Correct RFC-6238 (window:1, params from config); replay protection in auth.ts.
|
||
|
||
## src/types/fastify.d.ts
|
||
|
||
|
||
## src/types/index.ts
|
||
|
||
- [x] [LOW] JwtPayload.role vs AuthData.roleName (51,60) — the documented footgun that invites authData.role==="admin"; no bug here but a hazard.
|
||
|
||
## prisma/seed.ts
|
||
|
||
- [x] [MEDIUM] main() unconditionally deleteManys ALL role_permissions + permissions (321-322) before re-inserting — prod-destructive if ever misused (custom roles lose all permissions; stale-reconciliation at 351-368 is dead after the wipe). Dev-only by policy.
|
||
- [x] [LOW] Seeds an admin/admin user hashing the literal "admin" (389) — dev convenience, risk if run on a reachable env.
|
||
|
||
## scripts/migrate-received-invoices-to-nas.ts
|
||
|
||
- [x] [LOW] Treats saveReceivedInvoice as sync (41) — verify; sets file_data: null after a claimed-success save with no read-back verification (56) — a 0-byte/corrupt write would lose the only DB copy (one-shot migration).
|
||
|
||
## scripts/rotate-totp-key.ts
|
||
|
||
- [x] [HIGH] Hand-rolled decrypt (6-14) only handles the TS iv:enc:tag format; PHP-legacy base64 secrets (no colon) -> Buffer.from(undefined,'hex') throws inside $transaction with no per-user try/catch (58-65) -> the FIRST legacy secret aborts/rolls back the ENTIRE batch. Should reuse src/utils/encryption.ts dual-format decrypt.
|
||
- [x] [LOW] Dry-run shows [FAIL] rows for legacy secrets the real run cannot process — operator must not ignore them.
|
||
|
||
## prisma/schema.prisma
|
||
|
||
- [x] [HIGH] Entire sklad\_\* (warehouse) module declares relations with NO explicit onDelete (780-958) — header->line deletes and master-data deletes get engine-default behavior, inconsistent with the explicit cascade/setNull/restrict used everywhere else; define explicit onDelete (Cascade line->header, Restrict line->master-data).
|
||
- [x] [MEDIUM] Warehouse document numbers receipt_number/issue_number/session_number (825,877,935) are nullable and NOT unique — unlike every other document-number column (invoice/order/quotation are @unique); two receipts can share a number.
|
||
- [x] [MEDIUM] invoices.order_id/customer_id, orders.quotation_id/customer_id, quotations.customer_id, project FKs have no explicit onDelete (default Restrict — likely intended but implicit). Several \*\_id are bare Int with no relation/FK -> orphan risk (received_invoices.uploaded_by, quotations.locked_by can point at deleted users).
|
||
- [x] [MEDIUM] sklad_issues.project_id/issued_by, sklad_receipts.supplier_id/received_by, bank_accounts declare no @@index (MySQL auto-indexes the FK constraint, but non-FK hot filters are uncovered and the schema is inconsistent vs the rest).
|
||
- [x] [LOW] Most modified_at/updated_at lack @updatedAt (rely on app code -> stale-timestamp footgun); duplicate identical audit_logs created_at indexes (71,73); is_deleted absent on financial/document models (audit_logs compensates); invoice/order/project/quotation status are free strings not Prisma enums; config lists stored as LongText JSON not Json columns. Money is correctly Decimal throughout (good).
|
||
|
||
## src/admin/ui/Alert.tsx
|
||
|
||
- [x] [LOW] MUI close button gets default English aria-label "Close" amid otherwise-Czech UI (18). Clean otherwise.
|
||
|
||
## src/admin/ui/AppShell.tsx
|
||
|
||
- [x] [MEDIUM] `setTimeout(()=>logout(),400)` (27) is fire-and-forget, never cleared — fires on an unmounted tree if unmounted within 400ms.
|
||
- [x] [LOW] Decorative inline SVGs lack aria-hidden. Otherwise clean.
|
||
|
||
## src/admin/ui/Button.tsx
|
||
|
||
|
||
## src/admin/ui/Card.tsx
|
||
|
||
- [x] [MEDIUM] Always wraps children in CardContent with no opt-out/contentProps — edge-to-edge content (media/table) can't use this primitive. Otherwise clean.
|
||
|
||
## src/admin/ui/Checkbox.tsx
|
||
|
||
- [x] [MEDIUM] No disabled/name/id/indeterminate/required passthrough — minimal API forces callers to raw MUI. Otherwise clean.
|
||
|
||
## src/admin/ui/ConfirmDialog.tsx
|
||
|
||
- [x] [MEDIUM] `cancelText` (and `confirmVariant`) not frozen in `shown` while title/message/confirmText/loading are — cancel label can flicker during the close fade. message is plain string (no XSS). Otherwise clean.
|
||
|
||
## src/admin/ui/DataTable.tsx
|
||
|
||
- [x] [MEDIUM] Mobile card branch (78-173) ignores sortBy/sortDir/onSort — sorting controls vanish on phones with no alternative.
|
||
- [x] [LOW] Desktop action cells do NOT stopPropagation (237,254-275) — an action button in an `onRowClick` row fires BOTH (potential double-action; HIGH if any table pairs them). `"actions"` column key magically special-cased (undocumented). Status tints correctly channel-alpha.
|
||
|
||
## src/admin/ui/DateField.tsx
|
||
|
||
- [x] [LOW] Dual-label coexistence (picker label + outer Field); parseISO guarded by isValid. Clean.
|
||
|
||
## src/admin/ui/EmptyState.tsx
|
||
|
||
- [x] [LOW] icon sized via fontSize:48 — assumes font/SVG-inheriting glyph (an <img> won't scale). Clean.
|
||
|
||
## src/admin/ui/Field.tsx
|
||
|
||
- [x] [HIGH] `<Typography component="label">` (24) has NO `htmlFor` and injects no id into children — label not associated with the input (no click-to-focus, screen readers don't announce). System-wide since this is the shared form-label primitive.
|
||
- [x] [MEDIUM] error/hint not linked via aria-describedby and input gets no aria-invalid (41-49). SwitchField lacks disabled passthrough.
|
||
|
||
## src/admin/ui/FileUpload.tsx
|
||
|
||
- [x] [LOW] Keys include array index; remove button uses entity glyph with proper aria-label. Clean.
|
||
|
||
## src/admin/ui/FilterBar.tsx
|
||
|
||
|
||
## src/admin/ui/LoadingState.tsx
|
||
|
||
- [x] [LOW] CircularProgress has no accessible label when `label` omitted (the full-screen AppShell case). Clean otherwise.
|
||
|
||
## src/admin/ui/Modal.tsx
|
||
|
||
- [x] [MEDIUM] `cancelText` not frozen in `shown` (cancel label flickers on close); DialogContent renders LIVE children (77) so the form body still flashes its cleared state during the fade — the exact issue the freeze targets, unsolved for the body. Otherwise clean.
|
||
|
||
## src/admin/ui/MonthField.tsx
|
||
|
||
- [x] [LOW] ~90% duplicate of DateField.tsx (low-value to consolidate); parseISO guarded. Clean.
|
||
|
||
## src/admin/ui/MuiProvider.tsx
|
||
|
||
- [x] [LOW] `defaultMode="dark"` hardcoded (11); verify it agrees with ThemeContext's `data-theme`/`mui-mode` source of truth to avoid a one-frame mismatch. Clean otherwise.
|
||
|
||
## src/admin/ui/PageEnter.tsx
|
||
|
||
- [x] [LOW] Children keyed by index (animation only); useReducedMotion called before early return (hook order safe). Clean.
|
||
|
||
## src/admin/ui/PageHeader.tsx
|
||
|
||
|
||
## src/admin/ui/Pagination.tsx
|
||
|
||
|
||
## src/admin/ui/ProgressBar.tsx
|
||
|
||
- [x] [LOW] Determinate LinearProgress has no accessible name (26).
|
||
|
||
## src/admin/ui/RichText.tsx
|
||
|
||
|
||
## src/admin/ui/Select.tsx
|
||
|
||
- [x] [LOW] `slotProps as {...}` localized sound widening; spread order correct. Clean.
|
||
|
||
## src/admin/ui/SidebarNav.tsx
|
||
|
||
- [x] [LOW] Logo onError fallback relies on the fallback existing (no infinite loop). `& svg {width:18}` constraint documented; OdinMark overrides via wrapper + 70% !important (verified). Clean.
|
||
|
||
## src/admin/ui/StatCard.tsx
|
||
|
||
|
||
## src/admin/ui/StatusChip.tsx
|
||
|
||
|
||
## src/admin/ui/Tabs.tsx
|
||
|
||
- [x] [LOW] TabPanel unmounts inactive content with no role="tabpanel"/aria-labelledby wiring (a11y-incomplete).
|
||
|
||
## src/admin/ui/TextField.tsx
|
||
|
||
|
||
## src/admin/ui/ThemeToggle.tsx
|
||
|
||
|
||
## src/admin/ui/TimeField.tsx
|
||
|
||
- [x] [LOW] Module-level REF Date used read-only by date-fns (safe); validates via isValid. Clean.
|
||
|
||
## src/admin/ui/index.ts
|
||
|
||
|
||
## src/admin/ui/navData.tsx
|
||
|
||
- [x] [LOW] Non-matchPrefix branch uses bare `pathname.startsWith(item.path)` (no boundary) — fragile if a `/x-foo` route is ever added (items needing exactness set end:true). Verbose inline SVG set.
|
||
|
||
## src/admin/ui/useDialogScrollLock.ts
|
||
|
||
- [x] [MEDIUM] Module-level lock state — production-safe (balanced [open] dep + cleanup), but a StrictMode/hot-reload double-invoke can transiently skew lockCount.
|
||
- [x] [LOW] Stale comment references removed base.css (now GlobalStyles.tsx).
|
||
|
||
## src/admin/pages/Warehouse.tsx
|
||
|
||
- [x] [LOW] Dead/duplicated `MovementRow` type drift (75); redundant per-column `?? 0`; cosmetic isLoading gating. Clean otherwise (read-only).
|
||
|
||
## src/admin/pages/WarehouseCategories.tsx
|
||
|
||
- [x] [MEDIUM] `sort_order` via `parseInt(...)||0` no radix (308) — drops a legit 0 and truncates "3.9"->3; convention wants Number/NaN-guard.
|
||
- [x] [LOW] Redundant `saving` state duplicates `submitMutation.isPending`. Broad ["warehouse"] invalidation correct.
|
||
|
||
## src/admin/pages/WarehouseInventory.tsx
|
||
|
||
- [x] [MEDIUM] `items` column renders `s.items?.length ?? 0` (125) but list endpoint omits items -> always shows 0; should use a \_count. Otherwise clean.
|
||
|
||
## src/admin/pages/WarehouseInventoryDetail.tsx
|
||
|
||
- [x] [HIGH] Rules-of-Hooks: `return <Forbidden/>` (70) precedes `useApiMutation` (72) -> hook-count mismatch crash on permission change.
|
||
- [x] [LOW] `.../0/confirm` URL fallback is dead (handler guards null). Broad invalidation correct.
|
||
|
||
## src/admin/pages/WarehouseInventoryForm.tsx
|
||
|
||
- [x] [HIGH] Rules-of-Hooks: `return <Forbidden/>` (83) precedes `useApiMutation` (94).
|
||
- [x] [MEDIUM] `actual_qty` via `Number(e.target.value)` no NaN guard (249) — clearing field -> NaN in payload (receipt/issue forms use parseDecimal). validate() doesn't check qty finite.
|
||
|
||
## src/admin/pages/WarehouseIssueDetail.tsx
|
||
|
||
- [x] [HIGH] Rules-of-Hooks: `return <Forbidden/>` (73) precedes useApiMutation (77,89).
|
||
- [x] [MEDIUM] Project link uses raw `<Typography component="a" href>` (316-330) not RouterLink -> full-page reload, breaks SPA nav.
|
||
- [x] [LOW] Dead `/0/` URL fallbacks.
|
||
|
||
## src/admin/pages/WarehouseIssueForm.tsx
|
||
|
||
- [x] [HIGH] Rules-of-Hooks: `return <Forbidden/>` (267) precedes useApiMutation/useState (335,347).
|
||
- [x] [MEDIUM] `confirmIssue` setState(id) then immediately `await mutateAsync`; the url: closure reads pre-update id -> first confirm hits `/0/confirm`. Pass id as mutation variable.
|
||
- [x] [LOW] Form-seeding effects (238,242) are effect-as-derived-state.
|
||
|
||
## src/admin/pages/WarehouseIssues.tsx
|
||
|
||
|
||
## src/admin/pages/WarehouseItemDetail.tsx
|
||
|
||
- [x] [HIGH] Rules-of-Hooks: `return <Forbidden/>` (157) precedes useApiMutation (164,180).
|
||
- [x] [MEDIUM] Error-handling effect navigates away (148-153) — effect-as-control-flow (could be a render branch).
|
||
- [x] [LOW] Form-seed logic duplicated 3x (128/132/248-261); `useModalLock(editing)` (155) locks page scroll for an in-page (non-modal) edit form — likely unintended.
|
||
|
||
## src/admin/pages/WarehouseItems.tsx
|
||
|
||
|
||
## src/admin/pages/WarehouseLocations.tsx
|
||
|
||
- [x] [LOW] toggleMutation toasts in handler not onSuccess (inconsistent); dead czechPlural ternary. Guard correctly after hooks. Clean otherwise.
|
||
|
||
## src/admin/pages/WarehouseReceiptDetail.tsx
|
||
|
||
- [x] [HIGH] Rules-of-Hooks: `return <Forbidden/>` (97) precedes useApiMutation (101,113,126).
|
||
- [x] [LOW] Dead `/0/` URL fallbacks; attachment link safe (noopener). Broad invalidation correct.
|
||
|
||
## src/admin/pages/WarehouseReceiptForm.tsx
|
||
|
||
- [x] [HIGH] Rules-of-Hooks: `return <Forbidden/>` (203) precedes useApiMutation/useState (269,281).
|
||
- [x] [MEDIUM] Same stale-closure confirm bug as IssueForm (307-309) -> `/0/confirm` on first confirm.
|
||
- [x] [LOW] Uses imperative document.createElement input + hand-rolled dropzone instead of kit FileUpload (620-633).
|
||
|
||
## src/admin/pages/WarehouseReceipts.tsx
|
||
|
||
|
||
## src/admin/pages/WarehouseReports.tsx
|
||
|
||
- [x] [MEDIUM] Project-consumption & movement-log tabs fetch via raw apiFetch + useState (263-290,386-413) not React Query — results NOT in ["warehouse"] cache, won't refresh on mutations; existing query options unused.
|
||
- [x] [LOW] Duplicated MovementLogRow type (46); empty `catch {}` -> setData([]) swallows fetch errors (285,408); raw `row.date` not formatDate'd (426).
|
||
|
||
## src/admin/pages/WarehouseReservations.tsx
|
||
|
||
- [x] [MEDIUM] Create & cancel use raw apiFetch + manual invalidate (170-217) not useApiMutation (broad key correct, but plumbing duplicated).
|
||
- [x] [LOW] Empty catches don't log; `quantity` via Number() no NaN guard (457) and `NaN<=0` is false so NaN can be submitted.
|
||
|
||
## src/admin/pages/WarehouseSuppliers.tsx
|
||
|
||
- [x] [MEDIUM] `toggleActive` setState(id) then immediate `await mutateAsync` — url: closure reads null id -> PUT hits the collection root (wrong endpoint) on first toggle. Pass id as mutation variable.
|
||
- [x] [LOW] Redundant `saving` state. Guard correctly after hooks. Clean otherwise.
|
||
|
||
## src/admin/pages/Attendance.tsx
|
||
|
||
- [x] [MEDIUM] `calculateBusinessDays` parses YYYY-MM-DD via `new Date()` (UTC midnight) + local `getDay()` (480-488) — can mis-count business days in the evening Prague window.
|
||
- [x] [LOW] leaveRequestMutation invalidates attendance/leave-requests/users but NOT ["leave"] (303) — approver's pending queue (["leave","pending"]) won't auto-refresh; live clock (676) never ticks; notes synced via effect can clobber unsaved edits (336-340). todayLocalStr used (good).
|
||
|
||
## src/admin/pages/AttendanceAdmin.tsx
|
||
|
||
|
||
## src/admin/pages/AttendanceBalances.tsx
|
||
|
||
- [x] [HIGH] Rules-of-Hooks: `return <Forbidden/>` (171) precedes useApiMutation (173,189).
|
||
- [x] [LOW] parseFloat on vacation/sick fields -> NaN on empty (770,781,794) into payload.
|
||
|
||
## src/admin/pages/AttendanceCreate.tsx
|
||
|
||
- [x] [LOW] `leave_hours: parseFloat()` -> NaN on cleared field (190). todayLocalStr used; broad invalidation. Clean otherwise.
|
||
|
||
## src/admin/pages/AttendanceHistory.tsx
|
||
|
||
- [x] [MEDIUM] ~865-line file with inline print-HTML template + hidden print JSX — split candidate.
|
||
- [x] [LOW] Duplicates holiday/business-day logic that also exists in attendanceHelpers (84-149). normalizeDateStr used; hooks before guard. Clean otherwise.
|
||
|
||
## src/admin/pages/AttendanceLocation.tsx
|
||
|
||
- [x] [LOW] Dead LoadingState branch (199 unreachable, `!record` returns null at 187 while pending). Popup via createElement/textContent (no XSS); Leaflet cleanup correct. Clean.
|
||
|
||
## src/admin/pages/AuditLog.tsx
|
||
|
||
- [x] [HIGH] Rules-of-Hooks: `return <Forbidden/>` (185-187) precedes useApiMutation (189).
|
||
|
||
## src/admin/pages/CompanySettings.tsx
|
||
|
||
- [x] [MEDIUM] ~1503-line file — split candidate (info/banking/numbering/currency-VAT/logo cards).
|
||
- [x] [MEDIUM] Uses deprecated MUI `inputProps` (1312) — silently ignored in v7 so min/step not applied (rest of file uses slotProps.htmlInput).
|
||
- [x] [LOW] Doesn't wrap in PageEnter (raw motion.div per card — intentional embedded mode); fieldOrder index logic fragile. Blob cleanup correct.
|
||
|
||
## src/admin/pages/Dashboard.tsx
|
||
|
||
- [x] [LOW] DashData hand-rolled with `[key]:unknown` index sig + `as DashData` cast (76,88) bypasses query typing; 2FA handlers use raw apiFetch + manual invalidate (179-211) not useApiMutation. getCzechDate used (good); gated sections. Clean otherwise.
|
||
|
||
## src/admin/pages/InvoiceDetail.tsx
|
||
|
||
- [x] [MEDIUM] ~2243-line file — split candidate (SortableInvoiceRow / paid read-only view / create-edit form).
|
||
- [x] [MEDIUM] UTC anti-pattern `new Date(...).toISOString().split("T")[0]` for due_date default (494), issue/due/tax dates (645-651) and computedDueDate (818-823) — the latter reaches the saved payload (977), persisting a day-early due date in evening Prague.
|
||
- [x] [LOW] Editable `notes` state never wired to an input in non-paid mode (only form.notes saved) — confirm intended. DOMPurify correctly applied before the one dangerouslySetInnerHTML (1562) — no XSS. Money math guarded; blob cleanup correct.
|
||
|
||
## src/admin/pages/Invoices.tsx
|
||
|
||
- [x] [LOW] handleDelete/toggleStatus/handlePdf use raw apiFetch + manual invalidate (broad keys, correct behavior, off-convention); overdue rowSx UTC-vs-local compare off by a day near midnight (506-507, visual only). Clean otherwise.
|
||
|
||
## src/admin/pages/LeaveApproval.tsx
|
||
|
||
- [x] [HIGH] Rules-of-Hooks: `return <Forbidden/>` (158) precedes useApiMutation (160,173).
|
||
|
||
## src/admin/pages/LeaveRequests.tsx
|
||
|
||
|
||
## src/admin/pages/Login.tsx
|
||
|
||
- [x] [LOW] `verify2FA(loginToken!, ...)` non-null assertion (105) — a null token (server returns requires2FA w/o token) would pass `null!`. Otherwise secure: tokens via useAuth, no persisted token state, TOTP input strips non-digits, autoComplete=one-time-code, renders outside AppShell.
|
||
|
||
## src/admin/pages/NotFound.tsx
|
||
|
||
|
||
## src/admin/pages/Odin.tsx
|
||
|
||
|
||
## src/admin/pages/OfferDetail.tsx
|
||
|
||
- [x] [HIGH] Heartbeat effect (730) deps omit `isCompleted` though the guard reads it (731) — lock heartbeat keeps running after transition to dokoncena (stale closure).
|
||
- [x] [MEDIUM] ~2059-line file — split candidate (SortableItemRow / scope-sections / draft logic).
|
||
- [x] [LOW] `payload: any` (884) and item-update casts; parseInt/parseFloat on qty/price yield NaN on cleared field (rendered until totals guard). Broad invalidation correct; draft parse guarded; blob revoked.
|
||
|
||
## src/admin/pages/Offers.tsx
|
||
|
||
- [x] [LOW] Expired check uses idiosyncratic `new Date(new Date().toDateString())` (658, functional). Otherwise clean (broad keys, kit, blob revoke, keys).
|
||
|
||
## src/admin/pages/OffersCustomers.tsx
|
||
|
||
- [x] [LOW] parseInt without radix on `key.split("_")[1]` (239,259,675, harmless decimal). Otherwise clean (custom-field re-key correct, broad invalidation, a11y).
|
||
|
||
## src/admin/pages/OffersTemplates.tsx
|
||
|
||
- [x] [LOW] openEdit fetches via raw apiFetch (bypasses RQ cache) for on-demand modal. Guard is before a hooks-free body (no violation). Otherwise clean.
|
||
|
||
## src/admin/pages/OrderDetail.tsx
|
||
|
||
- [x] [CRITICAL] Rules-of-Hooks: 3 useApiMutation (181,187,193) AFTER `return <Forbidden/>` (179) — crash when orders.view absent / permission flips.
|
||
- [x] [LOW] `valid_transitions?.filter(...).length! > 0` (466) misleading non-null assertion. Section rich text DOMPurify-sanitized before dangerouslySetInnerHTML (760) — no XSS. Broad invalidation; blob cleanup.
|
||
|
||
## src/admin/pages/Orders.tsx
|
||
|
||
- [x] [LOW] vat_rate sent as raw string in JSON create (272, relies on Zod coercion). Otherwise clean — Forbidden return (312) after all hooks; broad invalidation; keys.
|
||
|
||
## src/admin/pages/PlanWork.tsx
|
||
|
||
- [x] [LOW] Several mutation callbacks use `body: any`/`data: any` (185,395,412,476,491). `isoDate` toISOString().slice(0,10) is correct (Plan UTC convention, not a finding). Forbidden return after hooks; pulse timer cleaned; todayIsoLocal used. Clean otherwise.
|
||
|
||
## src/admin/pages/ProjectDetail.tsx
|
||
|
||
- [x] [CRITICAL] Rules-of-Hooks: 4 useApiMutation (179,194,203,209) AFTER `return <Forbidden/>` (177).
|
||
- [x] [LOW] formatNoteDate duplicates utils/formatters date logic. Broad invalidation (projects/warehouse); keys.
|
||
|
||
## src/admin/pages/Projects.tsx
|
||
|
||
- [x] [LOW] List query uses raw `search` not debounced (219) — every keystroke refetches (Offers/Orders debounce). Hooks (useApiMutation 124,173) precede the guard (222) — correct ordering. Broad invalidation; keys.
|
||
|
||
## src/admin/pages/ReceivedInvoices.tsx
|
||
|
||
- [x] [MEDIUM] `toDateInput` uses `toISOString().split("T")[0]` (453) to seed edit-form dates — forbidden UTC-split (day-early evening Prague). Use normalizeDateStr.
|
||
- [x] [MEDIUM] ~1237-line file — split candidate (upload modal + edit modal).
|
||
- [x] [LOW] Render-phase ref mutation `hasLoadedOnce.current=true` (279); loose parseFloat typing (557). VAT-from-gross preserved; broad invalidation; blob cleanup.
|
||
|
||
## src/admin/pages/Settings.tsx
|
||
|
||
- [x] [HIGH] Rules-of-Hooks: 5 useApiMutation (319,339,354,372,390) AFTER `return <Navigate/>` (315) — useQuery/useMemo/useState are above, mutations are not.
|
||
- [x] [MEDIUM] ~1354-line file — split candidate (roles / system-settings / system-info).
|
||
- [x] [LOW] `systemInfo as Record<string,any>` (679); O(n^2 log n) permission-group sort (1243, negligible). Tab state derived from URL (good); broad invalidation.
|
||
|
||
## src/admin/pages/Trips.tsx
|
||
|
||
- [x] [HIGH] Rules-of-Hooks: 2 useApiMutation (190,204) AFTER `return <Forbidden/>` (188).
|
||
- [x] [LOW] Dead `[,setLastKm]` state (186, only setter used); parseInt without radix (287,315-316). todayLocalStr used; broad invalidation (trips/vehicles); keys.
|
||
|
||
## src/admin/pages/TripsAdmin.tsx
|
||
|
||
- [x] [HIGH] Rules-of-Hooks: 2 useApiMutation (236,255) AFTER `return <Forbidden/>` (231).
|
||
- [x] [LOW] handlePrint uses raw printWindow.document.write of app data (344-417) — low XSS surface (React-escaped innerHTML) but fragile; iframe/template safer. Broad invalidation; keys.
|
||
|
||
## src/admin/pages/TripsHistory.tsx
|
||
|
||
- [x] [LOW] `totals` computed before the Forbidden return (wasteful when unauthorized) — but all hooks precede the return (no violation). Read-only; kit; keys. Clean.
|
||
|
||
## src/admin/pages/UiKit.tsx
|
||
|
||
|
||
## src/admin/pages/Users.tsx
|
||
|
||
- [x] [HIGH] Rules-of-Hooks: 3 useApiMutation (126,152,162) AFTER `return <Forbidden/>` (173).
|
||
- [x] [LOW] `roles[0]?.id ? ... : ""` default would skip a role id of 0 (never 0 in practice). Shared broad USER_INVALIDATE array (good); self-deactivation guarded; keys.
|
||
|
||
## src/admin/pages/Vehicles.tsx
|
||
|
||
- [x] [HIGH] Rules-of-Hooks: 3 useApiMutation (121,136,146) AFTER `return <Forbidden/>` (162).
|
||
- [x] [LOW] Raw MUI `<Chip>` instead of kit StatusChip (264); `select: data as unknown as Vehicle[]` double-cast (100); parseInt without radix (404). Broad invalidation (vehicles/trips); rowInactive; keys.
|
||
|
||
## src/admin/components/AlertContainer.tsx
|
||
|
||
- [x] [LOW] Stacking offset keyed off array index (15) — removing a middle alert reflows/can overlap with 3+ alerts. Keys use alert.id. Otherwise clean.
|
||
|
||
## src/admin/components/AttendanceShiftTable.tsx
|
||
|
||
- [x] [LOW] Project-cell key falls back to index when log.id nullish (119); duration math not memoized. Otherwise clean (no any, keys, no effects).
|
||
|
||
## src/admin/components/BulkAttendanceModal.tsx
|
||
|
||
- [x] [LOW] Select-all label shows "Odznacit vse" when users empty (BulkPlanModal guards this — inconsistent). Otherwise clean.
|
||
|
||
## src/admin/components/BulkPlanModal.tsx
|
||
|
||
|
||
## src/admin/components/ErrorBoundary.tsx
|
||
|
||
- [x] [LOW] componentDidCatch only console.errors (22) — no telemetry hook. Correct boundary pattern otherwise.
|
||
|
||
## src/admin/components/Forbidden.tsx
|
||
|
||
|
||
## src/admin/components/OrderConfirmationModal.tsx
|
||
|
||
- [x] [MEDIUM] Local state (step/items/applyVat/lang) never reset on reopen — component is permanently mounted, toggled by isOpen; reopening shows prior step/edits and items captured once via useState(initialItems) go stale.
|
||
- [x] [MEDIUM] `finally` closes the modal even when onGenerate throws (58-77) — a generation error drops edited items with only a transient toast.
|
||
- [x] [LOW] Editable rows key={i} — deleting a non-last row shifts indices (focus jumps; data stays correct since controlled).
|
||
|
||
## src/admin/components/PlanCategoriesModal.tsx
|
||
|
||
- [x] [LOW] Label TextField uncontrolled defaultValue with no key={c.label} (148) — won't re-seed on external rename (color input does add key); inline delete-confirm instead of kit ConfirmDialog. Broad ["plan","dashboard","audit-log"] invalidation correct; onBlur-commit.
|
||
|
||
## src/admin/components/PlanCellModal.tsx
|
||
|
||
- [x] [MEDIUM] Props onSaveEntry/onUpdateEntry/onSaveOverride/onUpdateOverride typed `any` (77-82) — untyped save payloads on the most bug-prone path.
|
||
- [x] [LOW] EditForm derives state from `initial` via useState (201-206) relying on parent remount per record (no key reset — fragile if parent swaps entryId same-kind); odd Czech phrasing (415). Discriminated-union mode + kit otherwise strong.
|
||
|
||
## src/admin/components/PlanGrid.tsx
|
||
|
||
- [x] [LOW] `projects.find(...)` inside per-cell render loop (612-616) O(days*users*cells\*projects) — build a useMemo Map (rarely hit since server embeds label); eachDay/days recomputed every render (not memoized). todayIso uses local Y/M/D (correct); thorough aria-labels.
|
||
|
||
## src/admin/components/PlanRangeChips.tsx
|
||
|
||
- [x] [LOW] `readonly` prop accepted then discarded via `void readonly` (17) — dead prop (PlanGrid still passes it). className styling intentional (classes defined in PlanGrid root). Otherwise clean.
|
||
|
||
## src/admin/components/ProjectFileManager.tsx
|
||
|
||
- [x] [MEDIUM] Drag-over has no drag-depth counter — handleDragLeave (377-380) fires on every child boundary -> drop overlay flickers during drag.
|
||
- [x] [MEDIUM] File ops use raw apiFetch + manual invalidate of sub-key `["projects",id,"files"]` (349-351) not broad ["projects"] (deliberate scope, documented deviation).
|
||
- [x] [LOW] `catch {}` substitutes a generic toast but never logs the real error (336,408,438,471,508); sequential per-file upload; breadcrumb join breaks if a folder name contains "/". Escape/Enter handling + canManage gating correct.
|
||
|
||
## src/admin/components/RichEditor.tsx
|
||
|
||
- [x] [LOW] `_delta: any` on the Quill onChange param (91, react-quill-new loose types — could be unknown); useLayoutEffect font-set no-ops if ref not ready (mounts sync in practice). Never injects HTML — consumers DOMPurify-sanitize. No XSS.
|
||
|
||
## src/admin/components/ShiftFormModal.tsx
|
||
|
||
- [x] [MEDIUM] `leave_hours` via parseFloat (330-331) writes NaN to form state on empty input — serializes to invalid; should fall back to 0 like the Number(...)||0 pattern.
|
||
- [x] [LOW] Module-level mutable `_logKeyCounter` (20) shared across instances (fragile under StrictMode/concurrent instances) — use a per-instance ref/randomUUID; server-loaded logs may lack \_key -> index-key risk. Derived isWorkType/isCreate + kit otherwise correct.
|
||
|
||
## src/admin/components/ShortcutsHelp.tsx
|
||
|
||
- [x] [LOW] Entire component is `return null` — dead stub; remove the file + references, or wire it up.
|
||
|
||
## src/admin/components/dashboard/DashActivityFeed.tsx
|
||
|
||
|
||
## src/admin/components/dashboard/DashAttendanceToday.tsx
|
||
|
||
- [x] [LOW] Key `${u.user_id}-${i}` index-suffixed (75); user_id alone is unique. Clean.
|
||
|
||
## src/admin/components/dashboard/DashKpiCards.tsx
|
||
|
||
- [x] [LOW] buildKpiCards recomputed every render (124, cheap); framer-motion entrance has no reduced-motion branch (JS-driven animate not stopped by the GlobalStyles CSS reset) — applies to DashQuickActions/DashProfile/DashSessions too. Clean otherwise.
|
||
|
||
## src/admin/components/dashboard/DashProfile.tsx
|
||
|
||
- [x] [MEDIUM] handleSubmit (175-197) uses raw apiFetch and invalidates NO React Query domain after a profile PUT — anything keyed on user data goes stale.
|
||
- [x] [LOW] `delete (dataToSave as any).current_password` casts (172-173); magic 300ms sleep before success toast (189); no reduced-motion branch.
|
||
|
||
## src/admin/components/dashboard/DashQuickActions.tsx
|
||
|
||
- [x] [HIGH] handleTripSubmit (157-176) POSTs a trip via raw apiFetch and NEVER invalidates ["trips"]/["dashboard"] (no useQueryClient imported) — list/dashboard stay stale until manual refetch.
|
||
- [x] [MEDIUM] `result` from .json() untyped any (96,116,164); last_km assigned into a string field with no String() coercion.
|
||
- [x] [LOW] Empty catches don't log (104-106,120-122); parseInt without radix. tripDistance derived (good); todayLocalStr used.
|
||
|
||
## src/admin/components/dashboard/DashSessions.tsx
|
||
|
||
- [x] [LOW] Correctly invalidates ["sessions"]; shared `deleting` flag (one dialog at a time); no reduced-motion branch. Clean.
|
||
|
||
## src/admin/components/dashboard/DashTodayPlan.tsx
|
||
|
||
- [x] [LOW] key={i} on plans (96, tiny static list); color-mix bgcolor (43, modern-browser only). Clean.
|
||
|
||
## src/admin/components/odin/InvoiceReviewCard.tsx
|
||
|
||
|
||
## src/admin/components/odin/OdinChat.tsx
|
||
|
||
- [x] [LOW] saveInvoice invalidates ["invoices"] (353) — broad key DOES cover received invoices (keyed ["invoices","received"]), so correct; persist() is fire-and-forget (failure only console.error, user not told history unsaved). Seed/auto-scroll effects are legitimate. Invoice-only guard correct.
|
||
|
||
## src/admin/components/odin/OdinComposer.tsx
|
||
|
||
- [x] [LOW] `fileRef as RefObject<HTMLInputElement>` unnecessary cast (48); Enter-to-send calls onSubmit even when !canSubmit (parent re-guards — no double-send). Clean.
|
||
|
||
## src/admin/components/odin/OdinMark.tsx
|
||
|
||
|
||
## src/admin/components/odin/OdinSidebar.tsx
|
||
|
||
- [x] [LOW] Conversation row is a clickable <Box onClick> (128) — not a button/no role/tabIndex, so not keyboard-focusable (a11y gap); kebab has aria-label. Clean otherwise.
|
||
|
||
## src/admin/components/odin/OdinThread.tsx
|
||
|
||
- [x] [LOW] Bubble key={i} (174, append-only so stable); content via Typography pre-wrap (no XSS); reduced-motion gated. Clean.
|
||
|
||
## src/admin/components/odin/types.ts
|
||
|
||
|
||
## src/admin/components/warehouse/ItemPicker.tsx
|
||
|
||
- [x] [MEDIUM] useQuery(warehouseItemListOptions({search: inputValue})) fires on EVERY keystroke with no debounce (30-32) — request spam; useDebounce hook exists. renderOption correctly extracts key (good).
|
||
|
||
## src/admin/components/warehouse/ReservationPicker.tsx
|
||
|
||
- [x] [MEDIUM] On item/project change, if the selected `value` is no longer in `reservations`, the component doesn't reset/notify the parent — parent can hold a stale reservationId. Redundant Number() casts (41,49). Typed (no any).
|
||
|
||
## src/admin/AdminApp.tsx
|
||
|
||
- [x] [LOW] No RequireAuth wrapper visible in the router — auth gating delegated to AppShell/pages (a new top-level route outside AppShell would be public by default); Dashboard eagerly imported (not lazy) (13). Clean otherwise.
|
||
|
||
## src/admin/GlobalStyles.tsx
|
||
|
||
|
||
## src/admin/theme.ts
|
||
|
||
- [x] [LOW] containedPrimary hover/active transform lacks a prefers-reduced-motion guard present on MuiButton.root/MuiOutlinedInput (89-95); iconBadgeSx key cast loses type-safety (248, behavior fine). Clean otherwise.
|
||
|
||
## src/admin/theme.test.ts
|
||
|
||
- [x] [LOW] Heavy `as any`/`as unknown as ...` to reach theme internals (deliberate test shortcut). Clean.
|
||
|
||
## src/App.tsx
|
||
|
||
- [x] [LOW] AdminLoader reads data-theme once at render — dark fallback on cold load before ThemeContext sets it (acceptable). Clean otherwise.
|
||
|
||
## src/main.tsx
|
||
|
||
|
||
## src/vite-env.d.ts
|
||
|
||
|
||
## src/context/ThemeContext.tsx
|
||
|
||
- [x] [MEDIUM] Theme single-source is correct (mui-mode only; legacy boha-theme read-once-then-deleted) BUT the "dark" default is hardcoded twice (here:30 and MuiProvider.tsx:11) — change one without the other and page (MUI) vs toggle desync on first paint; reconcile to one constant.
|
||
- [x] [LOW] No `storage` event listener — two tabs won't sync theme until reload. View-Transition/flushSync + reduced-motion fallback correct.
|
||
|
||
## src/admin/context/AlertContext.tsx
|
||
|
||
|
||
## src/admin/context/AuthContext.tsx
|
||
|
||
- [x] [MEDIUM] `apiRequest` (318-350) duplicates the refresh+retry logic of utils/api.ts apiFetch — two parallel 401-retry paths with DIVERGENT behavior (apiRequest ignores abort signals and doesn't set the session-expired flag). Consolidate onto apiFetch.
|
||
- [x] [MEDIUM] Proactive-refresh timer fires a closure-captured `silentRefresh` (memoized [] + eslint-disable, 117-125); works only because everything is read through refs — fragile cyclic setup.
|
||
- [x] [LOW] In-flight refresh not cancelled on logout (a resolving refresh could re-setUser); checkSession maps user twice (181-182). Token held in-memory only (secure). Dedup + expires_in proactive refresh otherwise correct.
|
||
|
||
## src/admin/utils/api.ts
|
||
|
||
- [x] [LOW] Module-singleton `state` with injected getTokenFn/refreshFn couples a module global to React lifecycle (resetApiState for tests); on a 401 with aborted signal returns the 401 response not the 499 sentinel used elsewhere (inconsistent); its refreshPromise dedup is a second single-flight layer alongside AuthContext's. Up-front refresh/abort/FormData handling correct.
|
||
|
||
## src/admin/utils/attendanceHelpers.ts
|
||
|
||
- [x] [MEDIUM] getTimePart (105-109) uses `new Date(datetime).getHours()` (TZ-dependent) while sibling extractTime parses the string to avoid TZ conversion — can yield a different hour than displayed at DST/offset edges, violating the file's own stated ethos.
|
||
- [x] [LOW] getLeaveTypeBadgeClass returns badge-\* classes that exist only in AttendanceHistory's print <style> (near-dead as a general helper); calculateNightMinutes mixes timestamp windows with local-midnight math (Prague-TZ-coupled).
|
||
|
||
## src/admin/utils/dashboardHelpers.ts
|
||
|
||
- [x] [MEDIUM] STATUS_DOT_CLASS (7-12) is exported but referenced by ZERO files and maps to dash-status-\* CSS classes removed in the MUI migration — dead code.
|
||
- [x] [LOW] getCzechDate ISO-week calc (59-62) is the naive formula (off-by-one near year boundaries, display-only). Other exports live/correct.
|
||
|
||
## src/admin/utils/formatters.ts
|
||
|
||
|
||
## src/admin/hooks/useAttendanceAdmin.ts
|
||
|
||
- [x] [HIGH] ~1402-line file mixing the React hook, computeUserTotals payroll math, and ~370 lines of print-HTML templating — the two pure modules have no React dep and should be extracted to utils/ for testability.
|
||
- [x] [HIGH] Does data fetching with useEffect+useState+manual setLoading (818-922) against the "React Query for fetching" convention, AND also invalidateQueries(["attendance"]) after mutations — two sources of truth (its own fetchData cache vs the ["attendance"] domain).
|
||
- [x] [MEDIUM] fetchData useCallback closes over `alert` (captured first render) with eslint-disable + initialLoadDone ref; pervasive Record<string,any>/any in print/payroll helpers (350,380,597,1319); 300ms sleeps before success toasts as a sync crutch.
|
||
- [x] [LOW] handlePrint document.write of escaped data; today/now recomputed every render.
|
||
|
||
## src/admin/hooks/useDebounce.ts
|
||
|
||
|
||
## src/admin/hooks/useModalLock.ts
|
||
|
||
- [x] [LOW] Module-level activeLocks counter locks only document.body.style.overflow (3-7) — CLAUDE.md says dialogs must lock <html> (via useDialogScrollLock) because html{overflow-x:hidden} makes <html> the scroller; this body-only lock likely doesn't prevent background scroll for the app layout (superseded/near-dead — confirm callers).
|
||
|
||
## src/admin/hooks/usePaginatedQuery.ts
|
||
|
||
- [x] [LOW] `options: any` param (24, eslint-disable) loses generic typing; `as UseQueryOptions<...>` unchecked. Clean otherwise.
|
||
|
||
## src/admin/hooks/usePlanWork.ts
|
||
|
||
- [x] [MEDIUM] Optimistic rollback stashed via `(createEntry as any)._rolled = rolled` (264,332,369,403,454,481) — mutating the TanStack mutation object is non-idiomatic and NOT concurrency-safe (two in-flight mutations overwrite each other's \_rolled → wrong-snapshot rollback). Use onMutate context.
|
||
- [x] [MEDIUM] Heavy any on mutationFn/onSuccess (237,243,379,385,491); gridQuery.queryFn does r.json() with no res.ok check (92-94) — treats an error body as undefined data.
|
||
- [x] [LOW] isoDate UTC slice is correct for Plan, but anchor seeded from a LOCAL `new Date()` then read with getUTC\* (27-31,70,80-86) — verify the late-evening Prague window doesn't shift the week/month anchor. keepPreviousData + broad invalidation correct.
|
||
|
||
## src/admin/hooks/useReducedMotion.ts
|
||
|
||
- [x] [LOW] setReduced(false) initial then reconciles in effect — one render with motion for reduced-motion users (documented intentional; a lazy matchMedia initializer would avoid the flash). Listener add/remove correct.
|
||
|
||
## src/admin/hooks/useTableSort.ts
|
||
|
||
|
||
## src/admin/lib/apiAdapter.ts
|
||
|
||
- [x] [LOW] paginatedJsonQuery fallback uses per_page=items.length, total_pages=1 when server omits pagination (74-80) — masks a missing-meta bug; `result.data as T`/`items as T[]` unchecked (by design). 401/success/error handling correct.
|
||
|
||
## src/admin/lib/entityTypeLabels.ts
|
||
|
||
|
||
## src/admin/lib/queryClient.ts
|
||
|
||
|
||
## src/admin/lib/queries/ai.ts
|
||
|
||
- [x] [LOW] Singular/plural key split: list ["ai","conversations"] vs messages ["ai","conversation",id,"messages"] — a broad ["ai","conversations"] invalidation won't touch open threads (intended; relies on staleTime Infinity/gcTime 0 to refetch on mount). Correct.
|
||
|
||
## src/admin/lib/queries/attendance.ts
|
||
|
||
- [x] [MEDIUM] attendanceLocationOptions (166-185) bypasses jsonQuery: reads `result.data.record||result.data` from untyped `response.json()` with NO response.ok/401 guard — a 500/non-JSON throws an opaque error instead of the adapter's normalized one.
|
||
- [x] [LOW] historyOptions hardcodes limit=1000 in the queryFn (not in the key). Keys unique per action/year; enabled:!!id correct.
|
||
|
||
## src/admin/lib/queries/auditLog.ts
|
||
|
||
- [x] [LOW] queryFn returns Record<string,unknown>[] rows (23-26) — untyped; consumers cast to AuditLogEntry[] at the page. Key ["audit-log",filters] consistent with broad invalidation.
|
||
|
||
## src/admin/lib/queries/common.ts
|
||
|
||
|
||
## src/admin/lib/queries/dashboard.ts
|
||
|
||
- [x] [MEDIUM] sessionsOptions (27-38) hand-rolls fetch with untyped json() and no response.ok/401 guard before reading data.success — inconsistent with jsonQuery.
|
||
- [x] [LOW] dashboardOptions returns Record<string,unknown> (untyped payload). Keys broad/unique.
|
||
|
||
## src/admin/lib/queries/invoices.ts
|
||
|
||
- [x] [LOW] receivedInvoiceListOptions (157-169) manually lists filter fields into the key instead of spreading filters — a new filter silently won't bust the cache (inconsistent with invoiceListOptions); ["invoices",id] shares prefix with list/stats/received (no real collision). NOTE: received invoices ARE under ["invoices","received"], so a broad ["invoices"] invalidation covers them.
|
||
|
||
## src/admin/lib/queries/leave.ts
|
||
|
||
- [x] [LOW] Two key families for one entity: ["leave-requests","mine"/"all"] vs ["leave","pending"/"processed"] — mutations invalidate both domains (consistent) but it's avoidable duplication that's easy to under-invalidate later; rows typed Record<string,unknown> despite a LeaveRequest interface in-file.
|
||
|
||
## src/admin/lib/queries/mutations.ts
|
||
|
||
- [x] [LOW] invalidate() loops single top-level string keys (100-104) — can't express a multi-segment broad key (fine for current usage); onSuccess user override force-cast hides signature mismatch. ApiError.status/401 sentinel/broad-invalidation default correct.
|
||
|
||
## src/admin/lib/queries/offers.ts
|
||
|
||
- [x] [LOW] offerListOptions (90) omits the <T> on paginatedJsonQuery → rows are `unknown` (the only list option missing its generic); retry:false on detail justified/documented.
|
||
|
||
## src/admin/lib/queries/orders.ts
|
||
|
||
- [x] [LOW] orderListOptions (75) also omits <T> → unknown rows; same theoretical id-vs-literal key note.
|
||
|
||
## src/admin/lib/queries/plan.ts
|
||
|
||
|
||
## src/admin/lib/queries/projects.ts
|
||
|
||
- [x] [LOW] projectFilesOptions treats HTTP 404 as empty result (97-99, reasonable but undocumented divergence from jsonQuery). Keys safe.
|
||
|
||
## src/admin/lib/queries/settings.ts
|
||
|
||
|
||
## src/admin/lib/queries/trips.ts
|
||
|
||
- [x] [LOW] tripListOptions and tripHistoryOptions hit the SAME endpoint under two key families (redundant cache); tripVehiclesOptions duplicates vehicleListOptions (same /vehicles endpoint cached twice — both invalidated correctly, verified). Redundant footprint only.
|
||
|
||
## src/admin/lib/queries/users.ts
|
||
|
||
- [x] [LOW] userListOptions key ["users",{permission}] fans out cache per-permission (a ["users"] mutation invalidates all via prefix — fine). Clean otherwise.
|
||
|
||
## src/admin/lib/queries/vehicles.ts
|
||
|
||
- [x] [LOW] Returns Record<string,unknown>[] (untyped; consumers cast). Broad ["vehicles"] key invalidated correctly.
|
||
|
||
## src/admin/lib/queries/warehouse.ts
|
||
|
||
- [x] [LOW] Detail keys ["warehouse",entity,id] share prefix with list (no collision); locationListOptions defaults filters in two places that must stay in sync (251-253); undefined filter segments in stock-status/movement-log keys. Every mutation invalidates broad ["warehouse"] (verified).
|
||
|
||
## src/__tests__/ai.test.ts
|
||
|
||
- [x] [MEDIUM] Budget-block test inserts cost_usd=budget+1 cleaned only in afterAll — inflates getMonthSpendUsd for other current-month tests in the file (order-safe today, fragile); "sums only current month" asserts spend<6 relying on no other current-month rows.
|
||
- [x] [LOW] Configured chat path + the text-only canned-reply guard are untested (avoids API spend — acceptable); duplicated buildApp/token helpers; ai.use grant migration not exercised (relies on admin bypass).
|
||
|
||
## src/__tests__/auth.service.test.ts
|
||
|
||
- [x] [HIGH] Only failure/null branches of verifyAccessToken tested — the happy path (valid token → loadAuthData → AuthData) has ZERO coverage.
|
||
- [x] [LOW] hashToken tests don't pin a known vector (a silent algo change staying 64-hex deterministic would pass).
|
||
|
||
## src/__tests__/auth.test.ts
|
||
|
||
- [x] [HIGH] No successful-login test — the whole happy path (tokens issued, refresh cookie set, 2FA loginToken branch) untested; only 401/400 negatives.
|
||
- [x] [MEDIUM] /refresh only tests the no-token 401 — success/rotation and expired/invalid paths untested (security-critical).
|
||
- [x] [LOW] Logout asserts only statusCode<500, never that the refresh cookie is cleared (name overstates the assertion).
|
||
|
||
## src/__tests__/customers.schema.test.ts
|
||
|
||
- [x] [MEDIUM] Trivial: only name empty/valid + one partial update; no NaN-coercion, email/ICO/DIC format, or nullable-field coverage.
|
||
|
||
## src/__tests__/env.test.ts
|
||
|
||
- [x] [LOW] Asserts shape only, never the validation FAILURE path (missing/short secret, bad port) which is the point of env validation.
|
||
|
||
## src/__tests__/exchange-rates.test.ts
|
||
|
||
- [x] [HIGH] Module-level rateCache/rateCacheTime never reset between tests (beforeEach resets only mockFetch) — the "throws when API fails and no cache" test passes only because it happens to run before the cache is populated; order-dependent/brittle.
|
||
- [x] [MEDIUM] Conversion tests share the uncleared cache — once "today" is cached, later mockFetch resolutions are ignored on hit, asserting stale rates (false confidence).
|
||
- [x] [LOW] No test for the amount!=1 path (CNB rates per 100 units, e.g. JPY/HUF) where rate/r.amount matters — the most money-bug-prone branch is untested.
|
||
|
||
## src/__tests__/helpers.ts
|
||
|
||
- [x] [LOW] extractCookie splits on first "=" via split("=")[1], truncating base64/JWT values containing "=" (latent; no caller exercises it); `response: any`.
|
||
|
||
## src/__tests__/invoices.service.test.ts
|
||
|
||
- [x] [MEDIUM] Inputs use duck-typed {toNumber:()=>N} stand-ins, not real Prisma.Decimal — a regression in real-Decimal handling wouldn't be caught; the "null quantity" test (40) actually passes 0, not null (name lies).
|
||
- [x] [MEDIUM] No apply_vat:true with mixed per-line vat_rate, no currency-conversion, no negative/discount line — narrow surface for money logic.
|
||
|
||
## src/__tests__/manual-create.test.ts
|
||
|
||
- [x] [MEDIUM] afterEach deleteManys number_sequences type="shared" (16) — destructive shared global state also used by numbering.test.ts (serialized so safe, but resets the live shared counter).
|
||
- [x] [LOW] Assertions guard with `if (!("id" in res)) return;` (37,50,116,132) — a failed create silently passes the test; attachment test checks only name+truthiness, not size/content.
|
||
|
||
## src/__tests__/nas-file-manager.test.ts
|
||
|
||
- [x] [LOW] Only traversal REJECTIONS tested (moveItem destination path not tested; "rejected" asserts a substring without confirming nothing was deleted/moved); no positive "legitimate path accepted" test (a reject-everything bug would pass).
|
||
|
||
## src/__tests__/nas-project-folder.test.ts
|
||
|
||
|
||
## src/__tests__/numbering.test.ts
|
||
|
||
- [x] [MEDIUM] Skip-taken preview tests assume a fresh counter, but real projects/quotations rows in the test DB may already hold the first number — second!==first can pass for the wrong reason / the create could collide on the unique column.
|
||
- [x] [LOW] "increments" asserts only num1!==num2 (not strictly +1 / year/format preserved); NO concurrency test for the SELECT...FOR UPDATE path the whole fileParallelism:false workaround exists to protect.
|
||
|
||
## src/__tests__/plan.test.ts
|
||
|
||
- [x] [MEDIUM] noPermUserId is "second user in DB" reused for two meanings across two beforeAlls; if the DB has only one user it falls back to admin and the scoping assertion toEqual([]) is wrong-for-the-right-reason masked.
|
||
- [x] [LOW] Far-future created_at (2037) to dodge the 2038 ceiling (brittle); updateEntry doesn't test inactive/unknown category or date_from>date_to; assertNotPastDate "today" boundary (the bug-prone path) untested.
|
||
|
||
## src/__tests__/planAuditDescription.test.ts
|
||
|
||
|
||
## src/__tests__/planCategory.test.ts
|
||
|
||
- [x] [MEDIUM] Dedupe test asserts \_2 but cleanup is keyed to in-memory ids (afterAll), so a mid-run crash leaves rows that make the next run assert \_3 and fail (slug-prefix cleanup would be safer).
|
||
- [x] [LOW] No duplicate-label-on-update negative test, no deactivate/reactivate test; slugify empty-result collision (slugify("\*\*\*")→"") untested.
|
||
|
||
## src/__tests__/received-invoices-vat.test.ts
|
||
|
||
- [x] [LOW] Solid value checks but no half-cent rounding edge, no negative/credit-note amount; rounding pinned via toBeCloseTo rather than the stored 2-dp value.
|
||
|
||
## src/__tests__/schema-nan.test.ts
|
||
|
||
- [x] [MEDIUM] Proves "not-a-number" is rejected but never that a valid numeric STRING ("2") is coerced/accepted — the whole point of the form-coercion helper (string→number) is untested.
|
||
- [x] [LOW] Only CreateOrder/CreateQuotation covered; the ~150 other duplicated coercion sites (invoices/received-invoices/trips/plan) have no NaN test.
|
||
|
||
## src/__tests__/setup.ts
|
||
|
||
- [x] [LOW] Loads .env.test but doesn't assert it exists or that DATABASE_URL targets a TEST db (vitest.config also loads it with override:true — redundant); a missing .env.test silently runs against whatever env is present (non-test-DB risk).
|
||
|
||
## src/__tests__/warehouse.test.ts
|
||
|
||
- [x] [MEDIUM] Shared `counter` for location codes + per-describe createdItemId/createdCategoryId in closure → tests within a describe are order-dependent (one early failure cascades).
|
||
- [x] [LOW] "deactivates an item (soft delete)" actually asserts a HARD delete (404) — name lies and would mask a future intended soft-delete; FIFO verified only by summed batch quantity, never that the OLDEST batch is consumed first (core stock-valuation logic unverified); no partial-issue-across-batches / negative-qty rejection / reservation+issue-simultaneous-subtraction tests; hardcoded item_number "ITM001" + item_id:1 in a 403 test (fragile).
|
||
|
||
## Suite-level (tests)
|
||
|
||
- [x] [MEDIUM] Server-side only — ZERO frontend/component tests: React Query mutations, api.ts token-refresh/dedup, UI VAT/number formatting, and permission-gated rendering are entirely unverified.
|
||
- [x] [LOW] buildApp/generateToken/no-perm-user bootstrap copy-pasted across ai/plan/warehouse tests instead of helpers.ts (~150 lines of drift-prone duplication).
|
||
|
||
## index.html
|
||
|
||
|
||
## .claude/settings.json
|
||
|
||
|
||
## boneyard.config.json
|
||
|
||
|
||
## .claude/settings.local.json
|
||
|
||
- [x] [LOW] Permission allowlist contains stale entries pointing at the OLD PHP app (find ...boha-app ...\*.php, lines 4-9) — harmless (gitignored, local-only) but dead config from the pre-rewrite project.
|
||
|
||
## CLAUDE.md
|
||
|
||
- [x] [LOW] Otherwise excellent and current (counts verified: 52 models, 17 test files/195 tests, 114 admin tsx). One doc-vs-code drift: it states `vatFromGross` (gross VAT) is used "by both the manual form and the AI import," but the JSON single-create path in received-invoices.ts still uses the net `amount*rate/100` formula (see that file's [HIGH]) — the convention is documented as fully applied when one code path was missed. Strength worth preserving: the conventions section + the 2026-06-06 audit reference keep entropy in check.
|
||
|
||
## Config & build files (assessed at PROJECT-LEVEL)
|
||
|
||
|
||
## package.json
|
||
|
||
|
||
## tsconfig.json
|
||
|
||
|
||
## tsconfig.server.json
|
||
|
||
- [x] [HIGH] Excludes `src/__tests__` (with tsconfig.app.json not including it, tests are never type-checked — see PROJECT-LEVEL). strict:true, CJS/ES2022 correct otherwise.
|
||
|
||
## tsconfig.app.json
|
||
|
||
- [x] [MEDIUM] `noUnusedLocals:false`/`noUnusedParameters:false` (compiler won't flag dead code) and tests not included (not type-checked). strict:true, bundler resolution correct.
|
||
|
||
## vite.config.ts
|
||
|
||
|
||
## vitest.config.ts
|
||
|
||
|
||
## .env.example
|
||
|
||
- [x] [MEDIUM] Stale — missing many env vars config/env.ts reads (ANTHROPIC_API_KEY, NAS_FINANCIALS_PATH, NAS_OFFERS_PATH, SMTP_FROM_NAME, INVOICE_ALERT_EMAIL, RATE_LIMIT_*, TRUST_PROXY, TOTP_*). See PROJECT-LEVEL.
|
||
|
||
## .claude/hooks/block-env.js
|
||
|
||
|
||
## .claude/hooks/format-on-save.js
|
||
|
||
- [x] [LOW] Runs `npx prettier --write` but prettier is not a declared devDep and there's no `.prettierrc` (see PROJECT-LEVEL) — formatting is unpinned/non-reproducible.
|
||
|
||
---
|
||
|
||
## Fix Log — Wave F-1: Schemas (verified: tsc 0, vitest 195/195)
|
||
|
||
**Foundation:** added NaN-guarded coercion helpers to `src/schemas/common.ts` (`numberFromForm`, `numberInRange`, `nonNegativeNumberFromForm`, `positiveNumberFromForm`, `intIdFromForm`, `nullableNumberFromForm`, `nullableIntIdFromForm`, `booleanFromForm`, `isoDateString`, `timeString`) — the root-cause fix for the ~150× duplicated unguarded idiom. Replaced `ZodSchema` import with `z.ZodType`.
|
||
|
||
- **[HIGH] common.ts no shared helpers** → FIXED (helpers added above).
|
||
- **[HIGH] trips.schema NaN start_km/end_km/ids** → FIXED (`intIdFromForm`/`nonNegativeNumberFromForm`/`isoDateString`/`booleanFromForm`, route `end_km<start_km` guard now sees finite numbers); route bug closed.
|
||
- **[HIGH] received-invoices month/year/amount/vat** → FIXED (`numberInRange(1,12)`/`(2000,2100)`/`(0,100)`, `nonNegativeNumberFromForm`).
|
||
- **[HIGH] warehouse movement quantities/FKs NaN/negative** → FIXED (`positiveNumberFromForm`/`nonNegativeNumberFromForm` for qty/price, `intIdFromForm`/`nullableIntIdFromForm` for all FKs).
|
||
- **[HIGH] attendance pervasive NaN** → FIXED (ids→intId, project_id→nullableIntId, numerics→numberFromForm, times→`timeString`, minutes→`numberInRange(0,59)`).
|
||
- **[MEDIUM] auth/users/profile password no .max()** → FIXED (`.max(200)`); totp_code digits-only regex; usernames/names capped.
|
||
- **[MEDIUM] settings security numerics unguarded** → FIXED (`max_login_attempts`→1..100, `lockout_minutes`→1..1440, `max_requests_per_minute`→1..100000, vat 0..100, breaks non-negative); array(any)→array(unknown).max; email fields `.email()`.
|
||
- **[MEDIUM] customers/settings z.array(z.any())** → FIXED (`z.array(z.unknown()).max(N)`).
|
||
- **[MEDIUM] bank-accounts unbounded iban/bic/currency** → FIXED (`.max(255)`; format regexes intentionally not added to avoid rejecting valid edge cases).
|
||
- **[MEDIUM] plan/projects update-FK inconsistency** → FIXED (shared helpers; plan local intFromForm/isoDate now alias shared ones; project Update FKs → nullableIntId).
|
||
- **[LOW] vehicles/scope-templates/offers/orders/invoices/leave/roles/ai NaN + unbounded text + bool idiom** → FIXED (helpers + `.max()` caps + `booleanFromForm` + date `isoDateString` where safe). vat_rate→numberInRange(0,100). permission_ids→array(int().positive()).max(500).
|
||
- **[LOW] "plain z.object not z.strictObject" (all schemas)** → RESOLVED (intentional): converting request bodies to `strictObject` would reject forward-compatible payloads (extra fields the client may send); the strip-unknown behavior is the safer default. Convention noted; not changed.
|
||
- **[LOW] status/currency free strings (invoices/orders/received/projects/settings)** → RESOLVED: capped `.max(20)`/left as bounded strings; enum values not guessed (a wrong enum would reject valid requests). Models with an existing `z.enum` (offers/orders status) already correct.
|
||
|
||
---
|
||
|
||
## Fix Log — Wave F-2a: Backend routes + NAS/utils/scripts (verified: tsc 0, vitest 195/195)
|
||
|
||
**Security (HIGH):**
|
||
- roles.ts → POST/PUT now require admin role; reject role named "admin", enforce name uniqueness, block rewriting the admin role's permissions; role+permissions wrapped in one `$transaction`.
|
||
- totp.ts → `/enable` (and `/setup`) now use `config.totp.algorithm/digits/period` (not hardcoded SHA1/6/30); backup-code loop early-exits on match.
|
||
- services/auth.ts → on refresh-token reuse, the whole token family is revoked (`deleteMany` by user) + warn-logged, before 401.
|
||
- trips.ts `GET /users` and vehicles.ts `GET /` → now permission-guarded (`requireAnyPermission`) instead of bare `requireAuth` (info-disclosure closed).
|
||
- received-invoices.ts → JSON single-create now uses `vatFromGross` (gross-VAT consistent across all 3 paths).
|
||
- orders-pdf.ts → custom client-supplied items now require `orders.edit`; `orders.view`-only callers get STORED order items (fabrication closed); `.passthrough()`→`z.looseObject` + bounded item schema.
|
||
- nas-offers-manager.ts → all 5 silent `catch {}` now `console.error` with context.
|
||
|
||
**Correctness/convention:**
|
||
- Raw `reply.send` envelopes → `success()`/`paginated()` across attendance/customers/leave-requests/projects/invoices/received-invoices.
|
||
- scope-templates.ts → `logAudit` added to all template mutations; DELETE id NaN-guarded + existence-checked.
|
||
- leave-requests.ts → approval path now persists `reviewer_note`.
|
||
- dashboard.ts → `today_plan` N+1 replaced with one `findMany({where:{key:{in}}})` + Map.
|
||
- audit-log.ts / company-settings.ts → numeric query/body NaN-guarded (→400 not 500).
|
||
- projects.ts → POST `/:id/notes` audited; DELETE body validated; trips odometer no longer regresses (recompute via MAX).
|
||
- project-files.ts → folder-create/move bodies validated via parseBody (inline Zod).
|
||
- invoices-pdf.ts → `JSON.parse(custom_fields)` guarded; offers-pdf cleanQuillHtml strips js/data/vbscript.
|
||
- orders.ts → from-quotation multipart `quotationId` validated via schema (NaN-safe).
|
||
- quotations.ts → redundant 2nd findUnique removed; NAS-delete catch logged; `(result as any)` typed.
|
||
- html-to-pdf.ts → chromium path now probed via `fs.existsSync` through the fallback list.
|
||
- rotate-totp-key.ts → reuses the dual-format `decrypt` from utils/encryption (handles PHP-legacy secrets); encryption.ts refactored to add `decryptWithKey`/`encryptWithKey` (backward compatible, wire format unchanged).
|
||
- migrate-received-invoices-to-nas.ts → read-back size check before nulling `file_data`.
|
||
- seed.ts → hard guard refusing to run when `APP_ENV==="production"`.
|
||
|
||
**Resolved-as-noted / deferred (ticked):**
|
||
- attendance.ts `project_logs` ownership → FIXED (owner-or-`attendance.manage`).
|
||
- nas-financials-manager.ts "sync fs blocks event loop" → DEFERRED (async rewrite large/risky; documented); `saveIssuedInvoicePdf` isConfigured guard added.
|
||
- nas-file-manager.ts TOCTOU/symlink, encryption.ts format-detection, pagination/czech-holidays TZ → documented via comments (no behavior change; crypto wire format untouched).
|
||
- totp.ts `/enable` change-flow (LOW), plan.ts UTC note → low-risk, left as-is with rationale.
|
||
- scope/item-template audit entries omit `entityType` (no matching `EntityType` union member; action+entityId+Czech description still recorded).
|
||
|
||
---
|
||
|
||
## Fix Log — Wave F-2b: Services + warehouse route (verified: tsc 0, vitest 195/195)
|
||
|
||
**Concurrency / data-integrity (HIGH):**
|
||
- warehouse.service: `cancelReservation` now in `$transaction` with FOR UPDATE on the reservation row; `confirmIssue`/`confirmInventorySession` lock affected item + batch rows (ascending-id order, no new deadlock) before read-modify-write; `selectFifoBatches` adds `{id:"asc"}` tie-break; confirm rejects `batch.item_id !== line.item_id`; `getBelowMinimumItems` N+1 → one `groupBy`.
|
||
- warehouse.ts route: report endpoints bounded (pagination + `REPORT_SOURCE_CAP=5000`); PUT line replacements wrapped in `$transaction`; attachment save does a compensating NAS delete on DB-insert failure; NaN/status filters guarded. **Report default limit raised to 2000 (max 5000) via new `parsePagination(query,{defaultLimit,maxLimit})` so reports stay complete, not silently truncated to 25.**
|
||
- attendance.service: `lockUserRow` `$executeRaw`→`$queryRaw` (lock now holds); `getBalances` N+1 → one `findMany({user_id:{in}})`; `getStatus` independent queries via `Promise.all`; `createLeave` books Dec→Jan days against each day's own year; `deleteAttendance` gained optional owner-or-admin params (backward compatible); `DEFAULT_VACATION_TOTAL=160` constant.
|
||
- exchange-rates: `fetch` now has `AbortSignal.timeout(8000)`; per-key TTL (not one global timestamp); `data.rates` array-validated.
|
||
- numbering.service: first-of-year INSERT race handled (P2002 catch + single retry); `releaseSequence` in `$transaction` with FOR UPDATE.
|
||
- users.service / offers.service: uniqueness checks moved INSIDE the create transaction (TOCTOU → proper 409, not 500).
|
||
|
||
**Correctness / quality:**
|
||
- invoices.service: three money/VAT impls unified through one `computeMoney` core (outputs byte-identical; test still passes); `getInvoiceStats` awaits via `Promise.all`; `Record<string,any>`→typed `InvoiceInput`; deleteInvoice year-parse guarded.
|
||
- invoice-alerts: created-invoice amount now gross; per-invoice log N+1 → one findMany; dead ternary removed; `createMany({skipDuplicates})`.
|
||
- orders.service: status→project-status sync extracted to one helper; deleteOrder FK guard moved inside the tx.
|
||
- offers.service: deleteOffer cleans the NAS PDF. projects.service: deleteProject is DB-first then NAS (no orphan); updateProject logs rename failure.
|
||
- planCategory.service: `̀-ͯ` combining-marks strip; bounded uniqueKey loop. plan.service: audit-label N+1 batched via `Promise.all` (descriptions identical; bulk skips per-entry labels).
|
||
- mailer: optional SMTP transport when `SMTP_HOST` env present (sendmail default unchanged; no .env edited); `to` validated.
|
||
- system-settings: JSON.parse shape-validated. leave-notification: `formatDate` uses `isNaN(getTime())`. ai.service: model-output `JSON.parse` wrapped.
|
||
|
||
**Deferred (ticked, rationale):** God-file splits (warehouse.ts route 2399, attendance.service 1700) — large refactors deferred for unattended safety, documented in-file; `getInvoice` null-return kept (route depends on it); nas-financials sync-fs kept (async rewrite risky), documented.
|
||
|
||
**Controller fix:** `invoices.service.ts` `InvoiceItemInput` widened to allow `null` (schema yields nullish) to clear a tsc error from the refactor.
|
||
|
||
---
|
||
|
||
## Fix Log — Wave F-3a: Frontend UI kit + warehouse pages + components (verified: tsc 0, build 0)
|
||
|
||
**Rules-of-Hooks (HIGH) — moved every hook above the early `return <Forbidden/>`:** WarehouseInventoryDetail, WarehouseInventoryForm, WarehouseIssueDetail, WarehouseIssueForm, WarehouseItemDetail, WarehouseReceiptDetail, WarehouseReceiptForm.
|
||
**Stale-closure confirm bugs (MEDIUM):** WarehouseIssueForm/ReceiptForm `confirm*` and WarehouseSuppliers `toggleActive` now pass the id as the mutation variable (no more `/0/...` or collection-root PUT).
|
||
**UI kit:** Field.tsx → `useId` label `htmlFor` + child `id` injection + `aria-describedby`/`aria-invalid` (system-wide a11y); ConfirmDialog/Modal freeze `cancelText`(+`confirmVariant`); Card `disableContentPadding`/`contentProps`; Checkbox `disabled/name/id/indeterminate/required`; DataTable mobile sort control + desktop action-cell `stopPropagation`; ProgressBar/Tabs aria; AppShell logout-timeout cleanup; navData boundary-safe active; Alert Czech close label; LoadingState aria fallback.
|
||
**Warehouse pages:** WarehouseIssueDetail project link → RouterLink (SPA nav); WarehouseInventoryForm `actual_qty` parseDecimal/NaN-guard; WarehouseInventory `items` col → `_count.items`; WarehouseReports tabs → React Query (in `["warehouse"]` cache) + error Alert + formatDate; WarehouseReservations → `useApiMutation` + NaN guard; WarehouseCategories `sort_order` Number+guard; dead `/0/` fallbacks + duplicated MovementRow types removed.
|
||
**Components:** OrderConfirmationModal resets state on reopen + no close-on-error; ShiftFormModal `leave_hours` NaN→0 + per-instance ref key counter; PlanCellModal typed payload props (no `any`); ProjectFileManager drag-depth counter + logged catches; PlanRangeChips dead `readonly` prop removed (+ PlanGrid call site); PlanGrid `useMemo` project Map + memoized days; PlanCategoriesModal label `key`; AlertContainer stacking offset; misc keys/labels.
|
||
|
||
**Deferred (ticked, rationale):** Modal live-children freeze (snapshotting ReactNode form children breaks controlled inputs); WarehouseItemDetail error-effect→render-branch (also toasts; behavior risk); WarehouseReceiptForm hand-rolled dropzone→kit FileUpload (kit lacks drag-drop/auto-upload); ShortcutsHelp `return null` (still rendered by AppShell — kept, commented); ErrorBoundary telemetry (no telemetry service exists — commented hook); various LOW form-seed-effect patterns (intentional).
|
||
|
||
---
|
||
|
||
## Fix Log — Wave F-3b: Remaining pages + dashboard/odin components + hooks/data-layer (verified: tsc 0, build 0, vitest 195/195)
|
||
|
||
**Rules-of-Hooks (CRITICAL/HIGH) — moved every hook above the early permission return:** OrderDetail, ProjectDetail (CRITICAL); Settings, Trips, TripsAdmin, AttendanceBalances, AuditLog, LeaveApproval (HIGH). (Users, Vehicles were verified ALREADY correct — finding prose said "after" but line numbers showed before.)
|
||
**UTC-date persisted-value fixes (MEDIUM):** InvoiceDetail due_date default/issue/due/tax/`computedDueDate` → local-date helper `addDaysLocalStr`/`normalizeDateStr` (no more `toISOString().split` reaching the saved payload); ReceivedInvoices `toDateInput` → `normalizeDateStr`; Attendance `calculateBusinessDays` → local parse; Invoices overdue tint → local string compare.
|
||
**Invalidation gaps (HIGH/MEDIUM):** DashQuickActions trip POST → invalidate `["trips"]`+`["dashboard"]`; DashProfile PUT → `["dashboard"]`+`["users"]`; Attendance leave submit → added `["leave"]`.
|
||
**Other:** OfferDetail heartbeat effect dep `isCompleted` added (stale closure); Projects search debounced; Vehicles raw `<Chip>`→kit StatusChip; parseInt radix (Trips/Vehicles/OffersCustomers); Login `loginToken!` guarded; AttendanceLocation dead LoadingState branch fixed; ReceivedInvoices render-phase ref→effect.
|
||
**Hooks/data-layer:** usePlanWork optimistic rollback moved to `onMutate`-context (+typed bodies, `res.ok` guard); attendanceHelpers `getTimePart` now string-parses (TZ-safe); dashboardHelpers dead `STATUS_DOT_CLASS` removed; ThemeContext `DEFAULT_THEME_MODE` constant + cross-tab `storage` listener (+ MuiProvider now imports the constant — drift closed); theme.ts reduced-motion guard on containedPrimary; `attendanceLocationOptions`/`sessionsOptions` routed through `jsonQuery` (ok/401 guards); useReducedMotion lazy initializer; ItemPicker debounce; ReservationPicker stale-selection reset; OdinSidebar row keyboard-accessible; dashboard widgets reduced-motion branch.
|
||
|
||
**Deferred (ticked, rationale):** AuthContext `apiRequest`↔`apiFetch` consolidation (high auth-regression risk; cross-referenced in comments); useAttendanceAdmin split/RQ-migration (large refactor); big-file splits (InvoiceDetail/OfferDetail/Settings/CompanySettings/ReceivedInvoices/AttendanceHistory); `usePaginatedQuery` precise option typing (TanStack `enabled` contravariance over-constrains callers — reverted to documented `any`, data shape still enforced); untyped query-row `Record<unknown>` LOWs (typing every row = large churn, accepted); useModalLock body-vs-html (still used; documented).
|
||
|
||
**Controller fixes:** projects.ts DeleteSchema default, invoices.service InvoiceItemInput null-widening, usePaginatedQuery generic revert, OdinComposer ref cast restored, DashQuickActions message fallback, MuiProvider DEFAULT_THEME_MODE import.
|
||
|
||
---
|
||
|
||
## Fix Log — Wave G: PROJECT-LEVEL + infra (verified: tsc 0, build 0, vitest 195/195, lint 0 errors)
|
||
|
||
- **[HIGH] No linter / unpinned formatter** → FIXED: added ESLint flat config (`eslint.config.mjs`) with `typescript-eslint` + **`react-hooks/rules-of-hooks` as ERROR** + `react-refresh`; Prettier as a real devDep (`.prettierrc.json`/`.prettierignore`); npm scripts `lint`/`lint:fix`/`format`/`format:check`/`typecheck`. **The linter immediately caught a real Rules-of-Hooks bug the fix-agents had missed (`PlanCellModal` EditForm — `useState`s after an early `return null`); fixed by extracting `computeEditFormInitial` and moving the guard after all hooks.** `npm run lint` = 0 errors (110 advisory warnings).
|
||
- **[HIGH] Tests never type-checked** → FIXED: added `tsconfig.test.json` (node + `vitest/globals`, includes `src/__tests__` + the fastify `.d.ts` augmentation) referenced from the solution `tsconfig.json`, so `tsc -b` now covers the suite. It surfaced **7 real type holes** in `manual-create.test.ts`/`plan.test.ts` (optional `id`/`unknown` data) — all fixed.
|
||
- **[MEDIUM] Dependency vulns** → FIXED: `npm audit fix` (non-breaking) took **8 vulns → 3** (remaining `quill`/`file-type` need breaking majors — DEFERRED; quill XSS is mitigated by the DOMPurify-everywhere sanitization).
|
||
- **[MEDIUM] Dead dependencies** → FIXED: removed `react-datepicker`, `hi-base32`, `@types/mysql`.
|
||
- **[MEDIUM] No README / stale `.env.example`** → FIXED: wrote a real README (setup/run/build/test/gates/migrations); rewrote `.env.example` with every env var `config/env.ts` reads (+ optional SMTP, ANTHROPIC_API_KEY).
|
||
- **[MEDIUM] ~77 `any`** → REDUCED: typed InvoiceInput, PlanCellModal props, usePlanWork bodies, dashboard fetch results, etc.; the remainder are now surfaced as ESLint warnings. Tracked, not silent.
|
||
- **[MEDIUM] God-files** → DEFERRED: large splits are unsafe in an unattended pass; documented in-file and in CLAUDE.md.
|
||
- **[LOW] `.gitignore`** → FIXED: added `*.tsbuildinfo`, `*.bak`, `.playwright-mcp/`.
|
||
- **[LOW] Per-request auth caching** → DEFERRED: adding a cache to the auth hot path unattended is risky; documented as a future optimization.
|
||
- **Infra LOW notes** → audit.ts now uses `instanceof Prisma.Decimal`; types/index.ts JwtPayload.role hazard documented; server.ts vite-import / env Date.toJSON / security CSP-prod-only / totp logging confirmed intentional (by-design, no change).
|
||
- **CLAUDE.md** gate line updated to include `npm run lint` + the test-typecheck note.
|
||
|
||
## Fix Log — config/doc closures
|
||
- tsconfig.server.json/tsconfig.app.json "tests not typechecked / noUnusedLocals off" → FIXED (tsconfig.test.json now type-checks tests; ESLint no-unused-vars covers dead-code).
|
||
- .env.example stale, format-on-save prettier undeclared → FIXED (rewritten; prettier now a real devDep + .prettierrc.json).
|
||
- CLAUDE.md vatFromGross doc-vs-code drift → CLOSED (received-invoices JSON path now uses vatFromGross, so the doc is accurate).
|
||
- .claude/settings.local.json stale PHP allowlist → RESOLVED (gitignored local-only config; harmless, left to the user).
|
||
|
||
## Fix Log — Prisma schema (verified: prisma validate ✓, generate ✓, tsc 0, vitest 195/195)
|
||
Edited `prisma/schema.prisma` + regenerated client + staged a **PENDING** migration `prisma/migrations/20260609000000_audit_schema_hardening/` (NOT applied — needs the user to run `prisma migrate deploy`/`migrate dev` with the dev server stopped, per policy; the migration header documents the data-safety checks).
|
||
- [HIGH] sklad_* relations now have explicit `onDelete` (Cascade line→header, Restrict line→master-data, SetNull on the genuinely-optional `*_by`/supplier/category refs; required "project" refs → Restrict since SetNull is invalid on a non-null column).
|
||
- [MEDIUM] `@unique` on receipt_number/issue_number/session_number.
|
||
- [MEDIUM] Financial FKs (invoices/orders/projects/quotations) made explicit `onDelete: Restrict` (prevents orphaning; note: some were the optional default SetNull — RESTRICT is the safer intent).
|
||
- [MEDIUM] New `@@index` on sklad_issues(project_id, issued_by), sklad_receipts(supplier_id, received_by), bank_accounts(is_default).
|
||
- [LOW] Removed the duplicate audit_logs created_at index. Did NOT add @updatedAt (runtime risk), convert status→enum (data risk), or add FKs to deliberately-FK-less user-reference columns (by design).
|
||
|
||
## Fix Log — Tests (verified: tsc 0 incl. tests, vitest 243/243 — was 195)
|
||
**Flakiness/correctness (the real bugs):**
|
||
- exchange-rates: added `__resetRateCacheForTest()` (test-only, no runtime change) + `beforeEach` reset → no more order-dependent stale-cache; added the `amount!=1` (per-100-unit) test.
|
||
- ai: over-budget row cleaned in-test + "sums only current month" now asserts an exact delta → order-independent.
|
||
- numbering: "increments" now asserts strict +1 / format / year; skip-taken previews seed high so they can't pass for the wrong reason.
|
||
- planCategory: slug-prefix cleanup so a crashed run can't shift dedupe to `_3`.
|
||
- plan: dedicated distinct non-admin `scopeUserId` for employee-scoping (can't be the admin / collapse to 1).
|
||
- manual-create: `if (!("id" in res)) return` → `failResult()` that throws (no silent pass); attachment test now asserts exact bytes.
|
||
**Misleading names fixed:** warehouse "soft delete"→"hard-deletes…404"; invoices "null quantity" now passes a genuine null.
|
||
**New high-value coverage:** auth happy-path login + refresh-rotation + logout-cookie-cleared; `verifyAccessToken` happy path + pinned `hashToken` vector; schema-nan valid-string-COERCION positives (orders/quotations/trips/received-invoices/warehouse); invoices real `Prisma.Decimal` + mixed-VAT + discount line; customers schema breadth; received-invoices half-cent rounding pinned to 2dp; nas-file-manager POSITIVE accept + destination-traversal reject; plan today-boundary; warehouse FIFO oldest-first.
|
||
**helpers/setup:** `extractCookie` no longer truncates `=`-containing values; setup.ts now warns if DATABASE_URL isn't a test DB.
|
||
**Suite-level:** [MEDIUM] no component tests → DEFERRED (a component-test harness is a substantial separate initiative; backend coverage materially improved); [LOW] duplicated buildApp/token bootstrap → acknowledged (consolidation is a safe future cleanup; not done to avoid churn across passing suites).
|
||
|
||
> ⚠️ **Finding for the user:** there is **no `.env.test` file** — the suite runs against the dev DB named `app` (per `.env`). Create a dedicated `.env.test` pointing at a throwaway test DB, then the new `setup.ts` guard can be upgraded from a warn to a hard throw.
|
||
|
||
---
|
||
|
||
## Verification Round (5 independent adversarial verifiers + fixes)
|
||
|
||
After all findings were fixed and ticked, 5 fresh agents re-audited the fixes against the original findings + current code, and hunted for regressions. **Most fixes VERIFIED correct; the verification caught issues a self-review would have missed — all now fixed:**
|
||
|
||
**3 REGRESSIONS I had introduced (validation now rejecting valid input) — FIXED + regression-tested:**
|
||
1. `settings` email fields `.email()` rejected the empty string the Settings form submits for un-filled emails → it blocked saving ALL system settings. FIXED: new `emailOrEmpty` helper (accepts "" | valid email) in common.ts; applied to settings + warehouse-supplier emails.
|
||
2. `warehouse` supplier `email` same issue → FIXED (emailOrEmpty).
|
||
3. `trips` `trip_date` (and by extension `projects`/`leave` date fields) used strict `isoDateString`, which rejected the `"YYYY-MM-DDT00:00:00"` an edit form re-submits (the `@db.Date` toJSON round-trip) → FIXED: `isoDateString`/`timeString` now strip a trailing time component before validating (output stays date-only). Added 4 regression tests (schema-nan.test.ts → 24 tests).
|
||
|
||
**Concurrency issues the verifier found — FIXED:**
|
||
4. `cancelIssue` was left OUT of the new lock discipline (locked batches in unsorted issue-line order, no item gate) → a residual deadlock window vs `confirmIssue`. FIXED: now locks affected items then batches in ascending id order, matching confirmIssue/confirmInventorySession.
|
||
5. `$executeRaw` vs `$queryRaw` inconsistency: the warehouse lock helpers (`lockParentRow`/`lockRowsForUpdate`) + `cancelReservation` still used `$executeRaw` for `SELECT … FOR UPDATE` while the attendance fix switched to `$queryRaw` (the reliable form). FIXED: unified all warehouse FOR-UPDATE locks on `$queryRaw`.
|
||
6. `createUser` concurrent-duplicate surfaced as 500 not 409 → FIXED: catch P2002 → 409.
|
||
|
||
**Verified clean (no issues found):** all frontend Rules-of-Hooks (enforced by the lint rule), UTC-date fixes, invalidation gaps, stale-closure confirms, Field a11y; all infra (eslint/test-typecheck/dead-deps/audit/README/.env); prisma schema + migration consistency; the security fixes (roles escalation, totp, token-family revoke, VAT gross, orders-pdf authz, NAS catches).
|
||
|
||
**Acknowledged residual (LOW, pre-existing, not introduced here):** roles.ts PUT metadata-update sits just outside the permission-rewrite transaction; the refresh-reuse→401 path is now tested but the family-revocation side-effect lacks a dedicated assertion.
|
||
|
||
### FINAL GATES (after verification fixes): tsc 0 · build 0 · vitest **247/247** (was 195) · eslint **0 errors**.
|