docs: bring README/STATUS/CLAUDE/DECISIONS current (D34-D37)

Records the work since the last doc pass: employee master import + invited/first-login
flag (D34), client type PACS/Store + directory-sourced LTD numbers and the name-correction
session (D35), login history with IP/device (D36), and the society category matching the
Kerala co-op directory taxonomy (D37). Adds the login_event table, the Login history
screen, and the client type/category fields; bumps the schema table count 31->32. Decisions
D34-D37 appended to 06-DECISIONS.md.
main
Thomas Joise 2 days ago
parent 9cb5127faf
commit a9e5aa79d9

@ -17,7 +17,7 @@ dashboard, an append-only audit log, an APEX importer, and a scheduler.
| Part | Where | Notes |
|---|---|---|
| Backend | `apps/hq/src` | Express over `better-sqlite3`, DB at `data/hq.db` (WAL) |
| Frontend | `apps/hq-web/src` | React + Vite. **Home** card launcher is the landing (`/`); Dashboard (My Day) is at `/dashboard` (D30). Pages: Home / Dashboard / Pipeline / Reminders / Tickets / Onboarding / Clients / ClientDetail / Documents / NewDocument / DocumentView / Renewals / Modules / Module data / Portals / Reports / MIS cockpit (owner-only) / SMS credits / Employees (owner-only) / Profile / APEX Import (owner-only) / Audit log (owner-only) / Settings. Theme = light/dark, 7 accents, 4 neutral palettes (Warm/Slate/Graphite/Zinc), Geist font -- all in Settings > Appearance (D29) |
| Frontend | `apps/hq-web/src` | React + Vite. **Home** card launcher is the landing (`/`); Dashboard (My Day) is at `/dashboard` (D30). Pages: Home / Dashboard / Pipeline / Reminders / Tickets / Onboarding / Clients / ClientDetail / Documents / NewDocument / DocumentView / Renewals / Modules / Module data / Portals / Reports / MIS cockpit (owner-only) / SMS credits / Employees (owner-only) / Profile / APEX Import (owner-only) / Audit log (owner-only) / Login history (owner-only) / Settings. Theme = light/dark, 7 accents, 4 neutral palettes (Warm/Slate/Graphite/Zinc), Geist font -- all in Settings > Appearance (D29) |
| Repos | `apps/hq/src/repos-*.ts` | one file per domain; plain functions over a `DB` handle |
| Schema | `apps/hq/src/db.ts` + `apps/hq/schema.sql` | `CREATE TABLE IF NOT EXISTS` + additive `migrate()` |
| Shared packages | `packages/*` | trimmed forks — see below |
@ -70,6 +70,11 @@ cd apps/hq-web && npm run dev
- **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.
- **Clients carry a type + category** (D35/D37): `client_type` (PACS/Store) and `category`
(Kerala co-op taxonomy — Service Bank / Vanitha / Housing / Employees / …), both derived from
the name and filterable on the Clients screen. **Employees** log in by username; imported staff
get a default `<name>@123` with a `must_change_password` "Invited" flag (D34). **Login history**
(D36): every sign-in is logged with IP + device (`login_event`), owner-viewable.
- 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 }`.

@ -39,6 +39,11 @@ document and conversation we've had with them.
- **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.
- **Employees** with per-user logins (imported from the master; an "invited" flag until they
change their default password), and a **login history** (every sign-in with IP + device).
- **Client classification** — each client is a **PACS** or **Store**, and carries a **society
category** matching the Kerala co-op directory (Service Bank / Vanitha / Housing / Employees /
…); both are filterable on the Clients screen.
## Build state (start here)
@ -58,7 +63,7 @@ runbook (server, HTTPS, backups, Gmail/AWS/import wiring, the Postgres switch) i
- **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.
Profile, APEX Import (owner), Audit log (owner), Login history (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).

@ -1,7 +1,7 @@
# SiMS HQ — Build Status
_Internal ops console for running our software business. Not shipped to clients._
_Last reviewed: 2026-07-19 · Version 0.1.0_
_Last reviewed: 2026-07-20 · 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 /
@ -35,6 +35,18 @@ clear** — `HQ_SECRET_KEY` was removed, so portal/gateway passwords, modul
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.
**Since 2026-07-19 (D34–D37).** **Employee master imported** (11 staff; username = first name,
default password `<name>@123`) with a `must_change_password` "Invited" flag that clears on the
first self password-change (D34). Clients gained a **type** (PACS vs Store) and a **society
category** matching the Kerala co-op directory taxonomy (Service Bank / Urban / Vanitha /
Housing / Employees / Agricultural / …), both derived from the name and filterable on the
Clients screen (D35/D37). A founder-approved **data-correction session** cleaned 187 client
names and appended **LTD/registration numbers to 141 PACS**, sourced from the Kerala govt co-op
directory via a multi-agent web-lookup (D35). **Login history** (D36): every sign-in attempt
(success/failed/locked) is logged with IP + device in a `login_event` table, viewable on a new
owner **Login history** screen; an invited user is sent to Profile to change their default
password on first sign-in.
## Verification state
| Check | Result |
@ -82,7 +94,7 @@ live under `apps/hq/test/`, `apps/hq-web/test/`, and
## Database schema (`apps/hq/src/db.ts`)
The schema creates 31 tables on open; `migrate()` additively backfills the
The schema creates 32 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
@ -95,7 +107,8 @@ and widens two CHECK constraints via a guarded, transactional table rebuild
| --- | --- |
| `staff_user` | Console logins / employees — owner · manager · staff role, scrypt salt+hash, active flag |
| `session` | Bearer session tokens with expiry |
| `client` | Client registry — code, name, GSTIN, state, contacts (JSON), status, account owner (`owner_id` → `staff_user`), source |
| `login_event` | Login attempt log — staff_id/username, outcome (success/failed/locked), IP, user-agent (D36) |
| `client` | Client registry — code, name, GSTIN, state, contacts (JSON), status, account owner (`owner_id`), `client_type` (PACS/Store, D35), `category` (society type, D37), 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, provider/username/password (plaintext, D32), module-defined `field_values` (D21) |
@ -296,6 +309,7 @@ command palette, Gmail status pill, theme/accent switcher and a user menu
| `/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 |
| `/login-history` | `LoginHistory` | Owner login history: every sign-in (success/failed/locked) with IP + parsed device, outcome filter, paginated (D36) |
| `/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) |

@ -578,3 +578,41 @@ install. Restore: `pg_restore --clean --if-exists --no-owner -d "$DATABASE_URL"
Priority rose with D32 — dumps now contain **plaintext credentials**, so the `backups/` dir
must stay access-controlled and off shared drives. Still pending in the ops slice: schedule
it (Task Scheduler / cron), off-box copy, and getting the DB password out of `server.ts`.
## D34 — Employee master import + invited/first-login flag (2026-07-19)
Imported the 11-row employee master (`empmaster.xlsx`) into `staff_user` via
`scripts/import-employees.ts`: username = first-name slug, default password `<username>@123`
(founder's explicit choice), role `staff`, active per `ACTIVE_STAT`. New `must_change_password`
column drives an "Invited · must change pw" badge on the Employees page and sends the user to
Profile to change it on first sign-in; the flag clears on that first self-change. A background
security review flagged the predictable default password — kept per the founder's explicit
instruction, with the invited flag + change-nudge as the mitigation (full first-login
enforcement offered, not yet enabled).
## D35 — Client type (PACS/Store) + directory-sourced LTD numbers (2026-07-19)
`client.client_type` = **PACS** (co-op society) vs **Store** (retail), backfilled by name
(ends in STORE → Store) and filterable on the Clients screen. **Data-correction session**
(founder-approved, review-sheet-first): 187 client names cleaned mechanically (co-op style →
`CO-OPERATIVE`, run-on splits, `ARP00KARA`→`ARPOOKARA`, spacing), applied after a backup and
audited. **LTD/registration numbers** looked up from the **Kerala govt co-op directory** (the
source of truth) via a multi-agent web-lookup workflow — 259 PACS across two runs (the
first-wave batches needed a smaller-batch re-run to clear a transient safety-classifier block);
**141 now carry their LTD number in the name** (` LTD NO. <n>`, guarded against duplicates).
The ~26 real banks not found online + non-bank societies were left untouched (no guessing).
## D36 — Login history (IP + device) + first-login nudge (2026-07-19)
Every `/auth/login` attempt is recorded in a `login_event` table (staff_id or null, username
tried, outcome success/failed/locked, IP, user-agent) — security telemetry, separate from the
domain audit, best-effort so it never breaks login. Owner-only **Login history** page (Admin):
who signed in / failed, when, from which IP + parsed device, outcome filter, paginated.
`login()` now returns `mustChangePassword` (D34) so an invited user lands on Profile to change
their default password.
## D37 — Society category (Kerala co-op directory taxonomy) (2026-07-20)
`client.category` classifies each client the way the official Kerala co-op directory does:
**Service Bank · Urban Bank · Vanitha · Housing · Employees · Agricultural · Marketing ·
Bank (other) · Society (other) · Store · Other**. Derived from the name (most-specific-first;
a "VANITHA … SOCIETY" is Vanitha, not the generic Society), backfilled by a portable CASE and
applied to new/edited clients via `categoryFromName`. Clients screen gains a Category filter +
column. Live backfill: 135 Service Bank, 38 Employees, 15 Vanitha, 14 Store, 12 Agricultural,
11 Urban, …

Loading…
Cancel
Save