Mike O'Dell Surveys · Build Recap
Last updated 2026-07-15 · reflects Qfactor-surpass through Phase 7 + grounded AI foundation · regenerated after every shipped change, not a one-time snapshot

What's shipped so far

A recap of the work in the grundysf/mos repo to date — from the first planning commit through the complete Sprint-0 foundation and the post-Sprint-0 domain build: auth/RBAC spine, Jobs/ Dispatch/Documents/Employees/Equipment end-to-end, a real Dashboard, the professional design concepts, and both native mobile plans. Generated from the actual git history; everything below is in the repo.

40
Commits
136
Tracked files
5 / 5
Sprint-0 build days done
Live
on Dokploy · :8000
00

Where we are: Sprint 0 complete (Day 5 of 5)

Sprint 0 (the discovery-independent foundation) is sequenced as five days — all five are now committed. Day 5 shipped with the deliberately minimal placeholder Job schema; the field set widens after the client discovery meeting. Since then, the domain-adjacent entities (Dispatch, Documents, Employees, Equipment) and a real Dashboard have shipped ahead of that meeting too — see §04. Full detail: implementation-plan.html.


DAY 1
Monorepo
4fb1510
DAY 2
Design tokens
f08252b
DAY 3
App shell
7c15af7
DAY 4
Auth + RBAC
44e9dbe
DAY 5
Jobs slice
bb32e7d
Placeholder schema, on purpose. The Jobs slice runs end-to-end (login → typed client → RBAC-guarded API → Postgres) with the minimal core fields (job number, client, site, status, crew, due). Real MOS fields, status-transition rules, and the per-screen RBAC matrix wait for the client discovery meeting (docs/discovery-guide.html) — client back in town next week.
01

Commit timeline

Thirty-three commits across six threads: planning & design, deploy infrastructure, design refinement, the Sprint-0 platform build, the mobile app plans, and the post-Sprint-0 domain build (Dispatch/Documents/Employees/Equipment, a real Dashboard, the Job Detail retheme, and the fixes that followed it).


