diff --git a/CLAUDE.md b/CLAUDE.md index 994f8ea..6f91a81 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 `@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 }`. diff --git a/README.md b/README.md index d634ff0..bc44396 100644 --- a/README.md +++ b/README.md @@ -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). diff --git a/STATUS.md b/STATUS.md index cf79d5d..7841280 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-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 `@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) | diff --git a/docs/06-DECISIONS.md b/docs/06-DECISIONS.md index 0fa6187..020dfc9 100644 --- a/docs/06-DECISIONS.md +++ b/docs/06-DECISIONS.md @@ -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 `@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. `, 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, …