- listClientsByModule rows carry cmId; ClientModulePatch gains kind/edition
(kind validated against module.allowedKinds — same audited write path as
Client 360, so the two views can never disagree)
- Modules roster: inline edit (kind/edition/renewal/status), ✕ unassign with
confirm (deactivates the link, totals shrink), Assign client… type-ahead
- Documents register: issued proformas get a Convert & send quick action
(same convert-and-send endpoint + confirm as the document view)
- 2 tests; suite 333 green
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- listClientsByModule: reverse lookup over active links, ordered by client name,
paginated with a true total; per-row dated price via priceOn + next renewal;
footer revenue total spans ALL links (active=0 excluded by design, stated)
- GET /modules/:id/clients (paginated) + /clients.csv (streams every row, no cap)
- POST /modules/:id/notify (owner/manager): resolves the full roster first, sends
via the Gmail path, one email_log + one audit row per attempted recipient,
names every unreachable client in the response (warn, never truncate)
- Modules page roster panel: table + revenue footer + Notify-all composer + CSV
- 8 tests; suite green
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>