LaneAward Operations

Time On Tasks & Operations Runbook

Quick-reference procedures for operating and maintaining the live production environment. For staging operations, development workflows, and the full process library see the Staging Runbook in the staging console.

Time On Tasks: timeontasks.laneaward.com Console: console.laneaward.com App Launchpad: ai.laneaward.com
Production Environment Reference
Time On Tasks URLhttps://timeontasks.laneaward.com
Console URLhttps://console.laneaward.com
App Launchpadhttps://ai.laneaward.com
ToT web root/var/www/laneaward-timeontasks/
Console web root/var/www/laneaward-console/
Database/var/lib/laneaward/workforce.db
Backend servicelaneaward-workforce-api.service · port 9194
App source/opt/laneaward/
TLS cert/etc/letsencrypt/live/timeontasks.laneaward.com/ — covers both domains, auto-renews
API healthhttps://timeontasks.laneaward.com/api/health
SSHssh -i ~/.ssh/lane_webserver.pem ubuntu@3.130.69.109
Production Is Live — Real Users, Real Data

Every operation here targets the live production database and services. There is no undo for destructive commands. Before any operation that writes, restarts, or replaces data, ask: have I taken a snapshot and confirmed this is the right environment?

Never run database reset or activity wipe commands in production. Those procedures exist only in the staging runbook and are intentionally excluded here.

Source Code Is Tracked With Local Git

The repository at /Users/donaldscott/Project-Code/laneaward/repo/ is under local Git version control. Deploy scripts read from the current working tree, so the branch that is checked out at deploy time determines what gets pushed to production. Before running any deploy, confirm the intended branch with git branch --show-current — production deploys should generally run from main. Full background is in the Version Control And Source Management section of the Project Reference document.

Contents

Processes
  • 1 — Deploy Approved Changes To Production The only authorized path for promoting tested changes to production. Deploys both frontends (Time On Tasks and Operations Console) plus all four console documents in one run, then verifies health of both live URLs. Does not restart the backend or touch the database. Includes a quick-reference table of all deploy scripts — verify staging is healthy before starting.
  • 1B — Deploy Console Documents To Production Deploys only the four console documents (runbook, user guide, project reference, topology) without touching app code, the database, or the backend service. Use any time only documents have changed.
  • 1C — Maintenance Mode Takes an app offline behind a holding page and brings it back, using the same command in both directions. The real page is preserved automatically, so restoring service needs nothing to be remembered or redeployed.
  • 2 — Backup and Recovery The three layers protecting production: offsite copies pushed by the server to Amazon S3 four times daily, local copies pulled onto the Mac every 12 hours, and machine images of the whole server. Explains where each lives, how to confirm it is working, and why the offsite copies cannot be destroyed even by a fully compromised server. Always take a manual snapshot before any risky change — there is no undo.
  • 2B — Restore Production Database From Backup Full guided restore procedure using restore_production_db.sh. Auto-selects the most recent backup or accepts a specific file. Requires two typed confirmations, takes an automatic safety backup, stops the service, restores the database, and verifies health. The service is never left stopped — recovery is attempted on failure.
  • 2C — Rebuild The Server From A Machine Image Recovery when the server itself is lost rather than the data. Covers which image to start from, restoring current data on top, moving the reserved public address, and the two things that catch people out — the rebuilt machine claiming the office connector identity, and the public address not following automatically.
  • 2D — Operating System Updates Security patches install themselves, but the restart that activates them does not happen on its own — so the real risk is a reboot that has been silently owed for months. Take an image, rehearse the update on a disposable copy, apply to production in a quiet window, verify by boot time rather than by reconnecting, then take a second image.
  • 3 — Health Check and Status Quick curl and systemctl commands to confirm the production API and both app URLs are responding correctly. Run after every deploy and any time application behavior seems off.
  • 6 — Backend Service Commands Manual restart, stop, start, and enable-check commands for the production backend service. Deploy scripts handle restarts automatically — use these only for manual intervention. Always confirm the correct service name before running; two services share this VM.
  • 7 — Update SQLite From ProfitMaker Files Reference data (customers, orders, descriptions) is refreshed automatically every night by the ProfitMaker pipeline, which imports staging first and then production. This section explains how the nightly refresh works, how to check it, the interim drop-a-zip step, and the retired manual scripts kept only as an emergency fallback. Never replaces the database file or touches users, sessions, or activity.
  • 8 — Reliability Verification For Task Writes Documents the Phase 1 database reliability features in production (WAL, busy timeout, synchronous FULL, BEGIN IMMEDIATE) that must be preserved on every backend update. Includes a manual checklist for verifying write behavior under concurrent load.
  • 9 — Service Worker Deploy and Cache Invalidation Explains the production cache strategy (network-first for HTML, cache-first for versioned assets, network-only for API). Covers when sw.js must be updated and how to prevent stale content from being served to tablets after a deploy.
  • 10 — Concurrent Stress Test Simulates up to 20 concurrent tablet operators against production. Run after significant backend changes or scale increases. Zero hard failures required to pass — retryable busy responses are acceptable.
  • 11 — Server Access And Security Group Management How to reach the server — through the public address from an authorized location, or through Twingate from anywhere. Covers Security Group rule updates when an authorized address changes, and documents what each of the three permitted addresses is for. One of them is the development Mac's own address and carries all automated tooling: read that table before removing anything. Never remove all SSH rules simultaneously.
  • 12 — Verify PIN Hash Status Confirms all active user PINs are Argon2id hashed and that HMAC login tokens are populated. Users missing a token self-heal automatically on their next successful login — no manual action required.
  • Salary Labor Cost — Calculation Methodology

Process 1: Deploy Approved Changes To Production

Use this process when changes have been validated on staging and are ready to go live. This is the only authorized path for promoting code and data to production.

Use The Right Script For Every Task

Each deploy task has a dedicated script. Using the wrong script can destroy production data. In particular, promote_staging_db_to_production.sh replaces the entire production database with staging — it must only be used for schema or structural database changes and includes a mandatory double-confirmation gate. For all routine work, use the targeted scripts below.

