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>
Promote the AI chat from a dashboard widget to a dedicated page:
- new sidebar item "Odin" under the Přehled section, gated on ai.use, → /odin
- new /odin route + Odin page (reuses the assistant component, with a light
ai.use guard)
- remove the widget from the dashboard
- rebrand the assistant header "Asistent" → "Odin" and give the thread more
height now that it owns the page
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a "Můj plán na dnešek" card near the clock-in: for the logged-in
user it shows today's work-plan entry — category (coloured dot + label),
the project/site (the "where"), the note, and a "součást rozsahu …" badge
only for genuinely multi-day ranges. A muted "Pro dnešek nemáte
naplánováno." empty state when nothing is scheduled.
Server: GET /api/admin/dashboard now returns today_plan, reusing
plan.service resolveCell (override-beats-range precedence) for the current
user + today's local (Prague) date, enriched with the category label +
colour so the widget needs no extra query. Gated by attendance.record /
attendance.manage (the plan permission); omitted otherwise. No migration.
Verified in Chrome: renders today's entry (Práce · OBJ-2026-006 — koko)
with the category colour. tsc -b --noEmit, npm run build, vitest 152/152.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The labelled icon-badge tiles were inconsistent: some used a same-hue
light tile + colored glyph (e.g. the Settings 2FA row: success.light tile
+ success.main glyph — low contrast, the reported "badly visible" icon),
and the solid-tile ones used the bright .main fill in dark mode without
darkening, so a white glyph on bright green/amber was low-contrast too
(DashProfile's success badge was ~1.9:1).
Add a single `iconBadgeSx(color)` helper (theme.ts): solid `<color>.main`
fill + white glyph, darkened in dark mode (FILLED_DARK_BG) so white stays
legible; `default` = neutral grey. Applied to every badge so they can't
drift: StatCard, DashActivityFeed, DashProfile (2FA badge), Dashboard
(2FA banner icon), AttendanceHistory (month tile), and the Settings 2FA
row tile (now solid green when required / grey when optional + white lock,
instead of the low-contrast light-green tile). Also switched DashProfile's
backup-codes warning callout from warning.light to a subtle warning wash
so its amber text stays readable in dark mode.
tsc -b --noEmit, npm run build, vitest 152/152 all clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two issues:
1. Icon badges read as a coloured glyph on a same-hue LIGHT tile
(bgcolor X.light + color X.main) — low contrast, "badly visible".
The AttendanceHistory month tile and the Dashboard 2FA-banner icon now
use a SOLID tile (X.main) + white glyph, matching the StatCard badge
convention. Readable in both schemes (verified: info.main #1d4ed8 light
/ #3b82f6 dark, error.main, white glyph).
2. Theme reverted to light on F5 while the toggle still showed dark.
Cause: two independent stores — our ThemeContext key (boha-theme) and
MUI's own cssVariables mode key (mui-mode). The toggle only wrote
boha-theme, so on mount MUI restored its stale mui-mode and overrode
data-theme. Collapsed to a SINGLE source of truth: ThemeContext now
reads/writes MUI's `mui-mode` key directly (with a one-time migration
from the legacy boha-theme, which is then removed). Verified: toggle
dark -> F5 -> stays dark; only `mui-mode` remains in localStorage.
tsc -b --noEmit, npm run build, vitest 152/152 clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The per-status row backgrounds used MUI's solid `.light` palette shades
(error.light/success.light/warning.light). Those are LIGHT colors in both
schemes, so in dark mode the row's white text sat on a light fill and was
effectively invisible; in light mode they read as garish saturated blocks.
Replaced with low-alpha washes via the palette channel vars
(rgba(var(--mui-palette-X-mainChannel) / 0.12), 0.18 on hover), which keep
the row's normal theme text colour fully readable in BOTH schemes:
- Offers: completed = subtle green wash; invalidated = faded/muted
(opacity + secondary text, the original "voided" look) instead of a
solid red block; expired = subtle amber wash.
- DataTable rowDanger (kit-wide — Warehouse, WarehouseReports): subtle
error wash for both the desktop row and the mobile card (card keeps its
error.main border as the danger cue).
- Invoices overdue rows: subtle amber wash.
- Dashboard 2FA banner Card: subtle error wash (keeps the error.main
border) so the banner copy stays readable in dark mode.
Verified live in Chrome on Offers (completed + invalidated, light + dark):
faint tint + normal readable text, channel switches per scheme. tsc -b
--noEmit, npm run build, vitest 152/152 all clean. (Left untouched: small
icon tiles / callout boxes that pair a .light bg with an explicit dark or
coloured text/glyph — those are readable in both schemes by construction.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adopt the PageEnter wrapper as every page's outermost render element and remove the ad-hoc per-page entrance motion.div wrappers. Every page now enters the same way — all top-level sections rise+fade in, staggered — so nothing appears instantly and the motion is identical app-wide. Presentation-only: no data/logic/hooks/invalidate/permissions touched. Embedded sub-tabs (CompanySettings, ReceivedInvoices), Login (auth shell) and the dev UiKit are intentionally excluded. Gates: tsc -b --noEmit=0, build=0, vitest 152/152.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Change attendance idx_attendance_user_date from unique to index (allow multiple shifts per day)
- Reset migrations to single baseline init migration
- Add seed script with admin user (admin/admin)
- Update CLAUDE.md with migration workflow documentation
- Various frontend fixes (queries, pages, hooks)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Replace hand-coded skeleton CSS/JSX with boneyard-js auto-generated bones
- Remove skeleton.css and @keyframes shimmer from base.css
- Add <Skeleton> wrappers with fixtures to all 25+ page components
- Generate 20 bone captures via boneyard CLI (CDP auth-gated capture)
- Refactor data fetching from useEffect+useState to TanStack Query
- Extract query hooks into src/admin/lib/queries/ and apiAdapter
- Add usePaginatedQuery hook replacing useApiCall/useListData
- Fix parseFloat || 0 anti-pattern in OfferDetail and OffersTemplates inputs
- Fix customer_id mandatory validation on offer creation
- Fix leave-requests comma-separated status filter (Prisma enum in: [])
- Add cross-entity cache invalidation for orders/offers/invoices/projects
- Make rate limits configurable via env vars (RATE_LIMIT_MAX, RATE_LIMIT_REFRESH, etc.)
- Add boneyard.config.json with routes and breakpoints
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Queried status "converted"/"expired" but actual DB values are
"ordered"/"invalidated". Updated label "Prošlé" → "Zneplatněné".
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Route handlers: add exhaustive return after error checks so TypeScript
narrows the union and result properties are accessible without casts
- attendance.service: use Prisma.attendanceGetPayload for included relations
- projects.service: remove unnecessary cast on orders relation
- Dashboard.tsx: replace Record<string,any> with proper DashData interface
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
API now only returns data sections the user has permission to see:
- my_shift: attendance.record
- attendance: attendance.admin
- offers: offers.view
- projects: projects.view
- invoices: invoices.view
- orders: orders.view
- leave_pending: attendance.approve
- recent_activity: settings.audit
Frontend hides KPI cards, activity feed, and attendance sections
for users without the matching permissions.
Regular employees now only see their shift status, quick actions,
profile, and sessions — not company KPIs or admin data.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>