Pannell Co · Time-Tracking Integration · Path B

From two systems to one

A walkthrough of what's being built, what's already decided, and — the part worth sitting with — what's already proven working, not just planned. Six lanes of work. Four of them already have real, verified proof behind them — not descriptions of a plan, things that were built, run, and checked.

What this project is

Pannell runs an older, standalone time-tracking application — Vue on the front end, Supabase underneath — completely separate from Pannell's main app. The goal: fold it into the main app so there's one system, one login, one database, one codebase. Not two stacks running side by side forever.

6
lanes of work
54
screens to re-skin
99
backend functions in scope
4
lanes with real, verified proof
0
genuinely open decisions

The one big decision that shapes everything

There were two honest ways to do this. One was chosen deliberately, on 2026-07-30.

Considered · Not Chosen

Path A — keep it sealed

Leave the old backend running as its own black box, put a new login system (AWS Cognito) in front of it, and build a bridge so the main app can talk to it. Fully scoped, kept for reference — but not what's being built.

Chosen · In Progress

Path B — absorb it

Move the old backend's actual logic into Pannell's main app, retire Supabase entirely, end up with one unified system. More one-time work up front — no second stack to maintain forever, no bridge to keep alive.

Two calls made early that remove a lot of downstream work

What "done" looks like

How the six lanes fit together

Not a strict assembly line — several lanes run in parallel on purpose. The frontend is independent and starts immediately; the backend is the spine everything else leans on; roles and data-migration ride alongside it; verification is the convergence point; mass-upload is "trigger zero," built last.

01 · INDEPENDENT TRACK
Frontend integration
Proven — prototype live
Re-skin the old Vue screens into Pannell's Mantis/Next look. Starts immediately, blocks nothing.
54 screens · proven pattern + a repeatable conversion tool
↓ runs alongside ↓
02 · THE SPINE
Backend conversion
Proven — prototype live
Moves the old backend's logic into Pannell's app. Everything downstream leans on this — keep-RLS gets implemented here.
99 functions in scope · proven pattern + a repeatable conversion tool
03 · FEEDS 02
Roles, identity & auth
Proven — decision-complete
Maps the old app's roles onto Pannell's, so migrated accounts land with the right access. Decides the role claim 02 injects.
No Cognito, no new login · mapping confirmed & dry-run proven 2026-08-02
↓ 02 + 03 feed into ↓
04 · CUTOVER WAITS ON 02
Data migration
Mechanism proven
Moves the live database — every entry, contract, and record — into its new home, filtering rules intact.
Restore + RLS mechanism dry-run proven · real cutover still waits on live data access
↓ convergence point ↓
05 · THE SAFETY GATE
Verification & cutover
Tooling proven
Old and new backend run side by side against the same real data, diffed automatically.
Convergence point · nothing ships until this is green · checking machinery built and tested, gate itself waits on 02+04
↓ only once everything above is stable ↓
06 · TRIGGER ZERO
Mass-upload intake
Template + mechanics proven
Rebuilds the spreadsheet-based employee upload HR already knows, inside the new app.
Built last on purpose, plugs into a finished system · template + core create/update/deactivate logic already proven against real code

This isn't a plan on paper.

The two lanes that carry the actual bulk of the work — frontend and backend conversion — each already have a real, working, verified prototype behind them. Not a description of an approach. A thing that was built, run, and checked. Two more lanes joined them the same way — the role mapping and the data-migration mechanism are both dry-run proven against real, running systems too. The safety gate that closes the project out can't be dry-run the same way yet — it needs the other lanes further along first — but its checking machinery is built and proven, not left for later.

Frontend · Lane 01

The time-entries screen — converted and running, with only one layer rebuilt

One real screen taken all the way from the old Vue app into Pannell's Mantis/Next pattern — tested, wired up, working end to end. Not a guess at what the conversion would look like.

Vue (old)
React / Mantis (new)
Screen
Quasar component
Rebuilt
Screen
Mantis/MUI component
Store logic
Pinia store
Same behavior, ported
Hook logic
React hook, same rules
API calls
Vue's endpoints
Same contract
API calls
Identical request/response shape