Task Correct Script
Both frontends + production documents deploy_to_production.sh
Time On Tasks frontend only deploy_tot_to_production.sh
Operations Console frontend only deploy_console_to_production.sh
Console documents only deploy_console_docs_to_production.sh (Process 1B)
Backend code only (server.py or schema.sql changed) deploy_backend_to_production.sh (Step 5)
ProfitMaker reference data update Automated nightly — no script to run (Process 7); emergency fallback only
Icons only deploy_icons_to_production.sh
Schema or structural database change only promote_staging_db_to_production.sh — requires double confirmation
Critical Rules
  • Staging is the source of truth. Only changes fully tested on staging should be promoted. Never edit production files directly.
  • For deploys that include schema or structural changes, the staging database is promoted to production via sqlite3 .backup — no shutdown required. Reference data (customers, orders) and all contributor activity data are never cross-promoted — each environment's operational data is always managed independently.
  • Staging is preserved. Deploying to production does not affect staging. Both environments continue to operate independently after the deploy.
  • Backend updates require a separate deploy. If server.py or schema.sql changed, use Step 5 (deploy_backend_to_production.sh) — not the main deploy script. It uploads backend files, restarts the service, and verifies health in one run.
What deploy_to_production.sh Does
  • Uploads timeontasks/ and promotes to /var/www/laneaward-timeontasks/
  • Uploads ops_console/ and promotes to /var/www/laneaward-console/
  • Verifies and deploys production documentation to the console web root — no transformation, production docs are purpose-built
  • Prints a live health check for both production URLs
  • Does not promote the database or restart the backend service. Use promote_staging_db_to_production.sh for DB promotion and Process 6 for service restart.

Step 1 — Confirm staging is in a deployable state

Use the Staging Runbook Process 3 (Health Check and Status) to confirm staging is healthy and the changes you intend to deploy have been tested end-to-end before proceeding.

Step 2 — Optional: take a manual production DB snapshot before deploying

Recommended before any deploy that includes schema changes or significant data migrations.

[VM]
sudo cp /var/lib/laneaward/workforce.db /var/lib/laneaward/workforce-pre-deploy-$(date +%F-%H%M%S).db

Step 3 — Run the deploy script

Run from the repo root on the Mac. The script uploads both frontends, deploys production documents, and verifies health. It does not touch the database or restart the backend.

[Mac-local]
/Users/donaldscott/Project-Code/laneaward/repo/scripts/deploy_to_production.sh

A clean deploy finishes with:

=== Verifying production health ===
timeontasks : {"ok": true, "status": "ok", ...}
console     : {"ok": true, "status": "ok", ...}

=== Deploy complete ===
  https://timeontasks.laneaward.com
  https://console.laneaward.com

Step 4 — Schema change only: promote the staging database to production

Only run this step if the deploy includes a schema or structural database change. This script replaces the entire production database with the staging database. It requires a manual backup, two explicit confirmations, and restarts the backend service automatically. Do not run this for frontend-only deploys, ProfitMaker updates, or any routine task.

[Mac-local]
/Users/donaldscott/Project-Code/laneaward/repo/scripts/promote_staging_db_to_production.sh

The script will display both database file sizes, require you to type PROMOTE then YES, take an automatic timestamped backup, and verify health before finishing.

Step 5 — Backend-only deploy (only when server.py or schema.sql changed)

If backend code changed, use the dedicated backend deploy script. It uploads server.py and schema.sql, promotes them to /opt/laneaward/workforce_app/backend/, restarts the service, and verifies health in a single run. Schema migrations (new columns, indexes) are applied automatically by ensure_schema_upgrades() on startup — no manual SQL required.

[Mac-local]
bash /Users/donaldscott/Project-Code/laneaward/repo/scripts/deploy_backend_to_production.sh

A clean run finishes with the service showing active and a live health response:

=== Production backend deploy complete ===
  https://timeontasks.laneaward.com/api/health

Step 6 — Individual app deploys (frontend only, no DB)

For routine frontend updates that do not require a DB promotion or service restart:

[Mac-local — Time On Tasks only]
/Users/donaldscott/Project-Code/laneaward/repo/scripts/deploy_tot_to_production.sh

[Mac-local — Console only]
/Users/donaldscott/Project-Code/laneaward/repo/scripts/deploy_console_to_production.sh
Do Not Confuse Production and Staging Services

Two backend services run on the same VM — one for staging, one for production. Always confirm you are targeting laneaward-workforce-api.service (production) and not the staging service before running any systemctl command.

Process 1B: Deploy Console Documents To Production

Use this when only documents have changed — runbook, user guide, project reference, or environment topology. This script deploys documents only and does not touch app.js, index.html, the database, or the backend service.

Documents deployed by this script:

Deploy: Run The Production Console Docs Script

Run from the repo root on the Mac. The script verifies all source files exist, sanity-checks that the production runbook contains no staging infrastructure references, uploads all four documents, promotes them to the production console web root, and cleans up temp files.

[Mac-local]
bash scripts/deploy_console_docs_to_production.sh
When To Use This vs. deploy_console_to_production.sh

Use deploy_console_docs_to_production.sh any time only documents changed. Use deploy_console_to_production.sh when console app code (app.js, index.html) also changed — that script deploys both the frontend and documents in a single run.

Process 1C: Maintenance Mode

Puts an app behind a holding page and brings it back afterwards. Use it when an app must be visibly unavailable for a while — a migration, an incident, or work that would confuse anyone who reached the app mid-change.

Turning This On Stops Work

With Time On Tasks behind the holding page, contributors on the tablet fleet cannot record anything. Timers already running are unaffected — the backend and database keep running throughout, only the page is replaced — but nobody can start, stop, or change a task until the app is back. Tell the production manager before using this during a shift.

Turn It On
[Mac-local]
bash /Users/donaldscott/Project-Code/laneaward/repo/scripts/maintenance_mode.sh timeontasks on

Asks for a typed confirmation, preserves the live page on the server, puts the holding page in place, then fetches the live address and confirms the page being served is actually the one it deployed.

Turn It Off
[Mac-local]
bash /Users/donaldscott/Project-Code/laneaward/repo/scripts/maintenance_mode.sh timeontasks off

Puts the preserved page back with its original ownership, verifies what is being served, and only then removes the preserved copy. No confirmation is asked for — restoring service is never the dangerous direction.

Things Worth Knowing
  • Both arguments are required and there are no defaults. Replace timeontasks with console to do the same for the Operations Console.
  • Running on twice is safe. The second run refuses and changes nothing, so the preserved page can never be overwritten with the holding page.
  • Running off when the app is not in maintenance mode refuses and tells you which deploy script to use instead.
  • The holding page is taken from the repository, not from the server, so it cannot go missing during unrelated tidying.
  • If a restore ever fails verification, the preserved copy is deliberately left in place at index.html.live-backup in the app's web root, and can be copied over index.html by hand.

Process 2: Backup and Recovery

Production is protected by three independent layers. They are deliberately different from one another: each one survives a failure that would defeat the others.

