Console demo: the customer console's real pages over an invented clinic, read-only (#11)
The console's pages moved into app/console/_pages and read through one ConsoleData (lib/console-data.ts): the real account's, or the demo's. /console and /demo/console render the same pages; only the data and the links' base differ.
lib/console-demo.ts: Harbor Physio, 2,290 invented leads worked out from their place in line and the time now (nothing stored), so the board's last 24 hours always read live and each lead keeps its number and story.
Every action keeps its dialog and says what it would do instead (resend, routes, Connect GHL, disconnect, invite, roles, remove). The frame carries a read-only band with the way to the operator demo.
A lead's GHL calls show what Fernway sent above GHL's answer, so long lines are no longer cut.
Break it ends with the two console demos. CI opens the demo with no account behind it.
Operator: Fernway's own console, its watchers and incidents, and a public read-only demo (#10)
Migration 0013: staff (owner, support, viewer), operator_audit (written once; the app can't change a line) and incidents (one open per watcher).
Staff only, with two-step sign-in on; for anyone else the pages and actions are a plain 404. Every action asks for a fresh authenticator code (a recovery code isn't enough) and is written to the operator audit log.
Overview: ten watchers (worker, disk, memory, queue, GHL refusals, GHL tokens, backup, restore test, alert emails, payments), open incidents, the parts and their commits, the box's processor, memory and disk, the service in numbers, the last deploy, backup, off-box copy and restore test.
Queue, GHL across every account, Accounts (found by name, owner email or AC number; extend a trial, sync with Stripe), Stripe events and money (test mode), alert emails, Incidents (acknowledge, notes), Staff.
The worker reads the watchers every minute: a watcher gone wrong opens an incident once, raises it if it gets worse and closes it when fine again.
/demo/operator: the same views over an invented Fernway; every action only shows what it would do.
CLI staff-owner makes the first owner. Privacy 1.3 says what staff see.
Tests: operator.test.ts against a real database; CI checks the console is a 404 for a customer and the demo opens.
Console: the customer console, from the delivery board to the team (#9)
* Console: the customer console, from the delivery board to the team Migration 0012: audit_log (who did what in an account, kept with the name it was written with) and invites (by email, 7 days, one open invite per email per account).
Delivery board: where every lead is now, the last 24 hours hour by hour, the lead waiting longest, why GHL is refusing with Resend for each reason, the latest movements.
Sync log: every lead, 15 a page, by state and found by waybill, name, email, company or phone. A lead opens to every scan, each GHL call showing what Fernway sent and what GHL answered.
Routes, drawn as routes: picked up, contact and tags, meeting, stage, signed for, named from GHL's own pipelines and calendars. Owners and admins make and change them; a key stays as made.
GHL connection: state, token health, GHL's answers over 24 hours, approved permissions. Reconnect and Disconnect from the console; the connect state carries who started it, so the audit log names them.
Team: invite by email, join page with name and password, roles by the owner, removal ends the sign-in at once. One account per person for now.
Audit log: resends, routes, GHL, team, API keys, by kind, 15 a page.
Closed invites are deleted a day after they close, with the hourly sign-in clean-up. Privacy 1.2 says so.
Tests: console.test.ts and manage.test.ts against a real database (scoping, roles, an invite good once, audit).
* Console: Connect GHL without a link or a session still lands on the public connection page Only a signed-in member is sent back to the console (only the owner or an admin connects); CI's check of the bare start address holds again.
Every pressable control shows the hand cursor (#8)
* Every pressable control shows the hand cursor One rule for the whole app (tokens.css): buttons, switches, radios, tabs and selects show the pointer; a disabled one shows not-allowed. Several buttons styled as links (Start Growth, the forms' Send, Docs' Run it) showed an arrow. Measured on 20 pages: every visible control shows the hand.
* Test: count two-step codes from the spent step, not the clock CI caught it: when a 30-second boundary passed mid-test, the 'spent' code was already the next step's and was accepted. The test now takes the step recorded when two-step was turned on, so the clock can't move it.
Accounts: confirm the email before paying or making an API key (#7)
Sign-up still signs straight in. Starting a plan and making a key act in the account's name, so both wait for a confirmed address: the API answers 403, and the page shows Confirm your email first with Send the link again in place of the buttons.
The merge gate's own runs (skipped on main) made every commit on main read as not passed. The deploy now requires exactly checks and container, each passed at its latest run; a failing commit still refuses (checked against pull request #2).
Privacy and Terms 1.1: accounts, payments and emails (#5)
Privacy: what an account keeps (hashes and fingerprints only for anything that signs in, the two-step secret encrypted, devices without IP addresses, invoices without cards), Stripe in test mode only, the move to customer in GoHighLevel, Resend for emails, how long each part is kept, deleting an account step by step, the one sign-in cookie.
Terms: accounts on a 14-day trial, plans paid only in Stripe's test mode.
The worker now deletes sign-ins 30 days after they end and spent email links after a day, as the policy says.
Merge gate: squash, so every commit on main is signed and Verified (#4)
Branch commits are signed with Jerome's own SSH key (fernway-signing); the squash GitHub makes on main is signed by GitHub. A rebase merge drops signatures, so main would read Unverified.
CI gate: changes reach main only through a pull request that passed CI
.github/workflows/merge.yml: a pushed branch gets its pull request at once; it merges by rebase only after CI passed on its latest commit. Opened and merged as jeromebadour (repository secret FERNWAY_MERGE_TOKEN).
The ruleset on main (set in GitHub): pull request required, checks and container from GitHub Actions must pass, branch up to date, rebase only, no force pushes, no deletion, no bypass.
README: the gate in one line. The deploy script's own check stays: two gates, merge and deploy.
Alert emails: a lead held, a lead returned, Monday's summary
Migration 0011 `alerts`: one row per email, made once per lead and person (and once a week for the summary), sent by the worker with waits of 1, 5, 15, 30, 60, 120 and 240 minutes, given up after 8 tries with the reason.
Only confirmed addresses, only people who kept that email on; never the demo or the site's own account.
Each email is a small waybill (waybill, lead, route, when, GHL's reason in words or its last answer), readable as plain text, in the person's own date style and time zone, with the receipt's link.
Monday's summary at 08:00 each person's time: last week's leads signed for, held and returned, GHL's slowest day.
The letter and sending moved into the shared package; the worker sends through Resend or the CI outbox.
Preferences says only what the emails do (the receipt, not a resend: that comes with the console).
Tests 119; a deliberate break (unconfirmed addresses emailed) caught.
Account area: security, devices, API keys, preferences, your record, delete
Two-step sign-in with an authenticator app (RFC 6238): the secret kept sealed, a code accepted once per 30-second step, 10 recovery codes kept as hashes and spent once; sign-in asks for the code with a 5-minute ticket. Setup in one dialog: QR label, six code boxes, the recovery codes as a tear-off strip.
Change password with the current one; other devices sign out, this one stays.
Devices as stubs with their session number; sign out one or every other; ended ones in the last 30 days with why.
API keys made, listed and revoked in the app (owners and admins), shown once, who made each, last used.
Preferences: date style and clock shown on a live receipt line; which emails arrive.
Your record: Fernway's side and GHL's side read live, line by line, the leads with receipts, a JSON export.
Delete account: GHL first, then Stripe, then Fernway, each step reported; nothing in Fernway goes if GHL or Stripe can't be reached.
Retention keeps the move to Customer while its account exists (it would have stopped on the account's link).
Migration 0010; tests 111; deliberate breaks (code replay, deleting past a GHL failure) caught.
Billing: plans through Stripe (test mode), hidden with accounts
Migration 0009: the account's plan, Stripe customer and subscription, status and period; every Stripe event handled once; invoices as Stripe last reported them; "stripe" as a lead source.
packages/delivery billing: the three plans and the customer portal made from code by lookup key (a price that drifts gets a new one that takes the key); Checkout for a plan, the portal for card, switch and cancel; the webhook reads only Stripe-signed, recent bodies and re-reads each subscription and invoice from Stripe, so late events never undo a newer state; the first paid invoice moves the owner to Customer in Fernway's own GHL, once, as a lead with its receipt. A live key is refused.
Plan and billing page: the plan and where it stands (trial, renews, ends, payment failed), leads signed for against the plan, the three plans with Start or Switch, the move to Customer with its waybill, the invoices.
CLI stripe-catalog: makes or checks the plans in Stripe and prints them.
Tests 23, against a fake Stripe that answers like the real API; two guards broken on purpose were caught.