Every release across the platform — features, fixes, and the AI getting sharper. Pulled from the product's own release notes.
20 issues closed in two days. Small-window, high-impact release. Three threads:
LockProvider integrations landed on top of last week's Nuki / 2N / Tuya facade — igloohome, TTLock, SwitchBot, Tedee. Portfolio now supports 7 lock brands with zero feature-code changes needed to add the next one._providerKit, WhatsApp first), per-tenant Hospitable + Nuki credentials (kills the global-env split-brain), MCP-server rebuilt on tRPC callers with tenant scoping. Plus three security follow-ups: SSRF on outbound webhook URLs, MCP cross-tenant leak, tenant clobber from Hospitable payload.The LockProvider facade shipped last week paid off immediately. Four new lock integrations, each a self-contained LockProvider implementation with zero feature-code touch:
Why operators care: If a property has any of these locks, the code path is now identical to a Nuki property — generate code, expiry rule, revoke, activity log all work out of the box.
Copilot spend is now capped + billable in three tiers:
Why operators care: Copilot cost is predictable at the tenant level; users hitting quota can upgrade themselves without ops involvement — the SaaS motion becomes real.
_providerKitThe same _providerKit pattern that hid Nuki + 2N behind LockProvider now hides WhatsApp behind a MessagingProvider interface. Feature code (send text, send template, send image) no longer imports the WhatsApp Cloud API SDK directly. Foundation for adding Telegram / SMS / iMessage as future MessagingProvider implementations without touching send-path feature code. (#976)
Hospitable API tokens + Nuki API tokens previously lived as global env vars — meaning every tenant used the same credentials. This blocked adding tenant #2 and produced sneaky bugs where "which Hospitable account is this reservation from?" answered by looking at the env. Now: creds live in tenant_credentials (encrypted), keyed by tenant. Global env removed. (#978)
The MCP server (which exposes tRPC tools to Claude Code + other MCP clients) was rebuilt on top of tRPC's createCaller pattern with explicit tenant scoping on every tool call. Kills the gh#982 cross-tenant leak (8 of 13 MCP tools were previously unscoped by construction). (#980)
Task-level EDIT + DELETE actions have been removed from the Pulse (agenda) task cards — kept in the Operations task view. Pulse is for reading the day; Operations is for managing tasks. Reduces mis-clicks on the dashboard. (#998)
A stuck / overdue AUTO Generate-access-code task previously had NO Dismiss button — operator couldn't resolve-without-action when the guest was already handled manually. Dismiss button restored (parity with other AUTO tasks). (#1001)
Check-in instructions sent twice (text body) + images dropped when nginx returned 504. Root cause: on 504, the task re-fired the full send instead of resuming with the failed images only — text hits 200 both times, images fall through. Same family as gh#453 + gh#856. Fix: partial-send-state persisted per task; retry now resumes from the failed batch offset. (#992)
Outbound webhook subscription URLs weren't validated against the internal-network SSRF allowlist. Latent (the public-API layer that would let a tenant subscribe wasn't shipped yet) but the code path was there. Validation added + a CI ratchet forbids new URL-accepting endpoints without the SSRF guard. (#981)
8 of the 13 MCP-server tools were unscoped by construction — a query for tenant A's data would return tenant B's rows because the tool never applied a tenant filter. Fixed by rebuilding the MCP server on tRPC callers with per-tool tenant scoping (see gh#980). (#982)
The Hospitable webhook handlers were reading tenantId from the payload (attacker-controlled) instead of the URL-scoped auth. Same shape as gh#623. Now retired before tenant #2 lands. (#983)
A billing analysis surfaced sandbox running 2x-2048 24/7 + a demo-db idle machine. Sandbox rightsized to auto-stop; demo-db idle stopped. Full retrospective on Fly infra sizing for the next scale-up. (#925)
The 2N lock automation path used page.setTimeout hardcoded to 3–5 s + a headless Chrome that never .close()d — every guest arrival stalled the cron 6–10 s + leaked a Chrome process. Fixed via waitForSelector + explicit browser lifecycle. (#938)
duration=1 + misspelled "Laundy" template; cleaner under-paid (#989)constraint-drift — 1 violation triaged (#996)Items shipped between 2026-07-05 and 2026-07-06. Full audit trail: closed GitHub issues. Screenshots for this release (quota banner, upgrade modal, new lock-brand config, MCP tools) deferred to a follow-up commit.
83 issues closed in six days. Five threads:
set_cleaner, FAQ, AI-cockpit config); a generalised anti-clobber read-merge covers every .update path; and a cost/quality refactor drops 3-agent plan/critique/revise/rehumanize into one specialist loop.LockProvider facade — no more direct provider calls from feature code. Foundation for adding new lock brands without touching feature paths./admin to move a price.schema_migrations tracker added (152 migrations no longer re-run every boot), cron leader-election wired (no more double-fire on 2 VMs), .data-as-cast ratchet enforced.Two ~11-hour outage postmortems drove structural work: the Nuki first-entry monitor silent since 2026-06-27 (no welcome messages went out for four days) and the QQQQ migration re-throwing every boot (drove a WA-inbound outage; prompted a full migration-safety failsafe pass).
Every Copilot mutation (intents + execute_trpc) now surfaces a confirmation preview before execution: "About to update Property X → base price 89 → 95. Confirm?" Applies to every write path, no exceptions. Companion fix: the agent can no longer self-confirm the gate (a bug where the agentic self-confirm loop bypassed the human approval on update/delete, defeating gh#897 / gh#855). (#897, #912)
Why operators care: No more "I asked it to change one thing and it modified three."
The Copilot previously silently forwarded template-edit requests into FAQ edits (it lacked a messageTemplates.update tool + reported success anyway — gh#893). Now it reaches every admin domain:
Bonus: message_templates + teammates joined the user-owned-config registry so Copilot writes to these tables are audit-logged (#902).
The Copilot's plan → critique → revise → rehumanize pipeline (four separate LLM calls per turn, ~72% of spend was prompt-cache writes) collapsed into one specialist loop with a mutation-only verifier. Model sizing also swept: everything right-sized to Sonnet 5 / Haiku 4.5 with self-healing model resolution (no hardcoded SKUs, auto-upgrade on retirement — prevents the gh#716 recurrence). Third piece: tool-result dumps capped + cache prefix stabilized so the 72% cache-write share falls dramatically. (#948, #949, #950)
Why operators care: Copilot responses become faster, cheaper, and stop stalling behind a 4-round LLM plan/revise loop.
LockProvider API, Tuya addedA unified LockProvider API hides Nuki + 2N behind one facade — feature code (send access code, generate PIN, unlock, revoke) no longer imports vendor SDKs. Immediate payoff: shipping Tuya smart-lock integration (Cloud API — reaches many OEM locks) as just a new LockProvider implementation, no feature-code changes. (#884, #965)
Why operators care: Any future property with a non-Nuki/2N lock (Tuya, Yale, August, Salto, etc.) is now a config-only onboarding.
The Beds24 connector was previously read-only (import bookings) — now it also pushes rates, availability, and content updates. Companion V28 change: the connector editor (provider + property id + Test Connection + Map room) is ported into V28 PM → Configuration tab. (#715, #722)
Bonus: the last per-property direct-Hospitable runtime call (syncMessages reservation-enumeration) now routes through the connector facade — a step closer to fully provider-neutral runtime paths (#905).
Editing property availability + pricing (base price, weekend override, seasonal ramps) previously required a bounce to legacy /admin/properties. Now native in V28 PM. (#875)
An epic just landed: Copilot full UI-parity task-template configuration as the enabler for the upcoming cleaner self-approve/reject redesign. Cleaners will be able to approve/reject their own walkthrough completion via WhatsApp buttons; the epic groundwork (Copilot-editable task-templates so the operator can build the flow in-app) shipped this week. (#855)
Every supply now has an auto-fetched product image (canonical name lookup + LLM verifier that rejects wrong pictures — no more "detergent" showing a bottle of shampoo). Manual override available. Images render in the Inventory list and the History tab. (#873, #961)
Units graduate from ad-hoc strings to a canonical set — pieces / L / kg — AI-assigned per product, with unit-aware merge across brands (e.g. 750 mL Finish + 1 L Somat merge into one product-type row measured in L). Checklist supply-consumption widgets now accept ml / l / g / kg inputs. Re-analysis migration recomputes existing rows against the canonical units. (#960)
The Smart Locks screen got a top-to-bottom rework:
The FINANCIALS panel now shows a Discount % line comparing app-defined base price to the real realized rate — quantifies the discount lift on every booking. (#871)
The REOPEN button (gh#232) required expanding a done task row to become visible — nobody knew it existed. Now surfaced on the collapsed row as a chip. (#906)
Legacy TaskInstancesTab allowed editing a task instance in place; V28 was read-only. Ported — updateTask mutation already existed. Sibling parity fix to gh#868 (reassign dropdown last release). (#919)
Sibling nav-refinement: the reassignee dropdown was REMOVED from Pulse dashboard cards — reassignment now only happens from the Operations/Tasks view (Pulse card shows read-only "Assigned · name"). Reduces mis-clicks on the agenda dashboard. (#958)
Three separate recurrences of "assign-cleaner task marked DONE but no cleaner assigned" had no monitor. A dedicated probe now fires on any active-stay reservation with assignCleaner.status='done' AND reservations.cleanerId IS NULL and auto-files. (#913)
The Nuki first-entry monitor stopped emitting guest_first_entry events at ~2026-06-27 — meaning no welcome message was sent to any guest for four days (including Michael). Root cause traced to a config drift after the gh#884 lock-abstraction landing; monitor re-wired via the new LockProvider API. Retroactive welcome-message send for affected guests. (#895)
processGuestArrival minted 19 duplicate Nuki/2N codes for one guestOne guest ended up with 19 duplicate access codes on the same lock because processGuestArrival had no idempotency key and the arrival cron re-fired the mint path on every retry. Companion symptoms: failing confirmation writes + wrong Offline state. Idempotency key added (reservation × arrival-day × lock). (#892)
Prod app hung for ~20 minutes with 502s + tRPC 500s and required a manual restart. Root cause: no Fly health-check probe, so Fly's auto-restart never fired. Health-check wired + a Fly restart policy configured; recovery time in future goes from operator-noticed to ~30 s automatic. (#890)
The teammate guestbook URL used a predictable token (sequential ID hash) — an outsider could enumerate URLs and read smart-lock codes + WiFi passwords for every cleaner. Fix: cryptographically random tokens + rotation on generation + rate-limit + audit-log every access. (#926)
The admin-chat RAG (retrieval-augmented generation) vector search used $queryRawUnsafe which bypassed Prisma's tenant scoping — a tenant A admin's question could pull knowledge-base rows from tenant B. Fix: raw query rewritten with explicit tenant scope + a CI ratchet forbids $queryRawUnsafe on any tenant-scoped table. (#951)
/adminDebug/* endpoints dumped cross-tenant PII on any authenticated GET (hardcoded date tokens in the query). Scoped down + auth-gated + audit-logged. (#927)
The Hospitable webhook-log path stored raw payloads with unredacted guest PII (the redactor existed and the WA path used it; Hospitable was left out). Fix: shared redactor now applies to all webhook ingestion paths. (#928)
Extension of gh#854 (last release) — the auto-resolve cron's re-validation was still firing on validator-blocked tasks every ~7 minutes, burning LLM-validator tokens + risking a leak. Sticky-blocked state persisted; re-validation now requires an actual draft change. (#891)
execute_trpc operations.tasks.update bypassed anti-clobberThe Pulse Copilot's raw execute_trpc path bypassed the gh#855 anti-clobber read-merge — a spurious unrelated write to task #42 clobbered skipIfManuallyDone. Fix: generalised anti-clobber read-merge now covers every clobber-prone .update path (gh#898), not just the messageTemplates.update case. (#894, #898)
Not a normal release: gh#292 lineage of "boot-time silent no-op" bugs and gh#947 11-hour WA-inbound outage prompted a systematic pass. Highlights (all shipped):
schema_migrations tracker (152 migrations no longer re-run every boot). Migration-safety failsafes prevent silent cross-domain outages of the gh#947 shape (a supplies migration killed WA inbound for 11 h). Includes: dry-run diff on boot, explicit rollback path per migration, per-migration timeout with hard-fail, migration-log persisted to schema_migrations (#932, #955)product_pack_sizes had source='backfill' (14 rows) not in the CHECK allowlist. Constraint absent on prod; retrofit + drop the throw (#947)db:push was a footgun); raw-SQL surface ratcheted at 917 sites (no new raw-SQL without a lint waiver) (#931)setInterval jobs would double-fire when a 2nd VM came online. Wired the leader-election guard + a per-cron advisory_lock (#933).data-as-cast ratchet — 13 live tRPC useQuery().data as X cast lies (including the gh#553 Pulse shape) fixed + a CI ratchet forbids new ones (#942)catch, 62 .catch(() => {})) in scheduler + webhook paths — all replaced with logAndRethrow or logAndTrack calls; ratchet forbids new empty catches (#943)normalizePhone helper; source-grep ratchet forbids new reimplementations (#944)webhook_logs.getStats was polling SELECT * every 2 min with no createdAt index + retention cron missing — indexed + retention added (#937)resolveAllPropertyIds re-resolved per-reservation, uncached, on every boot + every 6 h. Cached + hoisted out of the per-reservation loop (#939)messagesProcedures re-queried default_language 4× per call + sequential awaits + unbounded reservationIds. Batched + capped (#940)Assignment + tasks
recurring_expenses table; costs actually live in recurring_costs (#910)Supplies polish
Refine
Items shipped between 2026-06-29 and 2026-07-04. Full audit trail: closed GitHub issues. Fresh screenshots deferred to a follow-up commit — surfaces from this release (Copilot approval-gate preview, V28 Smart Locks overhaul, Beds24 connector editor, Base-price editor, Supplies with product images) are text-only this round.
35 issues closed in five days. Three threads dominate:
success:true while silently dropping images. Validator-block + retry no longer re-rolls non-deterministic verdicts until a blocked send leaks through.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.
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)
Every validator-blocked send now offers three actions instead of two:
Why operators care: Most validator blocks are pedantic — Refine lets the AI fix its own draft instead of forcing you to. (#879)
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)
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)
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)
The supplies model grew a third intermediate layer:
dishwasher_tab)Inventory now merges by product type so the dashboard shows "you have 87 dishwasher tabs across 3 brands" instead of three separate rows. (#878)
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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
Sends + check-in pipeline
success:true reported even when failedBatchOffsets had entries; no retry, no alert. Now: explicit fail-on-partial with retry + app_errors row (#856)reservation from the condition context, so reservation.bookedOn field_match hard-failed (gh#855 / gh#858 family + Pulse phantom) (#880)resolveFieldPath undefined → undefined → false → "parking" never auto-resolves (#858)Tasks + cron
notifyOnResolved fired only on cron auto-resolve — operator's manual markDone (approve OR reject) didn't trigger the notify (#851)V28 polish
todayProjection not invalidated) (#877)/admin (window.location.assign) — no V28-native review screen. Ported. (#861)markDone → rollback flap shown as DONE ↔ NOT_DONE state changes — now collapsed with the rollback reason inline (#846)Receipts + supplies
Reviews + import
csv vs csv_import) didn't dedup cross-path (no externalReviewId); now keys on reservationCode + a unique constraint (#842)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.
157 issues closed in ten days. Three threads dominate:
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).
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:
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.

The Supplies surface graduated from a flat inventory list into a real category-aware system:
Why operators care: Supplies became actually-useful for portfolio planning, not just an inventory list.
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.
The task-template checklist editor became substantially more expressive:
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-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)
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 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)
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)


A wave of nav reorganisation based on operator usage patterns:

/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)
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)
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)
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)
Sandbox mock data graduated from ad-hoc scripts into a real system:
Classic UI showed the assigned teammate on the task card; V28 was rendering only property + due date. Assignee back. (#748)
WA Communication translation now displays "Translated to communicationLanguage is prompted/defaulted on first use (gh#616 UX follow-up). (#681)
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)
platformCapabilities) for trigger + completion condition pickers; backfill missing fields (earlyCheckinRequested, lateCheckoutRequested, guestsTotal, guestMood, …) + CI ratchet (#798)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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
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 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)
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)
Messaging + sends
generateTwoNAccess reported twoNSuccess:true but twoNAccessCode never persisted (Klaudia/Vydrica) — guard retried forever, guest had no 2N access (#700)Tasks + auto-resolve
resolveItem vs delivery-guard rollback fought with no debounce (gh#724 family) (#743)createTask Zod wanted dueDateTime as string but dispatch passed a Date (#839)Calendar + reports
calculateSimulation 502 (Fly proxy timeout — 53 s+ cold-cache grid recompute, double-fired, hot-path JSON logging) + SSE collateral (#742)totalIncome/totalCosts/totalProfitLoss, client read m.income/m.costs/m.profitLoss (#745)Reviews
nullIfZero collapsed 0/late comfort) (#840)Environment
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).
52 issues closed. Four-day window dominated by three threads:
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.
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)

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)
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)
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)

Two improvements bundled:
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.
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)
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)
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)
The WA Communication view now:
ChatBubbleView component as the inbox (consistent visual + interaction model — no more two-flavored chat UIs)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)
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)
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)
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)
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)
gh#290 windowThe 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)
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)
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)
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)
messages.externalMessageId was a GLOBAL uniqueA 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)
runMigrationsKK silently no-op'd on prod + sandboxThe 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)
Tasks + auto-resolve
relative_to_due, lastAutoEvalAt blank → stale codes left departed guests with access (#574)supplyId + skipped the strip → raw {{supply.*}} written to task overview ("low stock of ( )") (#611)auto:true + defaultId) — gh#466 guard now honours the opt-in when the cleaner serves the property via teammates.assignedProperties (#670)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)Cleaning + reservations
fetchCleaningSchedule skipped resolvePropertyName fallback (#630)V28 conditions builder
(×) wrapped to its own line, field-path select hard-truncated. Controls now anchor right in nowrap zones (#659)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
Engineering infrastructure
guard-approved (+ siblings) as collateral. Now: additive-only label edits with grep ratchet (#604)_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).
283 issues closed in three weeks. Two themes dominate this release:
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).
A floating Report Bug button now lives on every authenticated screen. Tap it (or Cmd/Ctrl+Shift+B) and:
bug_reports table; engineering sees the full reproducible context immediatelyWhy 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)
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)
/changelog page
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)
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'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)
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 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)
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)
Two related shipments:
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).Why operators care: You can see what your AI is spending money on, and the most expensive flow got cheaper.
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)
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)
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)
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)
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)
Three improvements to V28 Supplies that came in via the new in-app bug-report channel:
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).
Two improvements to the template editor that came directly from operator frustration:
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.
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)
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)
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)
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)
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)
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)
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)
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)
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)
/m/pulse mountA 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)
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)
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)
Messaging + translation
detectGuestLanguage step-1 no longer over-trusts a single recent EN inbound (#436)translatedBody when guest's message language ≠ profile language (#426)languageOverride + thread re-translate) instead of being a transient AI-assist-only toggle (#461)Pulse Copilot
res.status() fixed (#299)execute_trpc for reservations queries — now does, no more "export data and analyse externally" responses (#377)Reservations + check-in
Cleaning + cleaner walkthrough
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)Tasks + automation
event.rating < N condition (0 of 11 webhook reviews fired since 2026-05-09 incl. 3 sub-10 Booking reviews) (#446){{property.name}} had no reservation→property fallback (#445)[draft] clone + active); "X days before after check-out" wording fixed (#452){label} objects (#451)task_template_validator false "cleaner null" warnings (ignored useReservationAssignee) + hallucinated "LLM execution latency" warning (#450)autoCompleteOnCreate=1" — UI ignored column, read checklist sentinel only (#427)[draft] … — instances inherited [draft] (historical backfill + forward rename) (#482)V28 surface polish
aiOriginalBody on send — operator's AI-answer refinements weren't captured (#417)data: URI navigation) (#478)roles[] array — multi-role teammates excluded (#380)Accounting + payouts
payoutType (#456)language Zod-rejection on empty placeholder fall-through (#397)buildSupplierHtml ignored LabelSet (#410)Channel management
Notifications + observability
LLM responseRequiredScore universally NULL (0/2046 ever scored) — 2026-05-14 prompt migration dropped the field, consumer still read the deleted key (#439)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)properties + teammates (#321, #333, #334)bug_reports + media artifacts (#344)MOCK_HOSPITABLE=1 mock-mode bypass risk (#402)replaceGlobalUniqueWithTenantCompound 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)r.isActive was unquoted → Postgres case-folded to r.isactive → silently failed on every boot (#401)initializePostgresTables created unquoted lowercase columns; Prisma/queries expected quoted camelCase (#492)users.openId no longer contains literal template-string 'fallback_${input.email}' (#400)The release cycle itself shipped several process changes that change how engineering ships:
audit:validated. API probes, DB queries, unit-test runs are necessary but not sufficient. This is what catches V28-cast-lie + similar latent crashes.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).
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.

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)

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)
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)

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:

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)

The V28 Team & Account screen now matches everything the legacy editor did, and adds inline shortcuts:
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)

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)
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)
Two improvements to the WhatsApp cleaning walkthrough cleaners use 20+ times a day:
Why operators care: Cleaners are in-and-out faster, with less friction. (#151)
A pack of editor improvements that compound:
Why operators care: Building and editing templates is dramatically faster. Defaults are smart, dropdowns replace memorising IDs, and dead UI is gone.
Pulse's per-property panel now sits directly below the calendar — where the eye lands first — instead of below the action piles. (#158)
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)
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)
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)
Messaging
teammate.language end-to-end (#182)checkin_reminder (#155)Notifications
Reservations & calendar
6147057327 (and similar) no longer shows NULL propertyName (#137)V28 surfaces
V28 Task Template editor — smaller fixes that compound
Smart Locks
Behind-the-scenes work that doesn't change the UI but makes the platform safer and easier to evolve:
z.unknown() / .passthrough() / z.any() swept from all mutation inputs (#82).env.example, key rotation docs, attachment tenant-scoping (#85)convertPlaceholders + raw-SQL hygiene pass (#84).meta({ description }) coverage across the public router (#99)useMutationWithToast (#101)whatsapp/buttonHandlers.ts ORM migration complete; duplicate daily_postpone branches cleaned up (#135, #138)TileAction chips replaced with standard V28Button (#159)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.