LayerWhat it protectsRuns onSchedule
Offsite database copies Both databases, compressed, in Amazon S3 The AWS server itself 4× daily — 19:00, 01:00, 07:00, 13:00 Arizona
Local database copies Both databases, on the development Mac The Mac Every 12 h — roughly 08:20 and 20:20 Arizona
Machine images The whole server — operating system, packages, configuration, certificates Amazon EC2 On demand, around update cycles

Layer 1 — Offsite Copies To Amazon S3

The server backs itself up and pushes the result outward. The Mac plays no part, so this layer keeps working whether or not the Mac is switched on.

Where It Lives
  • Bucket: laneaward-db-backups-364410974402 (region us-east-2)
  • Keys: production/workforce_<UTC timestamp>.db.gz and staging/…
  • Script: /opt/laneaward/backup/s3_db_backup.py
  • Timer: laneaward-s3-backup.timer
  • Log: /var/log/laneaward-s3-backup.log
  • Retention: copies expire after 365 days, superseded versions after 90
Why It Is Safe From A Compromised Server

The server authenticates using an attached identity role, so no password or access key is stored anywhere on disk. That role grants permission to write new backups and nothing else — it cannot delete, read, or even list what is already in the bucket.

Combined with bucket versioning, this means an attacker who takes full control of the production server still cannot destroy or steal the backup history. Ransomware that encrypts the server cannot reach the archive.

Check It Is Working
[VM]
systemctl list-timers laneaward-s3-backup.timer --no-pager
sudo tail -20 /var/log/laneaward-s3-backup.log
sudo journalctl -u laneaward-s3-backup.service -n 20 --no-pager

Check the file log first. The systemd journal is written to the same disk as everything else, so when the disk is full journald stops recording and journalctl shows a clean, gap-free history for a period in which jobs were actually failing. That happened on 22 August 2026: an offsite backup run failed at 08:00 and the journal showed no trace of it, while the file log recorded the error correctly. The file log is written by the job itself and survives.

A healthy run logs one line per database ending in run finished OK (2 databases). To force a run at any time:

sudo systemctl start laneaward-s3-backup.service
If It Fails, You Will Be Emailed

A failed run automatically emails donald@laneaward.com with the recent log and the commands needed to investigate. The alert has been tested by deliberately breaking the job and confirming the message arrived. Silence means the backups are running.

Layer 2 — Local Copies On The Development Mac

The PWA BDR service — a menu bar application on the Mac, shown as a 🚀 icon — pulls copies down every twelve hours. Open the icon to see status, trigger a manual run, or adjust schedules. This layer is what the guided restore procedure in Process 2B uses, because a local file restores faster than anything fetched over the internet.

Where It Lives
  • Production database → ~/projectbackups/laneaward/production-database/
  • Staging database → ~/projectbackups/laneaward/staging-database/
  • Source snapshots → ~/projectbackups/laneaward/source/ (weekly)
  • Unified log → ~/projectbackups/backup_logs/backup.log
  • Retention: the 14 most recent copies of each database
How It Reaches The Server

The jobs connect through the laneaward-vm entry in the Mac's SSH configuration, which points at the server's public address. The connection depends on the security group rule for the Mac's own address — see Process 11 — and on the Mac being awake.

[Mac-local] Confirm recent runs
grep "LANEAWARD/VM" ~/projectbackups/backup_logs/backup.log | tail -6

Layer 3 — Machine Images

The database layers protect your data. They do not protect the machine — the operating system, installed packages, service definitions, web server configuration and certificates. A machine image captures all of that, so the server can be rebuilt rather than reconstructed from memory. See Process 2C for how to use one.

Manual Pre-Operation Snapshot (before any risky change)
[VM]
sudo cp /var/lib/laneaward/workforce.db /var/lib/laneaward/workforce-$(date +%F-%H%M%S).db
Restoring Production From Backup

Use the dedicated restore script — see Process 2B. The script handles confirmation, safety backup, service stop/start, and health verification in a single guided run. Do not restore manually.

Process 2B: Restore Production Database From Backup

Use this process when production data must be recovered from a Mac-local backup. The script is fully guided — it shows you the backup and current database file sizes, requires two explicit typed confirmations, takes an automatic safety backup before making any changes, and verifies health after the restore completes.

This Is A Last Resort — Understand The Impact First

Restoring from backup permanently destroys all production data recorded after the backup timestamp. Every work session, task, and contributor activity logged since that point will be gone. Before running this script, confirm:

  • The correct backup file has been identified (check the timestamp carefully).
  • The data loss window is understood and accepted.
  • No alternative — such as a correction or targeted SQL fix — would resolve the issue.
  • SSH to the server works from this Mac (verified in Step 1 below).
First Decide: Is This An Accident Or An Attack?

This process is the right response to accidental damage — a mistaken bulk edit, a bad import, a corrupted file. It is the wrong first response if the data was changed by someone who should not have had access.

For anyone to alter the database directly they needed either application access, through a stolen credential or a flaw, or operating-system access on the server itself. In the second case the machine is compromised, not just the data. Restoring a clean database onto a compromised machine achieves nothing: whatever foothold allowed the change is still there, and the restored data can be altered again within minutes.

If tampering is suspected rather than accident, stop here and go to Process 2C. Rebuild the machine from an image first, then restore data on top of the rebuilt server.

  • The offsite archive is the source to trust in this situation. Copies pushed to Amazon Simple Storage Service are append-only and versioned — the server can add backups but cannot read, alter, or delete them. A fully compromised server therefore cannot destroy its own recovery points.
  • Ransomware is loud; tampering is quiet. An encrypted database will not open at all, which is obvious immediately. The dangerous case is small targeted edits that leave a perfectly working database behind. Nothing in the current setup raises an alarm for that — it is found by someone noticing a number that looks wrong.
  • A structural check will not detect tampering. PRAGMA integrity_check validates the internal consistency of the file. It reports ok on a database where a role has been escalated or a labor total rewritten. Use it to confirm a file is not corrupt, never to confirm data is trustworthy.
  • Work backwards to find a clean copy. Compare the candidate backups by record counts and by the most recent invoice date rather than by file size, and restore the newest one that predates the damage.
What The Script Does
  • Shows the selected backup file and the current live database side by side
  • Lists the five most recent backups available for reference
  • Requires you to type RESTORE then YES — two separate gates
  • Uploads the backup to the server before stopping anything
  • Takes an automatic timestamped safety backup of the current production database
  • Stops laneaward-workforce-api.service
  • Clears any leftover write-ahead log before the swap — see the note below
  • Copies the backup into place and sets correct ownership
  • Restarts the service and runs a live health check
  • Restarts the service even if the restore fails — server is never left stopped