Vue syntax doesn't copy-paste into React — the screen and the store logic both get rewritten. What carries over is the behavior: the same rules, the same data shape, nothing invented.


  • A proven conversion pattern for the remaining screens — not a guess at one
  • A repeatable tool (migrator/) that picks the next screen and generates a complete conversion prompt
  • The developer runs the rest through the same tool — not starting from a blank page
Backend · Lane 02

customer-class-codes-upsert — ported and verified, with only one layer rebuilt

One real backend function, ported end to end and checked against the actual, unmodified legacy code — not a code review, an automated diff.

Deno function (old)
Next.js route (new)
Handler
Deno.serve()
Rebuilt
Handler
Next.js route export
Business logic
Validation, calculations
Reused ~1:1
Business logic
Ported close to unchanged
Database
Postgres + access rules
=Kept intact
Database
Same schema, same rules, untouched

The strongest reuse claim here is the database itself — same tables, same access rules, same data. Only the connection code changes, and it's written once, not per function.


  • Real local database, real schema, the original function actually executed against it
  • The new Next.js version run against the exact same data
  • 9 test cases — updates, edge cases, permission checks — diffed byte-for-byte. All 9 pass.
  • A repeatable tool (migrator/) that picks the next function and generates a complete conversion prompt
Roles & Identity · Lane 03 · Resolved 2026-08-02

All 7 migrated roles — created and logged in for real

The role-mapping decision, exercised against Pannell's actual account system — not just written down and signed off. This lane also carried the one genuinely open question in the whole package: is "Supervisor" the same job as "General Foreman"? The old app uses 7 role names; Pannell already has its own 8. Most of the mapping was already clear — one pairing wasn't, and it couldn't be resolved from the software. It needed someone who knows how the crews are actually organized. Answer: yes, the same job. Collapse it.

Old system — 7 roles
  • Admin
  • Executive
  • Project Manager
  • Supervisor ✓
  • Worker
  • Area Manager
Pannell — 8 roles
  • Admin — matched
  • Executive — matched
  • Project Manager — matched
  • General Foreman ✓
  • Worker — matched
  • Area Manager — matched

Why it mattered: in Pannell, your role decides how you log in — field employees use their employee ID, office/admin staff use a password. A wrong guess here wouldn't just mislabel someone — it could lock a real person out on day one. That's why this got a real answer instead of a best guess, and why the answer got tested, not just filed.


  • One real account created per legacy role against a real, running instance of Pannell's backend
  • All 7 landed with the correct Pannell role and the correct access tier, automatically
  • All 7 logged in successfully through the correct door
  • All 7 were correctly turned away at the wrong door — the lockout risk, disproven
  • Bonus find: Pannell's permission matrix was missing 34 actions its own role definitions already expected — completed and re-verified against a real role document, not just patched

Everything else in this lane was already decided

No new login system Pannell's existing dual login (password + employee ID) already covers everyone moving over.
No Cognito The sealed-box plan needed it; absorbing the backend makes that need disappear entirely.
No sync pipeline There's no second system to keep in sync once Path B lands.
Reuse Pannell's user creation Migrated accounts go through the existing process, not a new one.
Data Migration · Lane 04

The restore-and-filter mechanism — rehearsed and confirmed

The exact mechanism the whole "keep the database's own rules" decision depends on, tested against a real restored copy of the schema.

  • The real database structure — all 112 individual access rules — restored onto a throwaway test database
  • Real gaps found and fixed along the way, then re-verified until every rule landed clean
  • Two test employees could each see only their own records; an admin could see both
  • An account with no identity at all correctly saw nothing — the system fails safe
Verification & Cutover · Lane 05 · Tooling proven 2026-08-02

The safety-gate machinery — built and tested ahead of the work it will check

Lane 05 is the final gate: nothing ships until old and new backend answers match, function by function, role by role, against real data. The gate itself can't run at full scale yet — it needs the backend conversion much further along first. What doesn't need to wait is the checking machinery itself, and that's built, tested, and proven correct now, so it's a plug-in step once conversion catches up, not a second research phase.

A real finding, not just a tool: every in-scope function needs the strict check

