Changelog

What's new.

Every release across the platform — features, fixes, and the AI getting sharper. Pulled from the product's own release notes.

June 24 – 28, 2026

Validator tightens, send-pipeline reliability, PWA recovery

35 issues closed in five days. Three threads dominate:

  1. Outbound validator gets sharper teeth + safety valves. Per-severity blocking thresholds (block all / major+critical / critical-only), a third Refine option on every blocked send (autonomous auto-refine loop capped at 5 + operator notify), a proper audit log of every SEND-ANYWAY override + manual-send block, and a string of false-positive fixes that stop blocking correct sends.
  2. Send-pipeline silent-skip family closes. The gh#724 delivery guard previously covered access-code tasks only — now it covers Welcome / Pre-checkout / Check-in-reminder too (gh#865, 7 prod victims). Partial image-batch failures no longer report success:true while silently dropping images. Validator-block + retry no longer re-rolls non-deterministic verdicts until a blocked send leaks through.
  3. PWA recovery. An installed PWA opened to a blank white screen after a deploy when the server returned index.html (200) for missing /assets/* instead of 404. Now: 404 on missing chunks → service-worker cleanup → PWA reloads instead of staring at white.

Plus Supplies 4-level model (parsed / corrected / product-type / category) with merge-by-product-type, V28 task card reassign dropdown returned, V28 Pulse cache invalidation on reassign, and a wave of "silently never spawned" cron-side fixes.


✨ What's new

🎚 Configurable outbound-validator blocking threshold

Outbound validators now ship with a per-binding blocking threshold (block all findings / major+critical only / critical-only) plus a per-template override. Pair this with the per-validator confidence threshold shipped last release and operators get fine-grained control over what the AI is allowed to interrupt.

Why operators care: Stop a noisy validator without disabling it entirely — let it surface advisories on minor, only intercept on critical. (#850)


🪄 Refine — the third option on a validator-blocked message

Every validator-blocked send now offers three actions instead of two:

  • Dismiss — drop the draft
  • Send Anyway — operator-in-loop override (now properly audited, see below)
  • Refine — autonomous auto-refine loop: LLM addresses the validator's findings, re-validates, sends if clean. Capped at 5 iterations + operator notify. Works in both operator-in-loop manual flow AND directly from WhatsApp action buttons.

Why operators care: Most validator blocks are pedantic — Refine lets the AI fix its own draft instead of forcing you to. (#879)


🧾 SEND-ANYWAY override + manual-send blocks — now properly audited

A comment claimed these were audited; they weren't. Now they are — every operator SEND-ANYWAY override and every manual-send block produces a validator_decisions row with operator, finding, decision, and timestamp. Visible in the new Admin → Logs → Validator Decisions tab (with object filter); the bespoke inbox VALIDATOR DECISIONS panel was removed in favour of the consolidated logs view. (#832, #845)


⏰ V28 Early check-in & Late checkout cards — editable approved time

The read-only "requested time" field is now editable + savable on the Early Check-in and Late Checkout cards. Operator can approve a guest's request at a different time than they asked for (e.g. guest wanted 11:00, operator approves 13:00) without bouncing to a different screen.

Why operators care: One-click time negotiation, captured in the right place. (#863)


💰 V28 Reservation FINANCIALS — gross vs net + channel commission

The FINANCIALS panel headline rows now explicitly label gross vs net, and a new channel commission row surfaces so the math reconciles at a glance.

Why operators care: "Why doesn't 245 - 49 = the host-payout number I expected?" is now answered on the same panel. (#872)


📦 Supplies — 4-level model (parsed / corrected / product type / category)

The supplies model grew a third intermediate layer:

  • Parsed name — raw OCR / receipt-line text
  • Corrected name — operator/AI-canonical name (gh#808)
  • Product type (NEW) — abstract product the corrected names roll up to (e.g. "Finish dishwasher tabs" + "Somat tabs" → product-type dishwasher_tab)
  • Category — consumption-model bucket (gh#736)

Inventory now merges by product type so the dashboard shows "you have 87 dishwasher tabs across 3 brands" instead of three separate rows. (#878)


👤 V28 Tasks card — reassign dropdown returned

The classic UI had an editable "Assigned to" dropdown on every task card; V28 was rendering the assignee as read-only text (the updateAssignee mutation already existed). Dropdown back. (#868)


🐛 Critical bug fixes

🚨 PWA blank white screen after deploy

An installed iOS/desktop PWA opened to a blank white screen for ~5–15 minutes after every deploy. Root cause: the SPA shell requested old asset hashes that no longer existed; the server's catch-all returned index.html (200) for missing /assets/* instead of 404, so the browser tried to execute HTML as JS → crash → blank. Fix: missing assets now return 404 → service worker recognises the stale state → forces a reload of the new shell. (#874)

🔴 Outbound-message task settled DONE despite send validator-BLOCKED — 7 prod victims

The gh#724 delivery guard introduced last release covered access-code tasks only — Welcome / Pre-checkout / Check-in-reminder tasks still settled as done (+ "Task completed" WA notify) when the send was validator-blocked. Seven prod guests had a task marked complete with no message delivered. The guard now blankets every outbound-message task; a permanent E2E test exercises the blocked-send path per task type and asserts the task does NOT auto-resolve. Same family as gh#180 silent-skip. (#865)

🔴 Outbound validator defeated by retry — blocked sends leaked through

The auto-resolve cron re-runs the (non-deterministic LLM) validator every 6 minutes until a blocked send eventually PASSES — Patrik's check-in reminder reached the guest after ~30 minutes of retries despite being initially blocked. Fix: a blocked verdict now persists; re-validation requires an actual draft change (refine or operator edit), not a clock tick. (#854)

🔴 Validator FALSE-POSITIVE on empty thread context

A correct draft was being blocked because defaultLoadThread returned translatedBody='' (empty-string sentinel) — the validator saw a blank guest message + a contextual reply and flagged the reply as "out of context." Fix: producer-side cleanup (NULL instead of empty string; 195 prod rows backfilled) + consumer-side defensive read. Sibling fix: validator no longer blocks on non-issue findings like a "disregard / N/A" consideration or a truncated finding string. (#859, #860, #866)

🔴 Mobile PWA section crashes — "This section couldn't load"

The mobile PWA /m/* subtree was hitting a genuine render crash on load. Root cause traced via the app_errors.react_error_boundary:Mobile row. Fixed + added a stricter mobile ErrorBoundary path that captures the error + offers a recovery action instead of just a blank state. (#847)

🔴 Teammate notifications silently suppressed when WA window closed

The gh#484 cost-reduction (gate teammate messages by 24h service window) was silently dropping notifications when the window was closed — operator never knew. Fix: always deliver (template message when the window is closed), warn-log any silent loss path that remains. (#849)

🔴 Host-personality "Propose from history" compressing + language-switching

A prior fix shipped but was ineffective — the LLM kept ignoring "preserve formatting" and reading "host's primary language" as Slovak. Re-prompted + added a behavioral test that exercises a long English personality and asserts the proposed output is NOT compressed and stays English. (#819)

🔴 Daily-review walkthrough looped on the same task

The WhatsApp daily-review walkthrough's Dismiss / Next advanced currentIndex in-place without persisting (gh#729 regression) — cleaner kept seeing the same task. Fix: persist + add a regression test for the index-progression path. (#876)

🔴 Supplies Pack Sizes Edit/SAVE — client-side no-op

Edit → SAVE on a Pack Size returned silently with no DB write. Fix: wire the mutation + add scoped historic re-expansion when units-per-pack changes (property + date-from), so existing inventory rows are recomputed against the new pack size instead of staying frozen. (#853)


🐛 Bug fixes & polish

Sends + check-in pipeline - Check-in instructions: partial image-batch failure was silently dropped — success:true reported even when failedBatchOffsets had entries; no retry, no alert. Now: explicit fail-on-partial with retry + app_errors row (#856) - Pasted-image send showed transient "📷 stored on platform only" placeholder until attachment URLs propagated. Now: short-circuit the placeholder once attachment URLs land (#857) - Check-in reminder silently never spawned — before-rule pre-spawn cron omitted reservation from the condition context, so reservation.bookedOn field_match hard-failed (gh#855 / gh#858 family + Pulse phantom) (#880) - Auto-complete conditions silently failed for any reservation field outside checkIn/checkOut/cleanerId — eval context omitted the field → resolveFieldPath undefined → undefined → false → "parking" never auto-resolves (#858) - Wrong-language inbound-message notification sent RAW guest text (no translation) on both Hospitable + WA push sites — bypassed the gh#710 resolver (#848)

Tasks + cron - Assign-cleaner task (rule 10, existing reservation) completed without assigning the configured default cleaner — 24/28 turnovers unassigned, gh#642 disabled the auto-assign and nothing replaced it (#867) - Task-template notifyOnResolved fired only on cron auto-resolve — operator's manual markDone (approve OR reject) didn't trigger the notify (#851) - Cleaner-walkthrough operator completion digest fired 3× (no dedup), hardcoded Slovak, said "finished" on partial (18/19) cleans (#841) - Orphan manual-supply templates (id 65 "Buy oven cleaner", id 81 "Restock missing supplies") — ad-hoc "create supply task" landed as recurring TEMPLATE not instance (gh#447 class) (#852)

V28 polish - V28 Pulse reassigning a task persisted but the card showed the OLD assignee (todayProjection not invalidated) (#877) - V28 Knowledge-Corrections REVIEW card hard-navigated to legacy /admin (window.location.assign) — no V28-native review screen. Ported. (#861) - Misleading Audit Log: auto-resolve markDone → rollback flap shown as DONE ↔ NOT_DONE state changes — now collapsed with the rollback reason inline (#846)

Receipts + supplies - Receipt → supplies: ~50 s silent add (no ack) made teammates re-scan → DUPLICATE expenses + stock (#869) - Backfill probe de-duped 4 duplicate receipt-supply adds (1565/1566 + 1567/1568) that hit prod before the gh#869 fix (#870)

Reviews + import - Review import created DUPLICATE rows — two CSV paths (csv vs csv_import) didn't dedup cross-path (no externalReviewId); now keys on reservationCode + a unique constraint (#842)

Sandbox - GENERATE CODES broken on sandbox: guests.id was bigint (prod=integer) → BigInt guestId → prisma.accessLog.create rejected → arrival/regenerate pipeline crashed (prod safe) (#864)


Items shipped between 2026-06-24 and 2026-06-28. Full audit trail: closed GitHub issues. Screenshots wired from the live-sandbox 2026-06-23 capture pass — surfaces in this release (Validator Decisions tab, Refine button, 4-level Supplies, editable Early-check-in time) are text-only this round; can swap fresh screenshots in if requested.


June 14 – 23, 2026

Unified outbound validation, Supplies grows up, AI provider toggle

157 issues closed in ten days. Three threads dominate:

  1. One outbound-validation layer. Both send paths (autonomous + operator-in-loop) now route through a single validator with a block-and-fix vs auto-refine-and-send mode toggle, conditional question blocks, and per-validator confidence thresholds. Result: no more "the validator blocked a send via path A while path B sent through" inconsistencies.
  2. Supplies grows into a real product. Consumption-model categories (Guest Consumables / Cleaning Supplies / Stay Essentials / Reusables), per-supply spend config, AI-canonical naming with online product lookup, supply ignore list (skip shopping bags / packaging on receipt re-ingestion), kebab-menu row actions, % units (e.g. 20% of a detergent bottle).
  3. AI provider becomes a choice. A Setup toggle now flips between Anthropic and Ollama for the AI generation path — including a model failsafe that auto-switches to the current model when a deprecated one returns 404 (caught after Anthropic retired claude-sonnet-4-20250514 mid-day).

Plus a heavy nav + Admin cleanup, multi-tenant tenant-switcher for super-admins, prod-deploy guard mirroring the sandbox lock, mobile dashboard booking-calendar parity, and a wave of critical fixes (Pulse Copilot wiped a Nuki time-limit, prod white-screen risk, manual double-send TOCTOU, cross-tenant cleaner-manual exposure).


✨ What's new

🛡 Unified outbound-message validation — one layer, two modes

Outbound message validation previously split across two code paths (autonomous send vs operator-in-loop send), each with subtly different rules — same template would block on one path and ship on the other. Now: one validator layer wires both paths, with two modes:

  • Operator-in-loop — block-and-fix: validator surfaces the issue, operator addresses it, retry. Conditional question blocks (e.g. "is the parking info relevant for this guest?") inline in the validation result.
  • Autonomous — auto-refine-and-send: validator's findings feed an LLM refiner that adjusts the message, then sends.

Plus DB prompt-name cleanup (no more two prompts doing the same job under different names), per-validator confidence thresholds (don't fire below 60% by default), and "Send Anyway" override now consistent across every send surface (V28 inbox, Pulse tile, mobile thread, legacy). (#690, #769, #804)

Why operators care: Validator behaviour is now consistent everywhere. The "validation blocks on screen A but ships from screen B" class of bug is structurally impossible.


📦 Supplies — consumption-model categories, AI naming, ignore list, % units

V28 Supplies — Inventory with consumption-model categories + AI-categorize

The Supplies surface graduated from a flat inventory list into a real category-aware system:

  • Consumption-model categories — Guest Consumables / Cleaning Supplies / Stay Essentials / Reusables. Per-supply spend config. Stale-stock mis-config warning when an item hasn't transacted in N days but is marked active (#736)
  • AI-canonical supply management in the Pack-sizes view — parsed name vs corrected name (online product lookup), category correction inline (#808)
  • Supply ignore list — exclude items like carrier/shopping bags and packaging from inventory + auto-skip them on receipt re-ingestion (#815)
  • % units — "use 20% of a bottle" alongside the existing pieces unit (#791)
  • Checklist "Add supply" dropdown — grouped + filtered by consumption category (#788)
  • V28 Supplies row actions → kebab menu — declutters the overflowing 6-button row, mobile-friendly (#817)
  • Per-supply spend config + reordered fields for clarity (#736)

Why operators care: Supplies became actually-useful for portfolio planning, not just an inventory list.


🤖 AI provider toggle (Anthropic ↔ Ollama) + auto-failsafe model

A Setup toggle now flips the AI generation path between Anthropic (cloud, default) and Ollama (local). Includes a key-save gate fix that was previously rejecting valid keys.

Companion fix: when Anthropic returns 404 for a deprecated model (claude-sonnet-4-20250514 was retired mid-day on 2026-06-15, taking down Pulse Copilot + admin-chat agents), the system now auto-switches to the current model for ALL AI calls (not just chat) — prevents the gh#716 outage from recurring. (#778, #717)

Why operators care: Pick the AI provider that fits your privacy/cost stance; model deprecations no longer take down production.


📋 Checklist items — multiple ON-COMPLETE actions + autocompletion config + reorder

The task-template checklist editor became substantially more expressive:

  • Multiple ON COMPLETE actions per checklist item — e.g. create a task AND decrement supplies on the same checklist tick (#789, #793 — idempotency)
  • Define autocompletion (true/false) alongside name/type/duration for checklist-created tasks (#790)
  • Move items up/down — legacy parity gap (V28 editor previously had add/edit/remove but no reorder; the summary copy falsely promised "drag-reorder") (#813)
  • Supply units in checklist — pieces OR % (gh#791) (#791)

📨 Message templates are now per-property, not tenant-wide

Tenant-wide message templates moved out of Operations and into per-property scope. Reflects how operators actually edit them — most "differences" are property-specific (different check-in instructions per house, not one global template that branches). (#774)


🏢 Super-admin tenant-switcher in V28 top-nav

Super-admins can now flip between tenants from the V28 top-nav (sets x-platform-tenant-id header, no re-login required). Includes a super-admin grant mechanism. Foundation for true SaaS multi-tenancy where one super-admin operates several properties' tenants. (#714)


🌐 Platform-admin moves to its own subdomains

Costs and Infra Status previously sat inside the tenant app. They're operator/super-admin concerns, not tenant concerns — moved to dedicated subdomains (costs.flatsbratislava.com, infra.flatsbratislava.com) and infraStatus re-scoped to a platform procedure. (#685)

Why operators care: Tenants stop seeing platform-admin chrome they shouldn't see.


📱 Mobile dashboard — booking-calendar timeline view

Mobile previously had only the occupancy heatmap; desktop had the full BOOKING CALENDAR with check-in/check-out timeline. Mobile now matches — same timeline, mobile-laid-out. (#712)


🔔 Desktop PWA dock badge

The desktop PWA dock icon now shows the unread/task badge count (mobile shell got this in gh#561; desktop's useV28Badges was never wiring setAppBadge). (#677)


🧰 V28 navigation — Admin cleanup, Logs consolidation, Smart Locks consolidation

V28 Logs — consolidated Activity / Audit / Webhook / Errors under one nav

V28 Smart Locks — Lock Control + Codes + 2N QR + Charging history with Simulate-entry button

A wave of nav reorganisation based on operator usage patterns:

  • "Team & Billing" → "Team" moved from Admin to Operations; subscription/tenant-settings panel dropped from V28Admin (#684) — visible in the Team screen below

V28 Team — moved under Operations, members + role + access - One "Logs" entry with tabs — Activity / Audit / Webhook / Errors consolidated under a single nav, mirrors the AI Cockpit tab rail (#687) - Manual Trigger moved into Smart Locks — per-property button on the locks screen; standalone /v28/manual-trigger retired (#688) - START CLEANING admin button removed from V28 Cleaning Turnovers — admin never starts cleanings; cleaner self-starts via WA "začať" + Nuki (#678) - Mock WA + Mock Nuki impersonate pages merged into the WA Communication + Smart Locks screens (sandbox) — one screen to send + view (#679, #683)


📊 AI Cockpit — click-to-filter, per-validator confidence threshold

V28 AI Cockpit — Overview with health card, prompts, agents, validators, usage & cost

  • Clicking a feature row in AI Cockpit → Usage & Cost now filters the whole tab (Daily Spend chart + Usage Window cards), not just expands sub-features. The feature you click becomes the lens you read everything through (#806)
  • Per-validator confidence threshold — Task Template Safety Validator now exposes a 0–1 confidence per finding plus a configurable threshold (default 60%); below threshold, the warning doesn't surface (#804)

🛂 Prod-deploy guard — mirror the sandbox lock

After observing 14 sandbox deploys in 4 hours (agents bypassing the gh#472 lock, gh#615 RCA), the same guard now wraps prod deploys: pull-main-first + advisory lock + provenance metadata. Stale-tree clobbers on prod become structurally impossible. (#770)


🧹 Cleaning board — clickable cleaner roster

CLEANER ROSTER entries on the Cleaning board are now clickable → filters Upcoming turnovers to that cleaner. Lets the operator trace a per-cleaner count back to its turnover instead of mentally cross-referencing. (#698)


🤝 Simulate-entry picker — guests + teammates

The sandbox simulate-entry picker now lists guests arriving today distinct from teammates — lets the operator simulate a guest first-entry (welcome-message trigger) without choosing a teammate by accident. (#782)


🧪 Systematic sandbox mock-data mechanism + property-keyed Mock Nuki

Sandbox mock data graduated from ad-hoc scripts into a real system:

  • Named dataset registry + idempotent runner + restore-hook + agent-maintained dataset set (#680)
  • Mock Nuki is now property-keyed — auto-provisions test preconditions (simulate property access → triggers the due cleaning automatically, no hand-patching) (#635)

👥 V28 task card now shows the assignee

Classic UI showed the assigned teammate on the task card; V28 was rendering only property + due date. Assignee back. (#748)


🌐 WA Communication translation — surface the target language

WA Communication translation now displays "Translated to " so the operator sees which language was applied; the operator's communicationLanguage is prompted/defaulted on first use (gh#616 UX follow-up). (#681)


🎯 data-testid attributes — operator-facing interactive elements

Every operator-facing button / dropdown / dialog now carries a stable data-testid so the real-UI-walk close-gate (per CLAUDE.md) is mechanically enforceable. Foundation for headless screenshot automation, e2e tests, and validator UI walkthrough — all of which were previously fragile selectors against text content or DOM structure. (#734)


📅 Reservation + checklist polish

  • Show the requested early check-in time in the rule-45 overview placeholder (#638)
  • Clarify flat vs hourly rate basis in checklist DURATION (€5 flat-rate Laundry no longer ambiguously next to a duration field) (#721)
  • Portfolio Analytics: remove LLM Usage tab (it's admin metric, not property analytics); unify bespoke tabs with the shared Settings underline-tab component (#746)
  • V28: edit actions open in a modal (like task templates), not a top-of-page panel — sweep applied to message templates + similar V28 edit-at-top occurrences (#801)
  • Unified reservation field catalog — single source of truth (platformCapabilities) for trigger + completion condition pickers; backfill missing fields (earlyCheckinRequested, lateCheckoutRequested, guestsTotal, guestMood, …) + CI ratchet (#798)

🐛 Critical bug fixes

🔴 Pulse Copilot WIPED Klaudia's Nuki code time-limit — then hallucinated success

Operator asked Pulse Copilot to set Klaudia's access code expiry to 13:00. The Copilot called updateAccessCode with null dates (effectively removing the time-limit entirely) and then reported success. The code now had no expiry. Fix: explicit time-limit preservation in the tool schema + behavioral test that exercises a "set expiry to X" prompt and asserts the resulting Nuki API call has the right dates. (#707)

🔴 Pulse Copilot + admin-chat 404'd from a retired model

Anthropic retired claude-sonnet-4-20250514 on 2026-06-15 — the model id was hardcoded in Pulse Copilot + admin-chat, so both started returning 404s within an hour of the deprecation. Manual swap restored service; the gh#717 failsafe (auto-switch to current model on 404 for ALL AI calls) ensures this can't repeat. (#716)

🔴 Tetiana arrived with NO access code — Send-Checkin auto-send hard-blocked

A check-in instruction send to Tetiana (arriving tonight) was hard-blocked by an ai_validators MAJOR finding ([parking-info-irrelevant]), success:false, no override — yet the task was marked DONE despite the send failure. Code 881623 sat in the DB undelivered. Fourth recurrence of the send-checkin-blocked-code-present symptom class. Fix: validator now distinguishes "block with override available" from "fail-and-skip" + the task lifecycle no longer marks DONE on send failure. Plus a permanent E2E test that exercises a real-validator hard-block path and asserts the task does NOT auto-resolve. (#724)

🔴 Prod white-screen crash risk — ErrorBoundary had no chunk-load recovery

A single root ErrorBoundary, no chunk-load recovery, no app_errors reporting on client-side crashes — meaning any chunk-load failure (rare but real on a long-lived tab through a deploy) would produce a blank prod page with no telemetry. Added: SPA crash-resilience mechanism (per-route boundary + chunk-reload retry) + automated per-route mount deploy gate (mounts every route in a playwright pass before allowing the deploy). (#805)

🔴 Validators UI was unusable — HTTP 400 on every binding edit

PAUSE/DELETE/EDIT on a validator binding → HTTP 400 Id: expected number, received bigint. Operator couldn't disable a validator via the UI. Root cause: tRPC schema declared z.number() but the column type was bigint. (#741)

🔴 App failed to boot — 03-migrations-B retrospective expense loop timeout

A retrospective expense backfill loop (1010 round-trips, 0 expenses, 24 s/boot) breached the 60 s Fly proxy boot budget under load → boot timeout → app refused to start. Rewritten as a single set-based UPDATE. (#749)

🔴 Preprod completely down — migration 128 boot crash-loop

Migration gh#740 added an FK → supplies(id) but preprod's supplies.id had no PK → exit 1 → boot loop. Same shape as gh#655. Fix: migration now asserts the PK constraint up-front. (#827)

🔴 Refine/Improve-with-AI corrupted the draft

Operator hits "Improve with AI" on a draft reply → the result prepends an exact copy of the prior HOST message + mixed languages → validator re-blocks → operator can't send. Root cause: prompt was concatenating context with the draft and the LLM treated the concatenation as content to emit. (#836)

🔴 Cross-tenant cleaner-manual exposure (SECURITY)

The public /cleaner-manual/:slug route resolved a property by slug with no tenant scope — meaning a tenant A cleaner's slug could resolve to a tenant B property if slugs collided. Fix: tenant-scoped lookup + explicit 404 when scopes mismatch. Audit-walk through every public route that touches tenant-bound data. (#792)

🔴 Guest preferences silently lost — last-write-wins

reservation.specialRequest was overwritten by each new analysed guest message (last-write-wins), not merged. Tereza's "white wine" was clobbered by a later "no parking" message — operator preparing the welcome had no idea about the wine. Fix: special requests now merge into a structured list, not overwrite. (#833)

🔴 Manual APPROVE & SEND could double-send

Race window between the operator hitting APPROVE and the async pre-send probe completing — TOCTOU between client and the backend dedup. A guest got two identical messages. Fix: immediate button disable during probe + backend dedup tightened. (#756)

🔴 V28 infinite render→refetch loop

useV28Badges embedded new Date() in a React Query key → key changed every render → webhookLogs.getStats refetched ~4–5×/s continuously on every V28 screen. The supply-chart tooltip flicker was a downstream symptom. Fix: stable key + a CI ratchet that forbids non-stable values in React Query keys. (#762)

🔴 Admin WA notifications >1024 chars silently lost

Admin task notifications via WhatsApp interactive-list path were silently rejected (error 131009 from Meta) when the body exceeded 1024 chars — the interactive-list path was missing the 1024 cap that the regular path has. Body now truncated with an explicit "…see app for full text" suffix + length-cap test. (#784)

🔴 Auto cleaner-assign false-resolved as "met" without assigning

The "Assign cleaner" auto-task was marking itself resolved as "met" while never actually assigning a cleaner (gh#466 global-default suppression interaction). ≤14-day cleanings were silently left unassigned. Fix: assignment success is now a precondition for "met". (#797)


🐛 Bug fixes & polish

Messaging + sends - 2N access-code silent-success: generateTwoNAccess reported twoNSuccess:true but twoNAccessCode never persisted (Klaudia/Vydrica) — guard retried forever, guest had no 2N access (#700) - Validator-block "auto-send BLOCKED" WA alert spammed every cron tick (dedup not holding) — one per episode now (#754) - Task-drawer send (V28TaskRowActions) showed raw validator error + NO "Send anyway" override (#757) - Manual-send REPLY surfaces (V28 Pulse tile, mobile thread, legacy) showed RAW validator error + no Send-Anyway — only V28 inbox was handled (#769) - Wrong-language auto-send BLOCKED (Emanuelle/Vydrica welcome) — validator was language-non-transparent + auto-send couldn't produce non-EN/SK (#796) - AI reply-draft over-confirms ("thanks for info" + restates the request) — concise tone now in Natalia's personality, not hardcoded (#582)

Tasks + auto-resolve - Task status thrash: check-in tasks flipped DONE↔NOT_DONE every cron tick — resolveItem vs delivery-guard rollback fought with no debounce (gh#724 family) (#743) - "Create follow-up task" failed — createTask Zod wanted dueDateTime as string but dispatch passed a Date (#839) - Cannot deactivate a platform-shared outbound validator from the UI — OVERRIDE threw raw 23505 (#767) - Host-personality "Propose from history" compressed the detailed personality AND switched English→Slovak — prompt fix shipped but ineffective because the LLM ignored "preserve formatting" + read "host's primary language" as Slovak (#819)

Calendar + reports - V28 calendar promo/deal banner was window-dependent — same property+date showed "Getaway Deal" in one week-view, nothing/"Promo check unavailable" in another (gh#631 recurrence) (#731) - Revenue Simulation broken on prod: calculateSimulation 502 (Fly proxy timeout — 53 s+ cold-cache grid recompute, double-fired, hot-path JSON logging) + SSE collateral (#742) - Revenue Simulation Monthly breakdown = NaN € — server emitted totalIncome/totalCosts/totalProfitLoss, client read m.income/m.costs/m.profitLoss (#745)

Reviews - Review category sub-score dropped on ingestion — Paula's Comfort 7.5 stored as NULL (nullIfZero collapsed 0/late comfort) (#840)

Environment - Env stability — preprod cold-start "looks down" + sandbox deploy churn (14 deploys/4 h, agents bypassing gh#472 lock) — investigated + the prod-deploy guard (gh#770) closes the same class (#615)


Items shipped between 2026-06-14 and 2026-06-23. Full audit trail: closed GitHub issues. Screenshots captured live on sandbox 2026-06-23 (validator/synthetic mock mode, no real guest data).


June 10 – 13, 2026

Grounded AI Q&A, supply analytics, multi-tenant safety

52 issues closed. Four-day window dominated by three threads:

  1. Operator-uploaded manuals ground the AI. Upload PDFs / images per property (the canonical case: "how to use the air conditioning"). Every guest Q&A is now grounded against the operator's actual manuals — no more hallucinated answers when a guest asks how to use the washing machine.
  2. Supplies grew teeth. Per-category history filter + per-item pieces-remaining line chart show consumption patterns over time. The AI-categorize per-row button (legacy parity) finally landed in V28.
  3. Multi-tenant + reliability hardening. A global-unique on messages.externalMessageId was silently dropping a second tenant's messages — fixed with a composite key + provider column. A boot-migration silent no-op family fixed across runMigrationsKK. A new data-quality probe auto-files tickets when prod-data invariants are violated (reservations-without-guest-rows, done-salaried-task-without-fee-expense, etc.).

Plus the V28 inbox composer now accepts CMD+V image paste (parity with the Pulse Copilot from the May release), and watched-condition auto-dismiss got the operator-requested ANY-false semantics.


✨ What's new

📚 Per-property manuals — AI Q&A grounded in your actual documentation

Upload property manuals (PDFs, photos, text — air-conditioning instructions, the dishwasher manual, the WiFi setup, parking diagrams) on each property's page. Documents are embedded and searched per-tenant. When a guest asks "how do I use the air conditioning?", the AI answer is now grounded in your manual — not a generic plausible-sounding answer.

Why operators care: No more guests being told the wrong dishwasher mode because the AI hallucinated. The AI answers from the source you uploaded. (#542)


📋 V28 inbox — paste images directly into the composer

V28 Inbox — guest thread with composer + property/reservation context panel

Hit CMD+V with an image on your clipboard while composing a reply — it auto-attaches via the existing image pipeline. Same ergonomics as the Pulse Copilot got in the May release.

Why operators care: Send a guest the photo you just screenshot'd, without the paperclip detour. (#656)


📊 Supplies — per-item history chart + category filter

Click any inventory item — get a per-item pieces-remaining line chart showing restocks and use-downs over time. Filter the global supply HISTORY view by category to find the toilet-paper consumption pattern across all properties. Backend already supported it; the V28 UI now exposes it.

Why operators care: "How much detergent do we actually use per month?" is now a glance, not a CSV export. (#673)


🤖 V28 Supplies — AI-categorize per-row button (parity gap closed)

The AI-categorize button (which classifies a supply into a sensible category from its name) was present in the classic SuppliesTab but missing from V28 — operators had to bounce to legacy to recategorize. Ported. (#674)


🧹 Manual "start cleaning" trigger + Mock Nuki simulate-entry

V28 Cleaning Turnovers — Upcoming list + Cleaner Roster + Schedule Turnover

Two improvements bundled:

  • Operator-side: a "Start cleaning" admin button (no more waiting for the smart-lock unlock to spawn the walkthrough)
  • Cleaner-side: type "začať" in WhatsApp to start the walkthrough manually
  • For engineering: a new Mock Nuki "simulate entry" test page lets us reproduce smart-lock entry events on sandbox without physical hardware (#632)

Why operators care: No more "the lock unlock event didn't fire, please walk the cleaner through manually." Trigger it from the dashboard or via WhatsApp.


📅 V28 task params — occupancy-aware date picker

When a task asks "what day should this fire?", the V28 param widget now uses the existing occupancy-aware date picker (which shades booked/vacant days) instead of a raw native input. (#636)


💸 Reservation FINANCIALS — booking discount now shown

The Hospitable financials JSON stored discount lines exactly (e.g. "Promotion Discount -€54.40") but the V28 RESERVATION panel never rendered them. Now mapped + displayed. (#668)


👨‍👩‍👧 Reservation panel — guest breakdown (adults / children / infants / pets)

The "N guests" number now expands to the breakdown when Hospitable returns it ("3 guests (2 adults · 1 child)"). Across the property portfolio, 74 reservations have children — useful context for stocking child welcome packs vs not. (#671)


🌐 V28 WA Communication — thread translation + realistic bubbles

The WA Communication view now:

  • Translates thread messages to the operator's display language (same translation pipeline as the inbox)
  • Renders chat bubbles via the same ChatBubbleView component as the inbox (consistent visual + interaction model — no more two-flavored chat UIs)
  • Persists mock outbound replies properly (a bug surfaced during the rework) (#616)

🔄 Watched-condition auto-dismiss — ANY-false semantics

The "auto-dismiss when conditions stop matching" sweep previously required ALL watched conditions to flip false. Operator-dictated change: any single condition flipping false is enough. Prod impact verified zero (only one template — "Thank you" — used watched conditions). Tooltips + UI copy updated. Sibling fix: messageThread.* conditions are now watchable (auto-dismiss can re-evaluate them, parity with auto-COMPLETE which already could). (#660, #663)


🛡 Data-quality probes — auto-file tickets when prod invariants break

A new probe family runs on a schedule against prod data. When an invariant is violated, it auto-files a triaged GitHub issue. Probes now live: reservations-without-guest-rows (41 violations caught), done-salaried-task-without-fee-expense (8 violations — sibling family to gh#464 cleaner-underpaid), migration-dedup-guard-blocked (15 violations), demo-real-pii-leak. Tickets gate on total > 0 so a clean run doesn't spam the tracker.

Why operators care: Bugs that previously only surfaced when a guest complained are now flagged before anyone notices. (#648, #652, #654, #664, #665)


🔋 Nuki "Charge lock" notification — no more 1-hour delay

Battery polls happen hourly, so the operator's "lock needs charging" notification arrived up to ~60 minutes late. Fixed by surfacing the in-charge state earlier in the lock-state machine. (#580)


🐛 Critical bug fixes

🔴 "Improve with AI" was DELETING operator-written directions

Operators noticed the AI "Improve" button was rewriting (not polishing) their carefully-written guest directions — in one case, fabricating the wrong location. Root cause: the message_improvement prompt's "validate facts against KB, correct if incorrect" rule licensed full content rewrites. Improve must polish style, never reword content. Prompt rewritten + behavioral tests added that exercise the "operator wrote X; AI must not change X" invariant. (#657)

🔴 Post-cancellation guest messages were silently invisible

A direct-booking lead nearly lost: a guest cancelled, then messaged the inbox asking to rebook — the message was filtered out because the inbox query excluded cancelled/declined reservations. Now: cancelled-reservation messages are visible (with a distinct visual tag), so the operator can intercept rebooking attempts. (#627)

🔴 Auto-resolve dedup was silently DEAD in prod for the entire gh#290 window

The Layer 2 duplicate-welcome guard from gh#290 was keying off task.reservationId (a numeric code) but messages store under externalId (a UUID). Zero rows scanned, zero skips — all-time. Every duplicate the system was supposed to prevent slipped through. Fixed + a recurrence-proof test that exercises the key compatibility under the seed of a real duplicate window. (#599)

🔴 Cleaner-WA help-menu mixed SK + UK for non-Slovak cleaners

The cleaner WA help menu body was hardcoded Slovak (bypassing the cleanerStrings catalog) — Ukrainian-speaking cleaner saw Slovak help text. Fixed + audit-walk through every cleaner-WA surface that bypasses cleanerStrings. (#613)

🔴 Arrival-map POIs silently lost after adding one

Operator adds a POI in V28 Property Manager → save → reopen the map → it's empty. Root cause: navigationMap was double-stringified on save (V28 tab pre-stringified + server stringified again) — data intact in DB but un-parseable downstream. Restored + fixed the save path + added a parsing test. (#653)

🔴 Preprod 503 outage — boot crash-loop on missing unique index

A seedCostProviders ON CONFLICT (name) clause hit cost_provider without a unique index (schema drift between Prisma model + actual table) → boot crashed × 10 → max restart count → preprod 503 for ~3 hours. Env repaired manually; migration now asserts the index up-front. (#655)

🔴 Multi-tenant data loss — messages.externalMessageId was a GLOBAL unique

A global-unique constraint on messages.externalMessageId (no tenantId scope) meant tenant B's message with the same provider message-id as tenant A's would be silently dropped. Fix: composite unique (tenantId, provider, externalMessageId) + a provider column to disambiguate the upstream. Epic #614 (multi-tenant data integrity). (#628)

🔴 Boot migration runMigrationsKK silently no-op'd on prod + sandbox

The INSERT ... ON CONFLICT (slug) clause hit a partial unique index → threw 42P10 → caught by a try/catch that abandoned the idempotent UPDATE+audit path. Every "seeded-prompt" migration silently failed. Caught because operator-edited prompts kept "reverting" between deploys. Fix: predicate-restated ON CONFLICT for both prompt upserts (single DO-UPDATE shape, silent no-op when already current). (#672)


🐛 Bug fixes & polish

Tasks + auto-resolve - Cleanup access-code / guestbook tasks never auto-resolved — sweep skipped relative_to_due, lastAutoEvalAt blank → stale codes left departed guests with access (#574) - Template-edit propagate dropped supplyId + skipped the strip → raw {{supply.*}} written to task overview ("low stock of ( )") (#611) - Deploy-boot backfill re-spawned a 2-day-old maintenance task — boot gate was blind to messages absorbed by per-reservation dedup (#646) - Auto-assign-cleaner blocked even when operator explicitly opted in (auto:true + defaultId) — gh#466 guard now honours the opt-in when the cleaner serves the property via teammates.assignedProperties (#670) - "Send photo of city tax cash" auto-completed on ANY photo+keyword (no cash-content check) — phantom root cause of gh#480 (#512) - task_template_validator rule C2 false-positive — warned "not populated unless event=new_message" but the runtime lazy-loads for any reservation event (#666) - task_template_validator rule A3 over-escalated intentional delayed auto-send to CRITICAL — downgraded to suggestion (A1/A2/A4/A5/A6 stay critical) (#667) - New task templates: optional tRPC params now default SOURCE to "Ignore (don't send)" instead of "User input" (#662) - Early check-in request overview now includes the requested time (placeholder added to rule 45) (#638)

Cleaning + reservations - Cleaning Turnovers board showed "—" for property column — fetchCleaningSchedule skipped resolvePropertyName fallback (#630) - Promo cache routinely stale → "Promo check unavailable" — added background scrape refresh + full date coverage (#631)

V28 conditions builder - Row layout broke when value input present — (×) wrapped to its own line, field-path select hard-truncated. Controls now anchor right in nowrap zones (#659) - Field-search showed non-matching fields — FieldCombobox filtered only the live keystroke buffer (query=null on reopen → full list behind committed text); also committed invalid free-text on blur (#661)

V28 inbox + language - Inbox language dropdown ≠ AI-generation language — dropdown defaulted to operator global (EN), picker (country/phone → SK) overrode → "English" selected but Slovak drafted. Dropdown is now authoritative + defaults to detected ({EN, SK, CZ}) (#669)

Engineering infrastructure - Audit-context triage agent did destructive label replace-all → clobbered guard-approved (+ siblings) as collateral. Now: additive-only label edits with grep ratchet (#604) - Tech-debt: roam fitness gate red — 3 dep cycles + 10 functions >500 complexity (gh#276 successor) (#607) - Phase 2 — Shared provider-abstraction kit (_providerKit) + capability-flag model + per-property override (#619)


Items shipped between 2026-06-10 and 2026-06-13. Full audit trail: closed GitHub issues. Screenshots captured live on sandbox 2026-06-23 (synthetic mock-mode data).


May 21 – June 9, 2026

Bug reports go in-app, V28 deepens, multi-env topology

283 issues closed in three weeks. Two themes dominate this release:

  1. Closing the feedback loop. A new in-app Report Bug modal — screenshot, video, voice, auto-collected context — feeds a Claude Code triage agent that reproduces issues on preprod and files structured GitHub tickets. The result: turnaround from "operator hits a bug" to "engineering has a reproducible ticket" drops from hours to minutes.
  2. V28 stops being a partial port. Inbox search, native message-templates editor, 8-card conversation drawer, supplies filters, AI Cockpit agents tab, ~30 legacy escape-hatches replaced by V28-native dialogs. The phrase "I had to bounce to legacy for that" is now significantly rarer.

Plus a dedicated mock-mode preprod + 3-env Fly topology so feature work can ship without risking real WhatsApp / Hospitable / Nuki traffic, a public /changelog page for prospects, and a wave of critical guest-impacting fixes (duplicate welcomes, silent send failures, wrong-cleaner auto-assignment, mobile PWA crash).


✨ What's new

🐛 In-app Report Bug — screenshot + video + voice + auto-context

A floating Report Bug button now lives on every authenticated screen. Tap it (or Cmd/Ctrl+Shift+B) and:

  • Capture visual evidence — screenshot or up to 60 s of video, drag-rectangle region select for screenshots
  • Add narrative — type a description or record an audio voiceover (auto-transcribed)
  • Auto-collected context — URL, viewport, browser, current user/tenant/role, last 50 console events, last 20 tRPC calls, current task/reservation IDs if relevant
  • Anonymization mode — privacy toggle that black-boxes guest PII from the captured media + logs before submit
  • Submit — everything lands in a bug_reports table; engineering sees the full reproducible context immediately

Why operators care: No more "describe what you saw in Slack and hope it's enough." The bug report IS the repro. (#167, #342, #343, #344)

🤖 Bug reports are triaged by an AI agent — autonomously

A Claude Code triage agent reads new bug_reports, reproduces them on preprod (with mock services so no real guest is touched), analyses the codebase, and files a structured GitHub issue with root cause + proposed fix + reproduction steps. The operator goes from "I'll write this up later" to "engineering already has a ticket."

Why operators care: Bug reports stop being a chore. Your screenshot at 11pm becomes a tracked issue with a proposed fix by morning. (#168)


📰 Public /changelog page

Public changelog

A new public-facing changelog at flatsbratislava.com/changelog renders the latest release notes for logged-out prospects and existing operators alike. Entry cards link straight to the relevant section of this document.

Why operators care: A lightweight trust signal you can link to. Prospects evaluating the product see momentum; operators see what shipped without asking. (#305)


🔍 V28 Inbox — search across guest names + message bodies

The V28 Inbox now has a top-of-list search field. Type a guest name, a phone, a phrase from a guest message — results match across reservations.guestName AND messages.body (last 90 days), debounced, with the matching substring bolded in each result tile.

Why operators care: Stop scrolling through hundreds of threads to find "the guest who asked about parking." (#319)


✉️ V28 Message Templates editor — finally native

V28's PM → Templates tab was previously task automations (operators clicking expected to find guest message bodies kept landing on the wrong screen). Now there's a native V28 Message Templates editor — tenant-wide templates AND per-property overrides — with placeholder insert, language tabs, and a Save Anyway override when the LLM validator is too strict.

Why operators care: Edit a check-in message, a thank-you, a how-to-park snippet — without leaving V28. (#313)


📋 Pulse conversation drawer — now shows the full Inbox context

The 💬 "open conversation" slide-over on Pulse task tiles now renders 8 reservation-detail cards alongside the message thread in a 2-column layout — guest contact, phone, email, # of guests, # of nights, financials, Nuki PIN, AI tags, invoice status — everything you'd see in the Inbox, without leaving Pulse.

Why operators care: Triage a task without losing the agenda. Open, read, decide, close. (#268)


🚪 V28 escape-hatch sweep — ~30 legacy redirects replaced

V28 had ~30 buttons that quietly bounced operators out to the legacy /operations, /admin, /admin/properties UIs ("everything in V28 design should call V28 dialog" — operator). Shipped: V28-native dialogs for Settings → Notifications (#335), Houses portfolio (#336), AI Cockpit prompts + API rotation (#337), Pulse + Guests exports (#338), Operations + Teammate full editor (#339), Settings → Configuration + Integrations (#340), Accounting export + PM placeholder (#341).

Why operators care: The V28 shell stays consistent. No more "I'm in V28 but suddenly the UI looks different." (#314 — umbrella)


🤖 AI Cockpit — operator-manageable subagents

The AI Cockpit gets a new Agents tab where you tune each specialist subagent without a code deploy. Per-tenant DB-backed config: system prompt, model choice, thinking budget, max tool rounds, plan-then-execute toggle, max response tokens. Audit-logged via the user-owned config registry.

Why operators care: Want the invoice agent on a cheaper model? Want the cleaning-walkthrough agent to reason longer? Edit in the UI, save, done. (#415)


💰 AI cost dashboard — per-turn drilldown + invoice agent cost cut

Two related shipments:

  • The AI cost dashboard's opaque accounting_round_N rows are now turn-grouped with the operator's prompt + per-round scope so you can see exactly what cost what (#414).
  • Invoice generation previously cost $4–12 per turn (legacy Opus 4.0 + 54% cache-write churn + 12-round loop). Root-cause analysis + model + caching + loop fixes brought this down dramatically (#412).

Why operators care: You can see what your AI is spending money on, and the most expensive flow got cheaper.


📊 Per-property management fee config + AIRBNB-ONLY metric removed

Management fees were hardcoded at 25% gross. Now configurable per property as % of gross, % of net, OR static monthly value. Reflected across Revenue Report + Owner Statements + Owner Payouts. The AIRBNB-ONLY REV metric (per operator request) is removed from reports and payout calculations.

Why operators care: Your fee structure matches your actual contracts, not a hardcoded default. (#359)


📱 WhatsApp notification preferences — per-teammate, with template override

Notification routing previously branched on task_template.notifyWhatsApp (per template). Now it branches on teammate.notifyWhatsApp (per person) with a per-template override for critical cases ("always notify, regardless of teammate prefs"). Admins/managers get a scope toggle: all teammates vs mine only.

Why operators care: A teammate on holiday flips one toggle and stops getting pinged. No more chasing 12 template-level checkboxes. (#361)


🔐 Google OAuth — replaces Manus IAM

Login now uses direct Google OAuth (with a customer-gated allowlist) instead of the previous Manus IAM dependency. Cleaner auth, fewer moving parts, no third-party identity broker between you and your dashboard.

Why operators care: Faster login, simpler password recovery (it's just Google), and unblocks the upcoming community portal. (#430)


🧪 Mock-mode preprod + 3-env Fly topology

A new 3-environment topology on Fly: PROD (real traffic) / PREPROD (mock services + synthetic data, used by Claude Code for validation) / STAGING (mock services + prod-replica data, used by operator for final-test before prod). New mock-injection UI lets you simulate WhatsApp + Hospitable + Nuki events on preprod without bothering real guests / cleaners.

Why operators care: Engineering tests changes on a real prod-shaped dataset without risking a single real message. (#353, #374, #375)


🎬 Demo dataset + Demo tenant

A randomized privacy-safe replica of prod (every guest name swapped, every PII scrubbed) refreshed daily, served on a dedicated demo Fly app with lightweight auth — for showing the product to prospects or training new operators without ever exposing real guest data.

Why operators care: Show the product. Train new team members. No NDA conversation needed. (#473, #474)


📦 V28 Supplies — property filter, source linkback, "added by" history

Three improvements to V28 Supplies that came in via the new in-app bug-report channel:

  • Filter by property — parity with the legacy UI (#477)
  • "Receipt: …" labels in history are now clickable, opening the original receipt source (#475)
  • History rows show which property + who added the supply (#476)

Why operators care: Stop scrolling through every property's supplies; trace a receipt entry back to its source in one click. (#479 — receipt parser fixes shipped here too)


The V28 "Generate from reservation" invoice modal previously dropped the GUEST INVOICE DETAILS fields (name/email/address) — parity gap with legacy, now closed (#407). When an invoice already exists on a reservation, the V28 INVOICE card now shows a link to the existing invoice instead of a GENERATE button that would create a duplicate (#406).


🛠 Task template editor — readable validator + Save Anyway

Two improvements to the template editor that came directly from operator frustration:

  • Save Anyway override in V28 — when the LLM validator hard-blocks a save with a non-clean verdict, you now have the same escape valve the legacy editor has (#462, #428)
  • Operator-readable validator warnings — gone is "actionButtons[0].actionConfig.supplyId placeholder {{supply.id}} references the 'supply' namespace…". The validator now says, in plain language, what's wrong and what to fix (#488)

Why operators care: Edits land instead of bouncing. You can act on warnings instead of guessing what they mean.


🗓 V28 vacancy bar tooltips

The V28 booking calendar's vacancy bars now show both the guest-visible discount AND the net-margin discount in the tooltip (the old -N% labeled "platform promo" conflated three different metrics). Redundant 2N · €55 €55 label simplified — the two prices already convey 2 nights.

Why operators care: Pricing decisions made on what the guest sees vs what hits your margin. (#355, #356)


🐛 Critical bug fixes (guest-impacting)

🔥 V28 reply-with-template was DEAD for every property

A slug-vs-int propertyId mismatch caused the V28 inbox template picker to show "NO TEMPLATES · 16 OTHER" (disabled) on every thread, for every property. Operators couldn't reply with a template anywhere. The condition matcher compared a string slug to an integer FK; the inbox dropdown rendered the empty result. Fixed by aligning the comparison + adding a behavioral test that exercises a real template-send through the full picker → match → send → archive flow. (#418)

🔥 Send Check-in Instructions silently lost text body + first 3 images

A check-in-instruction send to Filipe (Vydrica, checkin TODAY) silently dropped the text body and the first 3 of 4 attached images — but the task auto-resolved as done. Root cause: a batched-send retry path didn't propagate partial-failure state. Now: explicit fail-fast on partial sends, no silent done, app_errors row + operator WA notification on every partial failure. (#386)

🔥 Duplicate welcome message sent to guest

João received two welcome messages on 2026-05-23 — the gh#290 follow-up that was meant to prevent this had never shipped Layer 1, and Layer 2 had two gaps (legacy-action-format coverage + a 0.85 Jaccard similarity threshold too strict for AI-refined manual sends). Both layers now shipped + monitored. (#324)

🔥 44 "New Task Created" WA notifications in 73 seconds

A daily cron processing 44 reservations fired one WA notification per task creation — operator phone vibrated 44 times in a minute and a half. Same symptom class as gh#181 (27-notification flood) — now digest-aggregated with a 60-second batch window. (#348)

🔥 Cleaner-fee expenses NOT created on auto-resolve — cleaners underpaid €410

When a cleaning auto-resolved (smart-lock entry triggered completion), the createExpenseFromTask step was missing from the auto-resolve cron path — so the cleaning-fee expense never landed. Miriam was missing 12 cleanings, Marina 2 — about €410 in unpaid fees. Backfilled + the cron now wires through the same expense-creation path as the manual completion. (#464)

🔥 Miriam auto-assigned to cleanings she shouldn't have (3× recurrence)

The "Approve cleaning" rule (operation_tasks.id=36) had a hardcoded defaultId=8 + auto=true that silently overrode the operator's cleaner assignment. Discovered when Miriam kept getting wrong cleanings; the same root cause recurred twice more under different proximate causes. reservations.cleanerId is now a registered user-owned-config table — every mutation audit-logged, no silent overrides. (#420, #437, #466)

🔥 V28 silently showed MOCK data when live queries failed

Nine V28 surfaces (properties, reservations, threads, team, cleanings, maintenance, automations, locks, channels) silently fell through to mock data when the live tRPC call hit a rate-limit or 5xx — operator couldn't tell real from fake. Now: a useV28Query ratchet enforces explicit error state on failure (no silent fallback), OR batches related calls into one query. (#363, #376)

🔥 V28 Cleaning Turnovers dropdown didn't persist re-assignments

Operator changes cleaner in the V28 cleaning dropdown → mutation fired but no onError handler, no toast, no retry — the change silently failed to persist. Marina invisible for 05-25 Castle&River because the operator's earlier reassignment never made it to the DB. Now: explicit success/error toast + retry + optimistic UI rollback. (#304, #352)

🔥 Mobile PWA crashed on /m/pulse mount

A latent TypeScript as cast lie shipped 26 days ago in #106 blew up on first real-user load of the mobile PWA's Pulse screen — operator saw a red error triangle, every other layer of validation passed because vitest mocks + tsc casts didn't exercise the real runtime shape. Fix: drop the cast, use the inferred type, add a CI ratchet that forbids as casts against tRPC useQuery().data. (#553)

🔥 Prod auth crash on first-time login

A bundled-ESM incompatibility (Dynamic require of "crypto" is not supported) crashed first-time logins on prod after a deploy. Operator was locked out for ~30 minutes until a SUPERADMIN escape hatch landed. The underlying ESM bundling issue is now resolved. (#393, #399)

🔥 Access codes shipped without lock-side validation

A "Generate access code" action returned success even when the Nuki API rejected the create — Piotr got a check-in code that didn't open the door. The lock-ops path now validates on the actual lock (round-trip Nuki API verify) before returning success. (#576)


🐛 Bug fixes & polish

Messaging + translation - Slovak guest gets English messaging (Marek case) — detectGuestLanguage step-1 no longer over-trusts a single recent EN inbound (#436) - Translation pipeline no longer writes wrong-direction translatedBody when guest's message language ≠ profile language (#426) - Translation engine refusal text no longer stored as guest message (#442) - Guest review text now translated on operator task card (French review no longer raw) (#448) - V28 inbox: IMPROVE keeps the source language; REFINE rewrites to English (the previous inconsistency is fixed) (#434) - V28 inbox language switch now persists (languageOverride + thread re-translate) instead of being a transient AI-assist-only toggle (#461)

Pulse Copilot - "Cannot set headers after they are sent" on long chat turns — heartbeat race with tRPC res.status() fixed (#299) - Verbose tool-debug UI / chips visible again — now hidden by default (3rd attempt; explicit feature flag this time) (#350, #409) - Copilot didn't use execute_trpc for reservations queries — now does, no more "export data and analyse externally" responses (#377) - Markdown tables/headers rendered as raw text; chat window not resizable — both fixed (#378) - Web-search tool added — Copilot can now look up external company billing details for invoices (#408)

Reservations + check-in - Guest-requested early check-in times not captured (Tomasz, Tomas, Piotr) — now extracted + visible in calendar + reservation detail (#421) - Night-hour arrival (01:00) misclassified as early check-in — now correctly classified as late-night arrival (#443) - Check-in reminder firing late with "tomorrow" on the day of check-in (#387) - Checkout-day guest showed CHECKED OUT before checkout time (Aleksa 09:28, checkout 11:00) (#433) - V28 inbox "early · requested" badge false-positives on standard 15:00 arrivals (#425) - 41 prod reservations missing guest rows (Feb 20 – May 18) — backfilled (#392)

Cleaning + cleaner walkthrough - Cleaner walkthrough end-to-end was broken — "session expired" on every Scene 1–6 button tap (#382) - Greeting fired multiple times per task (no idempotency on existing active session) (#384) - Greeting mixed English + Slovak ("Cleaning is starting v Castle&River") (#383) - gh#189 Phase 3: 1,678 lines of source-grep tests, 0 behavioral coverage — backfilled with real-runtime tests + a ratchet that forbids source-grep-only test files (#385) - V28 teammate editor: "Use NEW walkthrough flow" toggle didn't persist (save payload missing field) (#381)

Tasks + automation - Pulse Copilot creating manual TEMPLATES instead of one-off task INSTANCES for ad-hoc requests (#447) - "Review received - how to improve" task was dead — review score arrives as string, never matched the event.rating < N condition (0 of 11 webhook reviews fired since 2026-05-09 incl. 3 sub-10 Booking reviews) (#446) - Invoice task overview empty ("Invoice requested for ") — {{property.name}} had no reservation→property fallback (#445) - AI "Drafting note for the AI" rendered on operator review task card (confusing) (#449) - "Approve cleaning" rule duplicated (stale [draft] clone + active); "X days before after check-out" wording fixed (#452) - Checklist rows rendered blank when array mixed plain-string entries with {label} objects (#451) - task_template_validator false "cleaner null" warnings (ignored useReservationAssignee) + hallucinated "LLM execution latency" warning (#450) - Task editor: long-text ANSWER param rendered single-line input instead of textarea (#444) - Pre-send "will-auto-fire" warning false-positives on future-dated tasks (#389, #435) - Rule activate/deactivate orphans past-message tasks (#424) - V28 task templates UI: Delete button added (parity with legacy) (#396) - "Show 'No auto-complete' when autoCompleteOnCreate=1" — UI ignored column, read checklist sentinel only (#427) - V28 template editor: missing "Save Anyway" dialog when validator errored (#428) - Cleaning task templates named [draft] … — instances inherited [draft] (historical backfill + forward rename) (#482) - Task template checklist auto-complete sentinel duplicated (validator B1 fires) (#429)

V28 surface polish - Multi-tab rate-limit storm — single per-tenant bucket × N tabs × retry made the app unusable (#398, #423) - V28 dashboard/inbox load hits 429 — rate-limiter too strict for legitimate batched page-loads (#441) - V28 conversation thread now shows date AND time (not just time) so you can distinguish 1-day-old from 20-day-old at a glance (#390) - V28 inbox drops aiOriginalBody on send — operator's AI-answer refinements weren't captured (#417) - V28 inbox conversation images broke in old threads — durable object storage + Hospitable backfill (#413) - V28 WA Communication — clicking a thread photo opened a blank tab (Chrome blocks data: URI navigation) (#478) - Pulse "Auto-resolves" card: bare "09:00" ambiguous + no "why didn't it fire yet" surface (#391) - V28 vacancy calendar "No active promo" chip false-positives on fully-booked properties (#454) - V28 topbar horizontal scrollbar when 7 nav groups overflow (#411) - V28 SYNC NOW button crashed preprod server (#394) - V28 task checklist omitted "Resolved by …" resolver/keyword/evidence text (#460) - V28 teammate-role filter only checked FIRST role in roles[] array — multi-role teammates excluded (#380)

Accounting + payouts - V28 payout report showed fabricated 60/40 wire/cash split — contradicted teammate payoutType (#456) - Payout report port gaps: expense rows missing activity + showing datetime, language switcher dead (#457) - City-tax cash payment showed silent €0 — now anchored on expected city tax + photo-sufficiency check (#458) - City-tax payments (id 50/51) showed €0 + no photo + no chip — backfilled (#480) - V28 Accounting Overview: 3 "Coming soon" KPIs implemented (PENDING PAYOUTS / monthly BUNDLE / CITY TAX) (#367) - Invoice language Zod-rejection on empty placeholder fall-through (#397) - EN invoice rendered Slovak supplier labels — buildSupplierHtml ignored LabelSet (#410)

Channel management - Platform-price scrape recurringly failed for one property (Work&Living Studio-Prístavná) — promo detection disabled + opaque "failed" banner (#455) - V28 Settings → Channels tab removed (no direct Airbnb/Booking integration; CONNECTED was misleading) (#365, #366)

Notifications + observability - Host WA task notification silently undelivered — Meta-accepted template (wamid) treated as delivered; delivery-status webhooks were discarded → zero observability. Now: webhook-status persisted + surfaced (#438) - LLM responseRequiredScore universally NULL (0/2046 ever scored) — 2026-05-14 prompt migration dropped the field, consumer still read the deleted key (#439) - Prompt↔consumer field-contract ratchet added to catch silent LLM-prompt field drift (#440)


🔒 Security & engineering hardening

  • operation_tasks audit log — every mutation by deploys / LLM / scripts / operator is now audit-logged with actor naming convention (manual:operator-N / copilot:tool / deploy:migration / script:name) and an operator-readable diff summary (#300, #301, #321)
  • User-owned-config registry — first-class registry of which DB tables are user-owned, with a generalised audit-log helper wired to properties + teammates (#321, #333, #334)
  • Bug-report GDPR retention — 30-day TTL sweep on bug_reports + media artifacts (#344)
  • Mock client refusal hardening — Hospitable client returns 401 on preprod despite MOCK_HOSPITABLE=1 mock-mode bypass risk (#402)
  • Multi-tenancy migration safetyreplaceGlobalUniqueWithTenantCompound no longer crashes on duplicate (tenantId, reservationId) rows; audit_and_guard_task_delete_fn trigger no longer P0001-throws to crash the migration process (#403, #404)
  • Quoted-identifier migration fixr.isActive was unquoted → Postgres case-folded to r.isactive → silently failed on every boot (#401)
  • Fresh-DB boot fixed — raw initializePostgresTables created unquoted lowercase columns; Prisma/queries expected quoted camelCase (#492)
  • Manus codegen artifacts cleanedusers.openId no longer contains literal template-string 'fallback_${input.email}' (#400)
  • Validator cron + sandbox-deploy lock — advisory lock with owner + TTL prevents concurrent deploys / closes from invalidating a validation round (#472)
  • Symptom-class registry + bug-class E2E tests — process-level addition: every symptom class now has a permanent E2E test that asserts the operator-visible outcome, so the same incident class can't recur silently (#262, #280, #281)
  • Layer 7 audit standard — operator-visible mental-model walkthrough added to the audit framework (caught gh#308 + gh#310 which both passed the prior 6-check audit) (#311)
  • CI quality ratchets — TS errors + ESLint warnings walked down to zero; lint-CI inversion fixed (#191, #255)
  • Visual regression + a11y — Playwright visual regression + axe-core a11y scaffolding extended to 7 remaining E2E specs (#346)

📋 Process changes worth knowing

The release cycle itself shipped several process changes that change how engineering ships:

  • Closure proposal model — implementers post a proposal, a cold-context validator runs 12 checks + closes (the implementer never self-closes). Reduces "looks done" → "actually done" gap.
  • End-user UI validation as the closing gate — closure-validators MUST walk the operator's real UI on sandbox before stamping audit:validated. API probes, DB queries, unit-test runs are necessary but not sufficient. This is what catches V28-cast-lie + similar latent crashes.
  • Behavioral reproduction required for confident claims — "X is broken because Y" requires inline command output (test pass/fail, query result, log line), not code-read inference. Multiple wrong-RCA tickets retroactively corrected.
  • Closure includes evidence — every closure proposal carries commands + outputs (not "PASS"), a per-route walkthrough table for UI changes, a video for visible surfaces, and a deployed-bundle grep proving the fix is in the running artifact.

Documented in CLAUDE.md for the engineering team; raised here so operators understand why closure turnaround is now slightly longer but the fixes that ship are more likely to actually stick.


Items shipped between 2026-05-21 and 2026-06-09. Full audit trail: closed GitHub issues. Screenshots captured live on sandbox 2026-06-23 (synthetic mock-mode data).


May 17, 2026

V28 Redesign reaches feature parity

This release closes out the V28 redesign punch list — 50+ operator-reported items shipped over the past 48 hours — and lights up a set of new operator-facing capabilities that go beyond the legacy UI. Two critical guest-impacting bugs were also resolved.


✨ What's new

🤖 Pulse Copilot now accepts pasted images

Pulse Today dashboard

Drop a screenshot into the conversation the same way you would in any modern chat app. Hit CMD+V with an image on your clipboard — a screenshot of a damaged item, a photo from a guest's WhatsApp, a snippet from another tool — and it attaches instantly. No more paperclip → file picker → folder hunting.

Why operators care: The Copilot becomes a first-class visual workspace. Show it what you're seeing instead of describing it. (#187)


📅 Vacancy Radar — now inside the booking calendar

Booking calendar with vacancy radar

The 14-day booking calendar now highlights vacant slots of 2+ nights in real time, with a promotion-status badge so you can see at a glance which gaps already have active discounts and which need attention.

Why operators care: Stop scrolling to a separate card to spot revenue leaks. The intelligence lives where you already look — and you can act on it before guests notice the price gap. (#160)


💬 "Open conversation" right from any Pulse task

Every task tile on the Pulse dashboard now has a chat icon. Click it — the guest's conversation slides in from the right edge without leaving your dashboard. Read what was said, then close the panel and move on.

Why operators care: Context is one click away, never a full page navigation. The legacy UI made you jump to /messages; V28 keeps you in flow. (#162)


📊 V28 Reports — full Revenue + Simulation parity

V28 Analytics — Revenue tab

The V28 Analytics surface is now organised as a tabbed workspace: Revenue · Simulation · LLM Usage — mirroring the legacy /metrics page and completing the financial picture with:

  • Net Revenue (your actual bottom line, not just gross)
  • Cleaning fees, management fees (with a tooltip showing the 25% formula)
  • Booking.com VAT carryover and city tax breakdowns
  • Airbnb-only revenue
  • Messaging AI performance summary
  • A month/year selector at the top — pick any historical month
  • Extended CSV export covering the full payout shape

V28 Analytics — Simulation tab

The Simulation tab — previously missing entirely from V28 — is back and lives natively inside the redesign.

Why operators care: You no longer need to bounce to the legacy URL for the numbers that drive financial decisions. (#161)


👥 Team & Account — full access-code and guestbook control

V28 Team & Account

The V28 Team & Account screen now matches everything the legacy editor did, and adds inline shortcuts:

  • Per-row "Generate guestbook" for any cleaner — auto-copies the URLs to clipboard, ready to paste into WhatsApp
  • Smart-lock codes editor inside the teammate modal — add an existing Nuki code, remove an access, or generate a brand-new PIN + 2N QR pair without leaving the panel
  • Property-scoped pickers prevent duplicate code assignment

Why operators care: Onboarding a cleaner — assigning codes, generating their guestbook URL, and handing it over — is now a 60-second flow inside one modal. (#164)


🔋 Smart Locks — Charging history is back

V28 Smart Locks — Charging tab

The Smart Locks screen now has the Charging tab restored. See each battery-charge event per lock with the exact transition ("1% → 92%"), absolute timestamp, relative time, and source (raw sensor reading vs. task-spawned recharge).

Why operators care: Know which locks were last serviced and when the "Charge nuki lock" tasks actually fired. No more guesswork before a guest checks in. (#165)


🌐 Cleaner walkthrough — special requests now translated

When a cleaner asks WhatsApp for their plán, the upcoming list now translates every line — including the guest's special requests — into the cleaner's chosen communication language. No more Slovak scaffolding mixed with English requests mixed with Russian guest notes.

Why operators care: Cleaners read once and know what to do. No more "what does this English sentence mean?" callbacks. (#152)


⚡ Cleaner walkthrough — faster and forgiving

Two improvements to the WhatsApp cleaning walkthrough cleaners use 20+ times a day:

  • Prefetched next item — the bot warms the LLM cache for item N+1 while item N is being sent. Perceived latency drops from 3-10 s per advance to near-instant on cache hits.
  • Photo without the "📷 Foto" tap — if a cleaner uploads a photo on a step that expects one, we presume the Photo button was pressed. One less tap per item × 17 items per cleaning = ~3 minutes saved per turnover.

Why operators care: Cleaners are in-and-out faster, with less friction. (#151)


🛠 V28 Task Template Editor — smarter parameter authoring

A pack of editor improvements that compound:

  • "User input" can now prefill from context — pick an LLM-extracted value (e.g. "early check-in at 11:00") as the starting value the operator can accept or override (#144)
  • Admin-selectable input widget — time fields get a real time picker, not a raw text input
  • DELETE button for templates (#145)
  • Insert Placeholder now exposes LLM / event / context-namespace placeholders (#143)
  • "From context" auto-matches the best placeholder by name + type (#139)
  • "Static value" for foreign-key fields renders a lookup dropdown instead of a raw number input (#146)
  • FLAGS section retired — duplicate / dead toggles removed (#140, #141)

Why operators care: Building and editing templates is dramatically faster. Defaults are smart, dropdowns replace memorising IDs, and dead UI is gone.


🎯 Pulse layout — Per-property panel up top

Pulse's per-property panel now sits directly below the calendar — where the eye lands first — instead of below the action piles. (#158)


🐛 Critical bug fixes (guest-impacting)

🔥 FAQ "Property not found" — every Add-to-Knowledge-Base click was failing

A Zod schema regression from May 13 was breaking the {{property.id}} placeholder in createFaq. Operators couldn't accept any AI-suggested FAQ entry for 4 days. Fixed and back-tested across all knowledge surfaces. (#185)

🔥 Silent skip on auto-resolve — guests weren't getting messages

A guarded "skip when runtime param is empty" path was returning ok: true and marking tasks as done — even when the actual guest-facing send had been skipped. Pre-checkout reminders were silently not arriving. The success notification fired, the work didn't happen. Now the gate distinguishes "intentionally empty (auto-fetch will fill it)" from "missing data — abort and surface to the operator". (#180)

🔐 Removing an access code now actually revokes the Nuki PIN

Operators expected that removing a teammate's access code deactivated the physical PIN on the lock. It wasn't — only the JSON column shrank, the Nuki authorization stayed live. Departed cleaners could still enter. Now the teammates.update mutation diffs the old vs. new code list and revokes any removed codes via the Nuki API. (#166)


🐛 Bug fixes & polish

Messaging - Cleaner walkthrough no longer sends mixed Slovak / Ukrainian / English messages — now respects teammate.language end-to-end (#182) - AI Suggest now picks the guest's language correctly — synthetic system messages no longer pollute the host-consistency signal (#156) - WA task notifications: unresolved placeholders now fall back gracefully instead of stripping to orphan punctuation, and emit telemetry (#154) - Thread language picker — "Original" option now always shows and behaves adaptively (#157) - Message template title — humanised "Check-in reminder" instead of raw slug checkin_reminder (#155)

Notifications - "27 New Task Created" notification flood after an admin photo send — now digest-aggregated instead of one-per-task (#181)

Reservations & calendar - Upcoming turnover row now shows the actual time, not '—' (#172) - Reservation 6147057327 (and similar) no longer shows NULL propertyName (#137) - Reservation-detail drawer: CONVERSATION card collapses when empty instead of reserving 60% of vertical space (#178)

V28 surfaces - V28 tables no longer show "Invalid Date" in WHEN columns or overflow chips into adjacent columns (fixed across Errors, Webhook Logs, Audit, +3 more) (#176) - Sub-nav badges removed from Audit Log + WA Communication (regression of 2026-05-12 policy) (#175) - Activity Log on Smart Locks now fills the viewport instead of being capped at 720 px (#170) - Pulse "UNANSWERED" cluster — internal scrollbar replaced with a sensible item cap (#169) - Pulse Copilot send button no longer clipped at the bottom edge (#187)

V28 Task Template editor — smaller fixes that compound - "Save with allowDismiss only, no action" no longer triggers a confusing migration error (#153) - Conditions builder — boolean fields now show "equals" instead of "is any of" (#142) - Conditions builder — long field paths get a tooltip + flex-grow, no longer truncate (#148) - Quick-fill buttons — empty labels say "Hidden" instead of "Untitled" (#147)

Smart Locks - "Candidate for removal" chips on the Codes tab now reads from the correct data source (#163)


🔒 Security & engineering hardening

Behind-the-scenes work that doesn't change the UI but makes the platform safer and easier to evolve:

  • XSS hardening — LLM output rendering now goes through a hardened sanitizer; the missing HTML sanitiser dependency was added (#74)
  • Webhook payload trust boundary — tenant-spoofing fallback removed, full Zod validation on every webhook ingestion (#80)
  • Schema strictnessz.unknown() / .passthrough() / z.any() swept from all mutation inputs (#82)
  • Operational hygiene.env.example, key rotation docs, attachment tenant-scoping (#85)
  • SQL helper hardeningconvertPlaceholders + raw-SQL hygiene pass (#84)
  • tRPC surface audit — every mutation in the public registry now has only operator-meaningful params; internal telemetry fields hidden via the autodiscovery filter (#149)
  • Shared zod schemas + .meta({ description }) coverage across the public router (#99)
  • Logging hygiene — scoped logger + error-message extraction + PII redaction (#100)
  • Client mutation/toast hook consolidated into useMutationWithToast (#101)
  • Misc tRPC hygiene helpers — boolean ↔ int coercion, JSON serialize, HTML escape (#103)
  • whatsapp/buttonHandlers.ts ORM migration complete; duplicate daily_postpone branches cleaned up (#135, #138)
  • V28 Pulse buttons unified — bespoke TileAction chips replaced with standard V28Button (#159)
  • Investigation closed: Airbnb does not expose a host-side guest-review WRITE API; documented in docs/airbnb-guest-review-write-api-investigation.md (#150)

All items above shipped between 2026-05-16 and 2026-05-17. The full audit trail lives in closed GitHub issues. Per the Definition of Done, every closure includes a prod-deploy timestamp + main-merge SHA on the issue thread.