Backup Files
  • Location: ~/projectbackups/laneaward/production-database/
  • Pattern: workforce_vm_prod_YYYYMMDDTHHMMSS.db
  • Schedule: every 12 hours via PWA BDR service
  • Retention: 14 most recent copies kept
[Mac-local] List available backups
ls -lht ~/projectbackups/laneaward/production-database/
Why The Write-Ahead Log Is Cleared Before The Swap

The database runs in write-ahead logging mode, which means recent changes can be sitting in a companion log file rather than in the database file itself. That log is validated structurally — by its own internal markers — and not against the contents of any particular database. A log left behind from the old file is therefore treated as valid beside the restored one, and its contents are applied on top of it.

The result would be a database that appears restored and quietly is not, carrying back exactly the changes the restore was meant to remove. If those changes were made by an intruder, they return with everything else — not as running code, since the log holds only data, but as the same edits applied a second time.

Stopping the service cleanly normally folds the log into the database and removes it, so this only becomes reachable when the service was killed rather than stopped, or another process still held the database open — which is to say, during the kind of incident that leads to a restore in the first place. The script now removes the log as part of the swap, which is safe because a restore already discards the current state deliberately.

Step 1 — Confirm SSH to the server works

The restore script connects through the laneaward-vm entry in the Mac's SSH configuration, which points at the server's public address. No tunnel or client software needs to be running. Confirm the connection before going any further:

[Mac-local]
ssh laneaward-vm "echo SSH OK"

If this times out, the cause is almost always the security group rather than the server — see Process 11 to confirm this Mac's address is still authorized.

Step 2 — Run the restore script

Run with no argument to auto-select the most recent backup, or pass a specific backup file path:

[Mac-local — most recent backup]
bash /Users/donaldscott/Project-Code/laneaward/repo/scripts/restore_production_db.sh

[Mac-local — specific backup file]
bash /Users/donaldscott/Project-Code/laneaward/repo/scripts/restore_production_db.sh ~/projectbackups/laneaward/production-database/workforce_vm_prod_TIMESTAMP.db
A Restore Signs Everybody Out

Login sessions live in the database like everything else, so they are replaced along with it. Any session created after the backup was taken simply ceases to exist, and the person holding it is signed out the moment the service comes back. During working hours this means the entire tablet fleet is signed out at once, and anyone part-way through a task lands on a login screen instead.

Contributors can sign straight back in with their usual number — nothing is broken and no action is needed on the tablets. The problem is purely that it is unexpected. Tell the production manager before restoring during a shift, so the floor is told it is coming rather than discovering it.

Running timers follow the same rule as all other data. A timer that was already open when the backup was taken comes back open and carries on. A timer started after the backup was taken is gone entirely, along with the work recorded against it — so a contributor may find that part of a shift has disappeared. That is the data-loss window described at the top of this process, seen from the floor's point of view.

A clean restore finishes with:

  Restore complete. Production is live and healthy.

  Restored from  : ~/projectbackups/laneaward/production-database/workforce_vm_prod_TIMESTAMP.db
  Safety backup  : /var/lib/laneaward/workforce-pre-restore-TIMESTAMP.db

Step 3 — Verify and clean up

A health check only proves the service started. Confirm the data is what you expect before deleting the safety backup — that is the point of no return.

[Mac-local]
curl -sS https://timeontasks.laneaward.com/api/health

Then compare record counts against the backup you restored from. Counts across every table are the right instrument here:

[VM] Record counts in the restored database
sudo sqlite3 /var/lib/laneaward/workforce.db "select 'customers', count(*) from customer_account union all select 'orders', count(*) from sales_order union all select 'sessions', count(*) from work_session union all select 'users', count(*) from app_user;"

[VM] Newest invoice date and the reference-data import stamp
sudo sqlite3 /var/lib/laneaward/workforce.db "select max(invoice_date) from sales_order; select last_imported_at_utc from profitmaker_import_manifest;"
Do Not Compare Checksums

A correct restore will not produce a database matching the backup's checksum, and chasing that difference wastes time during an incident. Two causes, both harmless: the database engine does not lay bytes out identically when a file is copied and reopened, and the moment anyone signs in to check the result a new login session is written, genuinely changing the file. Compare record counts and dates instead.

Expect one legitimate difference in the login session table for each person who has signed in since the restore. If a count differs anywhere else, identify the specific rows before concluding the restore failed.

The strongest confirmation is not a query at all — open the Operations Console profitability report for a date range you know well, and check the totals read the way they should. That is the same view the management reports draw on, and it catches problems a record count cannot.

Once confirmed, delete the server-side safety backup:

[VM]
sudo rm /var/lib/laneaward/workforce-pre-restore-TIMESTAMP.db
If The Health Check Does Not Return Ok After Restore
  • Check the service status: ssh laneaward-vm 'sudo systemctl status laneaward-workforce-api.service --no-pager'
  • Check the service log: ssh laneaward-vm 'sudo journalctl -u laneaward-workforce-api.service -n 50 --no-pager'
  • The safety backup is on the server at /var/lib/laneaward/workforce-pre-restore-TIMESTAMP.db — it can be used to roll back by running the restore script again with that file as the argument.

Process 2C: Rebuild The Server From A Machine Image

Use this when the server itself is lost or damaged beyond repair, rather than when only the data is wrong. Restoring a database into a broken machine solves nothing; this rebuilds the machine.

Available Images
ImageWhat it contains
laneaward-prod-20260812-pre-update The server before the August 2026 operating system update. Kept as a fallback.
laneaward-prod-20260812-post-update The current known-good state. Start here.
What An Image Does And Does Not Give You

It restores the operating system, installed packages, service definitions, web server configuration and certificates — everything that would otherwise have to be rebuilt from memory.

The databases inside it are only as recent as the image. Always restore current data on top, from Amazon S3 or from the Mac's copies.

Two Things That Will Catch You Out

The new server will claim the office network connector identity. Every image carries the Twingate connector, enabled and configured with the existing server's credentials. A machine started from an image will authenticate as that same connector, so two machines end up sharing one identity. Before or immediately after first boot, either stop it — sudo systemctl mask --now twingate-connector — or reinstall it with its own identity.

The public address must be moved deliberately. The address 3.130.69.109 is a reserved address attached to the current server, not to any machine started from an image. Nothing reaches the rebuilt server on the normal names until that address is reassigned to it. Once reassigned, no domain name changes are needed.

