RS Release Notes

StatusReleasesOps Dashboard

Production releases and updates to the RentSolutions platform.

v2026.06.11 Latest

Thursday, June 11, 2026

1 fix 1 total
Fix API

Dev:staging must force NODE_ENV=staging (was hitting prod DB)

#2368
Problem
npm run dev:staging was connecting to the production database, not staging.

dev:staging ran bare nodemon app.js and relied on config's process.env.NODE_ENV || 'staging' default. But app.js calls dotenv.config() (bare .env) at startup, and a local prod-shaped .env (NODE_ENV=production, prod DB URL) sets NODE_ENV=production before config reads it. config/database.js then resolves to .env and loads it with override: true → prod DB. Result: a script literally named dev:staging silently pointed at prod.

Fix
Force NODE_ENV=staging MODE=staging in the script so it always loads .env.staging, independent of the bare .env. dotenv won't override an already-set var, and database.js's override-load then pins the staging DB values.

start is intentionally left as bare node app.js (prod servers legitimately use the bare .env).

Note
The deeper hazard — keeping prod credentials in a local repo-root .env — is a setup concern outside this diff; this change makes dev:staging safe regardless.
v2026.06.10

Wednesday, June 10, 2026

1 new 3 fixes 5 total
Improvement API

Remove destructive DB-sync test tooling that wiped staging

#2365
What happened
An engineer ran npm run test:synctests/_shared/syncTestDatabase.js, which calls sequelize.sync({ force: true }). That helper trusted .env.autotest to point somewhere safe, but never verified the connection target. With no .env.autotest present it fell back to the default env (.envstaging RDS) and dropped/recreated every table — emptying staging's core tables (users, companies, owners, properties, tenants) and taking the app down (login → 500).

What this PR does
Removes the unused, dangerous tooling entirely and hard-guards what remains:

Delete tests/_shared/syncTestDatabase.js (the force: true schema wipe) and the test:sync npm script that invoked it.
Delete scripts/database-sync/ (old prod→local sync tool that drops/overwrites databases; unused).
Add tests/_shared/assertSafeTestDb.js — refuses any TRUNCATE/destructive op unless the live connection is a local host AND a test-named DB. No override flag.
Call the guard before every TRUNCATE in testSetup.js (mediumCleanup/fullCleanup).
Repoint docs at USE_LOCAL_DB=true npm run migration:apply for test-schema setup.

Guard behavior (verified)
| host | db | allowed |
|------|----|---------|
| rentsolutions-staging-db…rds.amazonaws.com | postgres | ❌ blocked |
| yamabiko.proxy.rlwy.net (old Railway) | railway | ❌ blocked |
| localhost | rentsolution_test | ✅ |
| 127…
New API

Notify assigned agent when an ownerLead contract is signed

#2364
Problem
OwnerLead (managed) contracts — e.g. a Property Management Agreement sent to an owner — fired no signing notifications. The only notification path in contractService.syncContractStatus was gated to the application-approval flow (contractType === 'exception' && parentType === 'application'), so when an owner signed, the assigned agent never heard about it. (Real case: agent reported "he signed but I didn't get notified.")

What this does
On a DocuEdit signer_signed / document_completed webhook for an ownerLead contract, notify the single responsible agent:
signer_signed → "{signer} signed — X of N … still waiting on …"
document_completed → "{contract} fully signed".

The DocuEdit webhook carries no signer identity, so the live signer roster is fetched and a pure core decides who just signed, the signed/total counts, who's outstanding, completion, and the recipient.

Recipient = one agent only (not the whole team): leasingAgentId, falling back to assignedToUserId (many ownerLeads — including the case that prompted this — only populate assignedToUserId).

Changes
ownerLeadContractSignedNotificationCore.js (new) — pure, fully unit-tested decision logic.
contractService.js — dispatch on the two events for ownerLead contracts; best-effort, never blocks the webhook ack.
notificationTemplates.js — two new templates (ownerlead-contract-signer-signed, ownerlead-contract-completed); in-app + email.
saketsarin · 19h ago
Fix API

Sync unit amenities/appliances/utilities into Property Profile

#2363
Problem
Agents reported that many properties have data in AppFolio for the Property Profile tabs (Amenities, etc.) but EDGE shows them blank, so they re-enter everything by hand. Confirmed on prod for 5635 Hollingworth Trl and 9108 Freedom Hill (and reproduced on a posted unit, CC-2814 W Price Ave, where AppFolio has 8 amenities + 4 appliances but EDGE stored []).

Root cause — three mapping bugs in the unit sync
1. Data API mapper read keys AppFolio doesn't send. AppFolio's /units returns Amenities ([{Name,Price}]), ApplicationURL, YouTubeURL. The mapper read AmenitiesIncluded, ApplicationUrl, YoutubeUrl → always empty.
2. Report API data went into customFields, not real columns. unit_amenities / unit_appliances / unit_utilities were stored in a JSON blob the Property Profile UI never reads, instead of amenitiesIncluded / appliancesIncluded / utilitiesIncluded. youTubeUrl (wrong casing) was silently dropped.
3. The real-time webhook clobbered data. The Data API upsert does unit.update(payload) with utilitiesIncluded/appliancesIncluded/parkingOptions/petsAllowed forced to [] (reading non-existent keys), so every webhook wiped values the nightly Report sync or an agent had set. This is why fixes "disappeared."

Fix (2 files)
unitAppfolioService.js (Data API): read Amenities/ApplicationURL/YouTubeURL correctly; map Amenities[].Name; omit the 16 fields App…
Fix API

Carry owner-lead roles into auto-spawned processes when source template lacks the slot

#2362
Problem

When the auto-pipeline spawns a process from an owner lead — Owner Lead → Owner Property Onboarding → Property Marketing — role assignments (Leasing Agent, Property Manager, Leasing Manager, Assistant PM) should carry forward so reps don't re-enter them.

They mostly do, but with a reachable gap: executeCreateProcessTask inherited roles only from the *source process's* processRoles. Those are populated by syncProcessRolesFromOwnerLead, which writes a role only if the owner-lead's own template defines it as a slot.

The default "Owner Lead" template's slots are *Business Development Specialist, Leasing Agent, Business Development Assistant, Project Manager, Leasing Manager* — it has no Property Manager / Assistant PM slot. Yet the owner-lead UI (PreQualify.jsx) always shows PM / Assistant PM fields regardless of template. So a PM assigned on such a lead lives only on the OwnerLead FK column, never reaches processRoles, and was silently dropped by the auto-pipeline — even though Owner Property Onboarding and Property Marketing both define a Property Manager slot.

There was also an auto-vs-manual asymmetry: the manual create-card already pre-fills these from the owner lead via getProfilePreview#suggestedRoles, so manual creation carried PM while the automatic spawn did not.

Fix

In executeCreateProcessTask, in addition to inheriting from the source process's processRoles, pull role assignments straight from the linked owner lead (reusing the existing getSuggestedRolesFromOwnerLead) and merge by roleId:
owner-lead-derived assignments fill gaps the source proc…
Fix App