Ran a direct scan of all 99 in-scope functions' real source — not a sample, all of them — for whether each one relies on the database's row-filtering or on its own manual access check. Result: 99 of 99 need the manual check verified explicitly. That's the stricter, more thorough test — it doesn't let a function pass just because its answer happens to match the old one; it has to prove it actually turns away someone who shouldn't have access. Better to know that now, before conversion starts at scale, than discover it function by function later.


  • The function classifier — run for real against all 99 functions' source, not inferred
  • The comparison engine itself — proven on a case that should pass and a deliberately broken case that should correctly fail, so it's known to work both ways
  • The connection-pooling identity-leak test — the single riskiest failure mode in the whole plan, reproduced on purpose: the safe pattern held, and the risky pattern correctly leaked, proving the test can catch the real thing
  • None of this needed the backend conversion to be further along — it's done, waiting, and reusable the moment more functions are ported
Mass-Upload Intake · Lane 06 · Template + mechanics proven 2026-08-02

The employee upload template and the create/update/deactivate logic — built and tested, not waited on

This lane is deliberately built last — it needs the rest of the system stable first. But "built last" doesn't have to mean "unstarted until then." Rather than wait on a real sample file from the client to know what to build against, we built our own version of that spreadsheet from what's already confirmed, and proved the actual logic against it — for real, against the real backend.

A real bug this found, not a footnote

One of our own earlier decisions — match employees by their Paycom ID — turns out not to hold for every employee. It works for field crew, but the real backend quietly clears that ID for office/admin-tier accounts today, since they log in a different way. That's not a guess; it's what actually happened running the real test against the real code, and it's now flagged as a decision that needs revisiting before this ships, not something waiting to be discovered later.


  • A real upload template — the old system's own known columns, plus the one addition it's actually missing
  • New-hire creation, an employee promotion, and a mid-stream new hire — all correct
  • A person dropped from a later file is turned off, never deleted — matching the required behavior exactly
  • A deliberately unrecognized job title correctly refuses to demote someone — proving the safety rule works, not just exists on paper

What's still genuinely unbuilt — not just undecided: matching a supervisor's code to their real account, and matching a contract code to its real contract record. Both are real, separate pieces of work the old system itself treats as a later pass, not something the template or this test solves. That's the honest gap between "the upload works" and "everything flows seamlessly into the time tracking module" — the first is proven, the second still needs this piece built.

Why this is the headline, not a footnote

The two heaviest pieces of this project are frontend and backend conversion — that's where the bulk of the remaining hours live. The risk that normally justifies a discovery phase, a padded estimate, or "we won't know the real cost until we're in it" — that risk has already been retired, on both of the expensive lanes, with working proof. What's left is applying an already-proven pattern through an already-built tool, screen by screen and function by function. That's scoped work, not open-ended exploration. The two lighter lanes — roles and data migration — got the same treatment: not left as plans, exercised against real systems before being called done. And the safety gate that closes the whole project out isn't a research question either — its machinery is already built and tested, ready to run the moment there's more converted code to check. Even the lane that's built deliberately last isn't sitting idle — its template and its core logic are already proven, ahead of when they're actually needed.

The safety net — and why "cheap to build" doesn't mean "cheap to trust"

This backend calculates and exports payroll. A subtle slip in a rewrite doesn't show up as a crash — it shows up as someone being paid the wrong amount. So the plan treats verification as its own real step, not a formality. The good news: proving it correct is also cheap — but only because it's built to be automatic.

Same real historical data
Real past inputs, not made up
↓ fed into both, automatically ↓
Old code
The current, live behavior
New code
The ported behavior
↓ every answer compared, automatically ↓
THE OVERWHELMING MAJORITY
Match
Proof the rewrite preserved behavior for that case. Nobody looks. Costs nothing.
RARE
Mismatch
A person looks — once per kind of difference, not per difference. See step 3 below.
1

Run the old code and the new code side by side, automatically

Same real historical data goes into both. The computer compares every answer, for every function, all at once.

2

Where they match — nobody looks. That's the overwhelming majority.

A match is proof the rewrite preserved behavior for that case. It costs nothing to confirm.

3

Where they don't match, a person looks — once per kind of difference, not per difference

A timestamp a moment later, a new auto-generated ID, a list in a different order — a person calls that harmless once, and the system recognizes every future instance of that same kind automatically. The work scales with the number of distinct kinds of surprise, not with the number of tests run.