Step 1 — Start a machine from the image

In the EC2 console, launch a new instance from the chosen image. Match the existing server: instance type t4g.micro, the same subnet, and the key pair lane_webserver. Attach the identity role laneaward-backup-profile so the new machine can continue writing offsite backups.

Step 2 — Silence the connector and verify the machine

Connect using the existing key and confirm the applications came up before exposing the machine to anyone.

[VM]
sudo systemctl mask --now twingate-connector
systemctl is-active nginx
systemctl list-units --type=service --no-pager "laneaward-*"
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1/

Step 3 — Restore current data on top

The databases in the image are stale by definition. Bring across the most recent copies before putting the machine into service — from the Mac using Process 2B, or from Amazon S3 for the newest available copy.

Step 4 — Move the public address

Reassign the reserved address 3.130.69.109 from the old server to the new one. Domain names and certificates then resolve correctly with no further changes. Verify from outside:

[Mac-local]
curl -s -o /dev/null -w "%{http_code}\n" https://console.laneaward.com/
curl -s -o /dev/null -w "%{http_code}\n" https://timeontasks.laneaward.com/

Step 5 — Restore the connector

Once the rebuilt machine is the only one in service, reinstall the Twingate connector with its own identity so remote office access works again.

Process 2D: Operating System Updates

Security updates install themselves automatically on this server. What does not happen automatically is the restart that activates them — so kernel and system library updates accumulate, already downloaded, waiting for a reboot that may otherwise only happen by accident at the worst possible moment.

The Reboot Is The Risk, Not The Update

Check whether a restart is already owed. If this file exists, the server is running older code than it has installed, and that gap is being carried whether or not anyone touches it.

[VM]
cat /var/run/reboot-required 2>/dev/null || echo "no restart pending"
apt list --upgradable 2>/dev/null | tail -n +2

Step 1 — Take an image first

Create a machine image before changing anything. Images can be taken while the server is running and serving users — no interruption, no maintenance window. This is the rollback point.

Step 2 — Rehearse on a disposable copy

Start a temporary machine from that image, in an isolated security group allowing only SSH from the Mac, and apply the update there first. Stop the connector on it immediately — see the warning in Process 2C — along with the scheduled jobs, so the copy cannot email anyone, import data, or renew certificates:

[VM — on the disposable copy only]
sudo systemctl mask --now twingate-connector weekly-production-report.timer pm-import.timer certbot.timer

Update it, restart it, and confirm the applications return. If something breaks, it breaks on a machine nobody depends on. Delete the copy afterwards.

Step 3 — Apply to production

Choose a time when no contributors are working and which avoids 22:15–23:15 Arizona, when the ProfitMaker import runs. Preserve existing configuration files rather than accepting new defaults:

[VM]
sudo DEBIAN_FRONTEND=noninteractive apt-get update
sudo DEBIAN_FRONTEND=noninteractive apt-get -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" upgrade
sudo systemctl reboot

The server returns in roughly fifteen seconds. Some packages are held back because they require new dependencies; that is expected and safe to leave.

Step 4 — Verify, then take a second image

Confirm the restart actually happened by checking the boot time — a successful connection proves nothing, because SSH may answer briefly before shutdown completes.

[VM]
uptime -s
systemctl is-active nginx twingate-connector
systemctl list-units --type=service --no-pager "laneaward-*"
sudo sqlite3 /var/lib/laneaward/workforce.db "PRAGMA integrity_check;"
systemctl --failed --no-pager
cat /var/run/reboot-required 2>/dev/null || echo "restart no longer pending"

Once everything is confirmed healthy, take a second image. That becomes the new known-good starting point, and the earlier one can be retired after a reasonable interval.

Process 3: Health Check and Status

Use these checks to confirm the production service and both apps are responding correctly.

API Health — Direct (VM)
[VM]
curl -sS http://127.0.0.1:9194/api/health
API Health — Public Endpoints
[Mac-local or VM]
curl -sS https://timeontasks.laneaward.com/api/health
curl -sS https://console.laneaward.com/api/health
Service Status
[VM]
sudo systemctl status laneaward-workforce-api.service --no-pager
Live Logs
[VM]
sudo journalctl -u laneaward-workforce-api.service -f

Process 6: Backend Service Commands

Use these only when the backend service itself needs attention. The deploy script handles restarts automatically — these are for manual intervention only.

Restart
[VM]
sudo systemctl restart laneaward-workforce-api.service
Stop / Start
[VM]
sudo systemctl stop laneaward-workforce-api.service
sudo systemctl start laneaward-workforce-api.service
Check on boot
[VM]
sudo systemctl is-enabled laneaward-workforce-api.service
Verify Before Acting

Two backend services run on this VM — staging and production. The production service is laneaward-workforce-api.service. Always confirm the service name before running any systemctl command.

Process 7: Update SQLite From ProfitMaker Files

The shared reference data — customer numbers, customer names, order numbers, and order descriptions — is refreshed automatically every night by the ProfitMaker reference pipeline, which has been the sole writer of production reference data since 2026-07-14. In normal operation no one runs anything by hand. This section explains how the nightly refresh works, how to check it, the one interim step during the ProfitMaker file migration, and the emergency manual fallback.

How The Nightly Refresh Works
  • The office file server lane-fs zips the newest ProfitMaker snapshot at 22:30 Arizona (scheduled task, service account svc-pmsync) and uploads it with a SHA-256 checksum to a restricted, upload-only SFTP drop on the VM.
  • At 23:15 Arizona the VM (pm-import.timer) verifies the checksum, extracts the snapshot, computes the rolling reference window, and runs the importer against staging first, then production, health-checking after each and archiving the snapshot.
  • Every successful run emails a step-by-step confirmation to donald@laneaward.com; a failure emails a separate alert. Silence means nothing ran — never assume success without the email.
Critical Rule — Never Replace The Production Database File For A Reference Refresh

Never copy a local workforce.db over production to update reference data. That permanently destroys users, sessions, and activity. The pipeline (and the manual fallback) update only the reference tables (customer_account, sales_order, profitmaker_import_manifest) and never touch app_user, work_session, order_task, or any other operational table.

Description Recovery For Shell Orders

The importer recovers descriptions from AINONOTE.DBF for orders not yet promoted to formal LNITM line items, so preliminary "shell" orders show real product text instead of "Order NNNNNN" placeholders. When a shell order later gets formal LNITM entries, the LNITM description wins on the next run. Schema is unchanged — this only enriches sales_order.description. Implementation lives in workforce_app/backend/import_profitmaker_reference.py.

