- staff_user gains must_change_password (SQLite migrate + PG migration 011): the
invited-employee indicator, cleared on the user's first self password-change.
- createEmployee takes phone/title/mustChangePassword; Employee carries mustChangePassword.
- scripts/import-employees.ts (tsx, Postgres- or SQLite-aware) seeds the 11 from
empmaster.xlsx: username = first-name slug, default password <username>@123, role staff,
active per ACTIVE_STAT, must-change flagged. Idempotent. Ran on live DB — 11 added.
- Employees list: 'Invited · must change pw' badge + a notice explaining the default
password pattern so the owner can share logins. Verified: adithya/adithya@123 → 200,
inactive raveena blocked. typecheck + employee tests green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- staff_user gains a unique username (both engines: migrate + pg 009); older
rows and the seed owner backfill username=email so every existing login keeps
working. login() matches username OR email (legacy fallback).
- createStaff/createEmployee take an optional username (defaults to email);
Employee/API expose it; POST /employees accepts username. Email stays a
required contact field on the table (making it optional is a later table
rebuild). Full backend suite 346 green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
P1: every repo, route, scheduler path and test now speaks one async DB
interface (all/get/run/exec/transaction). better-sqlite3 stays underneath
for dev/tests via SqliteDb (statement cache, manual BEGIN/SAVEPOINT
nesting); openDb stays sync so fixtures remain one-liners. Zero behavior
change: 346/346 tests, typecheck clean.
P2: migrations-pg.ts — numbered Postgres migration set (001-init: bigint
paise, integer flags, TEXT ISO dates, TEXT JSON; reminder CHECK born in
final widened form) + schema_migrations runner.
P3: db-pg.ts — pg Pool behind the same interface: ?->$n rewrite outside
string literals, transactions pinned to one pooled client via
AsyncLocalStorage with savepoint nesting, int8/numeric parsed to JS
numbers so paise math is identical on both engines.
Also: reminder upsert INSERT OR IGNORE -> ON CONFLICT DO NOTHING
(portable, same changes-count semantics).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- GET/PATCH /me (self-scoped whitelist — role/email untouchable) + POST /me/password
- changeOwnPassword: current password required; every OTHER session dies in the
same txn, the changing session survives; audited without hashes
- staff_user.phone/title additive columns, surfaced on Employees (columns + edit
dialog) and the new Profile page; user chip in the shell opens /profile
- 7 tests; suite 331/331 across 64 files
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review findings on the Phase 1-2 employee slice:
- setEmployeePassword now deletes the employee's sessions in the same
transaction — resetting a suspected-compromised password must not leave
the attacker's existing 14-day bearer token valid (deactivateEmployee
already did this; the reset path is the natural lockout action).
- updateEmployee throws 'Nothing to update' when the patch resolves to
zero recognized fields, instead of returning 200 and writing a no-op
before==after audit row; PATCH /employees/:id with {} or wrongly-typed
fields now 400s.
- createEmployee pre-checks UNIQUE(email) inside the transaction and
throws 'Email already in use' instead of surfacing the raw
engine-specific 'UNIQUE constraint failed: staff_user.email' text
(would differ on Postgres, the locked production engine — D15).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>