Lab Architect / Ellsworth

Afternoon Brief

Monday, September 28, 2026 — Twentieth consecutive night with no nightly-branch build on any app. There is no feature/nightly-20260928 or feature/nightly2-20260928 branch, and git log --all --since="36 hours ago" is empty on all four app repos. Codebase health: 0 analyzer issues on all four apps (git archive main snapshots). Buildr+/Closr+/Inspectr+ still hold 4/6/7 unpushed commits. Today's morning brief raised three ideas, and all three are confirmed in code and unbuilt. The worst is DailyReportr+: when the admin reports load fails, the page retries on every rebuild with no backoff and shows "No Reports Yet" in the meantime (Recommendation #2). Buildr+'s swipe-to-delete removes a project card before the server confirms the delete, and the same unguarded pattern is on all 10 of its swipe-to-delete list pages (Recommendation #1). Correction to the morning brief (fourth time): its "latest commit" for Buildr+ (b983e74) and DailyReportr+ (af49e33, Aug 11) comes from off-main working copies. The real main heads are a7a5a03 and 0433f44.

Last Night’s Builds

No nightly-branch build on any app, twentieth night running missed

Ran git fetch origin on all four repos. No new nightly branches exist; the most recent on any remote are feature/nightly-20260908 and feature/nightly2-20260909. Yesterday's assignments (Dominique → Buildr+ Honest Filtered Project Empty State, Katey → DailyReportr+ Sync Receipts That Tell the Truth) were not picked up. The morning brief again reports Hermes up and the review producer "retired". Neither explains the missing builds. The 00:15 and 01:15 builder launch jobs are the thing to inspect.

Buildr+ 4 commits unpushed · working copy off main

Local main head a7a5a03 (Sept 21) is 4 ahead / 0 behind origin/main (33c825f). The working copy is still on fix/project-card-contract-value-spacing. Unchanged.

Closr+ 6 commits unpushed · tracked node_modules

Local main head 0925b0b (Sept 21) is 6 ahead / 0 behind origin/main (7a3ca21). The deleted-but-tracked node_modules/ tree still fills git status. Unchanged.

Inspectr+ 7 commits unpushed

Local main head 8145093 (Sept 21) is 7 ahead / 0 behind origin/main (4140149, Sept 8). origin/main has been silent for 20 days.

DailyReportr+ silent 14 days · working copy off main

main = origin/main = 0433f44 (Sept 14, 3.8.1 TestFlight prep). The working copy is still parked on the Aug 11 fix/owner-report-media-web branch with modified tracked files.

Codebase Health

App Analyzer Issues Breakdown Last 3 Commits (local main)
Buildr+ 0 dart analyze lib on git archive main: "No issues found!" Unguarded deletes: 10 Dismissible list pages all delete in onDismissed with no try/catch (0 of 10 guarded). The filtered "No projects yet" empty state is still open. a7a5a03 merge harden-ios-release-gate • a2873c3 merge project-card spacing fix • fac04bd source protected secrets file (unpushed)
Closr+ 0 "No issues found!" Still open: silent catch (_) in the portfolio subcontractor provider, tracked node_modules/, and the in-app Procore secret. 0925b0b merge harden-ios-release-gate • d4c8952 gate TestFlight IPA on client IDs • ca33394 bump TestFlight 2.32.75 (unpushed)
Inspectr+ 0 "No issues found!" Raw Colors.green: 29 across 13 files (no commits, so unchanged). 8145093 merge Google sign-in client-ID fix • 5e0849b merge harden-ios-release-gate • 744cc12 add iOS artifact gate (unpushed)
DailyReportr+ 0 "No issues found!" Retry loop: reports_page.dart re-schedules _loadAdminReports() from build() whenever _adminReports == null, and a failed load leaves it null. Raw Colors.green: 40. Still no receipt tests. 0433f44 prepare 3.8.1 TestFlight • 2495d04 merge keyboard-recovery fix • 7253e65 stop keyboard recovery stranding text field (pushed)

Analyzer counts: Buildr+ 0 · Closr+ 0 · Inspectr+ 0 · DailyReportr+ 0. The analyzer is clean, but it can't see the failure paths behind today's findings: an unawaited-for-outcome destructive action and a load failure that never becomes UI state. Both fixes below add tests.

Recurring Themes from Morning Briefs

Sources: the Sept 28, 27, and 26 morning briefs (briefs/).

Feature Ideas + UI/Code Health Recommendations

There are 3 new recommendations: 2 UI/code health items and 1 feature. All three came from today's morning brief and were verified against main snapshots with grep and git log --all. The only related history is Buildr+ swipe-to-delete (never error-handled), DailyReportr+ 0f2e5cc (added delete to admin report cards, not load errors), and Inspectr+ 368d26f "allow multi-photo gallery uploads" (added the sequence, not a position cue). Yesterday's recommendations are still open and valid: DailyReportr+ sync receipts, Closr+ partial portfolio, Buildr+ filtered empty state, and DailyReportr+ green tokens.

1. Deletes That Wait for the Server Buildr+ — UI/Code Health

In lib/pages/projects/projects_list_page.dart:80-86, confirmDismiss only asks "are you sure?". The actual deleteProject call runs in onDismissed, after the card has already been removed from the tree, and has no error handling. If the Cloudflare request fails, the exception is unhandled, projectsProvider is never invalidated, and the project vanishes from the list even though it still exists on the server. deleteProject (cloudflare_project_service.dart:80) also skips the local-DB delete when the API throws, so local and server state stay consistent, and only the UI is wrong. The long-press path (lines 88-96) has the same unguarded call. The same pattern is copied across all 10 swipe-to-delete list pages: meeting minutes, safety plans, safety briefings, lien waivers, change orders, incident logs, RFIs, submittals, transmittals, and projects.

Why now: it's today's morning-brief idea #1, and the grep shows it's systemic, not a one-off (0 of 10 guarded). It's destructive-action UX, the highest-stakes version of the "failure must look like failure" theme. Fieldwire's selective-sync docs (today's morning intel) draw the same line between server deletion and hiding locally.

Sketch: Add lib/widgets/confirm_delete_dismissible.dart, a wrapper that takes onConfirmDelete: Future<void> Function(). Inside confirmDismiss, it shows the dialog, awaits the delete, returns true only on success, and on failure returns false and shows a SnackBar ("Couldn't delete — Retry"). Invalidate the provider only after success. Migrate projects_list_page.dart first, including the long-press path, then the other 9 pages in the same PR if time allows (each is a mechanical swap). While in there, use AppColors.primaryRed for the swipe background instead of raw Colors.red (see UI Inconsistencies). Widget tests: a failed delete keeps the card and shows the SnackBar, a successful delete removes it, and a cancelled dialog does nothing.

2. Admin Reports That Admit Failure DailyReportr+ — UI/Code Health

In lib/pages/reports/reports_page.dart, _loadAdminReports() (lines 36-59) catches errors with only debugPrint and resets _isLoadingAdminReports = false. build() (lines 67-74) schedules a new load whenever isAdmin && _adminReports == null && !_isLoadingAdminReports. After a failure, all three conditions are true again, and the setState in the catch triggers the rebuild. The page ends up re-requesting the admin endpoint back-to-back for as long as it stays open. Offline, each failure is near-instant, so this is close to a tight loop against Cloudflare and the battery. Meanwhile, _buildAdminReportsList (line 349) treats null the same as empty and renders "No Reports Yet — Create your first daily report to get started" (line 1045) to an admin whose company may have hundreds.

Why now: it's today's morning-brief idea #2, and the retry loop, which the brief didn't mention, makes it worse than a copy bug. It's one file and one state field. It also matches Procore Daily Log's pattern (cited in all three briefs) of making server state explicit.

Sketch: Add Object? _adminLoadError. Set it in the catch and clear it on success. Gate the build() auto-load on _adminLoadError == null so failure stops retrying, and keep pull-to-refresh as the explicit retry (it already calls _loadAdminReports). In _buildAdminReportsList, render a "Reports unavailable — Retry" card when there's an error, separate from the true empty state. The overview card should show "—" rather than a count. Move the auto-load into initState/ref.listen rather than build(). Widget tests with a fake AdminService: a failure shows the error card and makes exactly one call, Retry recovers, and a truly empty result still shows "No Reports Yet". Don't touch the regular-user list.

3. “Photo 2 of 5” in Multi-Photo Markup Inspectr+ — Feature

_captureImage in lib/pages/project_detail_page.dart:8215-8231 pushes PhotoMarkupPage once per gallery-selected photo. PhotoMarkupPage (photo_markup_page.dart:10) takes only imageBytes, so an inspector marking up five photos sees five identical screens with no idea how many are left. Cancelling markup on any photo silently keeps the original, which is reasonable but invisible. A small position label and an explicit "Skip markup" action make the sequence predictable without changing the upload contract.

Why now: it's today's morning-brief idea #3. Bluebeam iOS 1.31/1.32 and Procore's Sept 16 status-colored pins across Inspections (see Competitive Intel) both push toward traceable per-item photo review. It's a small, self-contained feature that doesn't add debt.

Sketch: Add optional int? batchIndex, int? batchTotal to PhotoMarkupPage. When both are set, the AppBar title reads "Photo ${i+1} of $total" and the page exposes a Semantics label with the same text. Pass them from the for loop via images.indexed. Keep the single-camera path unchanged (no label). Widget test: pump PhotoMarkupPage(batchIndex: 1, batchTotal: 5) and expect "Photo 2 of 5", and expect no label without a batch.

UI Inconsistencies

Competitive Intel

Tomorrow’s Build Suggestion

This is the twentieth night without builders, so check the 00:15 and 01:15 builder launch jobs directly. Both picks are one-night, test-backed failure-path fixes that start from main. Owner action before either build: push the unpushed main commits (Buildr+ 4, Closr+ 6, Inspectr+ 7). For Closr+, stage explicit paths rather than using git add -A.

Dominique → Buildr+

Task: Deletes That Wait for the Server (Recommendation #1). Build the shared ConfirmDeleteDismissible, migrate projects_list_page.dart plus as many of the other 9 pages as fit, and add widget tests. The filtered empty state moves behind it. Rationale: Buildr+ hasn't had a nightly feature since Aug 5, and a destructive action that can lie outranks a misleading empty state.

Katey → DailyReportr+

Task: Admin Reports That Admit Failure (Recommendation #2), a one-file fix that stops the retry loop and adds the error state and tests. Sync Receipts That Tell the Truth is next. Rationale: this is the stalest app (14 days on main). An unbounded request loop costs server load and battery every time an admin opens Reports offline, so it's the most urgent item in the app.