Interim Operator Routine (during the ProfitMaker file migration)

Until ProfitMaker's live files finish migrating onto lane-fs, the only manual step is to drop the newest ASIDTA snapshot zip into D:\Automation\bucket on lane-fs. A 22:15 Arizona loader validates and stages it; the 22:30 upload and 23:15 import (staging then production) then run automatically. Bad or password-protected zips are quarantined and the last good snapshot is left untouched.

Check The Automation
[VM] — next run and recent history
systemctl list-timers pm-import.timer
sudo tail -40 /var/log/pm-import.log
journalctl -u pm-import.service -n 60 --no-pager

Check the file log first. The systemd journal is written to the same disk as everything else, so when the disk is full journald stops recording and journalctl shows a clean, gap-free history for a period in which jobs were actually failing. That happened on 22 August 2026: an offsite backup run failed at 08:00 and the journal showed no trace of it, while the file log recorded the error correctly. The file log is written by the job itself and survives.

[VM] — run an import cycle now (safe: idempotent)
sudo systemctl start pm-import.service
Verify The Updated Data
[Mac-local or VM]
curl -sS https://timeontasks.laneaward.com/api/health

Should return {"ok": true, ...}. The health endpoint is public, but since the session-token release (Ver 070301, 2026-07-07) the order-search route requires an Authorization: Bearer token; a bare curl to /api/orders/search returns {"error": "Authentication required. Please sign in."} — the security layer working, not a failed import. Production has no active test accounts, so verify production order data one of two ways:

  • Sign in to Time On Tasks or the Operations Console with your own PIN and search for a recently imported order — it should show current ProfitMaker data.
  • Or run a read-only query directly against the production database over SSH:
[Mac-local]
ssh -i ~/.ssh/lane_webserver.pem ubuntu@3.130.69.109 "sudo sqlite3 -header /var/lib/laneaward/workforce.db \"SELECT source_order_number, description, order_date, billed_total FROM sales_order WHERE source_order_number='107923';\""
Emergency Manual Fallback — Retired Scripts

The old hand-run push scripts (refresh_workforce_reference_snapshot.sh, push_reference_to_production.sh) were retired on 2026-07-14 when the automated pipeline became the sole reference writer. They now abort with guidance so they cannot be run out of habit. Only if the pipeline is genuinely down and reference data must be refreshed immediately, an emergency override exists:

[Mac-local — emergency use only]
FORCE_MANUAL_PUSH=1 bash scripts/push_reference_to_production.sh

Both paths use the same importer and are idempotent. Full design detail: _documents/pm-reference-automation-architecture.html in the repository (also on lane-fs under D:\Automation\docs\).

Process 8: Reliability Verification For Task Writes

Phase 1 durability hardening is part of the production backend and should be preserved whenever the application programming interface, or API, is updated. The Time On Tasks API opens SQLite in Write-Ahead Logging (WAL) mode, waits up to 10 seconds for short lock contention, uses synchronous = FULL for safer commits, and wraps each mutating route in a short BEGIN IMMEDIATE write transaction.

Connection Settings
  • PRAGMA journal_mode = WAL — Write-Ahead Logging
  • PRAGMA busy_timeout = 10000 — up to 10,000 ms wait on a short lock
  • PRAGMA synchronous = FULL — favors safer disk writes over speed
Protected Writes
  • add task
  • start, pause, complete, cancel
  • material usage logging
  • admin user maintenance routes
Contention Response
  • lock contention returns HTTP 503 Service Unavailable
  • response includes retryable: true
  • treat as a transient retry condition, not a data-loss event

Iteration test checklist:

Current Reliability Status

Five database-reliability features are fully implemented and should remain in place together:

  • Write-Ahead Logging (WAL)
  • busy_timeout = 10000
  • synchronous = FULL
  • short BEGIN IMMEDIATE write transactions
  • retryable HTTP 503 responses for SQLite busy/locked contention

