# lfcs-signon — cutover checklist (VPS staging -> live)

Staging is running on this box at 127.0.0.1:8090 (systemd unit `lfcs-signon.service`), using
LocalStorage against a placeholder `/opt/lfcs-signon/test-jobs`. It is NOT wired to nginx and
NOT touching the live tunnel on 8085. This file is everything left before it can go live.

## (a) Service-account JSON

DriveStorage (`app/storage.py`) needs a Google service account with Drive API access. Nobody has
created one yet. Steps, in order:

1. https://console.cloud.google.com -> create a new project (or reuse an existing LFCS one if
   Rocky already has one for other tooling — check before making a duplicate).
2. APIs & Services -> Library -> search "Google Drive API" -> Enable.
3. APIs & Services -> Credentials -> Create Credentials -> Service Account. Name it something
   identifiable, e.g. `lfcs-signon`.
4. Open the new service account -> Keys -> Add Key -> Create new key -> JSON. Downloads a
   `.json` file — this is the only copy Google gives you.
5. Copy that file to the VPS as `/opt/lfcs-signon/sa.json` (`scp` it up, `chmod 600`, `chown
   root:root`). Never commit it, never paste its contents into chat/notes — reference by path
   only, same rule as every other credential.
6. Note the service account's email address (looks like
   `lfcs-signon@<project-id>.iam.gserviceaccount.com`) — needed for (b).

## (b) Drive folders to share with the service-account email

- **The shared "Jobs" folder** (the one holding Upcoming/, Live/, etc.) — share with the SA email
  as **Editor**. This is what `DriveStorage` walks for job folders, docs, registers, everything
  under Sign-On. Get its folder ID from the Drive URL — that ID is `JOBS_ROOT_FOLDER_ID` below.

- **"My Drive/Timesheets" — DOES NOT APPLY AS WRITTEN. Read this before doing it.**
  `TIMESHEETS_ROOT` (`app/timesheet.py` `CrewFiles`) is a **plain filesystem path** opened with
  `pathlib.Path` / `.open()` — it is never passed through `DriveStorage` and never touches the
  Drive API. On the laptop it works today only because "I:/My Drive/Timesheets" is a path inside
  a Google-Drive-for-Desktop mount — the OS does the syncing, not this app's code. Sharing that
  Drive folder with a service-account email changes nothing on a Linux box with no Drive mount.
  **This is a real gap, not a checkbox.** Before cutover, decide one of:
    1. Mount the Timesheets folder on the VPS via something like `rclone mount` using the same
       SA credentials (SA would then need Editor on that folder too, which is when sharing it
       actually matters), and point `TIMESHEETS_ROOT` at the mount path, or
    2. Change `CrewFiles` to write through `DriveStorage`'s API calls the same way everything
       else does (a real code change, not a config change), or
    3. Accept that in a first cutover, timesheets land on the VPS's local disk only and do not
       sync to Drive/anyone's desktop — probably not acceptable, flagging so it's a decision, not
       a silent gap.
  Flag this to Rocky before flipping the switch — worker timesheets are a payroll-adjacent record
  and "quietly stopped syncing" is the kind of failure that should never be discovered by someone
  asking where their hours went.

## (c) .env values DriveStorage needs (read from `app/storage.py::from_env`)

```
# Remove or leave blank — LocalStorage wins if this is set, and cutover means it must NOT be:
LOCAL_JOBS_ROOT=

GOOGLE_SERVICE_ACCOUNT_JSON=/opt/lfcs-signon/sa.json
JOBS_ROOT_FOLDER_ID=<folder id of the shared Jobs root, from its Drive URL>

SIGNON_SUBPATH=05 - Safety & Compliance/Sign-On    # match the live folder naming exactly
JOB_ALIASES=powerhouse:Ford Civil - Parramatta      # copy whatever is current on the laptop .env
PUBLIC_BASE_URL=https://signon.lfcs.com.au

# See (b) above before setting this — do not just copy the laptop value blind:
TIMESHEETS_ROOT=<resolved per the decision in (b)>

# Optional. Only needed if the mic/voice-note feature is wanted on the box. Not copied from the
# laptop .env during staging setup — it's a live API key, not a path, so it was deliberately left
# out. Only add it here if the owner explicitly provides a new key for the VPS.
DEEPGRAM_API_KEY=
```

`GOOGLE_SERVICE_ACCOUNT_JSON` and `JOBS_ROOT_FOLDER_ID` are both `os.environ[...]` (not
`.getenv`) in `from_env()` — the app throws `KeyError` on startup if either is missing once
`LOCAL_JOBS_ROOT` is blank. That is the tell that cutover config is wired: if the service starts
clean, both are read.

## (d) The actual cutover — nginx upstream flip + tunnel retirement

Current: `/etc/nginx/sites-available/signon` proxies `https://signon.lfcs.com.au` and the
nip.io name to `127.0.0.1:8085` (the reverse SSH tunnel from the laptop). Staging runs on 8090
and nginx does not know it exists yet — by design, this wave never touched nginx.

1. Confirm staging passes `smoke.sh` AND a manual phone test against 8090 (via SSH port-forward
   or a temporary nginx location, tester's choice) with real Drive credentials in place — not
   just the placeholder test-jobs folder.
2. Edit `/etc/nginx/sites-available/signon`: change `proxy_pass http://127.0.0.1:8085;` to
   `proxy_pass http://127.0.0.1:8090;`. One line.
3. `nginx -t` then `systemctl reload nginx` (reload, not restart — do not drop the cert/listener).
4. Immediately curl `https://signon.lfcs.com.au/j/2643` (or whichever live job is current) and
   confirm 200 and correct content. If wrong, see rollback below — do this before telling anyone
   it's live.
5. Once confirmed stable (Rocky's call on how long to watch it — suggest at least one full work
   day): retire the tunnel —
   - On the laptop: disable the scheduled task / process that keeps the reverse SSH tunnel to
     8085 alive.
   - On the VPS: nothing to clean up on the nginx side (8085 was never a proxy target on its
     own — it just stops receiving connections). Confirm nothing else on the box still expects
     8085 before removing any watchdog around the tunnel.
6. Stop treating `lfcs-signon.service` (8090) as staging — it IS the app now. Consider renaming
   the unit description at that point (cosmetic, not urgent).

## (e) Rollback

If the flip breaks anything: edit `/etc/nginx/sites-available/signon` back to
`proxy_pass http://127.0.0.1:8085;`, `nginx -t`, `systemctl reload nginx`. The laptop's tunnel
and its FastAPI process are untouched by any of this — as long as the laptop app and its
scheduled task were not yet disabled, rollback is a one-line config edit and a reload, nothing
else to undo. This is exactly why step (d)5 says wait before touching the laptop's tunnel.

## What's already done (this wave, staging only)

- `/opt/lfcs-signon/` — app source, venv, requirements installed. `deploy/push.sh` re-syncs code
  from the laptop on demand (tar-over-ssh, since Windows has no rsync).
- `lfcs-signon.service` — systemd unit, uvicorn on 127.0.0.1:8090, `Restart=always`, currently
  running against a placeholder LocalStorage folder (`/opt/lfcs-signon/test-jobs`) so the runtime
  is proven end-to-end without any Drive credentials existing yet.
- `/opt/lfcs-signon/smoke.sh` — curls the staging routes, exits non-zero on failure.
- nginx, certbot, the 8085 tunnel, and the live site were not touched. Verified 200 on
  `https://signon.lfcs.com.au/j/2643` before and after this entire wave.
