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 URL | https://timeontasks.laneaward.com |
|---|---|
| Console URL | https://console.laneaward.com |
| App Launchpad | https://ai.laneaward.com |
| ToT web root | /var/www/laneaward-timeontasks/ |
| Console web root | /var/www/laneaward-console/ |
| Database | /var/lib/laneaward/workforce.db |
| Backend service | laneaward-workforce-api.service · port 9194 |
| App source | /opt/laneaward/ |
| TLS cert | /etc/letsencrypt/live/timeontasks.laneaward.com/ — covers both domains, auto-renews |
| API health | https://timeontasks.laneaward.com/api/health |
| SSH | ssh -i ~/.ssh/lane_webserver.pem ubuntu@3.130.69.109 |
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.
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.
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.
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.
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 |
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.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.timeontasks/ and promotes to /var/www/laneaward-timeontasks/ops_console/ and promotes to /var/www/laneaward-console/promote_staging_db_to_production.sh for DB promotion and Process 6 for service restart.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.
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
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
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.
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
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
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.
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:
_documents/production/runbook.html → runbook.html_documents/user-guide.html → user-guide.html_documents/project-reference.html → project-reference.html_documents/laneaward_environment_topology.html → topology.htmlRun 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.
bash scripts/deploy_console_docs_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.
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.
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.
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.
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.
timeontasks
with console to do the same for the Operations Console.
on twice is safe. The second run refuses and changes nothing, so the
preserved page can never be overwritten with the holding page.
off when the app is not in maintenance mode refuses and tells you
which deploy script to use instead.
index.html.live-backup in the app's web root, and can be copied over
index.html by hand.
Production is protected by three independent layers. They are deliberately different from one another: each one survives a failure that would defeat the others.
| Layer | What it protects | Runs on | Schedule |
|---|---|---|---|
| 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 |
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.
laneaward-db-backups-364410974402 (region us-east-2)production/workforce_<UTC timestamp>.db.gz and staging/…/opt/laneaward/backup/s3_db_backup.pylaneaward-s3-backup.timer/var/log/laneaward-s3-backup.logThe 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.
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
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.
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.
~/projectbackups/laneaward/production-database/~/projectbackups/laneaward/staging-database/~/projectbackups/laneaward/source/ (weekly)~/projectbackups/backup_logs/backup.log
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.
grep "LANEAWARD/VM" ~/projectbackups/backup_logs/backup.log | tail -6
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.
sudo cp /var/lib/laneaward/workforce.db /var/lib/laneaward/workforce-$(date +%F-%H%M%S).db
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.
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.
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:
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.
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.
RESTORE then YES — two separate gateslaneaward-workforce-api.service~/projectbackups/laneaward/production-database/workforce_vm_prod_YYYYMMDDTHHMMSS.dbls -lht ~/projectbackups/laneaward/production-database/
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.
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.
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
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
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;"
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
ssh laneaward-vm 'sudo systemctl status laneaward-workforce-api.service --no-pager'ssh laneaward-vm 'sudo journalctl -u laneaward-workforce-api.service -n 50 --no-pager'/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.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.
| Image | What 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. |
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.
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.
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.
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/
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.
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/
Once the rebuilt machine is the only one in service, reinstall the Twingate connector with its own identity so remote office access works again.
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.
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.
cat /var/run/reboot-required 2>/dev/null || echo "no restart pending" apt list --upgradable 2>/dev/null | tail -n +2
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.
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.
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.
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.
Use these checks to confirm the production service and both apps are responding correctly.
curl -sS http://127.0.0.1:9194/api/health
curl -sS https://timeontasks.laneaward.com/api/health curl -sS https://console.laneaward.com/api/health
sudo systemctl status laneaward-workforce-api.service --no-pager
sudo journalctl -u laneaward-workforce-api.service -f
Use these only when the backend service itself needs attention. The deploy script handles restarts automatically — these are for manual intervention only.
sudo systemctl restart laneaward-workforce-api.service
sudo systemctl stop laneaward-workforce-api.service sudo systemctl start laneaward-workforce-api.service
sudo systemctl is-enabled laneaward-workforce-api.service
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.
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.
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.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.
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.
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.
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.
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.
sudo systemctl start pm-import.service
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:
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';\""
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:
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\).
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.
PRAGMA journal_mode = WAL — Write-Ahead LoggingPRAGMA busy_timeout = 10000 — up to 10,000 ms wait on a short lockPRAGMA synchronous = FULL — favors safer disk writes over speedHTTP 503 Service Unavailableretryable: trueIteration test checklist:
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 contentionPhase 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.
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.
| Request type | Strategy | Why |
|---|---|---|
HTML documents (index.html, user-guide.html) | Network-first | Always 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-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 via Process 1.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 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.
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 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.
ubuntu@3.130.69.109 with no passphrase prompt (BatchMode).https://timeontasks.laneaward.com.cd /Users/donaldscott/Project-Code/laneaward/repossh -i ~/.ssh/lane_webserver.pem ubuntu@3.130.69.109 echo "SSH OK"
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.
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.
--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 inspectionsudo journalctl -u laneaward-workforce-api.service -n 100ping timeontasks.laneaward.combash scripts/cleanup_stress_test_production.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.
| 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 |
| Development Mac — required | 72.208.129.218/32 · SSH allowed · carries all deploys, backups and restores |
| HTTP — port 80 | Open to all — 0.0.0.0/0 · required for Let's Encrypt HTTP-01 renewal |
| HTTPS — port 443 | 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 |
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 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.
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.
Security group sg-0cc9719fa0e029c40 permits SSH on port 22 from three addresses.
Before changing or removing any of them, understand what depends on it.
| Address | What it is | What 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. |
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:
ssh laneaward-vm 'echo ${SSH_CLIENT%% *}'
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.
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;'
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.
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.
sudo cat /etc/laneaward/workforce-api.env
Note the LANEAWARD_PIN_PEPPER value, then substitute it below in place of PEPPER:
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.
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.
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.