Phase 2 (client-side retry/backoff, temporary local storage, 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. Reconsider only if load grows significantly.

Process 9: Service Worker — Deploy Model And Cache Invalidation

Time On Tasks includes a service worker at timeontasks/sw.js that improves load speed on shared tablets by caching static assets locally after the first visit. Understanding the cache strategy is important before deploying any frontend changes.

Cache Strategy Summary
Request typeStrategyWhy
HTML documents (index.html, user-guide.html)Network-firstAlways fetches fresh HTML so deployed updates are visible on next page load without any SW changes.
Versioned static assets (app.js?v=…, icons, manifest)Cache-firstVersion token in the URL acts as the cache key. New token = new URL = automatic cache miss = fresh fetch.
/api/* and all non-GET requestsNetwork-onlyTask writes, session state, and PIN login must never be served from cache.
Deploying A Routine Frontend Update (app.js or assets)

No changes to sw.js are required. The version token does the work.

  1. Make your changes to app.js or other assets.
  2. Bump the version token in index.html (e.g. app.js?v=20260406-foreman1app.js?v=20260411-myfix1).
  3. Deploy index.html and the updated asset file via Process 1.
  4. On next page load: SW fetches fresh index.html (network-first), browser sees the new token URL, cache misses, fetches new asset, caches it. Done.
When You DO Need To Update sw.js
  • Adding a new file type or path pattern that needs different caching behavior.
  • Changing the SW strategy itself (e.g. switching an asset from cache-first to network-first).
  • Forcing a full cache wipe on all tablets — bump CACHE_VERSION in sw.js. The new SW deletes all prior caches on activate.

When updating sw.js, the browser detects the change automatically (byte-for-byte comparison on every page load). The new SW installs in the background, then activates and claims all open tabs immediately via skipWaiting and clients.claim.

What To Avoid
  • Do not reuse a version token after changing the underlying file. The cache will serve the old content forever until the token changes.
  • Do not deploy only the asset without updating its token in index.html. The old token URL stays in cache and will be served.
  • Do not assume rsync transferred app.js. rsync skips files whose timestamps have not changed, even when content changed. Verify the deploy output listed app.js as transferred, or add --checksum to force a content comparison.
  • Do not add index.html to a cache-first rule. HTML must always be network-first or the stale-app-shell problem returns.

Process 10: Concurrent Stress Test

Run this test against production to confirm that the Phase 1 reliability improvements (WAL, busy_timeout, synchronous = FULL, BEGIN IMMEDIATE) hold up under the expected concurrent load of up to 20 simultaneous tablet operators on the shop floor. This test was completed before go-live and passed cleanly. The procedure is preserved here as a reference for future validation runs (e.g. after significant backend changes or scale increases).

The test script is at workforce_app/backend/stress_test_concurrent.py and runs from your local Mac. It seeds its own test fixtures into the production database via SSH, runs the load, then cleans up after itself.

What It Tests
  • Up to 20 simulated tablet users running simultaneously
  • Full lifecycle per user: login → add task → start → pause → resume → complete → read
  • Human-paced by default — 1.5–4 s between each action per user, matching real operator speed
  • All users start their shift together so the server handles concurrent sessions throughout
What To Look For
  • Hard failures — must be zero. These are non-retryable errors or request timeouts.
  • Retryable busy responses — acceptable if zero hard failures also. Means SQLite queued the write safely.
  • p95 latency under 800 ms — good tablet UX. 800 ms–2 s is acceptable. Above 2 s warrants review.

Prerequisites

Step 1 — Verify SSH access before starting

ssh -i ~/.ssh/lane_webserver.pem ubuntu@3.130.69.109 echo "SSH OK"

Step 2 — Run the standard realistic test

Simulates 20 human-paced operators for 2 task cycles each. Expected wall time is roughly 20–40 seconds.

python3 workforce_app/backend/stress_test_concurrent.py --host https://timeontasks.laneaward.com

A clean pass looks like this:

====================================================================
  LANEAWARD TIME-ON-TASKS  —  CONCURRENT STRESS TEST
====================================================================
  Mode:             REALISTIC (human-paced 1.5–4.0 s)
  Target:           https://timeontasks.laneaward.com
  Concurrent users: 20
  Iterations/user:  2
  Total ops:        280
  Wall time:        31.4s

  Outcomes:
    Successes:        280  (100%)
    Hard failures:      0  (non-retryable errors or timeouts)
    Retryable busy:     0  (SQLite busy-wait — server queued OK)

  VERDICT
  ------------------------------------------------------------------
  PASS  All operations completed cleanly under concurrent load.

Step 3 — Optional: run the burst ceiling test

python3 workforce_app/backend/stress_test_concurrent.py --host https://timeontasks.laneaward.com --burst

Expect more retryable busy responses in this mode — that is normal. Zero hard failures is still required.

Common Options

  • --users N — number of concurrent users, 1–20 (default: 20)
  • --iterations N — task cycles per user (default: 2)
  • --burst — near-simultaneous writes, ceiling test only
  • --no-seed — skip seeding (test users already in DB from prior run)
  • --no-cleanup — leave test fixtures in DB for inspection

If the test fails

Hard failures (non-zero)
  • Check the error detail in the report — it includes which user and which operation failed
  • An HTTP 500 error means an unhandled exception — check sudo journalctl -u laneaward-workforce-api.service -n 100
High p95 latency
  • p95 above 2 s: check AWS instance CPU and disk I/O during the test run
  • Uniformly high latency across all ops usually means network — try ping timeontasks.laneaward.com
Manual Fixture Cleanup (if a run is interrupted before cleanup)
[Mac-local]
bash scripts/cleanup_stress_test_production.sh

Process 11: Server Access And Security Group Management

The server is protected by two layers: an AWS Security Group that restricts SSH (port 22) to authorized IP addresses, and a Twingate connector that allows SSH from any location through the Twingate client. Use this process to connect to the server and to update security group rules when IP addresses change.

Security Group Reference
Group IDsg-0cc9719fa0e029c40 (launch-wizard-1)
COX Fiber — office98.175.1.150/32 · SSH allowed
COX Cable failover — office72.215.199.214/32 · SSH allowed
Development Mac — required72.208.129.218/32 · SSH allowed · carries all deploys, backups and restores
HTTP — port 80Open to all — 0.0.0.0/0 · required for Let's Encrypt HTTP-01 renewal
HTTPS — port 443Open to all — 0.0.0.0/0
Twingate Reference
Remote NetworkLane Award PWA Server
Connectoreggplant-okapi
Resource address172.31.7.224 (server private IP)
SSH key~/.ssh/lane_webserver.pem
Never Remove The Port 80 Rule

Inbound port 80 (0.0.0.0/0) must stay open. Let's Encrypt renews every certificate via the HTTP-01 challenge, which the Certificate Authority validates over port 80. nginx redirects all real traffic from 80 to 443, so the only thing port 80 serves is the ACME challenge — but if it is closed, every renewal fails silently: HTTPS keeps working on the existing certs while they quietly march toward expiry. This exact failure was found and fixed on 2026-06-29 (port 80 had been removed, and two certs had already lapsed). If a renewal ever fails, first confirm port 80 is open: aws ec2 describe-security-groups --group-ids sg-0cc9719fa0e029c40 --query "SecurityGroups[0].IpPermissions[?FromPort==\`80\`]" --no-cli-pager. Re-add with: aws ec2 authorize-security-group-ingress --group-id sg-0cc9719fa0e029c40 --protocol tcp --port 80 --cidr 0.0.0.0/0.

The SSH Config Entry Every Deploy Depends On

The Mac's ~/.ssh/config must contain the server's public IP as a name on the laneaward-vm block, with IdentitiesOnly yes:

Host laneaward-vm 3.130.69.109
  HostName 3.130.69.109
  User ubuntu
  IdentityFile /Users/donaldscott/.ssh/lane_webserver.pem
  IdentitiesOnly yes

Eighteen deploy-script references connect as ubuntu@3.130.69.109 rather than by the alias. Without the IP on that line those connections match no host block, so IdentitiesOnly is not applied, and the SSH agent offers every key it holds. The server allows six authentication attempts, the agent currently holds seven keys, and lane_webserver.pem is last in the agent's order — so the correct key is never reached and every deploy fails with "Too many authentication failures".

This fails deterministically rather than intermittently, and it looks like a credential or firewall problem, which is the wrong place to start looking. Diagnosed and fixed 28 August 2026. A one-off workaround, if the config is ever lost mid-task, is to prefix the command with SSH_AUTH_SOCK= so the agent is bypassed entirely.

The same block also defines lane-fs and lane-gw. After the office move, change those two HostName lines and nothing else.

Option A — SSH via Twingate (from any location)

Use this method when connecting from home or any location not on an authorized static IP. The Twingate client must be running and connected before opening SSH.

Step 1 — Open the Twingate client on your Mac and confirm it shows Connected

The Twingate icon lives in the Mac menu bar. Click it and verify the connection status is active.

Step 2 — SSH using the server private IP

[Mac-local]
ssh -i ~/.ssh/lane_webserver.pem ubuntu@172.31.7.224

Option B — SSH directly from an authorized office IP

Use this method when connecting from the office on either the fiber or cable connection. Twingate does not need to be running.

Step 1 — SSH using the server public IP

[Mac-local]
ssh -i ~/.ssh/lane_webserver.pem ubuntu@3.130.69.109
If Twingate Is Running While SSHing via Public IP

When the Twingate client is active, it intercepts connections to the server's public IP and routes them through the connector. Pause Twingate first before using Option B, or use the private IP (172.31.7.224) with Twingate active instead.

Check Twingate Connector Status

Run this on the server to confirm the connector service is running. A healthy connector shows State: Online in the log output.

[VM]
sudo systemctl status twingate-connector --no-pager

Update Security Group — Replace a Changed IP

Run these steps when an authorized IP address changes. Requires the AWS CLI configured on the development Mac with IAM user donald.

Step 1 — Verify current rules before making changes

[Mac-local]
aws ec2 describe-security-groups --group-ids sg-0cc9719fa0e029c40 --query "SecurityGroups[0].IpPermissions" --output json --no-cli-pager

Step 2 — Remove the old IP

[Mac-local]
aws ec2 revoke-security-group-ingress --group-id sg-0cc9719fa0e029c40 --protocol tcp --port 22 --cidr OLD.IP.ADDRESS/32

Step 3 — Add the new IP

[Mac-local]
aws ec2 authorize-security-group-ingress --group-id sg-0cc9719fa0e029c40 --protocol tcp --port 22 --cidr NEW.IP.ADDRESS/32

Step 4 — Verify the updated rules

[Mac-local]
aws ec2 describe-security-groups --group-ids sg-0cc9719fa0e029c40 --query "SecurityGroups[0].IpPermissions" --output json --no-cli-pager
Never Lock Yourself Out

Do not remove an IP that is your current connection without first confirming Twingate SSH works, or without another authorized IP still in place. If all SSH access is lost, recovery requires the AWS Console. Never remove all three SSH rules at once.

Authorized SSH Sources — What Each Rule Is For

Security group sg-0cc9719fa0e029c40 permits SSH on port 22 from three addresses. Before changing or removing any of them, understand what depends on it.

AddressWhat it isWhat breaks if removed
72.208.129.218/32 The development Mac's own outbound address. Confirmed by asking the server what source address it sees. Every deploy script, both restore scripts, and the twice-daily database backups onto the Mac. All of them connect to the public IP from this address.
98.175.1.150/32 The office, on the Cox fibre connection. Direct SSH from the office on the primary connection.
72.215.199.214/32 The office, on the Cox cable failover connection. Direct SSH from the office when the primary connection is down.
Do Not Remove The Development Mac Rule

It is tempting to read 72.208.129.218 as a stale leftover and clean it up. It is not stale — it is the address the Mac connects from, and automated tooling depends on it continuously. Removing it causes deploys, backups and restores to fail simultaneously, with no obvious common cause.

It is a residential address assigned by the internet provider, so it can change. If it does, replace the rule using the procedure above rather than deleting it. Confirm the current value first:

[Mac-local] Ask the server what address it sees
ssh laneaward-vm 'echo ${SSH_CLIENT%% *}'

Process 12: Verify PIN Hash and Login Token Status

Use this to confirm all active user PINs have been migrated to Argon2id hashes and that the fast-login HMAC token has been populated. Both columns must be set for a user to take the fast login path (~230 ms). Users with an empty pin_token will take the slow fallback path on their next login, which automatically writes the token — no manual action required.

Step 1 — Check hash and token status for all active users

[VM]
sqlite3 'file:/var/lib/laneaward/workforce.db?immutable=1' 'SELECT id, display_name, CASE WHEN pin_code LIKE "$argon2%" THEN "hashed" ELSE "plaintext" END AS hash_status, CASE WHEN pin_token != "" THEN "token-ok" ELSE "no-token" END AS token_status FROM app_user WHERE is_active = 1 ORDER BY display_name;'
Expected Output After Full Migration

Every active user should show hashed and token-ok. If any show plaintext or no-token, that user will be upgraded automatically on their next successful login — no manual action required. The slow fallback path handles both cases gracefully.

Step 2 — Verify token values independently (spot check)

The PIN pepper is a server-side secret and is no longer stored in code or in this document. Read it from the environment file on the VM, then compute the expected HMAC token for a known PIN and compare against the database.

[VM] Read the pepper from the secret file
sudo cat /etc/laneaward/workforce-api.env

Note the LANEAWARD_PIN_PEPPER value, then substitute it below in place of PEPPER:

[Mac-local or VM]
python3 -c "import hmac; print(hmac.new(b'PEPPER', b'444444', 'sha256').hexdigest())"

Compare the output against the pin_token column for the user with that PIN. They must match exactly.

Login Sessions — 12-Hour Auto-Logout

Login Session Behavior

Signing in with a PIN issues a login session that lasts 12 hours as a sliding window — each use resets the clock, so an actively used device stays signed in and only idle time expires it. After roughly 12 hours of inactivity the app returns to the sign-in screen; the contributor or manager simply re-enters their PIN. No data is lost.

An automatic logout does not stop a running task timer. Work sessions live in the database and keep running until someone uses Pause, Stop, or Complete, or a manager applies a correction in the console. If a tablet logs itself out overnight while a task was left active, that task is still running — treat it as a potential runaway timer and correct it; do not assume the logout closed it.

Salary Labor Cost — Calculation Methodology

Salaried contributors log time through the Time On Tasks app exactly like hourly workers. The Order Profitability report converts their annual salary to an effective hourly rate using the U.S. Bureau of Labor Statistics standard:

Formula

Effective Hourly Rate = Annual Salary ÷ 2,080

Session Labor Cost = (Annual Salary ÷ 2,080) × (Session Minutes ÷ 60)

2,080 = 52 weeks × 40 hours — the standard used by ADP, Paychex, QuickBooks, and the BLS.

What Is Included
  • Base wage equivalent only — the annual salary stored in the contributor's profile.
  • Applies to all roles where compensation is tracked (Contributor, Foreman, Team Lead).
  • Salary workers appear in the same labor cost columns as hourly workers on the Order Profitability report.
Not Yet Included — Open for Future Discussion
  • Burden rate — payroll taxes (FICA ~7.65%), benefits, PTO, workers' comp, and overhead typically add 25–35% on top of base wage.
  • Blended rate approach — some operations use a single blended rate for all salaried staff in a role tier.
  • Part-time salary proration — the 2,080 divisor assumes full-time (40 hr/week).