Critical (data integrity):
- warehouse inventory confirm: throw (not return) inside $transaction so a
failed deficit line rolls back the surplus corrective receipt — retries
no longer accumulate phantom stock
- warehouse issue confirm: validate batches against the COMBINED quantity
of all lines (duplicate FIFO-resolved lines drove batches negative)
- attendance delete: restore vacation_used/sick_used for the deleted day
(in-transaction, clamped at 0)
High:
- auth refresh: terminated sessions (replaced_at only) get a plain 401 —
the theft branch (family revocation) now fires only on replaced_by_hash
- POST /users strips role_id for non-admin callers (mirrors PUT guard)
- issued-order transition flushes unsaved edits via the full save payload
when dirty; server contract (items+status in one PUT) pinned
- received-invoices list: usePaginatedQuery + pager (rows 26+ unreachable)
- received-invoice dates: nullableIsoDateString + NaN guard before NAS save
(Czech-format dates corrupted month/year, orphaned NAS files)
- leave approval skips Czech public holidays and books each calendar year's
hours against its own balance (mirrors createLeave)
Medium/Low (classes):
- 52 Zod caps aligned to DB column widths across 7 schemas (over-cap input
500ed at Prisma instead of a Czech 400)
- FK pre-validation: projects update + warehouse receipts/issues return
Czech 400s instead of P2003 500s
- invoice PDF degrades gracefully when the CNB rate is unavailable
(recap omitted instead of 500 + lost NAS archival)
- date boundaries: local-day filters (warehouse lists/reports, audit-log),
@db.Date coercion on invoice dates
- plan updateEntry re-checks the per-cell cap (self-excluding)
- {id} tiebreaks on customers/received-invoices/warehouse-items sorts;
/items honors the client sort param
- htmlToPdf relaunches once when the shared browser died mid-render
- offer number release parses the year from the document number (cross-year
finalize+delete left permanent sequence gaps)
- trips/vehicles km fields integer-coerced; AI budget regated to
settings.company|settings.system; Settings System tab no longer clobbers
Firma numbering patterns; draft invoices hide the dead PDF button;
dashboard quick-trip invalidates ["vehicles"]; TOTP secret cap 64;
audit-log + invoice month buckets day-shift fixes
Docs: corrected the stale "Chromium has no CSS margin-box footers" claim
(html-to-pdf.ts + CLAUDE.md — margin boxes render since Chrome 131); audit
report M3 withdrawn accordingly.
~65 new pinning tests; every finding reproduced RED against the real test
DB before its fix. Suite: 58 files / 634 tests green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One 0.2s enter-only fade (opacity + 6px rise) in the kit TabPanel covers
every tab switch (M3 clean-fade pattern; CSS animation so the global
reduced-motion kill switch applies). Orders and Invoices view switchers
now render through TabPanel (also fixes their dangling aria-controls).
Removed the inconsistent bespoke framer entrances inside tab content
(CompanySettings cascade, ReceivedInvoices card). Filter-style status
tabs (Projects/Offers/Invoices) deliberately keep the refetch dim only.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
get_document_totals gains type=received_invoices: whole-set gross sums per
currency + total_czk via the same toCzk logic as the stats route (unknown
currencies skipped + reported), month/year via the literal columns (bare
year supported), unpaid/paid filter, period echo. invoices.view joined the
coarse gate (the per-type re-check keeps other families locked); the tool
description routes 'kolik jsme utratili' questions here.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Quill 2's semantic HTML saves a blank line as a truly empty <p></p> (no
<br> placeholder) — verified in the DB (invoice_sections hold <p></p>,
zero <p><br>) — which collapses to nothing outside the editor.
cleanQuillHtml (PDF path) and the new restoreQuillBlankLines (order/
invoice read-only previews) restore the placeholder at render time, so
existing stored content is covered retroactively.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- section/Quill overrides, footers and the shared header now lead with
'Segoe UI' (installed on prod) instead of Tahoma (not installed — prod
silently rendered sections and footers in Noto Sans, mismatching the
Segoe UI body; dev previews showed real Tahoma and never matched prod)
- cleanQuillHtml no longer deletes ALL style attributes: color and
background-color survive with strictly validated literal values (hex,
rgb/rgba, keyword) so editor font colors finally reach the PDF; url(),
expression() and every other declaration are still stripped — new test
file pins both the kept colors and the attack vectors
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 'Podrobny popis' textareas in the shared items editor and the invoice
page now grow with content like the rich-text editor (fixed rows={2} ->
minRows/maxRows autosize), capped at 16 rows; the manual vertical resize
handle stays.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- SPZ matching normalized on both sides (4SY7039 finds '4SY 7039') in
list_vehicles and the list_trips vehicle filter; JS-side name matching
diacritic-folded to keep utf8mb4_unicode_ci parity; ICO typed with spaces
matches the space-less stored value (suppliers + customers)
- get_top_customers: whole-table per-customer counts of invoices (sans
drafts) / orders / projects, top 20 + customers_with_records, optional
year, per-type permission re-checks — answers 'pro koho pracujeme nejvic'
exactly instead of refusing
- find_employee, find_supplier and stock search now return total_matching
(stock search also gained its missing deterministic orderBy); system
prompt teaches the model to answer counts from total_matching and use
aggregation tools instead of summing 20-row list pages
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Seven read-only tools: leave balance (own/attendance.balances parity), work
fund + overtime (getWorkfund), project hours report (attendance.manage),
supplier lookup (inactive only for warehouse.manage — picker-route parity),
vehicles with the route's odometer rule, whole-set document totals per
currency (fixes the sum-of-20-list-rows trap; bare month defaults the year
and the applied period is echoed — adversarial review caught the silent
all-time-as-monthly figure), and received-invoice detail with match count.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Five read-only detail tools (offer, order, issued order, invoice, project)
looked up by document number (partial match). They call the same getX(id)
services the detail routes use under the same .view permissions (parity
verified route-by-route), and return compact payloads: line items capped at
50 rows with full totals, is_included_in_total respected, rich text
stripped to plain excerpts, invoice VAT broken down per the editor rule
(per-item rate + apply_vat).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A PTR reload lands while the standalone-PWA viewport is still settling and
re-races the --app-height measurement (Chromium settles without firing
resize), bringing the scroll room back ~50% of the time. The immersive page
is fixed-viewport, so overscroll-behavior-y: none on html+body removes the
gesture (and the race trigger) entirely; restored on route leave.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Installed PWAs (MIUI especially) compute svh/dvh against a viewport that
includes system UI the layout viewport doesn't have, so the immersive page
still scrolled in standalone mode. AppShell now measures window.innerHeight
into --app-height (refreshed on resize/visualViewport, covering the
minimize-restore stale-viewport Chrome bug and the keyboard), and the
immersive shell + chat size from var(--app-height, 100svh).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Xiaomi Chrome keeps 100dvh at the large-viewport size while the URL bar
overlays the page, so the shell's 100dvh min-height left the document one
bar-height taller than the screen (scrollable + a strip under the
composer). The immersive route now sizes the document to exactly 100svh
with hidden overflow down the chain — body scroll is impossible regardless
of the browser's dvh interpretation. Other routes unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- bubbles spring in anchored at their avatar corner; thinking state is a
typing bubble (three bouncing dots) that springs in/out where the reply
lands; reduced-motion falls back to opacity fades
- mobile /odin is immersive: AppShell hides its header and main padding
below md, the chat owns 100svh full-bleed with safe-area insets and a
back arrow (history back, home on deep links)
- desktop card border kept divider-colored via longhand border props (a
responsive border shorthand reset the color to black)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Assistant bubbles use the animated OdinMark instead of the plain 'O'
circle; the 'Pracuji…' row drops its horizontal padding and matches the
28px mark size, so the thinking mark sits exactly in the avatar column.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The system prompt now lists the data areas the caller's permissions filtered
out, so the model tells the user explicitly they lack rights to that module
(and can ask the admin) instead of guessing the feature does not exist or
pointing them at a module they cannot open. Tool-less users get the same
explicit phrasing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A failed call followed by a successful retry showed two identical chips
(one orange). Retries are loop mechanics: the trace now carries one entry
per tool with ok = delivered-at-least-once, for live chips and persisted
history alike (also keeps meta.tools under its 30-entry cap).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- the sent message now leaves the input the moment the optimistic bubble
appears (restored along with attachments if the request fails)
- text selected in the primary-filled user bubble inverts to
white-on-primary instead of vanishing into the global ::selection style
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- find_employee: name -> user_id with route-parity population scoping
(attendance perms see plan users, trips perms see drivers sans usernames,
full directory only with users.view; trips.history unlocks nothing)
- get_attendance_summary: day-range detail with worked hours (shared
calcWorkedHours, break subtracted) + totals; attendance.manage also
unlocks the tool
- list_trips: kniha jizd with route-identical non-manager self-scoping and
the stats-endpoint km coalesce; bare month defaults to current year
- get_work_plan: resolved plan grid rows; off-grid users need
attendance.manage (grid-route parity)
- executeTool flags handler {error} results not-ok (consistent denial chips)
- system prompt: resolve employee names via find_employee first (gated on
the tool being present)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- PlanGrid maxHeight gets a 360px floor: calc(100dvh - 240px) left
<160px in phone landscape and the rows vanished under the sticky
header
- Phones get a person picker above the plan grid (one column at a
time, defaults to the logged-in user) - the multi-person grid never
fit a portrait viewport; desktop unchanged
- Odin chat height uses 100svh instead of 100dvh: dvh grows live as
Android Chrome collapses its address bar, so the page always allowed
a chrome-height scroll; svh sizes for the bar-visible viewport
(verified zero overflow at 390x844 via Playwright)
- Plan toolbar stacks into full-width rows on xs (headerActionsSx
pattern from the detail pages); Dnes/arrows and Tyden/Mesic stay
paired on one row each
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@fastify/static's default Cache-Control management overwrote the
setHeaders values with 'public, max-age=0' (verified on prod after the
v2.4.8 deploy) - the README requires disabling it for custom headers.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
After a release the old hashed chunks are deleted, so an open tab from
the previous build failed its next lazy-page import until a manual
refresh. Three-part fix per the Vite deploy guide:
- vite:preloadError listener reloads the page once (10s sessionStorage
guard against loops) so stale tabs heal themselves
- static cache headers: /assets/* immutable for a year (content-hashed
names), index.html no-cache so new visits always see fresh chunk refs
- missing /assets/* now 404 instead of getting the SPA index.html
fallback (HTML-as-JS masked the failure)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two causes: theme h4 had no responsive size (desktop 2.125rem on
phones - now 1.5rem under 600px, applies to all page headlines), and
the Zpet+title header row had no flexWrap so long document titles
pushed past the viewport (issued orders, offers, both invoice views).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>