LaneAward Handoff Reference

LaneAward Project Reference

This document is the browser-friendly companion to LANEAWARD_SETUP_LOG.md. The original log remains the chronological source of truth. This reference reorganizes that history into the current architecture, workflows, roles, operating rules, and design decisions so a new maintainer can understand the project without decoding the full timeline first.

Updated through 2026-05-13 Apps: timeontasks.laneaward.com · console.laneaward.com Architecture: Nginx + Python + SQLite + ProfitMaker DBF refresh

Introduction

LaneAward started in this log as a broader server and reporting effort, then narrowed into a workforce and production-floor system, and finally consolidated around two active frontends backed by one shared SQLite database: the contributor-facing Time On Tasks app and the desktop-first Operations Console. The current design intentionally keeps contributor workflow simple while allowing administration, tester onboarding, and later reporting to grow from the same shared data foundation.

The most important architectural decision preserved across the recent changes is the shared database model. Customer and order search, contributor task setup, user management, and future reporting all depend on the same SQLite file and shared API. That keeps the separate interfaces harmonized without duplicating business rules or reference data.

Executive Summary

Table Of Contents

Project Evolution

Phase 1

Initial Server And Reporting Setup

The log begins with a broad LaneAward server build on the Ubuntu VM: Nginx on port 8081, an initial SQLite database named engagement.db, migration scaffolding, and early customer, order, invoice, and reporting prototypes.

Phase 2

Workforce Tracking Pivot

On 2026-03-11 the project pivoted into a dedicated workforce tracking module under workforce_app. This introduced the contributor time-and-materials direction, role hierarchy, task states, and the first SQLite-backed workforce schema.

Phase 3

Time On Tasks + Console Split

By 2026-03-24 the project consolidated into the current two-frontend model: the the Time On Tasks contributor app and the Operations Console, both backed by the same API and SQLite database.

Historical note: the old broad reporting prototype and several demo-only design phases remain important as context, but they are no longer the primary runtime model. The current system should be understood as a workforce platform with shared reference data and role-aware interfaces, not as a general CRM or quoting app.

Current Architecture

Frontend split Time On Tasks at timeontasks.laneaward.com; Operations Console at console.laneaward.com. Staging mirrors at staging.timeontasks.laneaward.com and staging.console.laneaward.com. Legacy /workforce/ redirects to root.
Backend Python HTTP service in workforce_app/backend/server.py.
Shared database workforce_app/data/workforce.db locally. Each server environment has its own isolated database — staging and production do not share a file.
Reference data source ProfitMaker DBF-family snapshots promoted into pm_database, then imported into SQLite.
App Launchpad https://ai.laneaward.com/ — public landing page linking to both production apps, with a Staging tab for developer access.
Shared route rule Canonical API base is /api/; the backend still normalizes legacy /api/workforce/... paths during transition.
Environment parity rule Development, staging, and production should stay aligned on Python version, SQLite version, schema level, refreshed database content, backend code, frontend code, and service configuration so report and workflow behavior stays consistent.
Dev

Mac workspace in /Users/donaldscott/Project-Code/laneaward/repo. Source of truth for app code, docs, scripts, and local SQLite refreshes.

Staging

Validation runtime at staging.timeontasks.laneaward.com and staging.console.laneaward.com. Isolated web roots, database, and backend service. All changes are validated here before being promoted to production.

Production

Live runtime at timeontasks.laneaward.com and console.laneaward.com. Same AWS server as staging, fully isolated. Promoted from staging via scripts/deploy_to_production.sh. App Launchpad at ai.laneaward.com.

Active Applications

Time On Tasks

Audience: manufacturing contributors first, with tightly scoped shop-floor workflow. This is the tablet and mobile-first app used to sign in by PIN, set up workload entries, manage queued and paused tasks, and track active session time.

  • PIN-only tablet sign-in.
  • Fuzzy customer and order lookup backed by imported ProfitMaker reference data.
  • Task Description fuzzy search backed by contributor task templates.
  • Queued and Paused cards support priority sorting when unlocked.
  • Daily total and current runtime are shown in the top strip.
  • Designed to stay usable on iPad-class screens and installed web-app flows.
  • Task Repeat Multiplier: queued-card checkbox enables repeat mode. Contributor sets an action label (e.g. "setup") and quantity (≥ 2, max 3 digits). A badge confirms the config; tap to edit, uncheck with confirmation to remove. On Stop a confirm dialog shows the first-run time and projected total. Completed card shows ↻ Setup × 25 · 3m 12s per run · 80m total.
  • Two-step confirmations: Stop, Pause, and Cancel actions require a window.confirm() before executing to prevent accidental taps on touch screens.

Primary source: /Users/donaldscott/Project-Code/laneaward/repo/timeontasks

Operations Console

Audience: administrative and supervisory roles. This is the desktop-first shell used for user management, staging preparation, task-template assignment, and eventually reporting.

  • PIN-only sign-in.
  • Accounts sheet with role-aware visibility, sortable columns, and contributor-style task access.
  • Selected-account team management for visible memberships plus team manager / lead assignment inside the existing account workspace (Teams feature currently disabled console-wide as of 2026-06-03; see Roles And Permissions).
  • Reports tab with read-only contributor activity summaries and tap-to-open drill-down detail.
  • Selected-account workspace with identity, compensation where allowed, and audited time corrections.
  • View Tasks opens the reusable task-list modal for Team Lead, Foreman, and Manufacturing Contributor accounts.
  • Add user, reset PIN, deactivate user, and correct tracked session times from one shell.
  • Time Corrections modal can also edit repeat-multiplier fields (action, quantity, iteration time, total) when the task was logged as a repeat. Total can be manually overridden; an indicator shows when it diverges from the auto-calculated value.
  • Owner-specific: Reports tab loads first by default; Tax column is hidden in the order table; panel label reads "Tax Collected".
  • Bottom document tabs are role-gated and collapse left without leaving empty tab slots.

