- agentic chat loop (max 6 round-trips, budget re-checked between iterations) over 10 read-only, permission-delegated tools in src/services/ai-tools.ts - persist assistant tool trace in ai_chat_messages.content_json (+ migration) - consulted-tools chips in OdinThread; header now shows month spend only (budget cap hidden from the UI, server-side 402 guard unchanged) - seed: ai.use permission; Settings exposes the ai permission module - docs: REVIEW consolidation; deployment-guide.md superseded by docs/release.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
6.2 KiB
Deferred HIGH issues — performance & UX sprint
Created: 2026-06-03 Source audit: Parallel 5-agent production risk audit (2026-06-03) Context: All 8 CRITICAL and 3 of 8 HIGH issues were fixed in the same session. The 5 items below were intentionally deferred — they are not data-integrity or auth risks, but they are real production pain that should be addressed in a dedicated performance / UX sprint before the user base notices.
#9 — N+1 queries in warehouse list/detail/report
Files:
src/services/warehouse.service.ts:184-199(getBelowMinimumItems)src/services/warehouse.service.ts:629-646(somewhere in items list)src/services/warehouse.service.ts:681-683src/services/warehouse.service.ts:2182-2203(report)
Pattern: items.map(async (i) => { const stock = await getStock(i.id); const available = await getAvailable(i.id); const value = await getValue(i.id); }) — fires N+1 round-trips per item. For a list of 50 items, that's 150 sequential queries.
Production impact: Slow page loads (1-3s on 50 items, scales linearly). Users already complain about the items page; this is the root cause for the warehouse report being the slowest page in the app.
Fix sketch:
- Replace the
forloop with a single grouped query:prisma.sklad_batches.groupBy({ by: ['item_id'], _sum: { quantity: true }, where: { item_id: { in: itemIds } } })for stocks, similar aggregates for reservations. - Map the grouped results back to items in JS. One query instead of N.
- For the report view, do the same — group all batches and reservations by item_id in a single round-trip, then join with items in memory.
- Estimated effort: M (4-6 hours including testing).
#10 — Missing FK indexes on hot warehouse tables
File: prisma/schema.prisma (no @@index directives on the new warehouse tables)
Missing indexes (verify against prisma/schema.prisma and add to a new migration):
sklad_receipt_lines.item_id— everyconfirmReceiptreads/writes thissklad_issue_lines.batch_id— everyconfirmIssuereads/writes thissklad_issue_lines.item_idsklad_reservations.project_id— reservation list per projectsklad_reservations.item_idsklad_item_locations.item_idsklad_item_locations.location_idsklad_batches.receipt_line_id(if nullable FK)sklad_inventory_lines.item_idsklad_inventory_lines.session_id
Production impact: MySQL currently does table scans for these joins. With even 10k batch rows, the receipts/issues/reservations pages will start to crawl. On production data (likely already 50k+ batches), this is a real performance cliff.
Fix sketch:
- Add
@@index([item_id]),@@index([batch_id])etc. to each model inprisma/schema.prisma. - Run
npx prisma migrate dev --name warehouse_fk_indexes(after asking user to stop dev server per CLAUDE.md). - Run
npx prisma generate. - Verify with
EXPLAINon the slow queries after deploy. - Estimated effort: S (1-2 hours).
CLAUDE.md note: Before running prisma migrate dev, the user must stop the dev server.
#13 — Pagination does not auto-adjust after delete
Files: All list pages (Invoices, Offers, Projects, Orders, WarehouseXxx)
Pattern: User is on page 5 (e.g. 10 items per page, 47 total, pages 1-5). They delete the last item on page 5. Total becomes 46, but the page param is still 5. The list endpoint returns 0 results, the user sees an empty table, and the "next" button is disabled but there's no automatic jump back to page 4.
Production impact: Confusing UX. Users have to manually click "previous" or reset filters. Doesn't cause data loss but is a small papercut that erodes trust.
Fix sketch:
- In each list page's
useQueryerror/empty handler, detectpagination.total === 0 && page > 1and callsetPage(page - 1). - Or more cleanly: have the API include
pagination.total_pagesand the frontend clamppagetomin(page, total_pages)after each refetch. - Estimated effort: S (2-3 hours; touches ~10-12 pages).
#14 — Offer lock lifecycle has multiple silent failures
File: src/admin/pages/OfferDetail.tsx:472-516
Pattern: Lock acquire, heartbeat (interval), and unlock all use .catch(() => {}). If any of them silently fails, the user thinks they have the lock but actually don't — or worse, the lock is held by a tab that's already closed because the unload handler also swallowed the error.
Production impact: Two users editing the same offer can both see "You have the lock" and clobber each other's changes. Data loss in a real, low-probability but high-impact scenario.
Fix sketch:
- Replace
.catch(() => {})with.catch((err) => console.error("Offer lock error:", err))at minimum. - Better: surface lock acquisition failures via a toast (
alert.error("Nepodařilo se získat zámek, stránka bude pouze pro čtení.")). - For the unlock on unmount: best-effort
navigator.sendBeaconor a synchronous fallback so the lock is released even if the page is closing. - Estimated effort: S (1-2 hours).
#11 (audit item, not a real bug) — TOTP replay counter rewind
File investigated: src/utils/totp.ts:5-30, src/routes/admin/auth.ts:165-184
Status: False positive. The audit suggested verifyResult.counter could be lower than the actual step used. After reading the implementation:
const delta = totp.validate({ token: code, window: 1 });
// ...
const counterDelta = Math.min(delta, 0); // -1 or 0, never +1
const counter = currentCounter + counterDelta;
Math.min(delta, 0) clamps to ≤ 0, so counter is always the current step or one step in the past — never a future step. The counter <= lastCounter check in the route is correct and complete. No fix needed.
If anyone revisits this in the future, this analysis is the basis for closing the ticket.
Suggested order for the next sprint
- #10 (indexes) — cheap, high impact, addresses a growing data cliff. Do first.
- #9 (N+1) — bigger effort but the same root cause family. Tackle after indexes.
- #14 (offer lock) — small but prevents data loss; fits in a single PR.
- #13 (pagination) — UX polish; do last, batch with other UX cleanup.
Estimated total: 1-2 days of focused work.