Keep process list pagination from resetting to page 1

#2639
Problem
Pagination on the Process instance list (/admin-portal/process) is stuck on page 1 — navigating to page 2 instantly snaps back to page 1.

Root cause
SearchBar's debounce effect re-fires onChange(localValue) on mount and on every render (its onChange prop is an unstable reference). With an empty search box this calls handleSearchChange("")handleFilterChange({ search: null, page: "1" }), forcing page back to 1 ~300ms after any render. So ?page=2 reverts to ?page=1.

Secondary: the category and stage toteboard clicks (and reset) didn't reset page, so filtering while on a later page could land on an out-of-range empty page.

Fix
handleSearchChange now ignores no-op calls where the search term is unchanged. *(primary fix)*
Category click, stage click, and reset now reset page: "1", matching search/agent/sort.

Single file changed: ProcessList.jsx.

Verification (real browser, staging)
Before: ?page=2 reverted to ?page=1; page 2 showed page 1's rows.
After: ?page=2 sticks and shows the genuine tail of the list; page "2" active, next-chevron disabled.
Clicking "Open" while on page 2 → ?page=1&category=open.
Lint clean.
v2026.06.09

Tuesday, June 9, 2026

2 fixes 2 total
Fix App

Filter process new-card role dropdowns to users of that exact role

#2636
What

In the Process New Card modal, each role-assignment dropdown previously listed every company user, regardless of role. This filters each dropdown so it only shows users whose roleId matches that exact role slot — e.g. the "Leasing Agent" slot shows only leasing agents, the "PM" slot shows only PMs.

Change

src/pages/AdminPortal/Process/_components/ProcessCard.jsx — the per-role options now filter users by u.roleId === role.roleId before mapping.

Notes

Role slots come from the template's processRoles (each has a roleId that is a company-role id); users carry a matching roleId, so the filter is a direct equality.
Suggested-role pre-population is unchanged; suggested users should already match their slot's role.
Fix API

Strip empty CurrentManagementFeePolicy on property update

#2358
Problem

Editing a property and hitting Save fails with the red banner:

> Failed to update property in Appfolio: Insufficient permissions for CurrentManagementFeePolicy.

Investigating with a live test against the AppFolio API surfaced *two* problems on the property-edit → AppFolio sync (PUT /properties/{id}):

1. 403 on CurrentManagementFeePolicy. The Data API integration role cannot write this field at all (AppFolio's GET doesn't even return it). Sending it — even as an empty string — fails the whole PUT, and propertyService re-throws it as a 400 and rolls back the local edit. The create path already guarded the empty case; update didn't, so every edit without a policy hit it.

2. Successful updates silently wiped data. AppFolio's PUT is a full-replace of the fields present in the body — an empty field (''/[]/null) overwrites AppFolio's value, while an omitted field is preserved. The mapper emitted every field with empty defaults from our (sparser) local record, so simply removing the 403 would make updates succeed *and wipe* fields like Amenities. Verified live: sending Amenities: [] cleared 22 amenities; omitting the key preserved them.

Fix

mapPropertyDataToAppfolioFormat only includes CurrentManagementFeePolicy when there's a value (consolidates the inline guard create carried).
updatePropertyInAppfolio prunes empty fields before the PUT and never sends CurrentManagementFeePolicy, so updates succeed and only fields we actually have values for are written — no clobbering AppFolio data.

> Tradeoff: intentionally *clearing* an optional field vi…
v2026.06.08

Monday, June 8, 2026

4 fixes 4 total
Fix API

Trim name in createLinkToken to prevent ID verification freeze

#2343
Problem

A prospect (Beth Lewis) reported on prod that the self-show booking page's identity verification step stays stuck — "Failed to initialize verification".

Diagnosed against prod: her tenant lead's nameFirst is "Elizabeth " (trailing space). The public booking page (PlaidVerification.jsx) calls createLinkToken first, which passed the name to Plaid untrimmed. Plaid rejects with:

400  user.name.given_name must end with a non-whitespace character.


The booking page surfaces this as "Failed to initialize verification" and the step hangs — no showing is ever created, so the prospect can't proceed.

Root cause

createIdVerification and retryIdVerification already trim + validate the name (the NODE-EXPRESS-1S fix). createLinkToken was missed — and it's the *first* Plaid call in the self-show flow, so the whitespace never even reaches the methods that would have cleaned it.

Fix

Apply the same trim/validation in createLinkToken:
trim given_name / family_name
400 if either is empty after trimming

Behavior matches the two sibling methods exactly. No other call sites affected.

Verification

Reproduced against prod data with Beth's exact lead (name/email/phone): createLinkToken returned 400 before the change. With the trim applied, the whitespace is stripped before the Plaid request, so the link token issues normally.

A separate one-row data fix strips the trailing space on her existing lead to unblock her immediately.
Fix API

Coerce custom-field form values to declared types on save

#2341
Problem

Two production Sentry errors on PATCH /api/process/instances/:processId, both flagged unhandledPromiseRejection: true — reported by Elaine via Loom ("I fill out the data form on Edge, it's not saving, and it erases everything"):

NODE-EXPRESS-3KAPIError: Field holdingFeeAmount must be a valid number
NODE-EXPRESS-14APIError: Field hoaApprovalRequired must be true or false

Root cause

Two compounding issues in ProcessService.validateCustomFieldValue:

1. Unhandled rejection — the method was declared async but has no await. Its three callers invoke it without await (two inside synchronous forEach), so any thrown APIError became a rejected promise that escaped the controller's try/catch as an unhandled rejection instead of a clean 400 — the save crashed and the form lost all entered data.
2. Type mismatch — the Edge data form sends custom-field values as strings (holdingFeeAmount as "1000", hoaApprovalRequired as "true"), but the validator demanded a real number/boolean and rejected them. So even without the crash, the save could never succeed.

Fix

Make validateCustomFieldValue synchronous — throws now propagate normally and are caught by the controller (clean 400).
Coerce each value to the variable's declared type and return the normalized value for storage:
- NUMBER accepts numeric strings → Number
Fix API

Surface custom-field validation errors as 400s instead of unhandled rejections

#2340
Problem

Two production Sentry errors on PATCH /api/process/instances/:processId, both flagged unhandledPromiseRejection: true:

NODE-EXPRESS-3KAPIError: Field holdingFeeAmount must be a valid number
NODE-EXPRESS-14APIError: Field hoaApprovalRequired must be true or false

Root cause

ProcessService.validateCustomFieldValue was declared async but its body contains no await — it only ever throws synchronously. All three of its callers invoke it without await, two of them inside synchronous forEach callbacks:

processService.js ~L1421 (createProcess) — forEach
processService.js ~L2003 (updateProcess) — forEach
processService.js ~L2439 (processProcessFields) — for-of

Because the method was async, every validation throw became a rejected promise that nobody awaited. The rejection escaped the controller's try/catch and surfaced as an unhandled promise rejection instead of a clean 400. As a side effect, the forEach kept iterating and wrote the invalid value anyway.

Fix

