- 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>
The dodavatele modal is now an exact mirror of the customers modal
(minus the PDF field-order picker): Nazev+ARES, Ulice, Mesto+PSC, Zeme,
ICO+ARES, DIC, and the same "Vlastni pole" editor (maxWidth md).
Two migrations on sklad_suppliers:
- structured street/city/postal_code/country replace the free-text
address blob (best-effort split: street / PSC regex / city)
- custom_fields LONGTEXT replaces contact_person/email/phone/notes;
existing values are preserved as labeled custom fields
The encode/decode of the custom_fields blob is shared with customers
via the new utils/custom-fields.ts. The PO PDF supplier block renders
the structured address + custom-field lines like the customer block;
the supplier picker/detail selects and types follow. Legacy clients
sending the dropped keys are silently stripped (tested). Suppliers
list shows Ulice/Mesto instead of the dropped contact columns; search
covers name/ico/city.
BREAKING CHANGE: sklad_suppliers.address, contact_person, email,
phone, notes columns dropped (data migrated); deploy must run
prisma migrate deploy + generate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New /api/admin/ares proxy (browser cannot reach ares.gov.cz - CORS+CSP):
GET /ico/:ico and GET /search?q= map the MFCR REST API to the form
shape (street incl. orientation numbers, "511 01" PSC format, DIC).
Guarded by customers.create/edit + warehouse.manage; token errors map
to Czech messages. Shared AresAdornment button sits in the Nazev and
ICO fields of both modals: ICO mode fills directly, name mode searches
with a result picker. Tested against live ARES (BOHA, ICO 22599851).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Invoices now mirror the issued-orders document model:
- New invoice_sections table (CZ/EN rich-text "Obsah") edited via the
shared SectionsEditor, printed inline right after the items on the
PDF. Full-replace on update, same transaction as items.
- Printed notes dropped: the notes column is removed (migration merges
existing content into internal_notes first); the form field is now
"Interni poznamky", never printed. Legacy payloads sending notes are
silently stripped.
- Form cleanup: Cislo faktury and Vystavil fields removed (number lives
in the header, issued_by auto-fills); page header title restyled to
the orders/offers pattern (number span + status chip).
- Unified per-page PDF header for the red-accent family: shared
buildPdfHeaderTemplate in pdf-shared (22mm logo, red heading, red
rule) rendered by a Puppeteer headerTemplate on EVERY page of both
invoices and issued orders (incl. the /file fallback render); body
headers are print-hidden. htmlToPdf gained the headerTemplate option.
- Footer parity: invoices get the per-page "Vystavil + Strana X z Y"
footer; the invoice bottom block (notice + QR/VAT recap + Prevzal)
is break-inside: avoid so a page break can never split it.
- @page margins now match the template space (32mm top, 18mm bottom) -
Chromium lays out by CSS @page margins, which also fixes issued
orders' content running into the 18mm footer zone on full pages.
BREAKING CHANGE: invoices.notes column dropped (data merged into
internal_notes); deploy must run prisma migrate deploy + generate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
deleteOrder only nulled quotations.order_id and left status "ordered" -
a dead end (ordered only transitions to invalidated), so the offer could
never be ordered again and the "Vytvorit objednavku" button stayed
hidden. The delete transaction now reverts ordered -> active together
with the back-reference clear; regression test included.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
saveIssuedPdf ran through uniquePath, so whenever two archive calls raced (the fire-and-forget ?save=1 after a save vs. the /file fallback, or two quick saves) the second write landed as <number>_1.pdf - and those copies were invisible to the reader and cleaner (exact-name match), accumulating forever. Now the issued archive writes to the deterministic Vydane/YYYY/MM/<number>.pdf and overwrites (same model as the offers manager), and cleanIssued also sweeps stale <number>_N.pdf duplicates so existing junk self-heals on the next save/delete. Document numbers are digit-only (never contain _), so the suffix match cannot hit a different document. uniquePath remains for received-invoice uploads where name collisions are legitimate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The PDF now carries a real repeating footer on EVERY page via Puppeteer's footerTemplate (Chromium has no CSS margin-box support, so this is the only true footer): 'Vystavil: <logged-in user>' on the left, 'Strana X z Y' / 'Page X of Y' centered, localized by the document language. htmlToPdf gained an optional footerTemplate option (empty header suppresses Chromium's default; bottom margin grows to 18mm). The old body footer pinned by flex min-height is gone. The 'Obsah' sections no longer start a new page with a repeated header - they flow inline right after the items/totals block and paginate naturally (break-inside: avoid per section). Applied at all three render sites (PDF route, ?save=1 archive, /file fallback).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per user decision the rich-text sections now carry all free-form PDF content on issued orders: dropped columns delivery_terms, payment_terms, issued_by, notes (migration applied to dev+test) and their PDF blocks + form fields. Kept: order_text (moved from the bottom card into the Zakladni udaje grid, replacing the Vystavil field) and internal_notes (now the only field in the bottom card, never printed). Sections card is titled 'Obsah' on issued orders; offers keep 'Rozsah projektu' and are untouched. Legacy clients sending the dropped keys are silently stripped (tested). PDF footer 'Vystavil:' stays - it prints the logged-in user, not the dropped column.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One coherent pass over the two sibling document models, driven by a 3-lens
1:1 scan (~60 divergences inventoried) + user decisions. Migration adds
issued_orders.locked_by/locked_at and the issued_order_sections table
(mirror of scope_sections; applied to dev + test DBs).
Issued orders gained (offers as reference implementation):
- rich-text SECTIONS with CZ/EN titles, rendered on their own PDF page in
the PO template's red style (shared DocumentSectionSchema, one-transaction
create/update incl. items - fixes a torn-write bug)
- edit LOCKING (lock/heartbeat/unlock routes, 423 + holder name, 30s TTL =
3 missed 10s heartbeats; locked_by enrichment on detail)
- archived-PDF serving: GET /:id/file reads the NAS copy (new
readIssuedByNumber sweep) with live-render fallback + re-archive; offers'
/file got the same fallback (kills the 'ulozte nabidku' dead end)
- NAS cleanup on delete, in-tx po_number uniqueness (409), collision-advancing
number previews (also invoice previews), PUT returns assigned po_number
Offers hardened (issued as reference):
- VALID_TRANSITIONS enforced (no more numberless 'ordered' offers; invalidate
follows the table; order creation only from active offers) +
valid_transitions on detail
- explicit 400 instead of silent edits outside draft/active (mirrored on
issued: explicit 400 replaced silent drops)
- customer existence check + Number(null)->0 clear bug fixed; delete of a
linked offer -> 409 instead of P2003 500; error-token convention; Zod caps
DB-aligned (desc 500/unit 20/number 50/project_code 100, isoDateString
dates, ints for positions); audit old/new values + koncept fallbacks;
list id tiebreaks; stats include trimmed; NAS delete dedupe
Frontend unification (6 new shared modules):
- components/document/{DocumentItemsEditor,SectionsEditor,LockBanner}
- hooks/{useDocumentLock,useUnsavedChangesGuard,useDocumentPdf(+list variant)}
- OfferDetail 1940->1100 lines, IssuedOrderDetail 1400->980: headline-only
document number (form field removed), one form layout/readonly convention,
view-permission opens read-only everywhere (issued's editable-for-viewers
hole closed), server-driven transition buttons, dirty guard + Enter submit
on both, useApiMutation everywhere (new opt-in envelope mode), fixed
infinite spinner on failed detail fetch, draft PDFs hidden (no number yet)
- lists: supplier filter + count line on issued, Mena column dropped + mono
numbers on offers, shared hardened per-row PDF flow (spinner, double-click
guard, 401 close, blob cleanup), proper Czech quotes, real CTA empty
states, query-lib cleanups (["offers","customers"] key, typed list rows,
shared CurrencyAmount, retry:false on details)
- pdf-shared.ts: one escapeHtml/cleanQuillHtml(strict)/formatNum(NBSP)/
formatCurrency/formatDate for all four PDF routes; offer PDF keeps its
monochrome look (fractional qty fix: 1.5 no longer prints as 2); issued
PDF language now comes from the document column
+49 tests (suite 364 -> 413). Each stage passed an independent review; final
cross-stage integration review verified the FE<->BE contracts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 'Ceny jsou uvedeny bez DPH...' explanatory line is removed at user request from all three non-tax-document PDFs - the 'Celkem bez DPH' total label carries the information by itself. Dead vat_notice keys and .vat-note CSS removed; tests now pin the notice's ABSENCE (file renamed pdf-vat-note -> pdf-no-vat).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Accounting rule: nabidky, objednavky (incl. confirmation PDF) and objednavky
vydane are NOT tax documents - VAT belongs only on invoices. Per user
decision this is a FULL removal including DB columns.
- migration: DROP orders.vat_rate/apply_vat, issued_orders.vat_rate/apply_vat,
issued_order_items.vat_rate, quotations.vat_rate/apply_vat (applied to
dev + test DBs)
- services: net-only totals everywhere (computeIssuedOrderTotals(items) ->
{total}; enrichOrder/enrichQuotation net; per-currency list totals net)
- PDFs (offers, order confirmation, issued order): no VAT columns/summary,
single 'Celkem bez DPH' / 'Total excl. VAT' total, note under totals:
'Ceny jsou uvedeny bez DPH. DPH bude uctovano dle platnych predpisu.';
duplicate Cena/Celkem column merged (desc width 56%); orders-pdf render
extracted as exported renderOrderConfirmationHtml for testability
- frontend: Uplatnit DPH checkboxes, VAT selects and per-item VAT columns
removed from OfferDetail/OrderDetail/IssuedOrderDetail/
OrderConfirmationModal/ReceivedOrders manual-create; list footers read
'Celkem bez DPH'; invoice-from-order prefill now takes the company default
VAT (invoice decides its own VAT)
- tests: suites reworked to net math; new pdf-vat-note.test.ts pins the
exact cs+en note text and VAT-free layout on all three PDFs
Invoices and received invoices keep their VAT handling unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New nullable issued_orders.order_text column (migration applied to dev+test). The heading above the PDF items table is now editable per order, mirroring invoices' billing_text: empty falls back to the default, which is now 'Objednavame si u Vas:' per user wording. Field sits above Dodaci podminky in the detail form; persisted via create + update strFields (both schemas - the invoices Update-schema gap is not repeated). Tests: persistence round-trip incl. null-clears, PDF custom heading + default fallback.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Applies to both the issued-order (PO) PDF and the order-confirmation PDF: when apply_vat is off the %DPH and DPH columns are dropped entirely (widths redistributed) instead of printing 0% / 0,00, the redundant Mezisoucet row is omitted, and the grand total is labeled 'Celkem bez DPH' / 'Total excl. VAT'. The per-line DPH cell already contained only the line VAT (never netto+VAT) - now pinned by a regression test (2x100 @ 21%: DPH cell 42,00, Celkem cell 242,00).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Prisma truncates a JS Date used in a WHERE filter on a @db.Date column to
its UTC date part. Under TZ=Europe/Prague a local-midnight boundary
(new Date(y,m,d) = 22:00/23:00Z of the previous day) therefore filtered as
the PREVIOUS calendar date. Same class as the dashboard fix; full sweep of
every @db.Date filter in the codebase. New shared helper
utcMidnightOfLocalDay() in src/utils/date.ts.
Fixed (all previously off by one day):
- attendance.service: getStatus today+month windows; getWorkfund (last day
of each month was double-counted across months); getPrintData (monthly
print included prev month's last day); listAttendance (admin month view
included prev month's last day AND dropped the selected month's last
day); createAttendance duplicate/overlap validation (checked only the
PREVIOUS day - same-day duplicates were never caught, neighbors falsely
rejected)
- invoice-alerts: the 'splatnost za 3 dny' advance alert had NEVER fired
(due==today+3 was outside the fetched window); window computation
extracted as computeAlertWindow() + pure tests
- invoices.service: month list/totals filter dropped invoices issued on the
month's last day; getInvoiceStats month/year bounds; markOverdueInvoices
boundary; auto paid_date could store yesterday during 00:00-02:00
- received-invoices auto paid_date, issued-orders default order_date: same
night-window write hazard, now via the helper
- attendance.schema: shift_date hardened to isoDateString (bare YYYY-MM-DD)
+9 regression tests (month boundaries, same-day duplicate, last-day-of-month
invoice in list+totals, alert window).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Issued orders are purchase orders WE send - they must pick from suppliers
(sklad_suppliers), not from customers. Per user decision customer_id was
REPLACED (not kept alongside): migration drops issued_orders.customer_id and
adds supplier_id FK -> sklad_suppliers (existing rows lose their counterparty
- the feature is days old; re-point them in the UI).
- service: input/filters/search (suppliers.name + ico)/enrichment/detail all
supplier-based; create validates the supplier inside the transaction and
update before write -> Czech 400 'Dodavatel nenalezen' instead of P2003 500;
detail returns a minimal supplier field set (no internal notes leak)
- routes: supplier_id on list + stats; new GET /issued-orders/suppliers
lookup (orders.view/create/edit guard - orders users lack warehouse.manage
which guards the warehouse suppliers CRUD), active suppliers only,
name+id ordering
- PDF: Dodavatel block now renders the supplier (name, newline-split address,
IC/DIC), layout and both language label sets unchanged
- frontend: new SupplierPicker kit component (CustomerPicker untouched),
IssuedOrderDetail/IssuedOrders switched to supplier_id/supplier_name; the
picker keeps a fallback option for orders whose supplier was later
deactivated; WarehouseSuppliers CRUD now also invalidates issued-orders so
the picker can't go stale
- tests: issued-orders suite switched to supplier fixtures + new coverage
(lookup shape + 403, nonexistent supplier 400, PDF supplier block)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two dashboard bugs reported after clock-in:
1. 'Dochazka dnes' / 'Pritomni dnes' showed everyone absent even after
refresh: attendance.shift_date is @db.Date and Prisma truncates filter
Dates to their UTC date part, so the local-midnight boundaries
(new Date(y,m,d) = 22:00/23:00Z of the previous day) queried
[yesterday, today) and matched zero of today's punches. Boundaries are
now UTC-midnight instants of the local (Prague) calendar day.
Reproduced empirically against real rows; route-level regression test
added (dashboard.test.ts).
2. After logout + login as a different user, cached dashboards/buttons from
the previous user were served: query keys are user-agnostic and the
React Query cache outlives the session. logout(), login() and the 2FA
verify path now clear the whole cache.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The KPI cards and on-screen summary totals (computeUserTotals) are computed
client-side from one month fetch. The hook requested limit=1000 but
parsePagination silently clamped it to 100, so once a month exceeded 100
attendance rows (~5 employees x 21 shifts) the newest-first list dropped the
EARLIEST days of the month — screen totals undercounted while the print path
(complete server fetch) stayed correct. This is the data-volume cliff that
made the summary counting look like it changed without any formula change:
git archaeology confirms every counting formula is bit-identical from v1.9.1
through HEAD.
- routes/admin/attendance.ts: list endpoint passes maxLimit 2000 to
parsePagination (complete-month view; 2000 ~ 64 employees x 31 days)
- useAttendanceAdmin: month fetch requests limit=2000 with the constraint
documented
- regression test: a 120-row month returns all 120 records
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes every CRITICAL/HIGH finding from the 2026-06-09 full-codebase audit
(REVIEW_FINDINGS.md); each fix went through independent spec + code-quality
review. Plan and per-task log: docs/superpowers/plans/2026-06-10-audit-high-fix-pass.md
- attendance: schemas accept the combined local datetimes the forms/service
use (new dateTimeString helpers in schemas/common.ts), breaks persist on
create, AttendanceCreate submit rebuilt — every submit 400'd since 519edce
- 2fa: backup codes wired to /totp/backup-verify (+ remember-me parity),
enrollment QR generated locally via qrcode (CSP-blocked external service
also leaked the secret), dashboard shows per-user enrollment, not policy
- invoices/orders: per-line VAT survives re-saves (numberOr 0-respecting
coercion in formatters.ts), billing_text persists on update, issued-order
status transitions update UI gates
- trips: real pagination on all 3 pages, GET /trips/stats server aggregate
(shared buildTripsWhere + legacy distance coalesce), vehicle_id applies on
PUT with both-vehicle odometer recompute, print rebuilt (sync window.open,
escaped template, server totals)
- orders api: attachment_data PDF blob excluded from all non-binary reads
- warehouse: unit field is a Select over UnitEnum, receipt attachments
downloadable via new authenticated GET route
- downloads: shared RFC 5987 contentDisposition helper — Czech filenames no
longer 500 (warehouse, received-invoices, orders endpoints)
- misc: block-env hook actually blocks (exit 2 + stderr), project create
works with empty dates, NaN filter guards on trips endpoints
- deps: remove unused concurrently (clears both critical advisories), pin
@hono/node-server >=1.19.13 via overrides (clears the 3 moderates without
the Prisma 6 downgrade), drop deprecated @types stubs
Gates: tsc -b clean - vitest 30 files / 342 tests (31 new) - eslint 0 errors
- build OK
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mirrors the orders totals exactly: /stats (offers) + /list-totals (issued +
received invoices) endpoints sum each doc's total per currency across the active
filter (reusing extracted where-builders so they stay in sync with the lists),
rendered as the identical 'Celkem: …' block via the shared formatMultiCurrency.
Existing invoice/received KPI stats untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Keeps the 'Vystavil: <name>' line (logged-in user) and the removed Schválil
signature row; drops the e-mail line + its param field/translation keys per
request.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Issued-order PDF: drop the Vystavil/Schválil signature row; footer now shows
'Vystavil: <name>' + 'E-mail: <email>' from the logged-in user (authData).
- IssuedOrderDetail PDF button 'Export PDF' -> 'Zobrazit objednávku' (already
opens inline like invoices' 'Zobrazit fakturu').
- New /orders/stats + /issued-orders/stats endpoints sum order total (incl VAT)
per currency across the active filter; rendered as 'Celkem: …' beneath both
the received and issued lists. formatMultiCurrency lifted to a shared util.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The bank form posted {id, bank:{...}} (nested) but the route/schema read fields
at the top level; lenient z.object silently stripped the wrapper, so every field
saved as null while returning success. Flatten the payload to top level and switch
the create/update schemas to strictObject so a future nesting 400s loudly instead
of saving a blank record. Affects both create and edit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses review of the issued-orders service: deleteIssuedOrder now derives the release year from created_at (cross-year deletes reclaim the right sequence); list sort direction is normalized to asc/desc; status cast uses $Enums instead of as-never. Adds a cross-year delete test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The amount on a received invoice is the GROSS total the user pays (VAT
included), and VAT is the portion contained within it — not a net base with
VAT added on top. Switch the formula from amount*rate/100 to the
VAT-inclusive amount*rate/(100+rate) via a shared, tested vatFromGross()
helper used by both the create (manual upload + AI import share this
endpoint) and edit paths. Rate 0 → no VAT.
- AI extraction now asks Claude for the total INCLUDING VAT (was "základ bez
DPH"), reinforced in the structured-output schema description.
- Label the amount field "Částka s DPH" in the AI review card and the manual
upload/edit forms so the gross convention is explicit.
- Add unit tests with the user-confirmed figures (22 542,91 @ 21% → 3 912,41,
base 18 630,50) plus other rates and the no-VAT case.
Existing rows keep their stored (gross) amounts; their vat_amount recomputes
on next edit. A one-shot backfill can correct historical vat_amount if wanted.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Server-side, per-user chat history (the thread now survives reload):
- new ai_chat_messages table + migration; getChatHistory/appendChatMessages/
clearChatHistory; GET/POST/DELETE /api/admin/ai/history (ai.use-guarded);
service + HTTP tests (isolation, ordering, clear, perm, validation).
- DashAssistant loads the thread on mount and persists each turn; "Vymazat"
clears it. gcTime:0 so each mount re-reads the DB (no stale-cache resurrection);
composer gated until history settles + seed flag flipped on submit (no
seed/submit race clobbering a freshly-sent turn).
UX + correctness fixes from review/feedback:
- review cards moved OUT of the scrollable thread into a "Faktury k potvrzení"
section so Uložit is always reachable; chat auto-scroll no longer fires on
field edits.
- chat bubble text colour set on the Typography (GlobalStyles pins `p` to
text.secondary, which was overriding the inherited bubble colour → unreadable).
- never clear input/staged PDFs on a failed submit (rollback + keep), so a
budget error can't wipe staged invoices; roll back the optimistic user turn.
- stable attachment ids (no crypto.randomUUID — undefined over plain HTTP).
- editable Datum splatnosti + Popis fields; client-side extraction summary.
Security: DashProfile clipboard copy now uses an execCommand fallback (works
over plain HTTP) and only shows the success toast when the copy actually
succeeded — previously it falsely claimed to copy 2FA codes / the TOTP secret.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The order delete modal sends { delete_files }, but the route ignored the
body and deleteOrder never touched the NAS — so the project folder of an
order-spawned project always stayed on the share (the only way to delete
such a project is via its order, since deleteProject blocks order-linked
projects). Project delete (standalone projects) already worked.
- orders route: read delete_files from the body, pass to deleteOrder.
- orders.service deleteOrder(id, deleteFiles): select project_number and,
after the DB transaction commits, best-effort deleteProjectFolder for
each linked project when the flag is set. Non-fatal; idempotent.
- tests: NasFileManager.deleteProjectFolder coverage — removes by number
prefix, no-op when absent, false when not mounted (158 -> 161 tests).
Bump version to 2.0.4.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Manual project creation already creates the 02_PROJEKTY/<number>_<name>
folder (projects.service.createProject). Projects created from an order —
both from an accepted offer (createOrderFromQuotation) and a manual
offer-less order (createOrder) — did not, so they had no NAS folder until
a user opened the project and clicked "create folder" or uploaded a file.
- orders.service: best-effort, non-fatal createProjectFolder after each
project-creating transaction commits (mirrors createProject; gated on
isConfigured(), logs on failure, never rolls back the order).
- projects.service: log when the existing auto-create returns false
(was silently discarded — CLAUDE.md known-issue #4).
- nas-file-manager: optional constructor basePath seam for tests.
- tests: NasFileManager.createProjectFolder coverage — naming, idempotency,
Czech diacritics, empty name, not-mounted (152 -> 158 tests).
Bump version to 2.0.3.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>