Every release across the platform — features, fixes, and the AI getting sharper. Pulled from the product's own release notes.
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
- 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.
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:
- 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)

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