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.
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.
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.
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.
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:
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:
- Compute the HMAC token for the entered PIN — under 1 ms
- SQL lookup
WHERE pin_token = ? AND is_active = 1 via a partial index — under 1 ms
- 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.