Remove the unnecessary async. The validator is now synchronous, so its throws propagate normally through the forEach/for-of callers and are caught by the controller — returning a proper 400 and blocking the invalid write. No caller awaited it, so no call sites need to change.

Scope / follow-up

This makes the server respon…
Fix App

Prevent SlideOutPanel overlap with EdgePhone and Chat Assistant panels

#2617
Prevents SlideOutPanel from overlapping the EdgePhone and Chat Assistant panels by offsetting its width when those panels are open
Unifies backdrop transition and keeps SIDE_PANEL_OFFSET inline in SlideOutPanel
v2026.06.05

Friday, June 5, 2026

1 new 3 fixes 4 total
Fix App

Ignore literal undefined/null strings in URL filter params

#2611
Problem
Sentry [NODE-EXPRESS-3B](https://rent-solutions.sentry.io/issues/NODE-EXPRESS-3B): GET /api/leasing-properties 500s with invalid input syntax for type uuid: "undefined".

Stale or mangled URLs (e.g. ?leasingAgentId=undefined) cause the shared useFilterParams hook to return the literal string "undefined". It's truthy, so callers (e.g. RentalList) forward it as an API filter, and it lands in a UUID column → Postgres 500.

Fix
In useFilterParams, treat literal "undefined"/"null" URL values as absent and fall back to the default. Fixing it at this shared source covers every page that reads URL filters (rentals, showings, applications, feedback, etc.), not just the one endpoint.
Fix API

Send single rent param on unit sync to AppFolio

#2331
Problem

Users hit Failed to update unit in Appfolio: Only one of the following parameters is permitted: MarketRent (recommended), ListedRent, or ListedMarketRent when saving the Property Profile (reported by Joe Nunez on 2411 W Fig St Unit 1).

Root cause

mapUnitDataToAppfolioFormat (shared by unit create and update) always put both rent keys in the PATCH payload:

ListedRent: unitData.listedRent || null,
ListedMarketRent: unitData.marketRent || null,


AppFolio's unit endpoint permits only one rent parameter per request. Even when a value is null, the key is still present, so AppFolio rejected every unit save that ran through this mapper.

Fix

Send a single rent parameter, matching the known-good price-change sync path (priceHistoryService / leasingPropertyService):

appfolioUnitPayload.ListedRent = unitData.listedRent || null;
appfolioUnitPayload.AdvertiseUsingListedMarketRent = true;


The rest of the unit payload (bedrooms, description, marketing, deposit, etc.) is unchanged, so those fields keep syncing to AppFolio. All three AppFolio unit-rent write sites now use an identical rent payload. No other AppFolio write (owners/tenants/properties/groups) carries rent params.
Fix API

Replace TwiML app update with read-only fetch

#2330
Removes the update() call in ensureTwimlApp that was overwriting the TwiML app voice URL on every fresh process start
Replaces it with a read-only fetch() that only validates the SID exists
Renames cachedTwimlAppSid to validatedTwimlAppSid to reflect the actual behaviour
Voice URL is now managed exclusively via the Twilio console — no environment can overwrite it at runtime
New API

Alert assigned agent on new lead creation/assignment

#2328
Problem

Agents (Jennifer & Roberto Verbel) reported no text/email alerts for new leads — visible in Edge but no notification.

Root cause (confirmed vs prod): the only "new lead" alert (new-communication-from-profile) fires only when an inbound email/SMS is matched to a lead (sender == lead's stored email). Portal leads (Zillow/Realtor/etc.) arrive from masked relay addresses that never match, so brand-new leads were never alerted. Prod (1 company, 14d): 665 leads (465 Zillow), 11,678 inbound emails, only 78 matched. Roberto 0/51 alerted, Jennifer 1/63. Not a regression — structural.

Fix

Notify the assigned leasing agent directly on tenant lead creation/assignment, decoupled from email matching. Additive — existing reply alert kept.

New new-tenant-lead-assigned template (email/sms/in-app).
New leadAssignmentNotificationService.notifyTenantLeadAssigned helper (fire-and-forget, respects UserNotificationPreference).
Creation: fired from afterCreate via transaction.afterCommit — covers every creation path (manual, portal/webhook, bulk).
Reassignment: updateTenantLead + batchReassignTenantLeads alert the new agent post-commit.

Owner leads: unchanged. They already have a single robust assignment notification (owner-lead-agent-assigned from updateOwnerLead, with proper role labels). No duplicate mechanism added.
v2026.06.04

Thursday, June 4, 2026

1 new 3 fixes 6 total
Fix App

Close EdgePhone when other sliding panels open

#2609
SlideOutPanel (leads detail, course panels, etc.) now closes the EdgePhone on open, guarded so it won't close mid-call
AdminPortalLayout enforces mutual exclusion between the phone, AI chat, and CallQ edge panels — opening any one closes the others
Uses a useRef prev/curr diff instead of three separate single-dep useEffect hooks, which had a closure-stale cascade that caused the chat panel to immediately close after opening when the phone was already open
Fix API

Remove name-based TwiML app fallback

#2326
ensureTwimlApp had a fallback that searched for a TwiML app by friendly name (RentSolution Webphone) when the configured SID failed to update
On a shared Twilio account, this silently overwrote the production app's voice URL with the staging WEBHOOK_BASE every time a staging user hit the token endpoint with an invalid/mismatched SID
Removed the fallback entirely — TWILIO_TWIML_APP_SID is now required; a missing or unreachable SID logs an error and returns null instead of auto-discovering and clobbering whatever app it finds
Fix App

Surface property profile save errors inline

#2607
Problem

In Lease → Rentals → Property Profile (also reachable via Property Marketing), editing any field (e.g. Pet Policy) and hitting Save silently did nothing in staging/prod — the card exited edit mode and the field reverted, with no error shown.

Root cause (verified via real browser E2E)
The backend *does* return the error (e.g. an AppFolio sync rejection surfaces as a 400 with a real message), and the axios interceptor *does* call showErrorToast. But showErrorToast in @utils/toast is gated behind VITE_NODE_ENV === "local", so it's a no-op in every non-local environment. The error was generated and then thrown away before the user could see it — looking like a silent failure.

(Reproduced by forcing the 400 on the unit PUT and observing zero feedback.)

Fix (scoped, inline)

All in PropertyProfileCard.jsx:
On save failure, surface the actual backend message inline in a red banner at the top of the card.
Stay in edit mode so the user keeps their input and can retry.
Guard the form-hydration effect with if (isEditing) return; so an in-flight refetch no longer clobbers unsaved input mid-edit.
Error clears on Edit, Cancel, and a successful Save.
Update App

Prefill process role assignments from linked owner lead

#2605
What
When creating a process from the New Process card, the role selectors (Leasing Agent, Property Manager, Leasing Manager, Assistant PM) now pre-fill from the owner lead linked to the selected profile, instead of starting empty.

How
A new useEffect watches profilePreview.suggestedRoles (added to the GET /api/process/profile-preview response on the backend) and fills the role assignment selectors. It only fills slots the user has not already set, so manual choices are never overridden. No effect in edit mode.

Pairs with backend PR that adds suggestedRoles to the profile-preview response.
Update API

Inherit role assignments when spawning/creating processes from an owner lead

#2323
Problem
When a process was created from an owner lead, the role assignments (Leasing Agent, Property Manager, Leasing Manager, Assistant PM) were not carried over — the new process started with empty roles. This affected both:
1. Automatic spawnscreateProcess automation tasks (e.g. owner-lead onboarding spawning *Property Marketing* / *Property Onboarding*).
2. Manual creation — the New Process card had no way to pull the owner lead's people.

Changes
automationService.executeCreateProcessTask — before creating the child process, map the source process's processRoles onto the target template's role slots by roleId (a shared Role entity reused across templates) and copy the assigned userId. Roles unique to the source are dropped; roles unique to the target stay unassigned. This also avoids createProcess#detectRoleKeys throwing on a roleId the target template doesn't define.

processService.getProfilePreview — new helper getSuggestedRolesFromOwnerLead returns suggestedRoles derived from the most-recent owner lead linked to the selected owner (read from the owner lead's denormalized role columns, not its processRoles JSONB which can be the legacy {}). getProfilePreview now includes suggestedRoles so the frontend New Process card can pre-fill role selectors. linkedProfiles output is unchanged.

Pairs with frontend PR #2605.
New App

Add clear icons to New Process card fields

#2604
What

Adds a clear (✕) icon to the editable fields in the New Process card so a selection can be reset:

Template
Process Owner
Select property / unit / owner (searchable dropdown)
Tenant Lead
Role assignment selects (Property Manager, Leasing Manager, etc.)

Disabled preview fields (Bedrooms, Bathrooms, Address, Phone, Email…) are intentionally left untouched.

How

Reuses the existing showClearIcon prop already implemented on Field/Input and Dropdown — no new components or custom styling. The icon only renders when a value is set and the field isn't disabled; clicking it fires the field's existing onChange with an empty value.
v2026.06.03

Wednesday, June 3, 2026

1 new 8 fixes 9 total
Fix API

Stop auto-assigning lead owner into role_1 (Leasing Manager) slot

#2310
Problem
When creating a new owner lead, the Lead Owner was being auto-assigned into the role_1 process-role slot. On the company template whose role_1 is the Leasing Manager role, this dropped the creating user into the Leasing Manager slot while Leasing Agent / Property Manager stayed empty.

This surfaced in the stage's assign-roles task (and any role_1.* / role-name merge fields), which read process.processRoles directly. (The owner-lead PreQualify section reads the leasingManagerId FK — which stays null — so it correctly showed blank; that mismatch is what made this confusing.)

Root cause (confirmed against prod data)
createOwnerLead injected { key: 'role_1', roleId, userId: leadOwner } into processRoles at creation. Prod leads in early stages (New, Made Contact, Active Nurturing, Sent Agreement) already carried role_1 = Leasing Manager = lead owner — proving it's set at creation, not at the stage move.

Fix
Remove the role_1 auto-assignment. Role people-assignments now start empty and are set deliberately. The Lead Owner is unaffected — it's still persisted via assignedToUserId and process.processOwner, independent of any role slot.

Scope
Only affects new owner leads going forward. No backfill of existing leads (per request).
Templates where role_1 was used as a generic owner slot will now show that role blank by default (intended).
Fix App

Lead pipeline view renders blank on unrecognized view param

#2596
LeadList read viewMode straight from the view URL param. The Rentals page uses a view=Badge value that can persist in the URL when moving between sections (also reachable via bookmarks/back-button). On the Lead Pipeline, Badge matches neither the Pipeline nor List toggle option, so the render fell to the empty-DataList branch (whose query is disabled) — leaving the board blank with no toggle selected.

Fix: validate the param so any unrecogni
Fix API

Truncate overlong email subjects to varchar(255) [NODE-EXPRESS-3A]

#2308
Problem
A spam email arrived via the Gmail webhook with a ~700-char subject. Both EmailThread.subject and EmailMessage.subject are varchar(255), so the insert in EmailThread.findOrCreateByProvider threw:

SequelizeDatabaseError: value too long for type character varying(255)


This freezes the Gmail webhook's historyId in a retry loop — same failure class as the previously fixed NODE-EXPRESS-13/27.

Sentry: NODE-EXPRESS-3A.

Fix
Add a beforeSave hook on both EmailThread and EmailMessage that caps subject at 255 chars on every write path (create, findOrCreate, and the update calls in saveMessages). Both are fixed because the thread insert failed first — fixing only the thread would just move the same error to the subsequent message insert.
In saveMessages, compare the incoming subject against its truncated form so an overlong subject no longer looks 'changed' and thrash a redundant thread update on every sync.
Fix App

SideCards overflow navbar menu hover dropdowns

#2594
Problem
All SideCards (PropertyDetailCard, TaskDetailCard, LeadDetailCard, etc.) render through the shared SlideOutPanelModal. The Modal backdrop is z-50, which sits above the top navbar (header z-40, nav-tabs z-30). The navbar's hover dropdown menus are confined to the navbar's own stacking context, so when a SideCard was open the menu dropdowns rendered behind it — the panel overflowed the menu hover states.

SlideOutPanel already offsets its backdrop to start below the navbar (top: navHeight), signalling the navbar should stay above the panel; only the z-index was missing.

Fix
Drop the SideCard backdrop to !z-[25] — above page content (z-20) and below the navbar (z-30/z-40) — so navbar menu dropdowns render on top. One-line change in the root SlideOutPanel, so every SideCard is covered.
Fix App

Remove double scrollbar on tenant lead Comment box

#2593
Problem
On the tenant lead detail page, the Lead Info → Comment box renders two scrollbars on Windows but only one on macOS (see reported screenshot).

Root cause
The Comment box had two nested scroll containers:
The InnerBox body wrappermin-h-0 overflow-y-auto
The textarea itselfmax-h-16 overflow-y-auto

macOS uses *overlay* scrollbars (hidden until hover, zero layout width), so the redundant pair looks like one. Windows uses *classic* scrollbars that always occupy layout space, so both render side by side.

Fix
Disable the InnerBox body wrapper's scroll for this single instance via bodyClassName="!overflow-y-visible", leaving the textarea as the sole scroll owner. Scoped to this instance only — no other InnerBox usage is affected.
Fix API

Resolve solo apps merged into co-applicant in Findigs [ENG-545]

#2298
Problem (ENG-545)

Individual applications get stuck on "In Progress" forever when the applicant first applies solo and is later merged into a co-applicant's application in Findigs.

Findigs fires its decision webhooks (groups.approved / groups.declined) on the surviving group only, and deletes the orphaned solo group (its live fetch 404s). So our solo Application never receives a terminal event, and refreshApplicationFindigsCore has no removal logic — the row sits at in-progress indefinitely and clutters the In Progress toteboard.

Confirmed against prod: 2 affected apps, each sharing an applicant email with an approved application in a different group; the solo groups 404 in Findigs (deleted on merge).

Fix — cancel the merged-away solo app, recording the group's outcome

When an application reaches a decision (setStatusApproved / setStatusDeclined), we detect orphaned solo siblings and cancel them, recording the surviving group's outcome (Approved/Denied) in the cancel note:

supersedeMergedSiblingApplicationsCore (dependency-injected, unit-testable like refreshApplicationFindigsCore) finds open apps in the same company sharing an applicant email but in a different Findigs group.
It re-confirms the merge live before acting — only proceeds if the shared applicant is gone from the sibling's group, or the group 404s (deleted on merge). Avoids false-positives where someone legitimately holds two open applications.
_applyMergedSiblingOutcome cancels via the existing cancelApplication (history log +…
Fix API

Surface afterCreate skipping process creation (no default template)

#2305
Problem
When a tenant lead is created but its company has no default tenantLead process template, afterCreate silently skips process creation. The lead ends up with processId = null, never resolves to an active stage, and is invisible in the active pipeline / leads list (only shows under "Show Inactives"). This was logged locally only and went unnoticed.

Fix
Add Sentry.captureMessage (level warning) in the no-template branch of TenantLead.afterCreate, with the tenantLeadId + companyId, so process-less leads are caught.

Scope note
This guards the missing-template path. Leads inserted via a bulk path that bypasses the Sequelize hook (raw SQL / bulkCreate) never run afterCreate at all, so they won't be caught here — those must go through createTenantLead / upsertTenantLead (hooks on). Surfacing that class is a separate follow-up (implementing the tenant-lead import branch, currently a TODO stub in leadImportHandler).
New App

Manual application creation from the Applications list

#2592
What
Wires up the (previously non-functional) + button in the Applications list header to create an application manually for an existing tenant lead.

The + IconButton has existed since the page was built but never had an onClick, and createApplicationMutation was never invoked anywhere — so manual application creation was impossible in the UI.

Why
Companies that don't use Findigs (e.g. AppFolio guest-card workflows) had no way to start an application from the UI — the lead "Send App" flow only sends a Findigs link. POST /applications only needs a tenantLeadId (the lead already carries property/unit/company context), so a lead picker is sufficient.

How
ApplicationListNewCard.jsx (new): a CardWindow with a searchable Dropdown (server-side onSearch) to pick a tenant lead, then createApplicationMutation → navigate to the new application.
Eligibility: only active-status tenant leads are selectable (status: 'active').
useTenantLeads.js: extended useTenantLeadSearch to apply defaultFilters to the search branch too (eligibility applies while searching, not just the initial list). No other consumers.
ApplicationList.jsx: import the card, isCreateAppOpen state, wire the existing + onClick, render the card. Existing canCreate() && hasApplicationListFullAccess gating preserved.
Fix API

Default processRoles to [] so leads without assigned roles don't break

#2304
Problem

A manually-added tenant lead on Ellis got stuck on infinite loading. The lead's process had processRoles stored as {} (a JSON object) instead of [] (an array). Backend readers iterate it (for…of / .find / spread), so they threw processRoles is not iterable, 500ing GET /instances/:processId/tasks and /tasks/:id/instruction-details → the detail page's tasks panel span forever.

Root cause

Process.processRoles is documented and consumed as an array, but the column defaultValue was {}. The tenant-lead afterCreate hook builds the roles array from the lead's assigned Leasing Agent/Manager/PM, then did:

processRoles: processRoles.length > 0 ? processRoles : undefined


When a lead has no assigned roles, it passed undefined, so Sequelize wrote the {} default. Companies that assign agents on their listings always produced a non-empty array and never hit this; companies/properties with no roles assigned (e.g. Ellis manual leads) did.

Fix

models/processModel.js — column default {}[] (the real fix).
models/leasing/tenantLeadModel.js & services/leasing/tenantLeadService.js — always pass the roles array (drop the : undefined) so the create/update→create paths never fall back to the column default.
migration — normalize existing non-array processRoles to [] (jsonb_typeof <> 'array'), idempotent, lossless …
v2026.06.02

Tuesday, June 2, 2026

3 new 2 fixes 5 total
New App

Webphone panel and fixes

#2590
Adds the EdgePhone webphone panel with dialpad, contacts, recents, voicemail, and favorites
Fixes merge call ownership, caller ID display, conference label, and voicemail paste
Fixes handshake modal to open for personal-phone-only users and passes phoneKey correctly
Replaces composite icon with dedicated EdgePhoneIcon SVG
New API

Webphone panel and fixes

#2303
Adds backend webphone support including Twilio device integration and pending transfers service
Fixes heldCallLogId threading through hold→merge chain for correct ownership resolution
Fixes phone2 selection, callLogId creation, and pre-write race condition on handshake
Fix App

Show "updated" counts so a re-import isn't read as a no-op

#2585
Problem
After the importer started updating existing records (BE PRs #2299/#2300), the result screen still only rendered created/skipped/failed. A re-import of an existing portfolio is update-heavy — almost nothing is "created" — so the screen showed "0 created" + red bars even when thousands of rows updated successfully, making a healthy sync look like a total failure. (This is exactly what made a real Ellis import *look* failed when only 5 of ~3,720 rows actually failed.)

Change
Surface updated everywhere the result is shown:
Per-entity tiles: now " synced" = created + updated (both live during processing and on completion) — never misleadingly 0.
Per-entity bar chart: new Updated series (rsos-blue #0090CD).
Overall-outcome donut: new Updated slice.
Recent-imports "Result" column: " created · updated".
Additive-import description: corrected — existing records are *updated to match the upload* (matched by AppFolio id), not skipped.

The undo confirmation still uses created only (undo removes only created records — unchanged).
Fix API

Dedup existing records by appfolioId, not name alone

#2300
Problem
A real Ellis import (#2299 already merged) failed one property with:
duplicate key value violates unique constraint "properties_appfolio_uuid_company_unique"
Key ("appfolioId","companyId")=(24897e1a-be39-11ef-..., Ellis) already exists.

The property already existed in Edge under a slightly different name string, so the importer's name-only dedup missed it, tried to CREATE it, and hit the (appfolioId, companyId) unique constraint — failing that row and cascading "No property" to its unit + 2 tenants. The lone Peralta De Leon tenant failure was the same bug on tenants (dup Tenant Integration ID).

Net effect on screen: import showed red "failed" bars even though ~3,715 records updated fine — only these 5 rows died.

Fix
Match existing owners / properties / units / tenants by their AppFolio integration id as a fallback to the name/email/key match. A record whose stored name drifted from the export now routes to the update path instead of a doomed create. Plus:
When a property/unit is matched by appfolioId (name differs from the CSV), register the CSV name in the in-run lookup maps so child rows referencing it by the new name still resolve.
Integration ids are registered after each create so a duplicate id within one file can't create twice.

Vendors carry no integration id, so they're unchanged.
New API

Update existing records on re-import, not just create

#2299
Problem
The Directory Import was create-only. Existing owners/properties/units/tenants/vendors were dedup-matched and skipped with zero writes, so re-uploading a fresh AppFolio export couldn't fix drifted data.

Concretely (found while diagnosing Ellis HomeSource showing 5 Rentals listings vs 7 on their AppFolio website): the company has no AppFolio Report API credentials, so the nightly unit-report sync fails and units.postedToInternet is frozen. 4 currently-posted units sit at postedToInternet=false with inactive leasing properties → off the Rentals list. Re-running the import did nothing because those units already existed and were skipped.

Change
Existing records are now reconciled from the CSV instead of skipped:

Units — update all scalar fields + postedToInternet/postedToWebsite, and route the unit into the activate list (Posted=Yes) or a new deactivate list (Posted=No), bringing the leasing property's active/inactive status back in line with AppFolio. A posted existing unit with no leasing property gets one created.
Properties / owners / tenants / vendors — scalar field reconcile via direct Model.update (no per-row service hooks, AppFolio push, or workflow events — same reason the importer already bypasses the create services under bulk load). Contact child rows (Email/PhoneNumber) are left untouched to avoid duplicates.
Activation/deactivation only run when the unit CSV actually carries the Posted To Internet column, so a stripped export never wipes listings.
Stats gain per-type updated counters and le…
v2026.06.01

Monday, June 1, 2026

2 new 3 fixes 5 total
New App

Clearable date/time inline pickers in Task List

#2577
What

Adds the ability to clear an inline Due Date / Time cell in the Task List (List view) back to empty. Previously you could only overwrite a date/time with another — there was no way to unset it.

How

Clearing is now a first-class capability of the single component that owns all date/time picker behaviour — DateTimeInput — via a new clearable prop that surfaces MUI's native actionBar Clear button. No custom ✕ icon, no onMouseDown hacks, no changes to generic Wrapper/Input/Field.

DateTimeInput.jsx (gated behind clearable, default false):
Parse stored values tolerant of seconds ("HH:mm:ss"), so real task times aren't rejected as Invalid Date.
Fall back to the current value (pendingValue ?? validDayjsValue) so the native Clear produces a genuine value → null change (it was a no-op before because the picker's value started null).
Treat a null change as an immediate accepted clear (the actionBar Clear fires onChange(null) but not onAccept; normal clock/calendar interaction never yields null).

DataList.jsx (EditableCell):
Pass clearable for date/time cells and commit on change (pickers fire onChange only on accept), collapsing the two near-duplicate edit branches into one Field.

All other date/time fields (create modal, filter cards) are untouched — clearable defaults false, so the actionBar/value-fallback never apply; only the strictly-more-permissive parse-tolerance is shared.
Fix API

Drop users_email_key via DROP CONSTRAINT, not DROP INDEX

#2289
Problem
The migration merged in #2288 (20260601120000-drop-duplicate-users-email-indexes) failed on prod:

ERROR: cannot drop index users_email_key because constraint users_email_key on table users requires it


The base users_email_key is backed by a unique constraint (from the original CREATE TABLE), not a plain index — so DROP INDEX can't remove it. It failed on the first object, so nothing was dropped (the migration is unrecorded and re-runnable).

Fix
Detect which matching names are constraint-backed via pg_constraint and use the right statement for each:
constraint-backed (users_email_key) → ALTER TABLE users DROP CONSTRAINT IF EXISTS (drops its backing index too)
plain indexes (users_email_key1…N) → DROP INDEX IF EXISTS

Constraints dropped first; IF EXISTS on both so a re-run after the partial failure is safe. users_email_company_unique is retained.

After merge — re-run on prod
NODE_ENV=production npm run migration:apply

Expect ~1025 matching objects (1 constraint-backed, 1024 plain indexes) then done. Verify with scripts/diagnose-duplicate-unique-indexes.js → should report zero remaining.
New App

Edit/delete actions on list, optional address fields

#2576
What & why

Photographer list: edit & delete actions
Added an ActionMenu (edit / delete) to each photographer row in PhotographerListCard, gated on photographyManage full access (onEdit != null).
Wired delete through the useDeletePhotographerUser mutation with a confirmation DeleteModal; clears the selection if the deleted photographer was selected.
Photographers now passes onEdit (open edit card) down to the list, alongside the existing onAdd.

Address fields optional
addressLine1 / city / state / zip are now optional in the edit form's zod schema (zip still validates 5 digits *when provided*), and the required markers were removed from those fields.
Matches the backend validator change making photographer address fields optional.

Paired backend
rentsolution-backend#2288 (optional address validators + users.email index cleanup)
Fix API

Make address optional, dedupe users.email indexes

#2288
What & why

1. Photographer address fields optional
addressZip / addressCity / addressState / addressLine1 were required() on the create schema (and effectively required via the FE). They're now optional() (allow null/empty) on both createPhotographerUserSchema and updatePhotographerUserSchema, matching the FE form changes.

2. Remove column-level unique: true on User.email
This was the root cause of a prod issue. A *column-level* unique: true is an unnamed constraint that sequelize.sync() cannot match against the existing index, so it re-issued CREATE UNIQUE INDEX on every process boot during the pre-ENABLE_DB_SYNC-guard era. Postgres auto-suffixed the names, accumulating ~1024 duplicate users_email_key global indexes on prod (one per boot).

Per-company email uniqueness is already enforced by the named users_email_company_unique composite index (which sync() matches by name and never duplicates), so the column-level unique was both redundant and harmful.

> ⚠️ Reviewer note: dropping the column-level unique means email is now unique only per-company at the DB level (the composite index has WHERE companyId IS NOT NULL). Rows with a NULL companyId (e.g. super users) rely on app-level checks. This matches the existing composite-index intent.

3. Migration: drop the duplicate indexes
20260601120000-drop-duplicate-users-email-indexes.js dynamically finds and drops every ^users_email_key[0-9]*$ index, retaining users_email_company_unique. Verified via a prod sweep that…
Fix API

Skip not-found unit webhooks instead of throwing

#2286
Fixes the two highest-volume real-production worker errors:
NODE-EXPRESS-1JAppFolio API error (undefined): Unit not found in AppFolio (148 events, ongoing, server_name=worker)
NODE-EXPRESS-23Unit data mapping failed - property not found (10 events)
v2026.05.29

Friday, May 29, 2026

3 fixes 6 total
Fix API

Make leasingPropertyId nullable on photographyAssignments + inspections

#2281
Problem

Manual completion (and the automatic pipeline) of a photography/inspection task silently fails to create its scheduling record for processes that have a property profile but no published leasing listing (e.g. Property Marketing). Prod app log:

null value in column "leasingPropertyId" of relation "photographyAssignments" violates not-null constraint
at executePhotographyTask → executeTask → runManualTaskAutomationOnCompletion → markTaskCompletionByCriteria


Root cause

resolvePropertyFromProcess intentionally returns either leasingPropertyId or propertyProfileId. The model, and the existing CHECK constraint chk_photography_assignments_property_ref (leasingPropertyId IS NOT NULL OR propertyProfileId IS NOT NULL), both expect leasingPropertyId to be nullable.

Migration 20260323190907 was supposed to make it nullable, but its changeColumn(..., { allowNull: true, references: {...} }) step silently failed to DROP NOT NULL (Sequelize Postgres quirk when a references clause is included). The column stayed NOT NULL on already-migrated DBs. inspections.leasingPropertyId had the identical stale constraint.

Because the quirk is deterministic, fresh DBs reproduce it — hence this corrective migration (not just a one-off prod fix).

Change

Idempotent migration that explicitly drops NOT NULL on leasingPropertyId for both photographyAssignments and inspections (only when currently NOT NULL). DROP NOT NULL is instant (no table rewrite/scan); integrity remains enforced by the CHECK constraints. down restores NOT NULL only if …
Fix API

Prefer active leasing property when findigsId is duplicated

#2279
Backend for the auto-archive-on-qualifying-failure feature. When a tenant lead fails a qualifying answer, it is either auto-archived (unqualified → "Application Denied") or routed to a new non-terminal "Needs Approval" stage for manual leasing-agent review, based on a resolved preference.

Pairs with frontend PR rentsolution-frontend#2561.
Fix API

Create scheduling record on manual photography/inspection completion

#2280
Problem

Mike (Loom 2026-05-30): completing the "Schedule Marketing photo assignment" task in the *Property Marketing* process does nothing — no photographer-scheduling record is created. The task goes green but no PhotographyAssignment ever appears.

Root cause

photography (and inspection) tasks create their scheduling record (PhotographyAssignment, with category = the "type" set on the template) only when AutomationService.executePhotographyTask / executeInspectionTask runs. That executor is invoked only by the automatic task pipeline. A manual photography/inspection task has no execution trigger — taskService completion only special-cases actionType === 'call', and the FE just calls the mark-done mutation. So marking a manual photography task done flips isDone=true and the executor never runs.

Confirmed on prod: 2 photography task instances, both isDone=true, valid config (category: interior_exterior_tour), resolvable property — and zero PhotographyAssignment records, no execution trail. photography/inspection are the only executable task types with no manual trigger path (appfolio/createProcess run automatically; email/SMS get a modal; stageChange has a Run button).

Fix

Add TaskService.runManualTaskAutomationOnCompletion(task), called from both completion paths (markTaskCompletion and markTaskCompletionByCriteria) when a task is completed. It runs the matching executor so the record gets created on completion.

Gated on processConfig.manualTask === true (set at generation: manualTask: template.isAutomatic …
Update App

Pass templateSnapshot + processRoles to data-fields save from owner/tenant lead detail

#2572
What & why

Follow-up to PR #2570 — Mike reproduced the original "role assignment not saving" symptom on prod after #2570 deployed: https://www.loom.com/share/29c15178346c40b1a1d9ff86d0419ec1

Real bug I missed in the original PR

I confirmed the deployed bundle at app.rentsolutionsemail.com/assets/ProcessDetail.COJC9qFy.js contains the post-refactor handleDataFieldsSave from commit 1312e883:

Rs.mutate({
processId:n,
taskId:b?.id,
fields:e,
templateSnapshot:i?.templateSnapshot,
processRoles:i?.processRoles
}, {onSuccess:()=>x(null)})


So the fix IS live for the /admin-portal/process/ page. But Mike was on /admin-portal/grow/owner-lead/ — the OwnerLead detail page — not Process Detail. That page uses the same useUpdateProcessDataFields hook (the refactor target of #2570) but the callsite was never updated to pass the new params.

A sweep showed three callsites total of the refactored hook:
src/pages/AdminPortal/Process/ProcessDetail/ProcessDetail.jsx — fixed in PR #2570
src/pages/AdminPortal/Grow/OwnerLead/OwnerLeadDetail/OwnerLeadDetail.jsx:188broken (this PR)
src/pages/AdminPortal/Lease/Lead/LeadDetail/LeadDetail.jsx:870broken (this PR)

Without templateSnapshot + processRoles on the mutate input, the hook's resolveRoleAssignments produces an empty roleBySlotKey + roleByName map, every role-type field falls through every lookup, the partition returns {} (empty payload), the mutationFn returns Promise.resolve({success: t…
Update App

Inline error state for Send Email modal template load

#2571
What & why

Per Mike's Loom (0:11–0:23 and 6:24), clicking an email task opens the Send Email modal but the Subject + Message Content sit on a gray loading shimmer forever when the message-preview endpoint returns 5xx — the user has no way to recover other than refreshing the whole page.

Root cause
[SendEmailNewCard.jsx:376](src/pages/AdminPortal/_components/PopCards/SendEmailNewCard.jsx#L376)'s useQuery only destructured isLoading. When the query goes to error state (after the global retry: 1 exhausts), the shimmer condition isTaskPreviewLoading || (!!task?.id && !isFormReadyForEditor) keeps evaluating true via the !isFormReadyForEditor side of the OR — isFormReadyForEditor only flips when the preview's useEffect runs, which never happens on error — so the editor never replaces the placeholder.

Change

Single-file fix:
Destructure isError + refetch from the same useQuery.
Render a 3-state branch in the Message Content area: error (inline red banner with Retry button) → loading (existing shimmer) → editor.
Wrapper-based banner using existing Button + Wrapper with paddingHorizontal/paddingVertical + gap props (no raw
/
Update App

Route Instruction-card role data fields to processRoles

#2570
What & why

Per Mike's Loom (5:18–5:33), opening a task's Instruction modal in the Process detail page, picking a member for one of the right-column role-type Data Fields (Leasing Agent / Property Manager / Leasing Manager / Owner) and clicking Save silently did nothing. Mike's workaround was to switch to the Owner-Lead Pre-Qualify panel and re-assign the same roles there, which persists.

Root cause
ProcessDetail.jsx updateDataFieldsMutation sent every changed field as processFields[name] = value via PATCH /api/process/:id. Role assignments live on processRoles[] (the JSONB shape that the working ProcessDetailDueDateCard writes to), not processFields, so role values landed in the wrong column and were never read back.

Change

Single-file fix in ProcessDetail.jsx:

Partition the saved fields by type.
Role fields are resolved to roleId by matching the merge-field key (e.g. leasing_manager.fullNameroleByKey('leasing_manager')) or, as a fallback, the field label against processData.processRoles. Then sent as processRoles: [{ roleId, userId }] — the exact shape ProcessDetailDueDateCard.jsx:168-174 already uses successfully.
Non-role fields continue to flow through processFields unchanged.
Invalidate the process query on settle so Pre-Qualify and Due Date refresh with the new assignments without a manual page refresh.
If nothing maps, return a resolved Promise instead of a no-op PATCH.

No changes to InstructionCard.jsx, the AP…
v2026.05.28

Thursday, May 28, 2026

2 fixes 3 total
Fix App

Apply mergeFieldLabels to pre-wrapped spans in read-only previews

#2569
Bug

Follow-up to [#2568](https://github.com/rentsolutions-app/rentsolution-frontend/pull/2568) — that PR registered mergeFieldLabels for the execution Instruction modal, but the body still showed raw {{role_2.fullName}} tokens on prod ([screenshot](https://app.rentsolutionsemail.com/admin-portal/grow/owner-lead/ade9ed71-f853-4480-9da9-86166c576697)). Live console fetch on prod confirmed the BE returns the instruction HTML with merge fields already wrapped in ql-merge-field spans (because the template editor saves them that way):

<p>Assigned Leasing Agent : <span class="ql-merge-field" data-value="role_2.fullName" contenteditable="false">{{role_2.fullName}}</span></p>


QuillEditor.processValueForEditor stashes pre-wrapped spans verbatim — it only applied mergeFieldLabels to *bare* {{...}} tokens. In edit mode the MergeFieldBlot rewrites the inner text when Quill mounts the editor, but read-only previews (dangerouslySetInnerHTML) bypass blot creation, so the raw token leaks into the UI.

Fix

After the existing unwrap step, sweep any remaining pre-wrapped ql-merge-field spans whose inner text is still the raw {{key}} token and rewrite the inner to mergeFieldLabels[key] when registered. No-op when no label is registered — preserves current behavior.

Why this doesn't break anything

The change runs after unwrapResolvedMergeFieldSpansForQuill, so resolved values (e.g. Joseph) are unwrapped to plain text first and never enter my regex. Verified against 7 cases:

| Case | Before | After |
|---|---|---|
| A. Pre-wrapped raw, label registered (the bug) |…
Fix App

Render friendly role merge-field badges in execution modal

#2568
Bug

In the Instruction modal on owner-lead / process detail pages, role merge tokens in the instruction body rendered as raw {{role_2.fullName}} placeholders for unassigned roles (per [saket's Loom](https://www.loom.com/share/e529fb9e9cc34f31ba9b19b809060916)), instead of the friendly blue badges (Leasing Agent - Full Name) shown in the template editor.

Fix

InstructionCard in execution mode (showDataFields) doesn't fetch template injectable fields (read-only, no templateId in route params), so mergeFieldLabels was empty and the QuillEditor blot fell back to rendering the raw {{key}} token.

This PR builds the merge-field label map directly from the resolved dataFields the BE already returns ({ field, key, type }), and registers it via setMergeFieldLabels synchronously in a useMemo — so labels are populated *before* the read-only QuillEditor processes its value on the same render.

For role fields, the badge label is ${field.field} - ${camelCaseToTitleCase(subfield)} — e.g. "Leasing Agent - Full Name" — matching the template editor exactly.

New util camelCaseToTitleCase in mergeFieldUtils.js.

Files
src/pages/AdminPortal/_components/PopCards/InstructionCard.jsx (+25)
src/utils/mergeFieldUtils.js (+14)

Benefits all callers of InstructionCard with showDataFields={true}OwnerLeadDetail, tenant LeadDetail, ProcessDetail.

Verification

Reproduced the prod bug condition in local DB (4-slot template, instance has only role_1 assigned, task …
Update App

Revert #2560: restore agent skipConflictCheck on showings

#2567
Reverts #2560. Restores skipConflictCheck: true on authenticated-agent create-showing ([useScheduleForm.js](src/hooks/leasing/useScheduleForm.js)) and on all reschedules ([useShowings.js](src/hooks/leasing/useShowings.js)).
Reported by Joe on prod: Edge "Schedule Appointment" returns "Time slot conflicts with an existing calendar event" and agents have no way through. Pre-#2560, the FE silently passed skipConflictCheck:true so agents could double-book.
Per product: agents should be able to override conflicts when booking manually.
v2026.05.27

Wednesday, May 27, 2026

2 new 2 total
New App

Support merge fields in task template names

#2565
What
Adds a merge-field picker to the Task Name field in the process task template editor, so admins can insert fields like {{owner.name}} / {{unit.fullAddress}} into a task's name — the same authoring UX already available for instruction cards.

Why
Enables dynamic, context-aware task names. The backend PR resolves these tokens at view time, so names render with real values in the process instance and owner lead task records.

How
Reuses useTemplateInjectableFields + mergeFieldUtils (formatMergeFieldToken, insertTextAtCursor, focusInputAtPosition), matching the existing EmailTextTemplateNewCard pattern.
Picker lives in the modal header (rightContent); selecting a field inserts {{token}} at the cursor in the Task Name input.
templateId threaded through from TaskTemplateManagement.
New App

Add Leasing Manager to owner lead pre-qualify; fix property panel scroll

#2564
Adds Leasing Manager UserSelectionField below Leasing Agent in PreQualify form
Wires leasingManagerAssigneeId (form state) → leasingManagerId (API payload) through handleSave, growConstants, and optimistic update in useOwnerLeads
Fixes hori
v2026.05.26

Tuesday, May 26, 2026

1 new 1 total
New App

Auto-archive leads setting + settings alignment

#2563
Wires up the Auto-archive leads on qualifying failure setting and fixes the alignment of the two settings sections that expose it.

Changes
User Detail – Leasing Settings: Added autoArchiveLeads form field, hydrated from user.settingsJson?.autoArchiveLeadsOnReject, and mapped on save to settingsJson: { autoArchiveLeadsOnReject }.
Leasing Settings alignment: Both rows ("Updates Due On", "Auto-archive leads that fail qualifying") now share a 16rem label column, so labels right-align to the same edge and the dropdown + toggle line up.
Qualifying section alignment: The "Auto-archive failing leads" row used a fixed 12rem right-aligned label column, leaving a leading gap that made the label look indented. Set labelWidth="auto" so it sits flush-left with the Default Questions labels.

#
v2026.05.25

Monday, May 25, 2026

1 new 1 fix 2 total
New App

Add merge field csv import page

#2483
new admin page at /admin-portal/settings/process/merge-field/import
accepts the LeadSimple → Edge matches csv (cols: ls_field, edge_field, action, [label, type, profileType, confidence]), parses client-side, shows every row in a DataList with inline-editable Edge field, action, and label
two-step flow: Analyse → calls BE /api/imports/merge-fields/dry-run and shows per-row status (use existing, will create, already exists, skip, invalid); Commit → calls /commit and reports counts
target company picker so super-admins can run it for any company
"Import from CSV" button added to the existing Merge Field Settings header for discoverability
Fix App

Enforce conflict check on agent bookings and reschedules

#2560
Showings were not blocking already-taken times (reported on prod by Joe; Steve asked what changed). Root cause: the frontend deliberately disabled the backend conflict check in two paths by sending skipConflictCheck: true:

useScheduleForm.js — agent-initiated bookings (isAgentBooking)
useShowings.jsall reschedules

This PR removes both flags so every booking path runs checkCalendarConflicts. The backend reschedule path already excludes the current showing from its own conflict scan (showingService.js otherConflicts), so the flag was unnecessary there and only served to allow collisions with *other* showings.

Also surfaces the real backend error message in the schedule modal's inline error (was a generic "An error occurred while booking the showing"), so a blocked booking shows the actual "Time slot conflicts with an existing showing" reason.
v2026.03.31

Tuesday, March 31, 2026

1 fix 1 total
Fix App

Update next call rendering logic in OwnerLeadList component

#2255
Modified the rendering logic for the next call date in the OwnerLeadList component to utili