Primary source: /Users/donaldscott/Project-Code/laneaward/repo/ops_console

Shared Backend Principle

The contributor app and the console intentionally use different frontend codebases while sharing one backend, one schema, and one SQLite file. This was a deliberate design choice so contributor workflow could stay clean and mobile-first while admin and reporting tools could evolve separately without forking business logic.

Roles And Permissions

The role model evolved from the original OWNER / DEVELOPER / MANAGER / TEAM_LEAD / CONTRIBUTOR hierarchy and now includes Senior Management and Foreman. The current console rules are the most relevant operational version for handoff.

Owner Highest business authority. Only one owner account may exist. Sees active roles only, and intentionally does not see inactive users or the deactivate button in the console. The backend can still expose broad task visibility to this role, but the intended long-term surface for that visibility is the read-only reports experience rather than the contributor tablet workflow. Console-specific: the Reports tab is the default first tab on sign-in; the Tax column is hidden from the Order Profitability table (the "Tax Collected" metric panel is still visible); the panel label reads "Tax Collected" rather than "Taxes Collected".
Developer Same top-level system authority as Owner for management purposes. Sees inactive users and can see every console document tab, including Topology and Runbook.
Senior Management Can create and deactivate all non-owner roles. Does not see Owner. Can use the console correction workflow on eligible accounts and sees User Guide, Reports, and Reference, but not Topology or Runbook.
Manager Can create and deactivate Team Lead, Foreman, and Manufacturing Contributor users. Does not see Owner, Developer, or Senior Management. Can correct tracked time on eligible accounts and sees User Guide, Reports, and Reference.
Team Lead Can create and deactivate Manufacturing Contributor users only. Does not see higher roles. Can correct tracked time on eligible contributor-style accounts within that scope, but sees only the User Guide tab in the console and does not see compensation data there.
Foreman Shop-floor foreman role modeled after an industry quote-shop foreman. This is still a production contributor role, so it can create work, log time, and use contributor task templates in Time On Tasks. In the Operations Console it inherits Team Lead-level permissions, sees only the User Guide tab, and does not see compensation data there.
Manufacturing Contributor Direct production-floor user. Uses Time On Tasks for workload setup, session timing, and task progression.

Console Visibility Rules

  • Owner sees all active roles only.
  • Developer sees all visible users including inactive accounts.
  • Senior Management sees everyone except Owner.
  • Manager sees Manager, Team Lead, Foreman, and Manufacturing Contributor only.
  • Team Lead sees Team Lead and Manufacturing Contributor only.
  • Foreman sees Foreman and Manufacturing Contributor only.
  • Team Lead and Foreman do not see Pay Type or Amount in the Accounts tab or selected-account workspace.

Create / Deactivate / Document Rules

  • Owner and Developer can create all non-owner roles.
  • Senior Management can create all non-owner roles.
  • Manager can create Team Lead, Foreman, and Manufacturing Contributor.
  • Team Lead can create Manufacturing Contributor only.
  • Foreman can create Manufacturing Contributor only.
  • (when Teams feature is enabled) Manager and higher roles can assign visible team-manager and team-lead slots inside the selected-account workspace.
  • (when Teams feature is enabled) Team Lead and Foreman can add or remove Manufacturing Contributor accounts from their visible team scope.
  • Note (2026-06-03): The Teams feature is currently disabled console-wide. The two items above describe behavior when the feature is enabled.
  • Only one Owner is allowed in the system.
  • All console roles can see User Guide.
  • Reports is limited to Owner, Developer, Senior Management, and Manager.
  • Topology and Runbook are limited to Owner and Developer.
  • Reference is limited to Owner, Developer, Senior Management, and Manager.

Current Report Drafts

The Reports tab now hosts two read-only report drafts behind one Report selector. Contributor Task Activity summarizes contributor and team-lead workload without disturbing the shared SQLite data. Order Profitability combines invoice-backed orders and labor-backed orders inside the selected period, then joins invoice-linked order data so the console can show invoice number, taxes collected, ship charge (collected), shipping paid, tracked labor cost, current total cost, retail price, and margin percentage in one place. The Tax column is hidden for the Owner role.

