diff --git a/CLAUDE.md b/CLAUDE.md index 51bcfb8..4c0f108 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -43,7 +43,7 @@ distilled to what applies to HQ. A change that breaks one does not land. | Append-only audit | Every mutation calls `writeAudit(db, userId, action, entity, id, before, after)` in the **same transaction** as the change. The audit log is append-only — never update or delete rows. Previews and the public share route write nothing. | `audit.ts` | | Client id is the future tenant id | `client.id` is a client-generated UUIDv7 that becomes `tenant_id` when the cloud tier stands up — same rows, promoted once. Never re-key clients. | `repos-clients.ts` | | No silent caps | Every list that can grow paginates or warns; never silently truncate. | reports, directory queries | -| Strict TS, green before landing | Strict TypeScript everywhere; `npm run typecheck` and `npm test` both clean before anything lands. Money math is locked by tests — a rounding change without a test change does not land. | `tsc`, vitest (389 tests) | +| Strict TS, green before landing | Strict TypeScript everywhere; `npm run typecheck` and `npm test` both clean before anything lands. Money math is locked by tests — a rounding change without a test change does not land. | `tsc`, vitest (416 tests) | ## Running it @@ -61,12 +61,19 @@ cd apps/hq-web && npm run dev - Owner login on first boot: `admin@tecnostac.com` + the one-time password printed in the server log (change it immediately). The "Gmail disconnected" banner is expected until Gmail is connected. - Env: `DATABASE_URL` (set → Postgres engine, absent → SQLite at `HQ_DATA_DIR`; D19), - `SIMS_PIN_PEPPER` (set once, never change), `HQ_SECRET_KEY` - (before Gmail), `GOOGLE_*` (Gmail), `AWS_*` (Cost Explorer), `HQ_PORT` (default 5182). + `SIMS_PIN_PEPPER` (set once, never change), `GOOGLE_*` (Gmail), `AWS_*` (Cost Explorer), + `HQ_PORT` (default 5182). +- **Credentials are stored in the clear (D31/D32).** `HQ_SECRET_KEY` was removed — portal / + gateway passwords, module secrets, client DB passwords and the Gmail token are plaintext, + usable keyless (login passwords stay scrypt-hashed). A DB dump exposes them, so protect the + DB file and its backups. Back up with `npm run backup` (D33 → `pg_dump` to `./backups`). +- **SMS gateway** (D28): the SMS module's per-client balance is pulled live from the provider + panel (`sms.balance_api_url`, keyless) — manual "Refresh from gateway" + a daily scheduler + pass; low balances surface on the SMS credits screen, cockpit tile and dashboard alert. - Postgres integration tests: `HQ_PG_TEST_URL=postgres://hq:hq_dev@localhost:5432/hq_test npm test` (wipes that database each run — never point it at real data). - Health check: `GET /api/health` → `{ ok: true }`. -- Verified state: `npm install` + typecheck clean, 389 tests pass. Exercise real flows end +- Verified state: `npm install` + typecheck clean, 416 tests pass. Exercise real flows end to end before claiming done. ## Docs (kept in `docs/`) diff --git a/README.md b/README.md index d2c2d2c..fbd59a4 100644 --- a/README.md +++ b/README.md @@ -26,15 +26,23 @@ document and conversation we've had with them. - **Reports**, a **Dashboard**, an **append-only audit log** (every mutation), an **APEX importer** for the initial 300-client load, and a **scheduler** (runs on boot + every 6h: reminders, bounce polling, AWS cost pull). -- **A redesigned ops UI (2026-07-17)** — warm light/dark theme with a teal accent, grouped - sidebar with a live reminder badge, a **Ctrl-K command palette** (client search, pages, - actions), and a **Client 360** with tabbed sections and a 12-month **relationship-pulse - ribbon**; phone-usable drawer layout. Spec: - `docs/superpowers/specs/2026-07-17-hq-console-redesign-design.md`. +- **A redesigned ops UI** — light/dark theme with accent + neutral palettes, grouped + sidebar with a live reminder badge, a **Ctrl-K command palette** (clients, documents, + tickets, pages, actions), and a **Client 360** with tabbed sections and a 12-month + **relationship-pulse ribbon**; phone-usable drawer layout. +- **Support tickets** (aging/SLA/assignee workbench, bill-from-ticket), **onboarding** + milestones, and **per-module operational fields** each module declares (config, not code) — + surfaced per client on Client 360, across all clients in the **Module data** directory, and + as a **Portals** quick-list of every stored login. For the **SMS** module those fields + include a **live credit balance** pulled from the provider gateway (manual refresh + a daily + pass), with low balances flagged for a one-click top-up quote. +- **Renewals** command center, an owner **MIS cockpit**, an **audit-log viewer**, and + **database backups** (`npm run backup` → compressed, restorable `pg_dump`). Credentials are + stored in the clear (no `HQ_SECRET_KEY`), so the DB and its backups are the security boundary. ## Build state (start here) -Working codebase, not a plan. `npm install` + typecheck are clean and **389 tests pass**. +Working codebase, not a plan. `npm install` + typecheck are clean and **416 tests pass**. Current state and run-vs-pending detail live in **[STATUS.md](STATUS.md)**; the go-live runbook (server, HTTPS, backups, Gmail/AWS/import wiring, the Postgres switch) is in **[docs/DEPLOY-HQ.md](docs/DEPLOY-HQ.md)**. @@ -47,9 +55,10 @@ runbook (server, HTTPS, backups, Gmail/AWS/import wiring, the Postgres switch) i dev/test engine; Postgres is the locked production engine (see DEPLOY-HQ.md). DB at **`data/hq.db`**; server on **:5182** (also serves the built web UI and the public `/share/:token` route). -- **Frontend — `apps/hq-web`**: React + Vite. Pages: Dashboard, Pipeline, Clients, - ClientDetail, Modules, Reports, NewDocument, DocumentView, Employees (owner-only), - DocumentTemplate (owner-only). +- **Frontend — `apps/hq-web`**: React + Vite. Pages: Home, Dashboard, Pipeline, Reminders, + Tickets, Onboarding, Clients, ClientDetail, Documents, NewDocument, DocumentView, Renewals, + SMS credits, Modules, Module data, Portals, Reports, MIS cockpit (owner), Employees (owner), + Profile, APEX Import (owner), Audit log (owner), Settings. - **Shared packages** (trimmed forks): `@sims/domain` (ids, money, business-day, doc-series, gstin, documents), `@sims/auth` (pin/scrypt), `@sims/billing-engine` (compute, tax — our invoices are GST documents, computed to the paisa), `@sims/ui` (design system). diff --git a/STATUS.md b/STATUS.md index 3d60c62..cf79d5d 100644 --- a/STATUS.md +++ b/STATUS.md @@ -1,7 +1,7 @@ # SiMS HQ — Build Status _Internal ops console for running our software business. Not shipped to clients._ -_Last reviewed: 2026-07-17 · Version 0.1.0_ +_Last reviewed: 2026-07-19 · Version 0.1.0_ HQ is the back-office console we use to run ~300 Classic client relationships: the client book, our software module catalogue and dated price book, quotations / @@ -19,23 +19,36 @@ self-service profiles, My Day, client support-access data, the APEX import web UI, the editable module roster with one-click Convert & send, and the section-per-module quotation PDF — then closed all 8 findings from its review. +**Since 2026-07-17 (D20–D33).** Support **tickets** (aging/SLA/assignee workbench, +bill-from-ticket), **onboarding** milestones, client **branches**, and **module-defined +operational fields** each module declares in its `field_spec` (config over code) — shown per +client on Client 360, across all clients in the **Module data** directory, and as a **Portals** +quick-list of every stored login. For **SMS** those fields include a **live credit balance** +pulled from the provider gateway (`sms.balance_api_url`, keyless): a manual "Refresh from +gateway" and a once-a-day scheduler pass, with low balances flagged on the SMS-credits screen, +the owner cockpit tile and the dashboard for a one-click top-up quote. Also added: a **Renewals** +command center, an owner **MIS cockpit**, an **audit-log viewer**, a **global search** (Ctrl-K +now spans clients + documents + tickets), a **Settings → Operations** panel (SMS gateway URL, +low-balance threshold, ticket SLA), Postgres as a first-class engine (`DATABASE_URL`; D19), and +a **database backup** script (`npm run backup`; D33). **D31/D32: credentials are stored in the +clear** — `HQ_SECRET_KEY` was removed, so portal/gateway passwords, module secrets, client DB +passwords and the Gmail token are plaintext and keyless (login passwords stay scrypt-hashed); +a DB dump exposes them, so the DB file and its backups are the security boundary. + ## Verification state | Check | Result | | --- | --- | | `npm install` | clean | | `npm run typecheck` (root + workspaces) | clean, no errors | -| `npm test` (`vitest run`) | **389 tests pass across 74 files** | +| `npm test` (`vitest run`) | **416 tests pass across 85 files** (5 Postgres-gated skipped) | -The 346 figure is the live `vitest run` count measured 2026-07-17, after the -D18 go-live cluster **and its post-delivery review pass** (8 reviewer findings -— convert-and-send click bubbling, importer support-field mapping, Pipeline -convert parity, client-book filters, typed contact roles, due-date anchors on -dashboard/aging, and two atomicity gaps — all confirmed and fixed the same -day). Test files live under -`apps/hq/test/` (49 files, including `employees`, `employees-routes`, -`client-owner`, `pipeline`, `quote-followup`, and `reminder-schedule`), -`apps/hq-web/test/`, and `packages/{domain,auth,billing-engine,ui}/test/`. +The 416 figure is the live `vitest run` count measured 2026-07-19; the suite has +grown from the D18 go-live cluster through the D20–D33 work (tickets, module fields, +renewals, cockpit, audit viewer, SMS gateway, module directory/portals/search, and +the D31/D32 plaintext-credential change with its own migration + review). Test files +live under `apps/hq/test/`, `apps/hq-web/test/`, and +`packages/{domain,auth,billing-engine,ui}/test/`. ## Architecture @@ -60,13 +73,16 @@ day). Test files live under - Web: `apps/hq-web` — `npm run dev` / `npm run build`. - CLI: `npm run import` (APEX importer), `scripts/gmail-connect.ts` (one-time Gmail OAuth). - - Env: `HQ_PORT`, `HQ_DATA_DIR`, `HQ_SECRET_KEY` (64 hex / 32 bytes, for token - encryption), `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET`, - `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`. + - Env: `HQ_PORT`, `HQ_DATA_DIR`, `DATABASE_URL` (Postgres engine; D19), + `SIMS_PIN_PEPPER`, `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET`, + `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`. **`HQ_SECRET_KEY` removed (D32)** — + credentials are stored in the clear, no key needed. + - Backups: `npm run backup` (D33) — `pg_dump` to `./backups`, restorable with + `pg_restore`. ## Database schema (`apps/hq/src/db.ts`) -The schema creates 26 tables on open; `migrate()` additively backfills the +The schema creates 31 tables on open; `migrate()` additively backfills the post-launch columns on older DBs (`module.quote_content`, `email_log.bounced`, `document.due_date`, `staff_user.phone`/`title`, `client.owner_id` plus the D18 support fields `anydesk`/`os`/`district`/`sector`/`db_password_enc`, and @@ -82,7 +98,12 @@ and widens two CHECK constraints via a guarded, transactional table rebuild | `client` | Client registry — code, name, GSTIN, state, contacts (JSON), status, account owner (`owner_id` → `staff_user`), source | | `module` | Software module catalogue — SAC, allowed billing kinds, multi-sub flag, quote-content lines | | `module_price_book` | Dated prices per module/edition/kind (latest effective row wins) | -| `client_module` | A module assigned to a client — lifecycle status, install/complete/train dates, next renewal | +| `client_module` | A module assigned to a client — lifecycle status, install/complete/train dates, next renewal, provider/username/password (plaintext, D32), module-defined `field_values` (D21) | +| `usage_rate_band` | Dated per-unit usage pricing with a minimum order (e.g. SMS ≥ 50,000 units) | +| `client_branch` | Client branches/offices (APEX parity) | +| `project_milestone` | Onboarding checklist items per client_module | +| `ticket` | Support workbench tickets — status, assignee, opened/closed dates, source (`apex`/`hq`) | +| `sms_balance_check` | SMS gateway poll telemetry (status/balance/message/checked_at) — operational, not audited (D28) | | `tax_class` | GST rate classes (dated); `GST18` seeded at 18% | | `doc_series` | Per (doc_type, FY) running number + prefix | | `document` | QT / PROFORMA / INVOICE / RECEIPT / CREDIT_NOTE — totals, status, ref chain, JSON payload | @@ -90,7 +111,7 @@ and widens two CHECK constraints via a guarded, transactional table rebuild | `document_share` | Opaque public share tokens — expiry, revoked flag | | `payment` | Money received — mode, reference, amount, TDS | | `payment_allocation` | How a payment is applied across invoices | -| `email_account` | The single Gmail sending identity — encrypted refresh token, active/dead status | +| `email_account` | The single Gmail sending identity — refresh token (plaintext since D32), active/dead status | | `email_log` | Append-only send trace — sent/failed, gmail message id, error, bounced flag | | `setting` | Key/value store — `company.*` identity, `template.*` letterhead text, reminder + AWS config | | `audit_log` | Append-only before/after JSON for every mutation | @@ -166,7 +187,7 @@ Everything below is implemented and exercised by tests via the Express router renders the one document inline as PDF; unknown/expired/revoked tokens return a data-free "link unavailable" page. Tokens are never logged or audited. - **Email (Gmail)** — `gmail-connect` loopback OAuth (send + readonly scopes) - stores an AES-256-GCM-encrypted refresh token. Sends go over the Gmail REST API + stores the refresh token (plaintext since D32). Sends go over the Gmail REST API (no SDK): access-token exchange, raw MIME build with PDF attachment, send. `invalid_grant` flips the account to `dead` and raises the dashboard banner. Document emails and templated reminder emails share the send path; every @@ -267,6 +288,14 @@ command palette, Gmail status pill, theme/accent switcher and a user menu | `/documents` | `Documents` | Register of every document, newest first: type chips + status filter, honest server pagination with true totals, one-click confirmed **Convert & send** on issued proformas | | `/documents/new` | `NewDocument` | Quotation-in-minutes composer: client type-ahead, line editor, server-computed GST, due-date field on invoices (auto-filled from terms, editable), and a live PDF-fidelity preview (desktop split view / mobile sheet) | | `/documents/:id` | `DocumentView` | Document page: PDF preview, state-driven action bar (issue / send / mark / convert / cancel / credit note / record payment), share & download group (copy link / WhatsApp / native share / revoke), event timeline and email log | +| `/tickets` | `Tickets` | Support workbench: status chips + counts, per-row assignee, **Age** column with SLA overdue flag + Overdue chip, search, one-click **Bill** (New Document pre-filled) | +| `/projects` | `Projects` | Onboarding: per-client-module milestone checklist board | +| `/renewals` | `Renewals` | Module subscriptions + AMC coming due, earliest first, one-click renewal/top-up proforma | +| `/sms-balances` | `SmsBalances` | SMS credit balances (live from the gateway), lowest first; Low/All/Not-recorded chips, per-row top-up quote, owner **Refresh from gateway** | +| `/module-data` | `ModuleDirectory` | Pick a module → every client on it with that module's fields as columns, searchable, portal URLs clickable | +| `/portals` | `Portals` | Every stored login across all modules — Open ↗ links, 🔑 = password on file (reveal on Client 360) | +| `/mis` | `Mis` | Owner cockpit: this-month billing / collections / outstanding / pipeline / renewals / tickets / SMS-low, each tile click-through | +| `/audit` | `Audit` | Owner audit-log viewer: entity/action filters (from server facets), paginated before/after key-diff | | `/settings` | `Settings` | Settings hub (Pinterest-style left rail, `?s=` deep-links): Company profile · Document template (live preview) · Reminders (dated cadence rows, append-only) · Sharing default expiry · Integrations status · Appearance. Owner-only except Appearance; `/settings/template` redirects here | | `/employees` | `Employees` | Owner-only staff management: add / edit / re-role / reset password / deactivate / reactivate, surfacing the last-owner and self-deactivation guards | | `/profile` | `Profile` | Self-service: edit display name/phone (email + role read-only), change own password (current password required; other sessions purged) | @@ -295,7 +324,7 @@ pages exist; invoices carry **due dates** (terms default + override, stamped at issue — the overdue chase says "was due on X, N days overdue"); self-service profile + own-password change; sidebar collapses; the dashboard has a My Day mode (staff always scoped to their book); clients carry support-access data (AnyDesk, -OS, district/sector filters, an encrypted DB password with audited reveal); the +OS, district/sector filters, a DB password — plaintext since D32 — with audited reveal); the module roster is fully editable both ways (one audited write path with Client 360); "Convert & send" is one click from the register and the document view; and a multi-module quotation prints one titled section per module._