Sync from AppFolio — the three surfaces

Three detail pages carry a Sync from AppFolio action, and no others: work order, tenant, vendor. This page has been re-shot from scratch against the app as it stands after three merges — #5702 (daily work-order enrichment sweep), #5703 (4×-daily vendor contact sweep) and #5720 (the vendor page's header button moved into the kebab). Every picture below is a fresh capture from a signed-in app, with the menu actually open. The first version of this page argued the three surfaces were inconsistent and two of them had nobody driving them. Both halves of that are now fixed — and one of the two sweeps has visibly run in these pictures while the other has not yet had its first tick.

What the new pictures show

The three, side by side

PageWhat you clickWhat it callsWhat drives it automaticallyWhat the picture below reads
Work order
/maintenance/[displayId]
Kebab (⋯) → Sync from AppFolio POST /api/work-orders/[displayId]/enrich Scheduled, daily
cron(0 9 * * ? *) — stalest-first, budget-bounded (#5702). The work-order rows already refreshed every minute; this covers the extra detail the button pulls (vendor on file, access notes, urgency).
First tick pending
Synced 2 months ago. Across the whole table, 0 work orders carry an enrichment stamped in the last 24 hours. Honest reading: the schedule exists, the sweep has not run.
Tenant
/tenants/[id]
Kebab (⋯) → Sync from AppFolio POST /api/tenants/[id]/sync Running, daily
11:00 UTC, same sync for every active AppFolio-linked tenant, stalest first. Predates all three of today's PRs — this is the design the other two were made to match.
Observed
Synced about 15 hours ago. Of the first 25 linked tenants, 25 of 25 carry a snapshot, every one stamped inside the same three minutes at 11:00–11:02 UTC.
Vendor
/vendors/[id]
Kebab (⋯) → Sync from AppFolio
(was a header button until #5720)
POST /api/vendors/[id]/sync Running, 4×/day
cron(30 1,7,13,19 * * ? *) — stalest-first, paced against AppFolio's rate ceiling, never outranks a human-initiated sync (#5703). The hourly roster job still handles who exists; this handles what they are.
Observed
Synced about 12 hours ago. Of the first 25 linked vendors, 24 of 25 now carry a snapshot from the last 12 hours. In the first edition of this page that number was 1 of 25.

What changed since the first edition. That page argued two things: the control was inconsistent (a kebab on two pages, a bare button on the third), and for work orders and vendors somebody had to click or nothing refreshed at all. #5720 settled the first. #5703 settled the second for vendors, and the pictures prove it. #5702 settles it for work orders on a schedule that has not yet fired — which is why one cell in the table above is amber and not green.

1 · Work order

/maintenance/CAM-594

The header collapses every action into one kebab. Sync from AppFolio sits in its own group under Mark Complete. This is the same work order the first edition photographed, deliberately: it is the control case for whether anything has changed underneath.

Work order CAM-594, Mold/Mildew, with the kebab menu open showing Mark Complete and Sync from AppFolio, and a caption reading Synced 2 months ago
Shown at 1200px of a 1440px capture — scroll inside the frame for the rest.
Real page, real signed-in session, menu open. Calls POST /api/work-orders/[displayId]/enrich. The caption beside the kebab reads Synced 2 months ago on a work order still marked Dispatched · In progress — the record's enrichment is stamped 10 June. This is the state #5702's daily 09:00 UTC sweep exists to retire, photographed before its first run rather than after. Do not read this picture as the sweep failing; read it as the sweep not having happened yet.

The number behind the caption. Of 571 work orders in the table, 26 carry an AppFolio enrichment at all, and the freshest of those is 26 hours old — so zero were enriched in the last 24 hours. That is what "first tick pending" means, counted rather than assumed. The next honest version of this page is one taken after 09:00 UTC.

2 · Tenant

/tenants/[id]

Same control, same wording, one item in the menu. The caption reads Synced about 15 hours ago — not because anyone clicked, but because the 11:00 UTC sweep had been through.

Tenant detail page for Padma Whittaker with the kebab menu open showing Sync from AppFolio, and a caption reading Synced about 15 hours ago
Shown at 1200px of a 1440px capture — scroll inside the frame for the rest.
Real page, real signed-in session, menu open. Calls POST /api/tenants/[id]/sync. The AppFolio Profile card below the fold is the payload that sync maintains — financials, lease and occupancy, recurring charges, pets, vehicles, insurance. Names, email and phone are the anonymised stand-ins the stage table uses: the @example.test address and the (000) area code can never reach a person.

Not one lucky example. Of the first 25 AppFolio-linked tenants, 25 have a synced profile on file, and every stamp falls between 11:00:48 and 11:02:23 UTC — a single sweep walking the list, not 25 people clicking.

3 · Vendor — the page that changed

/vendors/[id]

This is the picture worth looking at. The labelled Sync from AppFolio button that used to sit in this header is gone; there is a kebab there now, and it is open in the shot. Beside it, Synced about 12 hours ago. The AppFolio Profile card below — which read Not synced yet in the first edition of this page, on this same vendor — is filled in.

Vendor detail page for Western States Fire Protection Co. with the new kebab menu open showing Sync from AppFolio, a caption reading Synced about 12 hours ago, and a populated AppFolio Profile card
Shown at 1200px of a 1440px capture — scroll inside the frame for the rest.
Real page, real signed-in session, menu open. Calls POST /api/vendors/[id]/sync. Two merges are visible in this one frame: #5720 in the kebab that did not exist here this morning, and #5703 in both the 12-hour caption and the populated AppFolio Profile card. That timestamp is the sweep's own first run at 13:30 UTC — nobody clicked anything to produce it.

The before-number, and the after-number. The first edition of this page counted the first 25 AppFolio-linked vendors and found 24 had never been synced at all, the one exception stamped 3 June by a human. The same count today: 24 of 25 carry a snapshot from the last 12 hours. The one that does not is a stage-only fixture with no real AppFolio counterpart to pull.

4 · A vendor that isn't matched

/vendors/[id]

The old header button had a disabled state and a tooltip that said "Match this vendor to AppFolio before syncing." A kebab has no room for that, so #5720 traded it for something you can read without hovering: no menu at all, and the words Not linked to AppFolio where the timestamp would be.

Vendor detail page for Rocky Mountain Locksmith with no kebab menu in the header and a caption reading Not linked to AppFolio
Shown at 1200px of a 1440px capture — scroll inside the frame for the rest.
Real page, real signed-in session, nothing clicked — there is nothing on this page to click. Compare the header with the picture above it: same layout, same position, kebab absent. The AppFolio Profile card also stops naming a control that would not be there: it now reads "This vendor isn't matched to an AppFolio record yet, so there's nothing to pull," where before it said "Click Sync from AppFolio."

Why no empty menu. A kebab that opens onto nothing is worse than no kebab, and the route would refuse the call anyway. 21 of the 839 vendors in the table are in this state — mostly in-house crews and locally-created vendors that have no AppFolio record to be matched to.

Where these pictures came from

Captured with the repo's own capture and safety modules (scripts/ui-capture/* — the same ones scripts/ui-evidence.ts uses), which refuse to run against a data source that is not anonymised and scan every rendered page for reachable contacts before anything is published.

App code
origin/main including #5720 (2f67aa5bd0), #5702 (1fe4170c39) and #5703 (f963b1ebcc). Photographed from a worktree whose src/ tree is byte-identical to origin/main — verified with git diff origin/main -- src, which was empty.
Data source
propflow-stage — the continuously-mirrored, anonymised copy of production, read back from the running server's own /api/health rather than from a label. Production itself was never written to and never screenshotted.
Captured
2026-08-13, 01:39–01:41 UTC. All four shots and all three negative controls come from this one run — no picture on this page predates it.
Schedules read back
Both EventBridge rules ENABLED in prod: …-wo-enrichment-schedule · cron(0 9 * * ? *) and …-vendor-contact-schedule · cron(30 1,7,13,19 * * ? *).
Asserted in the pictures
"Sync from AppFolio" on all three menu shots, plus "Mark Complete" and "CAM-594" on the work order and "Not linked to AppFolio" on the unmatched vendor. Every string was required to be present in the DOM after the menu was opened — a menu that silently failed to open would have failed the run rather than shipped a picture that looked fine.
Negative controls
All three pages re-loaded with the kebab CLOSED: "Sync from AppFolio" absent from every one, including the vendor page, which is the #5720 change expressed as a test.
Contact scan
Clean across all 4 pages — 0 reachable phone numbers or email addresses, checked with verifyRecordContacts, the same belt the stage replicator runs before writing a row.

What this page still does not settle. It shows where the control lives, what covers it automatically, and — for two of the three — that the cover has actually run. The work-order sweep is verified scheduled and not yet verified run; its first tick is 09:00 UTC on 13 August, and its run rows will land on the admin jobs surface like every other sync job. Nothing here was clicked: the sync routes write to AppFolio, and no button on a real customer entity was pressed to make these pictures. The counts quoted throughout are from the stage mirror, so they track production's shape rather than being production's own numbers.

PropFlow Docs