The order profitability draft currently uses these order-level fields from the shared SQLite data: invoice_number, invoice_date, and billed_total as the current retail price field. That retail value is imported from the ProfitMaker invoice amount when available, with a fallback to the order line-total summary when the invoice amount comes through as zero. Orders can show a Retail Price and Margin % even when no formal AR invoice has been posted — this is expected behavior. The billed_total field is sourced from the ProfitMaker order record (ORDER.DBF), which carries the order value from the time the order is created, before an invoice is formally posted in the accounting system. Margin is calculated as (Retail Price − Labor Cost) ÷ Retail Price using that order-level value. If the billed amount is revised before final invoicing, the margin will update on the next reference import. The same report also uses taxes_collected, shipping_collected (labeled "Ship Charge"), and a reserved admin_cost column that is not populated yet. Shipping Paid is now a live computed column sourced from APKGDTL.SHPCHG per order and stored as shipping_paid in sales_order at import time. Rows and the metric panel turn red when shipping paid exceeds shipping collected. See the Shipping Cost research findings below for the fulfillment-lag caveat. Total cost currently reflects tracked hourly labor plus admin cost when it exists. Salary workers contribute to labor cost using their annual salary ÷ 2,080 as an effective hourly rate (Task #9 — complete). Changing Period or either Date Range calendar now refreshes the report data automatically, while the Refresh button remains as a manual retry control.

Metric cards in both reports use the value itself as the hover/tap trigger for drill-down detail — there is no separate note button. On desktop, hovering the value opens the detail popover; on touch, a tap opens it. The grid fits all nine Order Profitability panels on one row at typical laptop widths (100 px minimum per panel).

Time On Tasks Audience Boundary

Time On Tasks should be treated as an operational contributor tool, not a universal workforce dashboard. Manufacturing Contributors, Foremen, and Team Leads are the intended daily users. Owner and Senior Management may still benefit from broad task visibility in the shared backend, but that visibility is being preserved primarily so the future reports view can show the full picture of open work without re-implementing task logic somewhere else.

When a contributor forgets to start, pause, or stop on time, the correction path is intentionally human-first: the contributor reports it to a Team Lead, Foreman, or Manager, and leadership applies the fix in the Operations Console with a required reason and an audit trail.

Shared Data And Imports

Shared SQLite Database

This project deliberately uses one shared SQLite database for all active interfaces. That decision supports a harmonious relationship between contributor workflow, user management, and future reporting.

  • Users, roles, and PIN-based auth come from the same database.
  • Customer and order fuzzy search comes from imported ProfitMaker reference rows.
  • Contributor task templates are stored once and exposed to both console and tablet flows.
  • Future reports are expected to read from the same shared truth.
  • Broader task-data visibility for Owner and Senior Management is being retained in the backend as a reporting foundation, not as a reason for those roles to use the contributor tablet flow.

ProfitMaker Import Purpose

The import path is not trying to clone the full source system. Its current high-value purpose is to keep contributor-facing search useful by importing current customers, order numbers, and ranked order descriptions.

  • Customer fuzzy search is driven by imported customer_account rows.
  • Order fuzzy search is driven by imported sales_order rows.
  • Order descriptions are now ranked from LNITM.DBF so hover/detail text uses real product descriptions.
  • The rolling reference window is refreshed from the newest snapshot date instead of a stale fixed end date.

Daily Snapshot Refresh Workflow

  • Newest asidta_file_* snapshot is auto-detected if no folder is passed.
  • Only changed DBF/FPT/CDX-family files are promoted into pm_database.
  • Duplicate junk files such as Copy of ... are ignored.
  • The importer refreshes workforce.db from the canonical promoted copy.
  • Stale reference-only orders are pruned from the rolling window during import.

Key scripts: refresh_workforce_reference_snapshot.sh, refresh_workforce_reference_data.sh, import_profitmaker_reference.py

Shipping Cost Data — Research Findings (2026-04-14)

A full investigation was conducted into the discrepancy between Shipping Collected and Shipping Paid in the Order Profitability report. Here is what was found and what decisions were made.

The Original Problem

The report initially sourced shipping paid from FFSHPLOG.PLPOST, which captures only USPS postage. 97% of records in that table are $0.00 — it misses UPS, FedEx, and all other carriers entirely. For Jan 1–Apr 14 2026, this produced a shipping paid total of only $352.97 against $8,412.87 collected — a 23× understatement.

The Correct Source

Shipping paid is now sourced from APKGDTL.SHPCHG, which records actual carrier costs (UPS, FedEx, Ground, Air, etc.) per package, linked to orders via PKORDNO. Voided records (VOID = 'Y') are excluded. This is the most complete per-order shipping cost record available in ProfitMaker.

The Remaining Discrepancy — Fulfillment Lag

Even with the correct source, shipping collected and shipping paid will rarely match within a narrow date window. This is a known business reality, not a data error:

  • Customers are invoiced (freight collected) on the order date.
  • Physical shipments may go out weeks or months later, especially for custom manufacturing fulfillment orders.
  • For Jan 1–Apr 14 2026: $3,568 paid (for orders placed in that window) vs $11,368 paid (for packages physically shipped in that window — many belonging to older pre-Jan orders).
  • The report intentionally uses order date as the filter anchor so that revenue and costs stay on the same order and margin calculations remain consistent.

Coverage Gap — Unrecorded Freight

897 of 1,403 orders placed Jan–Apr 2026 have no corresponding APKGDTL record, yet collectively show $7,013 in freight collected. All other ProfitMaker tables were checked (FFSHIPTO, FFSHIPHT, FFSHPLOG, COSTORD) — none contain carrier cost data for these orders. Likely causes:

  • LTL / common carrier freight orders that are invoiced directly by the carrier and never entered back into ProfitMaker.
  • Shipments processed outside the integrated parcel label system (carrier website, manual pickup calls).
  • Will-call / customer pickup orders where a freight fee is collected but no outbound cost is incurred.

Action item for discussion: Determine whether carrier invoices (UPS, FedEx, freight carriers) are reconciled in any external system (accounting software, spreadsheet). If so, that data source could be integrated to give a complete shipping cost picture. Until then, the report reflects the most accurate data available inside ProfitMaker and should be interpreted over longer date ranges (quarterly, annual) where the fulfillment lag averages out.

Contributor Task Templates

A user_task_template table was added so contributor users can have reusable task descriptions associated with them. These are created and managed from the console and then appear as fuzzy-search options inside Workload Setup in Time On Tasks.

A default standard set was seeded on 2026-03-24, including Order Intake, Artwork Preparation, Assembly, Packaging, and Shipping, with room for more contributor-specific templates over time.

Manual Entry Before Sync — Customer & Order Resolution (2026-04-24)

The Time On Tasks workload setup fields accept customer codes and order numbers that are not yet present in the local SQLite database. This is by design, so contributors are never blocked when an order is new, freshly entered in ProfitMaker, or simply not yet reflected in the latest snapshot. The following describes exactly what happens at each stage and what is safe to rely on.

Fuzzy Search Scope

Customer and order search queries hit /api/customers/search and /api/orders/search using LIKE-based fuzzy matching against imported ProfitMaker rows. The search requires at least two characters and returns up to ten ranked matches. If no suggestions appear, the contributor can type the value directly and proceed — a missing suggestion does not block task creation.

Stub Record Creation (Unknown Values)

When a contributor adds a task with a customer code or order number that does not yet exist in SQLite, the backend's ensure_customer_account() and ensure_sales_order() functions (in server.py) silently create lightweight stub records:

  • Stub customer: display_name = "Customer {code}", data_source = "LaneAward".
  • Stub order: description = "LaneAward task queue for order {number}", lifecycle_status = "READY_TO_START", data_source = "LaneAward".
  • Tasks and time sessions are linked to these stubs via the same foreign keys used for real ProfitMaker rows. No special-case logic is needed in the contributor workflow — the task behaves identically whether the order is a stub or a fully imported record.

What Happens at Sync

When the ProfitMaker reference import runs (import_profitmaker_reference.py), all customer and order inserts use ON CONFLICT(source_customer_code / source_order_number) DO UPDATE SET .... If the manually entered code or order number now appears in the ProfitMaker snapshot, the existing stub row is enriched in place: real display name, segment, lifecycle status, invoice number, billing totals, shipping data, and fulfillment mode all fill in. The database primary key (id) does not change. Because all order_task and work_session rows reference order_id and customer_id as foreign keys, they automatically reflect the enriched data the moment the sync completes. No explicit merge, re-link, or data-migration step is required.

Work Already Logged Is Never Lost

The prune_reference_orders function only removes orders that carry reference_only = 1, match the ProfitMaker data source, are absent from the current import window, and have no attached tasks, sessions, or material usage. A stub order that had any work logged against it will never be pruned — the presence of a task or session record is a hard guard.

One Edge Case to Be Aware Of

At task-creation time the server prevents assigning an already-known order to the wrong customer. However, if both the customer code and order number are new (both create stubs), and the typed combination does not match what ProfitMaker later says, the sync upsert will silently correct the customer_id on the order row to the ProfitMaker value. The tasks and sessions already attached to that order will then appear under the corrected customer name. This is the correct outcome in most cases, but it means typed customer-and-order pairs that are genuinely wrong will self-correct at sync rather than producing an error. If accurate attribution before the first sync matters, confirm the customer code against the ProfitMaker source before typing it in.

Reference Window & Order Retention Policy (2026-04-24)

The ProfitMaker reference import uses a rolling date window to keep the local SQLite database focused on current and near-term orders. As of 2026-04-24 this window spans 36 months (extended from the original 12 months) and the prune logic was hardened to also respect lifecycle status. Both changes were made together because either one alone leaves a gap.

Why Both Changes Were Made

The 12-month window was sufficient when all orders completed within a year, but LaneAward serves customers with long-cycle manufacturing orders, standing credits, and unfulfilled inventory that can span multiple years. Two failure modes were identified:

  • Window-only pruning: an order placed 13 months ago with no tasks yet logged gets pruned. If that customer places a new order, contributors see the new order but not the old open one, and the credit or unfulfilled inventory context is invisible.
  • Status-only pruning: if ProfitMaker never formally closes a fulfilled order, that row accumulates in SQLite forever with no automatic cleanup path — an unbounded growth condition.

The Combined Rule Now in Effect

A reference-only order is eligible for pruning only when all three of the following are true:

  • Its order_date falls outside the 36-month rolling window.
  • Its lifecycle_status is SHIPPED — the only terminal state the ProfitMaker import produces. Orders in OPEN or AWAITING_SHIPMENT status are never pruned regardless of age.
  • It has no attached tasks, sessions, or material usage (this guard was always in place).

The 36-month window acts as the baseline safety net and covers the vast majority of real-world order cycles without depending on upstream data quality. The lifecycle status condition provides the additional layer of protection for genuinely long-cycle or anomalous orders that outlive the window.

Why This Is Not Fully Self-Managing — Required Human Step

The lifecycle status gate introduces a dependency on ProfitMaker data accuracy that the automated import cannot resolve on its own. If an order is genuinely fulfilled but was never properly closed in ProfitMaker, it will remain in the LaneAward database indefinitely — the prune guard will protect it forever. Over time, without a periodic human review, this creates an unbounded accumulation of stale open-status rows.

The required human step: on a periodic basis (recommended quarterly), someone with ProfitMaker access should run a review of orders that are marked open or active in ProfitMaker but have had no invoice activity, shipment activity, or production work for an extended period. Orders confirmed as genuinely complete should be formally closed in ProfitMaker. The next reference import will then see the terminal status and allow normal pruning to proceed on the next cycle when the order also falls outside the 36-month window.

This step cannot be skipped or delegated to the import script. LaneAward reads ProfitMaker status — it does not write back to it. The authoritative close action must happen in ProfitMaker first.

Audit Indicators to Watch

  • If the Orders pruned count in the import log is consistently zero across many syncs, it is a signal that orders are not being closed upstream and the stale-open accumulation may be growing.
  • If the Orders upserted count grows significantly beyond the expected 36-month volume without a corresponding business growth explanation, trigger an upstream ProfitMaker status review.
  • The import log line app_user, work_session, order_task, and activity tables were not modified should appear on every reference-only sync. Its absence indicates the sync touched contributor data, which warrants investigation.

Version Control And Source Management

The project is maintained under local Git at /Users/donaldscott/Project-Code/laneaward/repo/. Git is a tool that takes named snapshots of the project files. Each snapshot — called a commit — preserves the exact state of every tracked file at the moment it was taken. Older snapshots remain available indefinitely, which makes it possible to view what the project looked like at any past point, see exactly what changed between any two points, and return the working files to any prior snapshot in seconds.

History

Every commit is preserved. The full project state at any past commit can be inspected with git log and git show.

Rollback

If a change breaks something, git checkout main restores all working files to the most recent main commit. No manual undo.

Branches

Parallel work streams. git checkout -b feature/x creates a new branch for experimental edits without disturbing main. Switching branches is instant.

Local-Only Configuration

The project intentionally uses Option A — 100% local Git. No GitHub, GitLab, or any remote server is configured. All snapshots, history, and branches live inside the .git folder of the repo on the development Mac. Nothing about the source code or its history leaves the machine through Git.

Off-machine history backup is provided through the same channel that already backs up source code: the PWA BDR weekly source snapshot. As long as the snapshot captures the .git folder, history is preserved off-machine. If a private remote (e.g. private GitHub repository) is ever desired for additional backup or collaboration, it can be added with a single git remote add command without changing any other behavior.

Safety By Default

Git does not modify working files unless explicitly told to (commit, checkout, reset). git status and git diff are read-only. Committed snapshots cannot be lost through ordinary operations — only the deliberately destructive commands git reset --hard and git push --force can discard work, and neither is used in this project's workflow.

The .gitignore file at the repo root defines what Git never tracks. Sensitive artifacts (SSH keys, .env files, large binary data such as the pm_database/ DBF tree, OS metadata like .DS_Store) are excluded so they cannot be accidentally committed into history.

Day-To-Day Workflow

The everyday loop is four commands:

git status              # see what changed
git diff                # review the actual edits
git add <file>          # stage the change
git commit -m "..."     # take a named snapshot

Editing files between commits is unchanged from working without Git. Only deliberate save points create commits.

Staging-First Branch Workflow

For changes intended to be validated on staging before reaching production:

git checkout -b staging/some-change
... edit, commit ...
... deploy that branch to staging, test ...
git checkout main && git merge staging/some-change
... deploy main to production ...

If staging surfaces a problem, git checkout main instantly restores the production-equivalent files on the Mac. Nothing about the live environments is affected by branch operations.

Relationship To Deploy Scripts

Deploy scripts (scripts/deploy_*) read from the working tree — the actual files in /Users/donaldscott/Project-Code/laneaward/repo/ — not from Git directly. The currently checked-out branch determines what the deploy scripts see. Before any deploy, confirm the branch is the intended one with git branch --show-current.

Operations And Deployment

Frontend Publish

Each environment has dedicated web roots on the AWS server. Files are promoted via rsync from the Mac repo. Production is deployed using scripts/deploy_to_production.sh or the individual app deploy scripts.

Backend Runtime

The VM service is laneaward-workforce-api.service. Nginx fronts the static apps and proxies API requests to the Python service.

Database Cutover

Staged databases are promoted with the VM cutover script, which validates, backs up, swaps, restarts, and verifies the service.

Current Runtime Status

  • Both staging and production are live on the same AWS server with isolated services, databases, and web roots.
  • The reference dataset includes customer_account = 3245 and sales_order = 4912 rows imported from ProfitMaker.
  • /workforce/ is no longer the main contributor route and now redirects to root.
  • Short 502 Bad Gateway responses observed during backend restarts are restart-window artifacts, not persistent failures.

Server Security And Access

The AWS server is protected by two independent layers of access control. Both layers must be understood before making any changes to server access or network configuration.

Layer 1 — AWS Security Group

IP-Restricted Firewall

A single security group (sg-0cc9719fa0e029c40, named launch-wizard-1) is attached to the instance. It is the only group attached — the default VPC security group is not. SSH (port 22) is restricted to three specific IP addresses. HTTP (port 80) and HTTPS (port 443) are open to the public. All other ports are closed by default.

Layer 2 — Twingate Zero-Trust

Remote Access Without Static IP

A Twingate connector (eggplant-okapi) runs on the Ubuntu server as a systemd service. It dials outbound to Twingate's relay — no inbound port is required. Administrators connect through the Twingate client on their device, which tunnels SSH traffic through the relay to the connector regardless of the administrator's current IP address.

Authorized SSH Sources (Security Group)
COX Fiber — office98.175.1.150/32
COX Cable failover — office72.215.199.214/32
Home lab (dynamic — pending removal)72.208.129.218/32

The home lab IP is a dynamic residential address and should be removed from the security group once Twingate is confirmed as the primary home access path. Twingate eliminates the need for static IP-based home access entirely.

Twingate Configuration
Remote NetworkLane Award PWA Server (AWS)
Connectoreggplant-okapi — Online
ResourceSSH — Lane Award PWA Server
Resource address172.31.7.224 (private IP)

The resource uses the server's private IP, not the public IP. This is required because the connector runs on the same machine — routing through the public IP would loop externally and fail.

ISP Network Details

COX Fiber (primary) Static IP 98.175.1.150 · Subnet 255.255.255.252 · Gateway 98.175.1.149
COX Cable (failover) Static IP 72.215.199.214 · Subnet 255.255.255.240 · Gateway 72.215.199.209
ISP COX Communications, Phoenix AZ · 1G Fiber primary, 512 Mb Cable failover on site

Security Model — Why This Is Secure

SSH port 22 is not exposed to the public internet. Only the three authorized IPs can reach port 22 directly, and all three require a valid .pem key to authenticate — an IP address alone is not enough. Twingate adds a second access path that requires authentication through Twingate's identity layer before any session can be established. Automated scanning attacks and brute-force attempts against port 22 are blocked entirely by the security group before they reach the operating system.

The Twingate connector requires no inbound security group rules of its own — it dials outbound, so the attack surface is not increased by running it. Outbound traffic from the server is unrestricted (required for Twingate relay connections, package updates, and API calls).

AWS CLI Access

Security group changes are made via the AWS CLI using IAM user donald (AmazonEC2FullAccess policy, region us-east-2). Root account access keys are not used. The CLI is configured on the development Mac. AWS CloudShell in the browser console is the fallback when local CLI access is unavailable. See the Production Runbook Process 10 for step-by-step procedures for SSH access and security group updates.

UI And UX Decisions

Contributor-App Decisions

  • PIN-only sign-in keeps the tablet login screen clean and avoids showing user lists.
  • Queued and Paused cards can be reordered only when explicitly unlocked.
  • That unlocked-card behavior now doubles as a management oversight workflow, because managers can review the live workload and reprioritize queued or paused tasks without receiving task-entry or time-logging controls.
  • Workload Setup no longer has its own pin because it did not add enough value.
  • Safari-specific issues with native datalist were resolved by replacing them with custom lookup menus.
  • Hover/detail order text depends on imported real descriptions, not hardcoded placeholders.
  • Top-strip and sign-in spacing were repeatedly normalized to match the visual density of the console.
  • Two-step confirmations (2026-04-14): Stop, Pause, and Cancel use window.confirm() so a mistouch on the shop floor does not silently end a session. Repeat tasks extend this with a calculated-total summary in the confirm text.
  • Task Repeat Multiplier (2026-04-14): per-card checkbox on queued tasks. Badge confirms the saved config; form pre-fills for edits. Completed card shows repeat summary inline. The decision to multiply a single timed iteration rather than require 25 separate start/stop cycles came directly from contributor feedback about repetitive short-cycle work (e.g., laser loading, wood-cutting setup).
  • UI sound feedback (2026-05-02): short audio cues confirm action completion on both iOS and Android. Platform detection selects the correct playback method at runtime. On iOS and Mac the sounds are synthesized entirely in-browser using the Web Audio API (no audio files required; avoids iOS PWA audio-file restrictions). On Android the installed PWA plays Samsung Galaxy OGG notification files served from the app's own sounds/ directory: Start/Resume = Contact, Pause = Meteor, Stop = Nebula, Cancel = Chaos. The Android path is necessary because the Web Audio API gesture-context expires across await fetch() boundaries on Android Chrome; installed-PWA elevated autoplay permissions allow new Audio().play() to fire reliably after async API calls. Cancel received a sound cue for the first time on this release. All gain values were doubled from the original values to improve audibility on shop-floor tablets.
  • Scrollbar layout stability (2026-05-02): overflow-y: scroll on the body element permanently reserves the scrollbar gutter, preventing the page from shifting left when the native scrollbar appears or disappears. The scrollbar track is styled to match the page background so the reserved space is invisible. This fix applies to both Time On Tasks and Operations Console.

Console Decisions

  • Current users table was reworked for tablet and laptop Safari widths to avoid early collapse and right-edge overflow.
  • PINs are managed exclusively through the Reset PIN function; the PIN column was removed from the accounts table after Argon2id hashing was implemented (2026-04-30).
  • Bulk actions are done through checkbox selection, not row-level action buttons.
  • User IDs render as zero-padded three-digit values such as 001.
  • Summary cards and sign-in cards were tightened to reduce wasted vertical space.
  • The runbook quick link is hidden for everyone except Developer.
  • PWA update check system (2026-04-24): both apps now include an About section (About tab in Operations Console; bottom of hamburger menu in Time On Tasks) that displays the app name, version number, update status, and copyright notice. Update detection uses a static version.json file fetched on every load and on tab-focus return. Two update types are supported: a soft reload (network-first cache refresh, no data loss) and a hard reinstall (service worker unregister + full cache clear, required for breaking changes). The reinstall flow will show a modal with step-by-step instructions for re-adding the PWA icon to the home screen on Samsung tablets — implementation pending tablet verification. An orange dot badge on the Time On Tasks hamburger icon surfaces the update notice without requiring the menu to be opened. The version number is maintained in version.json alongside a reinstall boolean flag; bumping that file is the only operator action required to trigger an update for all users.
  • Order Profitability row shading (2026-04-24, revised 2026-05-05): the report table uses simple alternating row shading by row index — every other row receives a light background via customer-group-a / customer-group-b CSS classes. This gives readers a horizontal guide rail across wide rows. The original implementation shaded by customer group (all rows for the same customer shared one shade), but this was replaced with standard index-based alternation because the table is already sorted by Customer# — repeated customer orders appear together naturally and are easy to read without special grouping logic. No backend or schema changes were required.
  • Accounts tab heading and user tab caret (2026-05-05): the Accounts tab heading was updated from "All Current Account Data" to "User Accounts & Profiles" with a revised subtitle focused on the management actions available. A caret indicator was added to the right of the user name in the selected-account tab to make it visually clear that the tab has a dropdown menu with additional sections (Profile, Teams, Time Corrections, Order Number Corrections).
  • Safari WebKit button centering (2026-04-14): Safari ignores text-align: center on <button> elements. Fixed using display: flex; justify-content: center on the button and an inner <span> as the text container. Applies to report metric cards and any future value-trigger buttons.
  • Metric card value-as-trigger (2026-04-14): the separate note-bubble button was removed from report metric cards. The value itself is now the hover/tap target for drill-down detail, reducing visual clutter. The popover position calculation was updated to handle the new class name.
  • Owner default tab (2026-04-14): set at the two explicit sign-in moments (PIN login completion and page-load bootstrap), not inside applyBootstrap() which runs on every save action. This is the correct pattern for any future role-specific default navigation.
  • Scrollbar layout stability (2026-05-02): overflow-y: scroll on the body element permanently reserves the scrollbar gutter so the page does not shift horizontally when switching between tabs with different content heights. The scrollbar track is styled to match the app background. Same fix applied to Time On Tasks simultaneously.

Web-App And Device Fit

The project intentionally treats this as a web app rather than a static mock. A web-app manifest, Home Screen icons, Apple metadata, and iPad standalone refinements were added so tablet install behavior and coarse-pointer layouts remain workable. Full offline support is not implemented, but a service worker update check system was added in April 2026: both apps register a network-first service worker and poll a static version.json file to detect when a new build is available. See the PWA update check system entry in Console Decisions for the full design detail.

Application Authentication Security

Both Time On Tasks and the Operations Console are protected by a 6-digit PIN authentication system. The core security layer (Argon2id hashing, lockout, HTTPS) was implemented 2026-04-30. A login performance fix (HMAC-SHA256 lookup token) was added 2026-05-01. This section documents the full security implementation, the rationale behind each design decision, and the protections now active in production.

Defense Layers — Active
  • Argon2id password hashing — industry-recommended algorithm; PINs are never stored in recoverable form
  • HMAC-SHA256 lookup token — fast O(1) user identification before Argon2id verify; eliminates sequential scan across all users
  • Constant-time comparison — prevents timing-based PIN inference attacks
  • Progressive lockout — failed attempts trigger escalating delays (60s → 5min → 15min → 1hr)
  • 15-minute failure window — attempt counts reset after inactivity
  • Client identity tracking — rate limiting is applied per device IP, not per account
  • Role enforcement at the backend — Operations Console rejects contributor PINs server-side
  • Cryptographically random PIN generation — using secrets.randbelow(), not random
  • PIN uniqueness enforcement — no two active users share the same PIN
  • HTTPS transport — PINs are encrypted in transit via Let's Encrypt (auto-renewing)
Risk Assessment

The applications serve a small, known workforce (maximum 10 active users) on a private manufacturing network. Exposed data is operational — task timers, order status, and team assignments. No financial transactions, no PII beyond employee names, and no external customer access. The threat model is proportionate: the primary risk is unauthorized internal access, not external attack.

This context informed the decision to implement strong hashing and brute-force protection without requiring multi-factor authentication or certificate-based identity — both of which would create operational friction on the shop floor that outweighs the marginal security benefit at this scale.

Why Argon2id

Four hashing algorithms were evaluated: Argon2id, bcrypt, PBKDF2+SHA-256, and scrypt. Argon2id was selected because it is the current recommendation of OWASP, NIST, and the 2015 Password Hashing Competition. Its key advantage over bcrypt and PBKDF2 is memory-hardness: each verification requires a configurable amount of RAM, making large-scale parallel cracking — including GPU and ASIC attacks — significantly more expensive than algorithms that require only CPU time.

Parameter Selection — Benchmarked on Production Hardware

Parameters were determined by running a benchmark on the production server (AWS Graviton ARM64) against a 300ms user-experience budget. Results:

time_cost (t)3 iterations
memory_cost (m)65,536 KB (64 MB per hash)
parallelism (p)1 thread
hash_len32 bytes (256-bit output)
salt_len16 bytes (128-bit, unique per hash)
Measured latency229 ms on production server
Budget headroom~70 ms remaining before 300ms threshold

These parameters meet OWASP's "interactive login" recommendation and provide meaningful resistance against brute-force attacks: at this cost, an attacker can attempt approximately 4–5 hashes per second on comparable hardware — requiring hours per user to exhaust the full 1,000,000 PIN space offline.

Migration Strategy — Zero Downtime, Zero User Disruption

Existing plaintext PINs were not force-migrated. Instead, the backend uses a dual-path approach: when a user logs in, the stored value is inspected. If it is a 6-digit plaintext PIN, it is verified using the original constant-time comparison, then immediately replaced with an Argon2id hash before the response is returned. If it is already a hash, Argon2id verification is used directly. This means every user's PIN is automatically and silently upgraded on their first login after deployment — no reinstall, no reset, no user action required.

Login Performance — HMAC-SHA256 Lookup Token (2026-05-01)

After deploying Argon2id hashing, a performance regression was identified: the anonymous PIN login path (authenticate_active_user_by_pin()) loaded all active users and ran a full Argon2id verify on each one sequentially until finding a match. With 13 active users and 229 ms per verify, the worst case was approximately 3 seconds. This was confirmed by live timing (2.97 s for a user near the end of the list).

The fix adds a HMAC-SHA256 lookup token stored alongside the Argon2id hash in a new pin_token column. The token is computed as HMAC-SHA256(PIN_PEPPER, normalized_pin) using a fixed server-side pepper constant (PIN_PEPPER = "laneaward-pin-pepper-v1"). Login now works in three steps:

  1. Compute the HMAC token for the entered PIN — under 1 ms
  2. SQL lookup WHERE pin_token = ? AND is_active = 1 via a partial index — under 1 ms
  3. One Argon2id verify on the matched row — 229 ms

Total login time is now approximately 230 ms regardless of how many active users exist. Measured in production: 258 ms.

Transition: Existing users have pin_token = '' after deploy. On their first login, the fast path finds no match, and a slow fallback scans only un-migrated users (WHERE pin_token = ''). The fallback writes the token immediately, so the user takes the fast path on every subsequent login. The fallback pool shrinks with each login until it is empty.

PIN uniqueness check: allocate_unique_pin() was updated to query WHERE pin_token = ? instead of the raw hash column, restoring correct collision detection after hashing made the old WHERE pin_code = ? check meaningless.

Important: PIN_PEPPER must never be changed after first deploy. Changing it invalidates all stored tokens and forces every user back to the slow fallback path until they log in again. It is a named constant with an explicit comment to this effect in server.py.

AWS Security Group — Hardened Same Day

As part of the same session, the AWS security group (sg-0cc9719fa0e029c40) was reviewed and port 80 (HTTP) was removed. The server now accepts inbound traffic on port 443 (HTTPS, open to public) and port 22 (SSH, restricted to three named IPs only). Port 80 was not in use — all traffic is served over SSL — and the open rule represented an unnecessary exposure.

Testing and Deployment

2026-04-30 — Argon2id hashing: Deployed to staging first and validated end-to-end: PIN login via Time On Tasks, PIN login via the Operations Console, hash verification in the SQLite database confirming Argon2id storage, and a second login confirming the verify path. Both applications were tested on tablet hardware (Samsung) and desktop. All active user accounts were updated through normal login or manual PIN reset within the same session.

2026-05-01 — HMAC lookup token performance fix: Deployed to staging and validated with a timed curl test (256 ms, down from 2.97 s). Token population was confirmed in the staging SQLite database for all five test accounts, with HMAC values independently verified against PIN_PEPPER. Production deployment followed immediately; measured login time 258 ms.

Pending Implementation Tasks

The following items are approved in direction but not yet implemented. They represent the next logical development phase before the system is considered production-ready for full go-live.

Task #9 — Salary Workers ✓ Complete

Salary Labor Cost Calculation

Implemented 2026-04-19. Salary workers now contribute to labor cost in the Order Profitability report using the U.S. BLS standard: Annual Salary ÷ 2,080 = effective hourly rate, applied against tracked session minutes. No schema changes were required — the existing pay_type and pay_rate fields on app_user were sufficient. The user_pay_rate_history table was not needed for this phase. The "Pending" placeholders have been removed from the console UI. Base wage only — burden rate (taxes, benefits, ~25–35%) is documented as a future discussion item.

Task #11 — Login Performance ✓ Complete

HMAC PIN Lookup Token

Implemented 2026-05-01. After Argon2id hashing was added, the anonymous login path degraded to ~3 seconds because it scanned all active users sequentially. Fixed by adding a pin_token column (HMAC-SHA256 of the PIN against a server-side pepper) that allows the backend to identify the correct user with a single indexed SQL lookup before running one Argon2id verify. Login time is now ~230 ms regardless of user count, measured at 258 ms in production. The pin_token column and partial index are applied automatically via ensure_schema_upgrades() on service startup — no manual migration required.

Task #10 — SQLite Hardening ✓ Complete

Database Resilience

Phase 1 hardening is complete and validated. WAL mode enabled; busy_timeout = 10000; synchronous = FULL; multi-step writes wrapped in BEGIN IMMEDIATE transactions; contention returns retryable HTTP 503. The backend surfaces a clean 503 on lock contention rather than an unhandled exception.

Phase 2 (client-side retry/backoff, offline-safe write handling, idempotent write keys) was evaluated and deferred. A concurrent stress test at 2× the expected user load passed cleanly with significant headroom — Phase 1 alone is sufficient at current scale. Phase 2 should be reconsidered only if load grows significantly beyond current projections.

Pay Rate History

Option B — Rate History Table

Selected approach before go-live: add a user_pay_rate_history table where each rate change is an INSERT (effective date + amount + pay type), and a denormalized cache column on app_user holds the current rate for fast reads. Reports join to the history table to find the rate in effect at the time each work session started. This enables accurate annual and semi-annual labor cost reports even after pay rate changes.

Task Repeat Multiplier — Schema Columns Added

Four nullable columns were added to order_task as part of the repeat multiplier feature (2026-04-14). All four use ADD COLUMN with no NOT NULL constraint so the migration runs without a table rebuild:

  • repeat_action TEXT — label set by the contributor (e.g. "setup")
  • repeat_quantity INTEGER — number of iterations (≥ 2); NULL = not a repeat task
  • repeat_iteration_minutes REAL — actual duration of one timed run, written at Stop
  • repeat_total_minutes REALround(repeat_iteration_minutes × repeat_quantity, 2); can be manually overridden by a manager in the Operations Console Time Corrections modal

The schema migration must be run via ALTER TABLE on both the staging and production databases before deploying any backend changes that reference these columns.

Handoff Priorities

1. Preserve Shared Truth

Do not split the contributor app, console, and reporting logic onto separate databases unless there is a compelling operational reason. The project is cleaner because user records, task templates, and reference search all live in one place.

2. Keep Docs In Sync

Update both the chronological log and the browser-facing docs whenever routes, roles, scripts, or workflows change. The handoff burden grows quickly if changes land only in chat or in code comments.

3. Treat Staging As Real

Use the VM cutover and status scripts, refresh reference data before testing, and validate the actual staged app behavior on Safari and tablet-class devices before treating a change as ready.

Recommended next steps before go-live: complete the Pay Rate History table (Option B) and validate the Task Repeat Multiplier end-to-end on staging. This reference page is intended to be the first-stop overview for anyone inheriting the project.