This is the quick-reference HTML runbook for the recurring processes we use to keep the Time On Tasks contributor app current in development and staging. It is intentionally separate from the chronological setup log so routine work is easy to find.
timeontasks
Console Draft: ops_console
Shared Backend: workforce_app/backend
Canonical DBFs: pm_database
Keep development, AWS staging, and the public runtime aligned on Python version, SQLite version, schema level, refreshed database content, backend code, frontend code, and service configuration. If those drift, report or workflow behavior can look broken even when the application logic itself is correct.
AWS staging is a production-like environment. Real contributors (Will, Alex) actively use it, and its database holds live work sessions and user records that cannot be recovered without a backup restore.
Treat every push, rsync, or database operation targeting staging with the same care you would give a production system. Before any staging operation, ask: does this preserve existing users and sessions? If the answer is not an obvious yes, stop and verify first.
/var/www/laneaward-staging/var/lib/laneaward-staging/workforce.dbssh -i ~/.ssh/lane_webserver.pem ubuntu@3.130.69.109ubuntu@laneaward-webserver:~$ — the internal AWS hostname ip-172-31-7-224 is the same machine; hostname was renamed from laneaward-staging to laneaward-webserver on 2026-05-01 to reflect that this server hosts both staging and production.aws-server-setup-summary.html
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
staging. Before running any deploy, confirm the intended branch with
git branch --show-current. Full background is in the
Version Control And Source Management section of the Project Reference document.
restore_staging_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.
| Contributor App | / = Time On Tasks |
|---|---|
| Console | https://staging.console.laneaward.com/ = combined Admin + Reports frontend |
| API | /api/ = shared LaneAward backend |
| Legacy Route | /workforce/ redirects to / |
Use this after changing the contributor frontend in timeontasks.
The staging host uses the contributor app at the root route.
Any time app.js changes, you must also bump the version token in index.html
before deploying. The service worker caches app.js by its token URL — if the token does
not change, tablets will keep serving the old cached version indefinitely.
index.html: e.g. app.js?v=20260410-signin1 → app.js?v=20260410-signin2.index.html and app.js in the same rsync run.app.js was picked up, add --checksum to force a content comparison: rsync -avh --checksum …index.html (network-first), sees the new token URL, misses the cache, and fetches the new app.js automatically.
Run from the repo root on the Mac. The script injects the staging label into
index.html and manifest.webmanifest at deploy time,
then pushes index.html, manifest.webmanifest,
app.js, sw.js, version.json,
sounds/, user-guide.html, and user-guide-images/
to the staging web root. No VM login required.
bash scripts/deploy_timeontasks.sh
Use this after changing the Operations Console frontend in ops_console.
The staging console is available at https://staging.console.laneaward.com/.
Any time app.js changes, you must also bump the version token in index.html
before deploying. The service worker caches app.js by its token URL — if the token does
not change, browsers will keep serving the old cached version.
index.html: e.g. app.js?v=20260410-console1 → app.js?v=20260410-console2.index.html and app.js in the same rsync run.app.js was picked up, add --checksum to force a content comparison.index.html (network-first), sees the new token URL, and fetches the new app.js automatically.
Run from the repo root on the Mac. The script injects the staging label into
index.html and manifest.webmanifest at deploy time,
then pushes index.html, manifest.webmanifest,
app.js, and version.json directly
to the staging console web root. No VM login required.
bash scripts/deploy_console.sh
Use this after updating any of the shared console documents: the staging runbook, user guide,
project reference, or environment topology. This script deploys documents only — it does not
touch app.js, index.html, or any application code.
Documents deployed by this script:
_documents/staging/runbook.html → runbook.html_documents/user-guide.html → user-guide.html_documents/project-reference.html → project-reference.html_documents/laneaward_environment_topology.html → topology.htmlbash scripts/deploy_console_docs.sh
Both LaneAward environments are protected by the 🚀 PWA BDR service — a shared macOS menu bar LaunchAgent on the development Mac. Nothing needs to be installed on the VM. Look for the 🚀 icon in the top menu bar to open the separate Lensboard and LaneAward dropdowns, trigger manual runs, adjust schedules, or mark jobs as Include or Skip.
| Job | What it protects | Default schedule |
|---|---|---|
VM·STAGING — Database |
/var/lib/laneaward-staging/workforce.db on AWS |
Every 12 h |
LOCAL — Source Code |
Full project on Mac (archives excluded), hardlink snapshots | Weekly |
~/projectbackups/laneaward/staging-database/~/projectbackups/laneaward/source/~/projectbackups/backup_logs/backup.log
Each VM backup is a safe online copy — the service SSHs into the VM,
runs sqlite3 .backup (no downtime, no locking), then
scps the result to the Mac and cleans up the temp file.
Every copy is independently restorable.
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.
sudo cp /var/lib/laneaward-staging/workforce.db /var/lib/laneaward-staging/workforce-$(date +%F-%H%M%S).db
Use this process when staging 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.
Staging holds live work sessions and user records from real contributors. Before running this script, confirm:
RESTORE then YES — two separate gateslaneaward-workforce-api-staging.service~/projectbackups/laneaward/staging-database/workforce_vm_staging_YYYYMMDDTHHMMSS.dbls -lht ~/projectbackups/laneaward/staging-database/
The restore script connects via laneaward-vm → 172.31.7.224. The Twingate client must be active before proceeding. Verify SSH access first:
[Mac-local]
ssh -i ~/.ssh/lane_webserver.pem laneaward-vm "echo SSH OK"
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_staging_db.sh
[Mac-local — specific backup file]
bash /Users/donaldscott/Project-Code/laneaward/repo/scripts/restore_staging_db.sh ~/projectbackups/laneaward/staging-database/workforce_vm_staging_TIMESTAMP.db
A clean restore finishes with:
Restore complete. Staging is live and healthy.
Restored from : ~/projectbackups/laneaward/staging-database/workforce_vm_staging_TIMESTAMP.db
Safety backup : /var/lib/laneaward-staging/workforce-pre-restore-TIMESTAMP.db
Confirm staging is responding correctly before deleting the safety backup:
[Mac-local]
curl -sS https://staging.timeontasks.laneaward.com/api/health
Once confirmed, delete the server-side safety backup:
[VM]
sudo rm /var/lib/laneaward-staging/workforce-pre-restore-TIMESTAMP.db
ssh laneaward-vm 'sudo systemctl status laneaward-workforce-api-staging.service --no-pager'ssh laneaward-vm 'sudo journalctl -u laneaward-workforce-api-staging.service -n 50 --no-pager'/var/lib/laneaward-staging/workforce-pre-restore-TIMESTAMP.db — it can be used to roll back by running the restore script again with that file as the argument.Use the VM status helper when you want one quick read on service health, HTTP routes, and database row counts.
vm_workforce_status.sh
SERVICE_NAME=laneaward-workforce-api-staging.service DB_DIR=/var/lib/laneaward-staging DIRECT_HEALTH_URL=http://127.0.0.1:9193/api/health NGINX_HEALTH_URL=https://staging.timeontasks.laneaward.com/api/health ROOT_URL=https://staging.timeontasks.laneaward.com/ CONSOLE_URL=https://staging.console.laneaward.com/ LEGACY_WORKFORCE_URL=https://staging.timeontasks.laneaward.com/workforce/ /opt/laneaward-staging/workforce_app/deploy/vm_workforce_status.sh
This is the verified safe reset for staging activity only. It preserves users, roles, teams, customer references, sales orders, and ProfitMaker import metadata.
vm_workforce_reset_activity.sh
sudo env SERVICE_NAME=laneaward-workforce-api-staging.service LIVE_DB=/var/lib/laneaward-staging/workforce.db DIRECT_HEALTH_URL=http://127.0.0.1:9193/api/health NGINX_HEALTH_URL=https://staging.timeontasks.laneaward.com/api/health STATUS_SCRIPT=/opt/laneaward-staging/workforce_app/deploy/vm_workforce_status.sh /opt/laneaward-staging/workforce_app/deploy/vm_workforce_reset_activity.sh --yes
order_task, work_session, and material_usage, and also clears work_session_correction_audit through the work_session delete cascade.app_user and current PINs.--no-backup is used.This does not remove seeded users. User cleanup still needs either the admin tool or a dedicated user-maintenance process.
Use this when backend code or schema changed and you need to push the update to staging without replacing
the SQLite database. The script uploads server.py and schema.sql, promotes them
to /opt/laneaward-staging/workforce_app/backend/, restarts the service, and verifies health.
Schema migrations (new columns, indexes) are applied automatically by ensure_schema_upgrades()
on startup — no manual SQL required.
bash /Users/donaldscott/Project-Code/laneaward/repo/scripts/deploy_backend_to_staging.sh
A clean run finishes with the service showing active and a live health response.
If you only need to restart the service without uploading new files:
vm_workforce_cutover.sh
sudo env SERVICE_NAME=laneaward-workforce-api-staging.service DB_DIR=/var/lib/laneaward-staging DIRECT_HEALTH_URL=http://127.0.0.1:9193/api/health NGINX_HEALTH_URL=https://staging.timeontasks.laneaward.com/api/health ROOT_URL=https://staging.timeontasks.laneaward.com/ CONSOLE_URL=https://staging.console.laneaward.com/ LEGACY_WORKFORCE_URL=https://staging.timeontasks.laneaward.com/workforce/ STATUS_SCRIPT=/opt/laneaward-staging/workforce_app/deploy/vm_workforce_status.sh /opt/laneaward-staging/workforce_app/deploy/vm_workforce_cutover.sh --restart-only
Use this section when a new asidta_file_* folder arrives from production and you need to refresh
the shared SQLite database with the latest:
The old process (steps 2–4) copied the local workforce.db to staging and replaced the entire
file. This permanently destroys any users, sessions, or activity that were added directly on staging and do
not exist in the local development database. Do not use that approach for reference data
updates. The two-step process below updates only the reference tables
(customer_account, sales_order, profitmaker_import_manifest) and
never touches app_user, work_session, order_task, or any other
operational table.
refresh_workforce_reference_snapshot.sh
asidta_file_* folder if no path is passed.DBF, FPT, and CDX files into pm_database.Copy of ....workforce.db with current customers and the rolling order reference window.push_reference_to_staging.sh
pm_database automatically.scp.import_profitmaker_reference.py directly on the staging VM against the live database.FORCE_REFRESH=1 to override on a weekend.import_profitmaker_reference.py
pm_database.profitmaker_import_manifest to skip unchanged customer or order groups.
Run the refresh script locally. This promotes any changed ProfitMaker files into pm_database
and updates the local workforce.db with current customers and the rolling order reference window.
/Users/donaldscott/Project-Code/laneaward/repo/scripts/refresh_workforce_reference_snapshot.sh
Run the staging push script. This copies only the six required DBF files to the VM and runs the importer directly against the live staging database. No file swap, no service restart, no data loss.
/Users/donaldscott/Project-Code/laneaward/repo/scripts/push_reference_to_staging.sh
The script prints the reference window, confirms each file transfer, shows the importer output, cleans up
temp files, and finishes with a live health check. If the data has not changed since the last run the
importer will report skipped (unchanged since last successful import) — that is correct and
expected behavior.
Confirm that the API is healthy and that current order search results reflect the latest imported ProfitMaker data.
curl -sS https://staging.timeontasks.laneaward.com/api/health
curl -sS "https://staging.timeontasks.laneaward.com/api/orders/search?q=107923&limit=3"
Phase 1 durability hardening is now part of the backend and should be preserved whenever the application
programming interface, or API, is updated. The Time On Tasks API now 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.
PRAGMA journal_mode = WAL, which enables Write-Ahead LoggingPRAGMA busy_timeout = 10000, which gives SQLite up to 10,000 milliseconds to wait on a short lockPRAGMA synchronous = FULL, which favors safer disk writes over speedHTTP 503 Service Unavailableretryable: trueIteration test checklist:
The current backend reliability work is best understood as safe but incomplete, not partially conflicting. The first five database-reliability features are fully implemented and should remain in place together:
Write-Ahead Logging (WAL)busy_timeout = 10000synchronous = FULLBEGIN IMMEDIATE write transactionsHTTP 503 responses for SQLite busy/locked contentionIn practical terms, these five changes make the shared SQLite backend safer under short write collisions, safer during commit, and clearer when contention happens. They do not depend on the unfinished Phase 2 client work in order to remain valid.
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. Phase 2 should be reconsidered only if load grows significantly beyond current projections.
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 frontend changes.
| Request type | Strategy | Why |
|---|---|---|
HTML documents (index.html, user-guide.html) | Network-first | Always fetches fresh HTML so deployed updates are visible on the next page load without any SW changes. |
Versioned static assets (app.js?v=…, icons, manifest) | Cache-first | Version token in the URL acts as the cache key. New token = new URL = automatic cache miss = fresh fetch. |
/api/* and all non-GET requests | Network-only | Task writes, session state, and PIN login must never be served from cache. |
No changes to sw.js are required. The version token does the work.
app.js or other assets.index.html (e.g. app.js?v=20260406-foreman1 → app.js?v=20260411-myfix1).index.html and the updated asset file.index.html (network-first), browser sees the new token URL, cache misses, fetches new asset, caches it. Done.CACHE_VERSION in sw.js. The new SW will delete 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.
index.html. The old token URL stays in cache and will be served.app.js as transferred, or add --checksum to force a content comparison.index.html to a cache-first rule. HTML must always be network-first or the stale-app-shell problem returns.
Run this test against staging 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 staging database via SSH, runs the load,
then cleans up after itself.
ubuntu@3.130.69.109 with no passphrase prompt (BatchMode).https://staging.timeontasks.laneaward.com.cd ~/Project-Code/laneaward/repoThe test seeds and cleans up via SSH. Confirm it works first:
ssh -i ~/.ssh/lane_webserver.pem ubuntu@3.130.69.109 echo "SSH OK"
You should see SSH OK with no password prompt. If you see Permission denied (publickey), the key path is wrong or the key is not present — check that ~/.ssh/lane_webserver.pem exists on your Mac.
This is the test to run before go-live. It 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
The script will print its progress as it seeds, runs, and cleans up:
[seed] Seeding 20 test user(s) on ubuntu@3.130.69.109 ...
[check] Verifying https://staging.timeontasks.laneaward.com/api/health ...
[run] Releasing 20 threads simultaneously ...
[cleanup] Removing stress-test fixtures from remote DB ...
At the end of the run, the script prints a report. A clean pass looks like this:
====================================================================
LANEAWARD TIME-ON-TASKS — CONCURRENT STRESS TEST
====================================================================
Mode: REALISTIC (human-paced 1.5–4.0 s)
Target: https://staging.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)
End-to-end latency (successful ops only):
p50: 210 ms
p75: 310 ms
p95: 480 ms
max: 740 ms
VERDICT
------------------------------------------------------------------
PASS All operations completed cleanly under concurrent load.
WAL + BEGIN IMMEDIATE handled 20-user contention with no busy errors.
Latency looks good for tablet UX (p95 = 480 ms).
The burst test fires all writes near-simultaneously with no human delay. This scenario cannot occur with real operators, but it validates the SQLite busy-timeout safety net as an absolute ceiling. Run it after the realistic test passes.
python3 workforce_app/backend/stress_test_concurrent.py --burst
Expect more retryable busy responses in this mode — that is normal and expected. The important thing is still zero hard failures.
--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--local — spawn a local server with a temp DB (no SSH, for dev use)--users to match headcount before each go-live milestone--iterations simulates a longer shift with more task cycles per personbusy_timeout plus network — very unlikely at human pace, indicates a server resource issuesudo journalctl -u laneaward-workforce-api-staging -n 100login only may indicate the pin-throttle table is accumulating rows — run Process 4 (Activity Reset) to clear it on stagingping staging.timeontasks.laneaward.com from the same machine
All stress test data is prefixed with stress_test_ so it is unambiguous in the
database. If a run is interrupted before cleanup, remove the leftovers manually:
bash scripts/cleanup_stress_test_staging.sh
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. This process applies to both the staging and production environments since both run on the same AWS server.
| Group ID | sg-0cc9719fa0e029c40 (launch-wizard-1) |
|---|---|
| COX Fiber — office | 98.175.1.150/32 · SSH allowed |
| COX Cable failover — office | 72.215.199.214/32 · SSH allowed |
| Home lab (pending removal) | 72.208.129.218/32 · SSH allowed |
| HTTP / HTTPS | Open to all — 0.0.0.0/0 |
| Remote Network | Lane Award PWA Server |
|---|---|
| Connector | eggplant-okapi |
| Resource address | 172.31.7.224 (server private IP) |
| SSH key | ~/.ssh/lane_webserver.pem |
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.
The Twingate icon lives in the Mac menu bar. Click it and verify the connection status is active.
[Mac-local]
ssh -i ~/.ssh/lane_webserver.pem ubuntu@172.31.7.224
Use this method when connecting from the office on either the fiber or cable connection. Twingate does not need to be running.
[Mac-local]
ssh -i ~/.ssh/lane_webserver.pem ubuntu@3.130.69.109
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.
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
Run these steps when an authorized IP address changes. Requires the AWS CLI configured on the development Mac with IAM user donald.
[Mac-local]
aws ec2 describe-security-groups --group-ids sg-0cc9719fa0e029c40 --query "SecurityGroups[0].IpPermissions" --output json --no-cli-pager
[Mac-local]
aws ec2 revoke-security-group-ingress --group-id sg-0cc9719fa0e029c40 --protocol tcp --port 22 --cidr OLD.IP.ADDRESS/32
[Mac-local]
aws ec2 authorize-security-group-ingress --group-id sg-0cc9719fa0e029c40 --protocol tcp --port 22 --cidr NEW.IP.ADDRESS/32
[Mac-local]
aws ec2 describe-security-groups --group-ids sg-0cc9719fa0e029c40 --query "SecurityGroups[0].IpPermissions" --output json --no-cli-pager
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.
Once Twingate is confirmed as the primary home access method, run this to remove the dynamic home lab IP. Do not run this until Twingate SSH has been verified working from the home location.
[Mac-local]
aws ec2 revoke-security-group-ingress --group-id sg-0cc9719fa0e029c40 --protocol tcp --port 22 --cidr 72.208.129.218/32
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:
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.
LANEAWARD_SETUP_LOG.mdworkforce_app/deploy/README.mdaws-server-setup-summary.htmlLANEAWARD_ENVIRONMENT_TOPOLOGY.htmltimeontasks/index.htmltimeontasks/app.jstimeontasks/sw.js — service workertimeontasks/manifest.webmanifesttimeontasks/user-guide.htmlops_console/index.htmlops_console/runbook.html — deploy copy of this document; canonical source is _documents/staging/runbook.htmlworkforce_app/backend/stress_test_concurrent.py — concurrent pre-go-live validation script