4

One deliberate exception: functions that were always "too open" get tested the opposite way

A handful of old functions returned everything to anyone, by design of the old system. For those, matching the old behavior isn't good enough — the new version is checked to confirm it actually turns away users who shouldn't have access. This is the one place the new system is supposed to differ from the old.

The release gate — nothing ships until every one of these holds

  • Every in-scope function checked against real data, across every role that touches it
  • Every difference explained as harmless, or fixed as a real bug — none left unexplained
  • The "trusted admin" functions proven to actually enforce access, not just checked for a match
  • A named person signs off — this touches live payroll data, so it's a planned event, not a routine push
This is not part of the Time-Tracking / Path B package above. It's a separate project — the React web → React Native mobile conversion — delivered as its own adjacent handoff. It's included here because the same "already de-risked, not a blank check" story applies to it too.

The mobile app conversion — also already proven

What this does, plainly

Pannell's admin app exists today as a React web app — pages that run in a browser. This takes each of those screens and rebuilds it as a React Native app — a real, installable app that runs natively on an iPhone or Android phone, not a browser view of the website.

Pannell's admin app is roughly 50 pages. Three of them were converted end to end and are running on the iOS Simulator right now — a real, working prototype, not mockups or static screens.

What's actually unique here: a mobile rewrite sounds like rebuilding the whole app twice. It isn't. Only one layer — the visible screen — actually gets rebuilt. The two layers underneath it, the ones that are expensive to get wrong, move over almost unchanged:

Web app
Native app
Screen (UI)
MUI / web components
Rebuilt
Screen (UI)
Native components, same layout
Hooks (logic)
Loading, saving, paging
Reused ~1:1
Hooks (logic)
Same logic, copied over
API layer
Real NestJS endpoints
Same endpoints
API layer
Identical contracts, no changes

Only the top layer changes per screen. The bottom two layers — the part that's actually hard to get right — are already proven and move over for free.

~50
web pages to convert
3
shapes proven (covers ~all pages)
2
app versions built — basic + styled

The three shapes, proven — not guessed at

CRUD list — Regions

A list screen with add / edit / delete. The most common page shape in the app.

Running on simulator

Form-heavy — Users

Conditional fields, real validation, an async-loaded dropdown — the hardest shape, proven.

Running on simulator

Chart / stat dashboard

KPI cards and charts, rebuilt with a native charting library — no web-only chart libraries used.

Running on simulator

These three cover essentially the whole app — every remaining page is a list, a form, a detail view (a subset of what the form canary already proves), or a dashboard.

Not just a pretty screen — the real data path is built

Easy to fake a nice-looking demo. This isn't one. Every screen's data flows through the same hook logic and the same real API endpoints your NestJS backend already exposes — GET /regions, POST /users, etc. — with a one-line switch to flip from an in-memory mock to the live server. Tapping Add / Edit / Delete really runs the create/update/delete path.

Styled and on-brand, not just structurally proven

A second pass re-skinned all three canaries with a proper component library (shadcn-style, the same idea used on modern web apps) themed with the app's actual green brand color — not a generic default look. Verified clean (tsc --noEmit) and running on the simulator.

Where this honestly stands — the frozen mapping vs. the automation

Two different things, and it's worth being precise about which is which. The conversion mapping is proven and frozen — how a web screen becomes a native screen is a known, repeatable recipe now, not something to figure out per page. The automation that runs that recipe at scale is planned, not yet built — a small tool that would batch-convert the remaining ~47 pages through a cheaper model, with the cost-saving levers (prompt caching, batch pricing) already identified and costed out. Building the automation before proving the rhythm by hand is the wrong order anyway — the plan is to hand-convert the next 5–10 pages first, confirm it stays this cheap and repetitive, then automate the rest. That's a 30-second approval checkpoint when the time comes, not a research phase.

Why this is here, in this package

Same reason as the four proven Path B lanes above: the risk that normally justifies "we won't know what mobile costs until we're in it" has already been retired, with working proof — running on a real simulator, wired to real endpoints, styled in the real brand. When mobile comes up later, it's applying an already-proven recipe to the remaining pages, not scoping a new project from zero.