ef87afeJun 22init
Initial commit — planning & design
Build plan, proposal, Phase-1 sprints, and the first concept storyboard + design system, branded for MOS.
7776421Jun 22infra
FastAPI backend skeleton + Docker/Compose
uv-based image, Postgres compose, Alembic scaffolding, /api/health.
2c39100Jun 22infra
Serve the storyboard statically (no DB)
Repointed the container to serve docs/ so the client can view the storyboard remotely.
88ebf0dJun 22infra
nixpacks.toml — Dokploy builds with uv
Fixed the Nixpacks python -m mos-platform start error; took control of the build with uv against the committed lockfile.
a8ef8fdJun 22infra
Deployment port 8888 → 8000
Aligned Dockerfile, compose, nixpacks, and env for the custom domain on port 8000.
2d93e6aJun 23design
Light-theme storyboard variant
A light/airy alternate concept derived from the dark storyboard, cross-linked via a hero toggle.
cf04efbJun 23design
Switch to flat design
Removed all gradients and blurred drop-shadows across both storyboards and the design system.
8399f00Jun 25design
Client discovery guide
Survey-specific interview guide — per-role question banks, end-to-end job trace, migration inventory, RBAC and MVP worksheets.
8fe6884Jun 25design
Local visual implementation plan + build recap
First versions of implementation-plan.html and this document — built with /visual-plan//visual-recap as local HTML only, no third-party tools, per the no-egress requirement.
63679fdJun 25design
Project hub — docs/index.html
One front door linking every artifact in the repo.
a61a686Jun 25design
HTML discovery guide; fix unreachable pitch link
Ported the discovery guide from markdown to a client-viewable HTML page; fixed pitch.html not being reachable from the hub.
8143118Jun 27design
Claude Design paste-ready prompt
Extracted the design-system tokens into a prompt so design-system.html can be edited directly in Claude Design.
4fb1510Jun 27Day 1
Restructure into apps/api + apps/web monorepo
Split into apps/api (FastAPI, still serving the storyboard) and apps/web (Next.js 16 + React 19 + Tailwind v4). Deploy retargeted to build from the repo root.
a76b54bJun 27infra
Fix Nixpacks build: stop requiring a nix 'uv' package
The pinned nixpkgs had no uv package; after repeated Nixpacks friction, standardized on the Dockerfile build in Dokploy instead.
f08252bJun 27Day 2
Port the approved design system into apps/web (Tailwind v4)
Design-system tokens ported into globals.css as Tailwind v4 CSS-first @theme tokens — flat dark-navy, shadcn/ui-ready (semantic vars, components.json, cn() util). User approved the token showcase.
7c15af7Jun 27Day 3
App shell + reusable primitives
Responsive AppShell (sidebar / mobile bottom-nav, top bar) + StatusPill, RoleBadge, DataState (loading/empty/error/populated) + Skeleton, using lucide-react icons.
4f6cbffJun 29scope
Add native iOS/Android apps as future scope
Declared native mobile companions in the roadmap, ahead of the constitution-governed plans that followed.
44e9dbeJun 29Day 4
Supabase auth + RBAC + first migration
profiles / roles / user_roles via Supabase migrations (RLS on, hardened after an advisor flag); FastAPI verifies Supabase ES256 JWTs via JWKS (no shared secret); require_roles() RBAC; /api/me, /api/me/roles, /api/admin/ping. The account solomonacquah@gmail.com was created and assigned the owner role — verified end to end.
8f152ddJul 01design
Design concepts C + D — professional dark & light
Two new prototyping directions: "Meridian" (graphite + brass, engraved-plate serif) and "Plat & Ink" (paper + ink-navy + flagging-tape orange) — a more industry-professional alternative to the original storyboards.
41acc16Jul 01mobile
iOS app plan — MOS Field
Constitution-governed native iOS plan (Swift/SwiftUI, MVVM, SOLID, zero third-party deps) mapping every platform phase to iOS Phases A–D.
6ae343fJul 01mobile
Android app plan — MOS Field
The Android twin: Kotlin + Jetpack Compose, StateFlow MVVM, Material 3, EncryptedSharedPreferences + Keystore, Room + WorkManager offline-first — same phases, same one authorized auth deviation.
bb32e7dJul 01Day 5
Jobs vertical slice — minimal placeholder schema
jobs table via Supabase migration (RLS on, 4 sample jobs seeded); RBAC-guarded FastAPI CRUD (/api/jobs — reads for any authenticated role, writes for PM/office/owner, deletes owner-only); typed TS client generated from OpenAPI (npm run gen:api); Supabase email/password login in apps/web; live Jobs list + detail wired through TanStack Query + the four-state DataState pattern.
e7d2cfdJul 01fix
Install greenlet — required by SQLAlchemy's async engine
sqlalchemy>=2.0.30 was declared without the [asyncio] extra, so greenlet never installed even though create_async_engine/async_sessionmaker need it — surfaced as a 500 on any DB-touching route (/api/jobs, /api/me/roles) the moment a real login was attempted locally.
e6c71e9Jul 01design
Real MOS logo on login + sidebar
Replaced the placeholder "M" square with the actual logo (apps/web/public/mos-logo.png, converted from the mislabeled-AVIF source) on /login and the app shell sidebar, with a theme-aware --logo-filter token.
135f55cJul 01fix
Sidebar logo always white
The Plat & Ink sidebar is dark ink in both themes, so the sidebar logo needed a fixed invert regardless of the toggle — only the login card (which flips white/graphite) uses the theme-driven filter.
126ab72Jul 01design
Storyboard rethemed to Meridian / Plat & Ink, with a live toggle
All 13 screens + the mobile gallery in docs/storyboard.html converted to the dual-theme system — dual token block (same variable names, new palette) plus ~50 hardcoded inline hex/rgba colors converted to var()-based equivalents so every avatar, status pill, dispatch-board cell, and chart bar re-themes automatically. Squared radii, Iowan Old Style/Palatino serif, always-ink sidebar. Persisted toggle button replaces the old static "switch theme" link. storyboard-light.html is now a redirect (sets light theme, forwards to storyboard.html).
d9554f3Jul 01design
Professional concepts adopted as the live web theme
Design direction decided. apps/web re-ported to the concept palettes — "Plat & Ink" (light) and "Meridian" (dark) — with a persisted runtime theme toggle (no-FOUC head script + useSyncExternalStore). Ink/graphite sidebar, squared mono status pills, serif headings, certificate-block login; both themes verified in-browser.
86b0479Jul 03design
Storyboard: rename field-crew persona to Mike Odell
Marcus Reyes → Mike Odell (name, email, MO avatar initials) across all 13 storyboard screens and the mobile gallery.
4a967a8Jul 05infra
Move docs off site root; standalone apps/web Dockerfile
Frees / on plan.sacqu.com for the Next.js app — the API's docs mount moves to /docs (relative cross-links unaffected); apps/web gets its own multi-stage standalone Dockerfile to run as a separate Dokploy service alongside the API.
5460f21Jul 06build
Dispatch, Documents, Employees, Equipment + job site map
Full-stack build cloning the Jobs vertical-slice pattern: employees/certifications/equipment/job_assignments/documents tables with RBAC-guarded FastAPI routers, Supabase Storage for document upload/download, a drag-and-drop Dispatch board (@dnd-kit), and a keyless Leaflet/OpenStreetMap site-location map on Job Detail.
90dac37Jul 06build
Real Dashboard, Job Detail retheme, document preview/versioning
Job Detail rebuilt to match the "Plat & Ink" concept (two-column field grid + map + activity feed, job-scoped documents, status-derived milestones); a real Dashboard route at / with KPIs computed from live data; document thumbnails, click-to-open large preview, and version-history grouping shared between /documents and Job Detail.
1bc1053Jul 07fix
Global account menu — sign-out reachable from every screen
Sign out lived only in the Jobs page's own header, which broke once the Dashboard (not Jobs) became /. Moved it into a click-to-open menu on the sidebar avatar in AppShell so it's reachable from any page; removed the now-redundant button from the Jobs page.
56a4294Jul 07infra
Restore easy access to storyboard/concepts/mobile plans on the live site
The Jul 5 docs-off-root move (4a967a8) relocated the storyboard and concept docs to /docs on the API service — still live there, but old bookmarks like plan.sacqu.com/storyboard now 404 against the Next.js app that owns /. Added next.config.ts redirects from the old bare paths to their /docs/*.html home, plus a "Docs & Concepts" link in the sidebar so the new location doesn't need to be memorized.
02

What's in the repo

136 tracked files across five groups — client-facing design artifacts, the Next.js app, the FastAPI backend, the Supabase schema, and deploy config.


Design & client-facing

docs/ — self-contained HTML, zero external requests
storyboard.html 13 screens · dynamic theme
storyboard-light.html → redirect
concept-professional-dark.html "Meridian" source
concept-professional-light.html "Plat & Ink"
design-system.html style guide
ios-app-plan.html · android-app-plan.html mobile
implementation-plan.html · index.html
discovery-guide.html · claude-design-prompt.md
build-plan.html · pitch.html

Frontend — apps/web

Next.js 16 + React 19 + Tailwind v4 + TanStack Query
src/app/page.tsx Dashboard
src/app/jobs/page.tsx · jobs/[id]/page.tsx Jobs list + detail
src/app/{dispatch,documents,employees,equipment}/page.tsx
src/app/login/page.tsx Supabase auth
next.config.ts /docs redirects
src/lib/api/schema.d.ts · client.ts typed client
src/lib/supabase.ts · src/app/providers.tsx
src/components/dashboard-live · jobs-live · job-detail-live.tsx
src/components/dispatch-live · documents-live · employees-live · equipment-live.tsx
src/components/document-preview.tsx preview + versions
src/components/job-map.tsx Leaflet/OSM
src/components/app-shell.tsx + ui primitives
src/app/globals.css MOS @theme tokens

Backend — apps/api

FastAPI, uv-managed
app/main.py /docs mount + API
app/core/{config,security,rbac,storage}.py JWKS/ES256 · RBAC · Storage
app/api/{jobs,dispatch,documents,employees,equipment}.py
app/api/routes.py /api/me·roles·admin/ping
app/models/{job,employee,equipment,job_assignment,document,certification}.py
app/schemas/{job,employee,equipment,job_assignment,document}.py
scripts/export_openapi.py · openapi.json contract

Supabase & deploy

schema source of truth + infra
supabase/migrations/…init_profiles_roles.sql
supabase/migrations/…harden_handle_new_user.sql
supabase/migrations/…create_jobs_minimal.sql Day 5
supabase/migrations/…dispatch_documents_employees_equipment.sql
supabase/migrations/…create_documents_storage_bucket.sql
.mcp.json Supabase MCP
Dockerfile API · uv · :8000
apps/web/Dockerfile standalone Next.js deploy
docker-compose.yml · .env.example
nixpacks.toml superseded by Dockerfile
03

Design evolution

Where the look started, where the shipped design-system tokens landed, and the two new professional directions now on the table for comparison.


Started as

  • Dark navy with gradients on buttons, avatars, bars
  • Drop shadows under frames and phones
  • Single (dark) theme
  • Generic "Summit Survey" placeholder brand
  • 13 desktop-oriented screens

Now (shipped in apps/web)

  • Professional concepts are the product theme: "Plat & Ink" light + "Meridian" dark
  • Runtime theme toggle — persisted, no flash-of-wrong-theme
  • Flat + squared — solid fills, 1px borders, 2–6px radii, engraved-plate serif, mono data
  • Ink/graphite sidebar, brass/flag accents, sage/amber/clay/steel status colors
  • Primitives restyled to match: AppShell, StatusPill, RoleBadge, DataState
Design direction: decided, and now everywhere. The flat-navy system (Day 2) served as scaffolding; the professional concepts are the live token source in both apps/web/globals.css (the product) and docs/storyboard.html (the client-facing storyboard), each flipping via the same .dark-class pattern. The 13-screen storyboard was fully rethemed rather than kept as a historical navy artifact — client and product now show the identical system.
04

Platform state (Sprint 0 complete)


Frontend: Next.js 16 + React 19 + Tailwind v4 monorepo app at apps/web, design tokens live, AppShell + primitives shipped, running locally with npm run dev.
Backend: FastAPI at apps/api boots without a database configured; verified via TestClient — / → 200 (storyboard), /api/health → 200, /api/me → 401 without a token.
Auth & RBAC: Supabase Auth (ES256 JWT via JWKS, no shared secret) verified server-side; profiles/roles/user_roles schema live with RLS; require_roles("owner") enforced on /api/admin/ping.
Real account, real role: solomonacquah@gmail.com signed up, auto-provisioned a profile via trigger, and was assigned the owner role — confirmed live against the Supabase project.
Jobs vertical slice: jobs table live in Supabase with 4 seeded samples; /api/jobs CRUD enforces role-tiered writes; the web app signs in with Supabase email/password and renders the live list + detail through the typed OpenAPI client (npm run gen:api) with TanStack Query. Build + lint green.
Build: Dokploy via Dockerfile (Nixpacks abandoned after repeated uv package friction), using uv against the committed lockfile. Listens on port 8000 at plan.sacqu.com.
Schema source of truth: Supabase migrations (supabase/migrations/), not Alembic — Alembic is retained only as a vestigial optional dependency.
iOS app kickoff (July 4): MOS Field native app started at xcode/mosmobile (separate repo) per the iOS plan — M0 foundation + M1 Jobs core built in one pass: GoTrue REST auth (no SDK) with Keychain sessions + actor-single-flight refresh, Face ID gate, certificate-pinning delegate, typed NetworkService, SwiftData job cache with offline read fallback, role-gated status transitions, and Plat & Ink / Meridian Color Sets. 47/47 unit tests green on the iPhone 17 simulator (iOS 17.0 target, zero third-party deps).
Dispatch, Documents, Employees, Equipment (July 6): full-stack build across web and iOS, cloning the Jobs vertical-slice pattern for each new entity — employees/certifications/equipment/job_assignments/documents tables + RBAC-guarded routers; web screens at /dispatch (full drag-and-drop via @dnd-kit), /documents (Supabase Storage upload/download via signed URLs), /employees, and /equipment; a keyless Leaflet/OpenStreetMap site-location map in Job Detail (jobs.latitude/longitude). iOS added a TabView shell plus the same four feature verticals (network-first, SwiftData-cache-fallback repositories) with tap-based Dispatch reassignment and a native MapKit job map — 55/55 iOS tests green. Browser-verified live against real seeded data (office_staff account): Dispatch board, Employee status toggle, Equipment status change, and the job map all confirmed working end-to-end.
Dashboard, Job Detail retheme, document versioning (July 6): Job Detail rebuilt to match the "Plat & Ink" concept end-to-end — two-column field grid + site map + a real-timestamp activity feed, job-scoped documents, and a milestones tracker derived from the actual JobStatus enum (no fabricated legal/survey fields ahead of the schema-widening pass). A real Dashboard route landed at / (KPI row + jobs table, all computed from live jobs/dispatch/ equipment/certification data — cross-checked against the Dispatch and Equipment pages and matched exactly); the Jobs table moved to /jobs. Documents get real thumbnails for photos, a click-to-open large preview (native image/PDF view, download fallback otherwise), and version-history grouping — re-uploading a same-named file still uploads and now surfaces "Updated to vN" instead of a duplicate row. Browser-verified live against the real seeded Supabase data.
Local Storage 502 (found July 6): in this dev environment, document upload/download calls 502 from apps/api/app/core/storage.py — likely a missing or wrong SUPABASE_SERVICE_ROLE_KEY / DOCUMENTS_BUCKET in apps/api/.env, or the documents bucket not yet existing in Storage. Every DB-backed feature (jobs, dispatch, employees, equipment, dashboard) works fine against the same Supabase project — only Storage calls are affected. The preview modal now shows a clear error instead of hanging when this happens.
Two follow-up fixes (July 7): moving Dashboard onto / had silently broken sign-out (it only lived in the old Jobs-page header) — now a global account menu on the sidebar avatar, reachable from every screen. Separately, the docs-off-root move from July 5 had left old bookmarks like plan.sacqu.com/storyboard 404ing even though the content was live at /docs/storyboard.html the whole time — added redirects for the old paths plus a permanent "Docs & Concepts" sidebar link so the URL never needs to be remembered again.
Profile & settings screen (July 10): the sidebar account menu now has a Profile & settings link above sign-out, opening a new /settings page. A single /api/me/profile endpoint returns the signed-in user's identity — profile name/email, platform roles, and (when they're an employee) their HR fields: employee ID, title, phone, hire date, status — rendered as a read-only identity card through the typed OpenAPI client. Verified against live data (solomonacquah@gmail.com → Mike Odell, owner, EMP-001). Typecheck + lint green.
Invite-to-employee flow (July 10, pending manual verification): adding an employee used to require the person to have already signed in once — now "Add employee" falls back to a new POST /api/employees/invite when no account exists yet, which calls Supabase's admin invite endpoint (service_role key, same pattern as Storage) and returns the freshly-created profile so the employee record attaches immediately. Route confirmed registered on the live API; typecheck + lint green. Not yet click-tested in a browser (tool permissions timed out mid-session) — needs a manual owner → Employees → Add employee pass with a fresh email before calling this done.
Android app kickoff (July 12): MOS Field's Android twin started at android/ (a subfolder of this monorepo, unlike iOS's separate repo) per the Android plan — Kotlin 2.2, Jetpack Compose, Material 3, minSdk 29. First feature ported: GoTrue REST auth (no Supabase SDK) with EncryptedSharedPreferences (MasterKey AES256_GCM) sessions, a Mutex-guarded single-flight token refresh, androidx.biometric BiometricPrompt re-entry gate (class 3 + device-credential fallback), and a session-state-driven single NavHost (SignedOut → Login, Locked → BiometricGate, Active → Home placeholder) — same shape as iOS's AppCoordinator. MOS brand tokens ("Plat & Ink" / "Meridian") ported into the Material 3 color scheme and squared 2–6dp shape system. 14/14 unit tests green (interface-based fakes, no mocking framework, per constitution §10). Not yet run on a device/emulator — build and tests verified via Gradle only so far.
Android Milestone 3 — Dispatch, Employees, Equipment (July 13): Android caught up to the three verticals iOS cloned on July 6 (see above) — a Material3 NavigationBar bottom-tab shell now mirrors iOS's five-tab TabView one-for-one (Jobs, Dispatch, Documents, Employees, Equipment), replacing the Milestone 1–2 single-Home-screen shape. Dispatch is a week-at-a-glance board grouped by employee (confirmed against iOS's actual List+Section layout, not a 2D grid) with tap-based reassignment, conflict-highlighted cards, and schedule/move dialogs; Employees adds the two-step "look up profile by email, then attach HR fields" add flow and a view-only certifications detail screen; Equipment splits into Instruments/Vehicles with an expiring-certifications alert banner. All three network-only, no Room cache, same trim as Documents. 91/91 unit tests green (45 new). One real, pre-existing bug found and fixed during live verification: every dropdown picker in the app (Equipment's category picker, Dispatch's job/crew/day pickers) silently failed to open because the anchor field was missing Material3 1.4.0's required Modifier.menuAnchor(...) — confirmed via uiautomator dump and the Material3 sources jar, then fixed everywhere it appeared. Verified live on the Pixel 10 Pro emulator against the real seeded dataset, including a full schedule → verify → delete round trip on the Dispatch board.
Every Android list screen was hiding its first row (found + fixed July 13): user-reported — Dispatch appeared blank on the current week. The shared DataStateContent.kt's Loaded branch called content() directly, never applying the modifier carrying the Scaffold's topBar padding — so every list (Jobs, Documents, Employees, Equipment, Dispatch) rendered starting at the very top of the screen, with the opaque TopAppBar drawn over whatever fell in that range. Invisible until now because every previously-tested list had enough rows to scroll past the hidden region; Dispatch's near-empty current week was the first list short enough to hide entirely. One-line fix (content()Box(modifier) { content() }) corrects every affected screen at once. Verified live: Dispatch's current week now renders its one real card; previously-hidden rows on Jobs and Dispatch's prior week both became visible for the first time. 91/91 unit tests still green (pure layout fix, no logic changed) — no Compose UI test added, since the project has no Compose UI testing infrastructure yet; caught via live device verification instead.
Qfactor-surpass roadmap kickoff — Phase 0 foundations (July 13): an 11-phase competitive roadmap was approved to close every feature gap against Qfactor (qfactor-llc.com: contacts, proposals, invoicing, profitability, tasks, time tracking) and leapfrog with native-mobile + AI differentiators. Phase 0 shipped the cross-cutting enablers: a grouped, role-aware sidebar nav (Operations / Office / Company) with a mobile "More" sheet replacing the 5-item cap; a useRoles()/RoleGated client-side gating pattern (roles-provider.tsx — UX only, server RBAC stays the enforcement layer); transactional email (core/email.py, Resend over httpx, no-op until configured — same pattern as APNs); shared PDF rendering (core/pdf.py, fpdf2 promoted to a runtime dep, latin-1 typography sanitizer); and the unauthenticated tokenized-link router (api/public.py) that emailed proposal/invoice links will use. Also established the API's first pytest suite (apps/api/tests/, 10/10 green) — including a guard proving the public router never accidentally inherits auth.
Qfactor-surpass roadmap — Phase 1: Clients & Contacts (July 14): the real client entity proposals/invoicing/profitability will all FK into. New clients/contacts tables plus an additive, non-breaking jobs.client_id — the migration backfilled one client per distinct historical client_name and linked every job (40 seeded jobs → 12 distinct clients, confirmed live). client_name stays a denormalized mirror during the transition; the jobs write path keeps it in sync with a provided client_id, and renaming a client cascades to every one of its jobs. New /clients list (trgm search, reusing the same fuzzy-match index proven on Documents) and /clients/[id] detail (contacts CRUD + full job history via StatusPill) — the first real page hung off Phase 0's grouped nav and client-side role gating. Verified live: all 12 backfilled clients render, search narrows correctly, a real contact was added through the UI and round-tripped, client detail shows Anderson Family Trust's actual 7-job history. Schema changes to the live database went through an explicit confirmation step first — Claude Code's safety layer blocks direct production migrations on a generic "continue" instruction and requires the specific action be named.
Qfactor-surpass roadmap — Phase 2: Services & billing rates (July 14): the pricing/rate data Invoicing and Profitability will run on. New services catalog (name, unit, price) with a /services page and active/inactive toggle. New employee_rates — an effective-dated history, never a column on Employee, so a raise never rewrites the profitability of jobs already worked. Gated by a new FINANCE_ROLES tier (office_staff + owner) one step up from the usual write gate, with cost_rate nulled out server-side for anyone who isn't owner. Built a proper employee-detail page (/employees/[id] — didn't exist before) to hold the new Billing rates panel, the first real use of client-side financial gating: verified live by switching between an office_staff test account (sees billable rate, no cost rate, no add-rate button) and the owner account (sees and can add both figures) — added a real rate to Jordan Cole ($140/hr billable, $65/hr cost) and confirmed both render correctly for owner only.
Real test-database infrastructure (July 14): the API test suite ran against nothing but 401/403 auth-gate checks through Phases 0–2 — no test database was configured, so none of the actual business logic had automated coverage. Fixed by standing up a real local Postgres (Homebrew, not Docker — the sandbox's Docker daemon wouldn't come up) at a dedicated mos_test database, with tests/conftest.py rebuilding the schema from the SQLAlchemy models themselves each run (drop + create_all, plus the same set_updated_at trigger production uses) and a fixed fake signed-in user whose roles a test can set exactly via seed_roles(). Caught a real schema-drift bug along the way: the employee_rates ORM model was missing the unique(employee_id, effective_date) constraint that the actual Supabase migration has — meaning the 409-conflict handling in create_rate had never been exercised against a real constraint. Backfilled 24 new real business-logic tests for Clients/Contacts (Phase 1) and Services/Employee-rates (Phase 2) — CRUD, role-gated writes, the client-rename-mirrors-onto-jobs rule, the effective-dated "current rate" resolution, and the cost_rate redaction for non-owner readers. 37/37 green, twice in a row. This is now a standing gate for every phase from here on.
Qfactor-surpass roadmap — Phase 3: Tasks, job notes & equipment compliance (July 14): a complete task vertical now schedules budgeted Field/Office work against jobs with status, due date, optional department, planned hours, team assignees, and free-form tags. Immutable, timestamped job notes are available on Job Detail and may be tied to a task; the API prevents cross-job task/note links. Equipment gains a next-calibration date plus indexed expired/30-day alerts rendered above the asset list. The proposal-line-item FK is consciously deferred until P7 creates its target table. New /tasks page and Operations navigation shipped through the generated OpenAPI client. Ruff, ESLint, TypeScript, and production build green; real Postgres API suite now 42/42 green. Isolated Chrome QA also passed linked task creation, immutable job notes, and the equipment calibration warning. The production migration was applied transactionally; live authenticated Chrome smoke tests then passed on Tasks and Equipment.
Qfactor-surpass roadmap — Phase 4: GPS time tracking & Time Off (July 14): GPS is required at clock-in and clock-out; a database-level partial unique index prevents overlapping active clocks. Entries link to Job and Task and capture non-billable time, breaks, travel, mileage, notes, and break-adjusted duration. Field crew see only their own ledger while management roles can review across employees. Time-off requests add date validation and office/owner approval. The new /time page provides browser-geolocation clock controls, recent entries, leave submission, and gated approvals. Native screens remain in the mobile catch-up milestone per the web-first decision. Ruff, ESLint, TypeScript, and build green; API suite 47/47 green. Isolated Chrome QA passed the time-clock control state plus leave submission and owner approval; the real GPS button remains privacy-gated while coordinate enforcement is covered by the API suite. The production migration and live authenticated /time smoke test also passed.
Qfactor-surpass roadmap — Phase 5: Native invoicing, approval & Sales Orders (July 14): MOS now owns a server-calculated invoice ledger with concurrency-safe per-job progress numbers, finance-only creation, owner-only approval/rejection, sent/paid/void handling, and linked signed-negative Credit Notes. Sales Orders track client PO references, authorized ceilings, invoiced value, remaining authorization, and over-billing warnings. Sent invoices get minimal tokenized public views and PDFs; configured email receives the PDF as a best-effort attachment. Finance-gated /invoices and public /i/[token] pages shipped. Ruff, ESLint, TypeScript, and build green; API suite 53/53 green. Isolated Chrome QA passed Sales Order creation, service-priced draft creation, approval transitions, PO-ceiling warnings, signed-negative Credit Notes, and the tokenized public view. The production migration, live authenticated invoice empty state, and public invalid-token privacy response were also verified in Chrome.
05

Mobile feature parity — iOS vs Android

Both native plans (iOS, Android) commit to identical feature scope per phase — same nine Phase A line items, same wording, same mapping to platform Phases 1–2. This table is the scannable version: what's actually built per feature, per platform, right now. Regenerate it whenever either app ships, so a gap is visible at a glance instead of requiring a side-by-side read of two changelogs.


Phase A — Field MVP

FeatureiOSAndroid
Sign in + biometric re-entryDone — Face ID gate, Keychain, actor-single-flight refreshDone — BiometricPrompt gate, EncryptedSharedPreferences, Mutex-single-flight refresh
My Jobs list + detailDoneDone — Room-cached offline read fallback
Status updates (Assigned → In Progress → Waiting → Completed)Done — role-gated transitionsDone — role-gated transitions
Job notesDone — immutable server-first creates, job-scoped SwiftData cache and offline read fallbackDone — immutable server-first creates, job-scoped Room cache and offline read fallback
Document viewingDone — QuickLook, cloned from the Documents verticalDone — global list, category filter, pagination; opens via a signed-URL hand-off to whatever app the OS picks (no in-app renderer)
Photo capture + upload (GPS + timestamp stamped)Done — camera capture, UTC/GPS evidence banner, job-scoped field upload; photo-library fallback on camera-less devicesDone — camera/gallery capture, UTC/GPS evidence banner, adaptive JPEG compression and job-scoped multipart field upload
Site maps + navigation hand-offDone — native MapKit job mapDone — geo-intent hand-off (deliberately not an embedded map, per §07 of the constitution)
Daily field reportsDone — job-scoped composer, weather/crew/work/issues/next steps, SwiftData offline readsDone — matching job-scoped composer, validation, server-first submission and Room offline reads
Push notifications (assignment, status, due-date)Done — APNs registration, token sync, assignment/status alerts, daily due-date cron, job deep linksDone — FCM registration/token sync, platform-aware backend delivery, foreground alerts and job deep links; live delivery activates when Firebase credentials are deployed
iOS Job Notes validation — July 14, 2026: the complete mosmobile scheme passed 83 tests with zero failures or skips on an iPhone 17 / iOS 26 simulator. A clean Debug simulator build installed and launched successfully. Android remained unchanged throughout the iOS-first gate.
iOS Field Photos validation — July 14, 2026: image stamping, missing-location labeling, JPEG generation, job/category/source upload mapping, success, and failure behavior are covered. The complete scheme passed 89 tests with zero failures or skips on iPhone 17 / iOS 26, followed by a clean Debug simulator build. Physical-device installation remains a signing/keychain environment gate, not an application test failure.
iOS Daily Field Reports validation — July 14, 2026: the backend enforces known jobs, required work summaries, and one report per author/job/date. The API suite passed 62 tests; the complete iOS scheme passed 91 tests with zero failures or skips on iPhone 17 / iOS 26.
iOS Phase A complete — Push Notifications, July 14, 2026: APNs permission/token registration, authenticated device-token sync, assignment/status delivery hooks, and notification-to-job deep links are wired. The API suite passed 62 tests; the complete iOS scheme passed 92 tests with zero failures or skips.
Android Job Notes parity — July 20, 2026: job detail now loads immutable job-scoped notes, creates trimmed notes server-first, and falls back to a Room cache when offline. The Debug APK assembled successfully and the complete Android unit suite passed 97 tests with zero failures.
Android Field Photos parity — July 20, 2026: camera/gallery selection, explicit permission handling, UTC/GPS evidence stamping, adaptive compression below the upload ceiling, and authenticated job-scoped multipart upload are wired. The Debug APK assembled and 101/101 Android unit tests passed.
Android Daily Field Reports parity — July 20, 2026: job/date-scoped history, required work validation, optional weather/crew/issues/next steps, server-first submission, draft clearing and Room offline reads now match iOS. The Debug APK assembled and 106/106 Android unit tests passed.
Android background navigation persistence — July 20, 2026: the active bottom tab and selected job ID survive biometric locking and process recreation; unlock reloads the job through the repository, invalid/deleted jobs fall back safely to Jobs, and sign-out clears the saved state. The Debug APK assembled and 110/110 Android unit tests passed.
Android Phase A complete — FCM Push, July 20, 2026: authenticated Android token registration, FirebaseMessagingService renewal, notification permission/channel handling, assignment/status job deep links, and platform-aware APNs/FCM backend delivery are wired. Android assembled with 111/111 tests; focused FCM backend tests passed 3/3. Live sends remain a deployment configuration step requiring Firebase credentials.
Android Phase B — GPS Time + Mileage, July 20, 2026: job/non-billable clock-in, GPS evidence, active recovery, breaks, travel, notes, validated odometer mileage, payroll-week summaries and 150 m job-site reminders now match iOS. The Debug APK assembled and 119/119 tests passed.
Android Phase B — Time Off, July 20, 2026: native date selection, date-order validation, optional reasons, request status history, server-first submission and Room offline history now match iOS. The Debug APK assembled and 124/124 tests passed.
Android Phase B complete — Equipment Checkout, July 20, 2026: field users can check out available assets and return held assets; server conflict rules prevent double checkout and unauthorized returns. The Debug APK assembled and the complete Android suite passed 128/128 tests.
Android Phase C complete — Management, Reports + Glance, July 20, 2026: office/owner financial metrics, job-performance bars, debounced intelligent cross-entity search, authenticated PDF report downloads, dispatch adjustments and a Glance active-jobs widget now match iOS. The Debug APK assembled and 138/138 tests passed.

Beyond Phase A — shipped on both platforms, itemized on neither plan

FeatureiOSAndroid
GPS time clock (Phase B)Done — job/non-billable clock-in, GPS evidence, geofenced arrival/leave reminders, breaks, travel, mileage, notes, active-clock recoveryDone — matching GPS lifecycle, active recovery, job/non-billable scope, breaks/travel/notes and 150 m site reminders
Time Off (Phase B)Done — request form, status history, API-backed submission, offline history cacheDone — native date form, validation, status history, server-first submission and Room offline history
Mileage + payroll week (Phase B)Done — validated odometer delta, persisted shift mileage, current-week hours and completed-entry summaryDone — matching odometer validation, manual fallback, persisted mileage and week/completed summaries
Equipment checkout/return (Phase B)Done — swipe actions, employee assignment, double-checkout conflict protectionDone — self-checkout/return actions with double-checkout and holder/manager conflict protection
Management & insight (Phase C)Done — executive financial dashboard, Swift Charts, cross-entity historical search, server-rendered PDF report library, dispatch adjustments, WidgetKitDone — role-gated financial dashboard, performance visualization, intelligent search, authenticated PDF library, dispatch adjustments and Glance
Native intelligence (Phase D)Done — on-device voice transcription, reviewable report structuring, private document extraction/summaries, natural-language search, VisionKit document scanningDone — offline-preferred native speech, reviewable report drafts, private summaries, intelligent search and ML Kit scanning/upload
Survey monument intelligence (P9A.2–P9A.4)Done — sensor evidence, offline queue, reviewed vision extraction and cited packet/PDFDone — stamped camera/library evidence, Room queue/sync, human confirm/reject, cited parcel packet and authenticated PDF
Native MOS Assistant (P9.7)Done — grounded SSE chat, citations/tools/usage and confirmable job actionsDone — grounded SSE chat, citations/tools, per-process tokens/cost and draft-confirm-cancel actions
Dispatch board (assign/reassign crew to jobs by day)Done — tap-based reassignmentDone — tap-based reassignment, matching iOS
Employees (list, detail, add via profile lookup)Done — existing-profile lookup and attach flowDone — matching existing-profile lookup and attach flow
Equipment (list, add, status, expiring-cert alerts)DoneDone
Home-screen widgetDone — WidgetKit, Phase C item shipped earlyDone — Glance active-job count and three-row snapshot, refreshed by authoritative job loads
Android Phase D + survey intelligence + assistant parity — July 20, 2026: ML Kit scanning, offline-preferred device dictation, private document summaries, the complete offline monument evidence/review/packet workflow, and grounded streaming assistant with citations, tools, token/cost detail and confirmation-gated actions are wired. Room schema v5 preserves pending monument captures across process loss. The Debug APK assembled and the complete Android unit suite passed 145/145 with zero failures.
Android navigation polish — July 20, 2026: the overcrowded ten-item bottom bar was replaced with five stable destinations: Jobs, Dispatch, Documents, Time and More. Time Off, Insights, Reports, Employees, Equipment and MOS Assistant now live in a role-aware More screen, while secondary screens retain the bottom bar with More selected. The updated APK passed 145/145 tests and Android lint.
Shared native app identity — July 20, 2026: Android now uses the same survey-instrument AppIcon artwork as iOS. Adaptive, round and legacy density assets are generated from the iOS 1024 px source with Android-safe padding so launcher masks preserve the antenna and tripod. The Pixel emulator launcher rendering was visually verified; the APK, 145/145 tests and lint remain green.
Out of sequence: the home-screen widget shipped before Phase A finished. "Home-screen at-a-glance" is a Phase C line item on both plans (WidgetKit / Glance) — yet iOS built MOSFieldWidget on July 8 before Phase A was complete. Phase A is now green on iOS; Dispatch, Employees, and Equipment were cloned to iOS on July 6 the same way — real, working screens, but outside the nine items either plan actually itemizes for Phase A; Android caught up to those three in its own Milestone 3 (July 13, table above), so the widget is now the only platform-exclusive item left. None of this is wrong, but it's exactly the drift this table exists to catch. The active gap has moved to Phase B and later parity rows.
Android kickoff followed the recommended order. Rather than letting iOS run further ahead, "Sign in + biometric re-entry" — the first Phase A line item — was ported to Android immediately (July 12) using the shipped Swift implementation as the reference, before starting anything further on either platform. Same intent as iOS: androidx.biometric BiometricPrompt gate (class 3 + device-credential fallback), GoTrue REST auth with no Supabase SDK, EncryptedSharedPreferences (MasterKey AES256_GCM) in place of Keychain, and a Mutex-guarded single-flight refresh in place of iOS's actor. 14/14 unit tests green. The remaining eight Phase A rows should follow this same one-feature-at-a-time cadence rather than batching.
Android Milestone 1 — Jobs (July 12). "My Jobs list + detail" and "Status updates" shipped next, per docs/android-parity-plan.md's ordering — the foundational vertical slice everything else (Documents, the map hand-off, Dispatch) attaches to. Room-cached offline read fallback (network-first, same pattern as iOS's SwiftData cache), role-gated status transitions identical to iOS's allowed-transition rules. 31/31 unit tests green; verified live against the real seeded dataset (5000-row kit) on a Pixel 10 Pro emulator — list rendering, status-pill colors matching the web app exactly, filtering, and a real status PATCH round-trip all confirmed. One real bug found and fixed during live verification: availableTransitions was a plain ViewModel property reading the StateFlow directly rather than a field derived from the observed uiState itself, so Compose rendered it once but never recomposed it after a status change (the status pill updated correctly; the transition list below it silently went stale). Fixed by moving it onto JobDetailUiState, with a regression test added.
Android Milestone 2 — Documents + site-map hand-off (July 12). A global Documents browser (category filter chips, pagination via "Load more", current- version badge) reachable from the Jobs toolbar — matches iOS's actual Documents tab, not just a job-scoped list. Opening a document fetches a real signed URL and hands it to Intent.ACTION_VIEW, letting whatever app the OS picks (Chrome, a PDF viewer, …) render it — deliberately no in-app renderer, same "hand off to the system" philosophy as the new "Get Directions" row on Job Detail, which builds a geo: URI instead of embedding a map (iOS uses native MapKit; Android's constitution explicitly rules out a bundled map SDK). 46/46 unit tests green — including a new GeoUriTest pinning the URI format, since the seeded 5000-row dataset has no jobs with real coordinates to click-test live against (the four original hand-seeded jobs do, but weren't practical to locate by scrolling). Verified live: real document metadata renders correctly, category filtering works, and the download/open flow correctly triggers a real OS app-picker hand-off with a valid signed Storage URL.
06

What's next


Storage config (one step remaining): DB-backed local dev already works end-to-end — uv run uvicorn app.main:app --reload --port 8000 in apps/api and npm run dev in apps/web, sign in at localhost:3000/login, and jobs/dispatch/ employees/equipment/dashboard all render live data. Document upload/download still 502s locally though; set a valid SUPABASE_SERVICE_ROLE_KEY + DOCUMENTS_BUCKET in apps/api/.env and confirm the documents bucket exists in the Supabase project's Storage (see §04).
Qfactor-surpass Phase 6 complete (July 14). MOS now has a finance-only /financials dashboard built from live task, time, employee-rate, invoice, payment and credit-note records. It reports earned revenue, WIP, AR, collections, backlog, labor cost, gross profit and margin by job, with department drill-down and explicit missing-rate warnings. Jobs now support hourly/fixed-fee billing terms and contract value. The fixed-fee recognition policy should receive a bookkeeper/accountant gut-check before use in formal statements. Full API regression: 55/55 green; Ruff, ESLint, TypeScript and the Next.js production build are green.
Qfactor-surpass Phase 7 complete (July 14). MOS now has a role-gated proposal pipeline with server-calculated price, cost, gross margin and probability-weighted value; submission/decision/validity milestone tracking; dynamic multi-line scopes; and a cost-redacted /p/[token] client presentation. Typed-name acceptance is expiration-aware, row-locked and idempotent: it creates exactly one fixed-fee job plus proposal-linked task budgets that feed time, invoicing and Phase 6 financials. Full regression: 60/60 green. Chrome QA covered both internal screens and a temporary $8,500 public proposal; the QA row was removed without recording a false acceptance. Commit fbc85aa deployed from main, after which authenticated Chrome smoke tests passed on the live plan.sacqu.com/proposals and /financials routes.
Qfactor-surpass sequence: Phases 6–7 are complete. The formal dependency path now moves to Phase 8 QuickBooks Online sync; the approved P9A.0 San Mateo/San Francisco/Roseville county-data feasibility spike can also run independently before the broader AI layer.
P9A.0 connector checkpoint implemented (July 15). The API now performs authenticated, server-side parcel queries against official San Mateo County, San Francisco Planning, and City of Roseville GIS layers, normalizes APN/MapBlockLot evidence, and returns source attribution, retrieval time and explicit non-survey-grade warnings. The iOS job screen exposes the pilot and may request an evidence-only OpenAI summary after parcel resolution. Native iOS dictation no longer calls AI. Live checks succeeded in all three pilot jurisdictions; recorded-map linkage, rights confirmation, the evaluation, packet exports and licensed-surveyor exit review remain before P9A.0 can be marked complete.
Roseville monument-photo checkpoint (July 15). iOS now preserves the most detailed stamped JPEG it can under a hard 950 KiB ceiling. Coordinates drive the county/parcel workflow, while readable monument characters remain necessary for visual extraction. The supplied Roseville coordinate did not intersect a parcel polygon, so the connector discloses a 10-meter proximity result (APN 496-520-007-000, lot 7, subdivision ID 1008) and links Placer County Assessor/Recorder research rather than asserting containment or a recorded-map match.
P9A.1–P9A.4 application workflow complete (July 15). The web app now includes a role-gated /assistant workspace. Its LangChain two-step RAG pipeline retrieves authorized live jobs, clients, tasks, notes and document metadata, augments them with semantic PDF/text chunks from a private Supabase pgvector index, and returns evidence cards with every answer. Field crews are restricted to assigned jobs; the model has no SQL tool. A hash-only audit records status and cited source IDs without retaining raw questions or answers. P9A.1 adds a typed authenticated SF/San Mateo/Roseville registry, health/schema monitoring, retry and warm-cache behavior, plus honest county record-portal leads. P9A.2 adds native iOS camera/library monument capture with GPS accuracy, heading, altitude, SwiftData-first offline persistence, idempotent retry, and confirmed per-item removal of stale pending/failed captures; syncing or uploaded evidence is protected. P9A.3 adds structured vision extraction with editable fields and mandatory human confirm/reject; unconfirmed observations cannot become packets. P9A.4 persists cited parcel research with official county, FEMA and USGS links, displays it on web and iOS job records, and renders a cautioned authenticated PDF field packet. Verification: full API regression 91/91, focused parcel tests 13/13, the complete iOS unit target, Ruff, ESLint, TypeScript, generated OpenAPI and the Next.js production build all pass. P9A.0's 40-case evaluation, rights confirmation and licensed- surveyor review remain external production-acceptance gates.
Mobile: both platforms underway. The iOS plan's M0 + M1 milestones are built and tested (see §04) — next iOS steps are running it against the live API end-to-end, then M2 field capture (photos, background upload queue, offline write-queue + SyncService). The Android plan has shipped auth + biometric re-entry, Jobs (list/detail/status updates), and Documents + the site-map hand-off — see §05 for the live parity table and docs/android-parity-plan.md for the ordered catch-up plan; Milestone 3 (Dispatch, Employees, Equipment) is complete on both platforms.
Design direction resolved: Concepts C ("Meridian") + D ("Plat & Ink") are adopted and live in the product with a runtime light/dark toggle — no re-theming debt going into the Phase-1 domain build.