LaneAward Operations

Time On Tasks & Operations Runbook

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.

Contributor App: timeontasks Console Draft: ops_console Shared Backend: workforce_app/backend Canonical DBFs: pm_database
Environment Parity Rule

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.

Staging Is Production-Like — Not A Sandbox

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.

Production-Like — Handle With Care

Staging

  • Primary host: https://staging.timeontasks.laneaward.com/
  • Contributor app: /
  • Console: https://staging.console.laneaward.com/
  • API health: /api/health
  • Web root: /var/www/laneaward-staging
  • Database: /var/lib/laneaward-staging/workforce.db
  • SSH: ssh -i ~/.ssh/lane_webserver.pem ubuntu@3.130.69.109
  • VM prompt: ubuntu@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.
  • Real users and live sessions — verify before every operation.
Environment

Public

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 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.

Contents

Processes
  • 1 — Publish Contributor App To AWS Staging Deploys the full Time On Tasks bundle to staging — index.html, manifest.webmanifest, app.js, sw.js, version.json, sounds/, user-guide.html, and user-guide-images/ — injecting the "- Staging" label into the app title at deploy time. Always bump the version token in index.html when app.js changes — tablets cache by token URL and will serve stale code if the token is not updated.
  • 1B — Publish Operations Console To AWS Staging Deploys the Operations Console bundle (index.html, manifest.webmanifest, app.js, version.json) to the staging console web root, injecting the "- Staging" label into the app title at deploy time. Always bump the version token in index.html when app.js changes.
  • 1C — Deploy Console Documents To AWS Staging Deploys all four console documents (runbook, user guide, project reference, topology) to staging. No app code is touched — safe to run any time a document changes.
  • 2 — Backup and Recovery The PWA BDR service runs automatic backups of the staging database every 12h and source code weekly. Covers manual pre-operation snapshot — verify the backup exists before any risky operation.
  • 2B — Restore Staging Database From Backup Full guided restore procedure using 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.
  • 3 — Staging Status Check Runs the vm_workforce_status.sh helper for a quick read of service health, HTTP routes, and database row counts in one pass. Run before and after any significant staging operation to confirm clean state.
  • 4 — Activity Reset for Staging Clears work sessions, tasks, and material usage from staging while preserving all users and ProfitMaker reference data. Caution: irreversible without a restore — an automatic backup is taken unless explicitly skipped with --no-backup.
  • 6 — Backend Deploy Deploys server.py and schema.sql to staging and restarts the backend service. Use only when backend code or schema changed — do not run for frontend-only updates. Schema migrations apply automatically on restart.
  • 7 — Update SQLite From ProfitMaker Files Reference data (customers, orders, descriptions) is refreshed automatically every night by the ProfitMaker pipeline (lane-fs → VM, staging 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 touches users, sessions, or activity data.
  • 8 — Reliability Verification For Task Writes Documents the Phase 1 database reliability features (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 cache strategy (network-first for HTML, cache-first for versioned assets, network-only for API). Covers when sw.js must be updated and what to avoid to prevent stale content being served to tablets after a deploy.
  • 10 — Concurrent Stress Test Simulates up to 20 concurrent tablet operators against staging. Run before go-live milestones and after significant backend changes. Zero hard failures required to pass — retryable busy responses are acceptable.
  • 11 — Server Access And Security Group Management SSH connection procedures via Twingate (any location) or direct public IP (office only). Also covers updating AWS Security Group rules when authorized IP addresses change. Never remove all SSH rules simultaneously.

Canonical Routes

Contributor App/ = Time On Tasks
Consolehttps://staging.console.laneaward.com/ = combined Admin + Reports frontend
API/api/ = shared LaneAward backend
Legacy Route/workforce/ redirects to /

Process 1: Local Contributor-App Publish To AWS Staging

Use this after changing the contributor frontend in timeontasks. The staging host uses the contributor app at the root route.

Critical Rule — Bump The Token And Deploy Both Files Together

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.

  • Bump the token in index.html: e.g. app.js?v=20260410-signin1app.js?v=20260410-signin2.
  • Deploy both index.html and app.js in the same rsync run.
  • rsync skips files whose timestamps have not changed, even if the content changed. If you are unsure whether app.js was picked up, add --checksum to force a content comparison: rsync -avh --checksum …
  • After deploy, a single normal page reload on the tablet is sufficient — no hard refresh needed. The SW fetches fresh index.html (network-first), sees the new token URL, misses the cache, and fetches the new app.js automatically.
Deploy: Run The Staging Deploy Script

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.

[Mac-local]
bash scripts/deploy_timeontasks.sh

Process 1B: Local Console Publish To AWS Staging

Use this after changing the Operations Console frontend in ops_console. The staging console is available at https://staging.console.laneaward.com/.

Critical Rule — Bump The Token And Deploy Both Files Together

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.

  • Bump the token in index.html: e.g. app.js?v=20260410-console1app.js?v=20260410-console2.
  • Deploy both index.html and app.js in the same rsync run.
  • rsync skips files whose timestamps have not changed, even if the content changed. If you are unsure whether app.js was picked up, add --checksum to force a content comparison.
  • After deploy, a normal page reload in the browser is sufficient — the SW fetches fresh index.html (network-first), sees the new token URL, and fetches the new app.js automatically.
Deploy: Run The Staging Console Deploy Script

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.

[Mac-local]
bash scripts/deploy_console.sh

Process 1C: Deploy Console Documents To AWS Staging

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:

Deploy: Run The Console Docs Deploy Script
[Mac-local]
bash scripts/deploy_console_docs.sh

Process 2: Backup and Recovery

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.

Active Backup Jobs
JobWhat it protectsDefault 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
Backup Locations
  • Staging DB → ~/projectbackups/laneaward/staging-database/
  • Source snapshots → ~/projectbackups/laneaward/source/
  • Unified log → ~/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.

Restoring Staging

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.

Manual VM-Side Snapshot (one-off, before a risky change)
[VM — Staging]
sudo cp /var/lib/laneaward-staging/workforce.db /var/lib/laneaward-staging/workforce-$(date +%F-%H%M%S).db

Process 2B: Restore Staging Database From Backup

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.

This Permanently Destroys Staging Data After The Backup Timestamp

Staging holds live work sessions and user records from real contributors. 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 Process 4 (Activity Reset) — would resolve the issue more safely.
  • SSH to the server works from this Mac (verified in Step 1 below).
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 staging database
  • Stops laneaward-workforce-api-staging.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/staging-database/
  • Pattern: workforce_vm_staging_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/staging-database/
Why The Write-Ahead Log Is Cleared Before The Swap

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

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. The script removes the log as part of the swap, which is safe because a restore already discards the current state deliberately.

A Restore Signs Everybody Out

Login sessions are stored in the database, so they are replaced along with it. Any session created after the backup was taken ceases to exist and that person is signed out when the service returns. Signing back in with the usual number is all that is required. Worth remembering when testing against staging with someone else mid-session.

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 — 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_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

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:

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

Then compare record counts against the backup you restored from:

[VM] Record counts in the restored database
sudo sqlite3 /var/lib/laneaward-staging/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;"
Do Not Compare Checksums

A correct restore will not produce a database matching the backup's checksum. The database engine does not lay bytes out identically when a file is copied and reopened, and signing in to check the result writes a new login session, genuinely changing the file. Compare record counts and dates instead, and expect one extra login session row per person who has signed in since the restore.

Once confirmed, delete the server-side safety backup:

[VM]
sudo rm /var/lib/laneaward-staging/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-staging.service --no-pager'
  • Check the service log: ssh laneaward-vm 'sudo journalctl -u laneaward-workforce-api-staging.service -n 50 --no-pager'
  • The safety backup is on the server at /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.

Process 3: Staging Status Check

Use the VM status helper when you want one quick read on service health, HTTP routes, and database row counts.

vm_workforce_status.sh

[VM]
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

Process 4: Activity Reset for Staging

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

[VM]
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

This does not remove seeded users. User cleanup still needs either the admin tool or a dedicated user-maintenance process.

Process 6: Backend Deploy (server.py or schema.sql changed)

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.

[Mac-local]
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.

Service restart only (no file changes)

If you only need to restart the service without uploading new files:

vm_workforce_cutover.sh

[VM]
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

Process 7: Update The SQLite Database From New 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. 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 from the newest snapshot date, and runs the importer against the staging database 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 Staging Database File For A Reference Refresh

Never copy a local workforce.db over staging to update reference data. That permanently destroys any users, sessions, or activity that exist only on staging. 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 also recovers descriptions from AINONOTE.DBF for orders not yet promoted to formal LNITM line items. Previously these "shell" orders displayed as "Order NNNNNN" placeholder text in Time On Tasks and the Operations Console.

  • Single-line preliminary orders show the plain product description, for example "plaque polshd blck acrylc 9" x 12"".
  • Multi-line preliminary orders show numbered items using AINONOTE.ONLINENO position.
  • Orders with no resolvable product information stay as "Order NNNNNN" — correct behavior when ProfitMaker has nothing more.
  • Retroactive automatically: when a shell order gets formal LNITM entries, the LNITM description wins on the next run.

No operational action required. Schema is unchanged; this is purely an enrichment of the sales_order.description field. Implementation lives in workforce_app/backend/import_profitmaker_reference.py (build_item_description_lookup and supplement_summaries_from_ainonote).

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 the lane-fs drop folder — the nightly pipeline does the rest:

  • Copy the newest ASIDTA zip into D:\Automation\bucket on lane-fs.
  • At 22:15 Arizona a loader validates it and stages it as the snapshot source; the 22:30 upload and 23:15 import then run as above.
  • Bad or password-protected zips are quarantined and the previous 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

Confirm the API is healthy and that current order search reflects the latest imported data. 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 returns {"error": "Authentication required. Please sign in."}. Sign in with the staging tester PIN first to obtain a token.

[Mac-local or VM]
curl -sS https://staging.timeontasks.laneaward.com/api/health
[Mac-local — obtain a session token (staging tester PIN)]
TOKEN=$(curl -s -X POST -H "Content-Type: application/json" -d '{"pin":"444444"}' https://staging.timeontasks.laneaward.com/api/auth/pin-login | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])")
[Mac-local — authenticated order search]
curl -sS -H "Authorization: Bearer $TOKEN" "https://staging.timeontasks.laneaward.com/api/orders/search?q=107923&limit=3"
Emergency Manual Fallback — Retired Scripts

The old hand-run push scripts (refresh_workforce_reference_snapshot.sh, push_reference_to_staging.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_staging.sh

Both paths use the same importer and are idempotent — a manual run followed by the next nightly run is harmless. 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 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.

Connection Settings
  • PRAGMA journal_mode = WAL, which enables Write-Ahead Logging
  • PRAGMA busy_timeout = 10000, which gives SQLite up to 10,000 milliseconds to wait on a short lock
  • PRAGMA synchronous = FULL, which 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 now returns HTTP 503 Service Unavailable
  • response includes retryable: true
  • treat this as a transient retry condition, not a data-loss event

Iteration test checklist:

Current Reliability Status

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 = 10000
  • synchronous = FULL
  • short BEGIN IMMEDIATE write transactions
  • retryable HTTP 503 responses for SQLite busy/locked contention

In 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.

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 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 the 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.
  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 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.

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. Always 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 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.

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. Non-zero count here opens Phase 2 client-side retry work.
  • p95 latency under 800 ms — good tablet UX. 800 ms–2 s is acceptable. Above 2 s warrants review of server resources.

Prerequisites

Step 1 — Verify SSH access before starting

The 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.

Step 2 — Run the standard realistic test

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 ...

Step 3 — Read the report

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).

Step 4 — Optional: run the burst ceiling test

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.

Adjusting the test

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
  • --local — spawn a local server with a temp DB (no SSH, for dev use)
Scaling Scenarios
  • Adding more people is the expected growth path — increase --users to match headcount before each go-live milestone
  • Increasing --iterations simulates a longer shift with more task cycles per person
  • Run both realistic and burst at each user count milestone to see where the ceiling is

If the test fails

Hard failures (non-zero)
  • Check the error detail in the report — it includes which user and which operation failed
  • A request timeout (25 s) means a write waited longer than SQLite's 10 s busy_timeout plus network — very unlikely at human pace, indicates a server resource issue
  • An HTTP 500 error means an unhandled exception on the server — check sudo journalctl -u laneaward-workforce-api-staging -n 100
High retryable busy count
  • In realistic mode this should be zero — if non-zero, writes are occasionally colliding despite human pacing
  • In burst mode this is expected and does not indicate a problem unless it accompanies hard failures
  • A persistent non-zero count in realistic mode opens Phase 2 work: client-side retry/backoff on HTTP 503
High p95 latency
  • p95 above 2 s on individual requests: check AWS instance CPU and disk I/O during the test run
  • High latency on login only may indicate the pin-throttle table is accumulating rows — run Process 4 (Activity Reset) to clear it on staging
  • Uniformly high latency across all ops usually means network, not SQLite — try ping staging.timeontasks.laneaward.com from the same machine
Test Fixture Identifiers

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:

[Mac-local]
bash scripts/cleanup_stress_test_staging.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. This process applies to both the staging and production environments since both run on the same AWS server.

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 / HTTPSOpen 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
The SSH Config Entry Every Deploy Depends On

The Mac's ~/.ssh/config must list 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

The deploy scripts 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 right key is never reached and the deploy fails with "Too many authentication failures".

It fails every time rather than intermittently, and it reads as a credential or firewall problem, which is the wrong place to start. Diagnosed and fixed 28 August 2026. If the config is ever lost mid-task, prefixing a command with SSH_AUTH_SOCK= bypasses the agent as a one-off.

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.

Do Not Remove 72.208.129.218 — Retracted Task

This section previously carried a pending task to revoke 72.208.129.218/32, described as a leftover home-lab address. That was wrong and the task is withdrawn. The address is the development Mac's own outbound address, confirmed by asking the server which source address it sees. Every deploy script, both restore scripts and the twice-daily database backups reach the server from it.

Revoking it breaks deploys, backups and restores simultaneously, with no common symptom to point at the cause. It is a residential address, so it can change — when 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%% *}'
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. Example: a $52,000/yr salary worker costs closer to $32–$36/hr fully burdened vs. $25/hr at base. Applying a burden multiplier (e.g. 1.30×) would give a more accurate true cost per order.
  • Blended rate approach — some operations use a single blended rate for all salaried staff in a role tier rather than individual salaries, simplifying the calculation at the cost of per-person accuracy.
  • Part-time salary proration — the 2,080 divisor assumes full-time (40 hr/week). Part-time salaried workers would need a different annual-hours denominator.

Staging Notes

Source References

Fast Links
  • LANEAWARD_SETUP_LOG.md
  • workforce_app/deploy/README.md
  • aws-server-setup-summary.html
  • LANEAWARD_ENVIRONMENT_TOPOLOGY.html
Current Working Frontends
  • timeontasks/index.html
  • timeontasks/app.js
  • timeontasks/sw.js — service worker
  • timeontasks/manifest.webmanifest
  • timeontasks/user-guide.html
  • ops_console/index.html
  • ops_console/runbook.html — deploy copy of this document; canonical source is _documents/staging/runbook.html
Stress Test
  • workforce_app/backend/stress_test_concurrent.py — concurrent pre-go-live validation script