Commit Graph

11 Commits

Author SHA1 Message Date
wmj 2549e9de0c feat: add Khairat, emergency-contact auto-notify, and 5 lifestyle-companion tabs
Big round covering two different asks: Khairat/emergency infrastructure
(tightly coupled to what's already built) and a Muslim-lifestyle
companion surface (a genuinely different product, added at the user's
explicit request after being offered a smaller scope).

Khairat & emergency contacts:
- nf_khairat_memberships (per-member scheme membership) + nf_emergency_fund
  (per-family shared reserve tracker) — records intent/contacts only,
  never holds or moves real money.
- nf_trusted_contacts gains category (mosque/police/ambulance/hospital/
  khairat/family/other) and email columns.
- New notify-emergency-contacts Edge Function, auto-invoked the moment a
  mutawalli fires a member's death trigger (the one place in this app
  where auto-send-on-trigger is actually correct), alongside — not
  instead of — the existing heir notification. Verified with a live fire
  end-to-end (e2e-emergency-notify.cjs, 5/5).

Lifestyle-companion tabs — all computed/searched live, nothing fabricated:
- Qibla: pure great-circle bearing math to the Kaaba from geolocation,
  no API/key. Verified against Kuala Lumpur (293°, matches expected ~292°).
- Prayer Times: client-side astronomical calculation (single-pass solar
  position, MWL angles), sanity-checked against known KL/London times
  before shipping.
- Locate: mosque/halal/cemetery search via the free, keyless OpenStreetMap
  Overpass API — real community-sourced results only, honest empty state
  when nothing's mapped nearby.
- Quran: Surah list + Arabic/translation via the free alquran.cloud API,
  fetched fresh each time, nothing stored in this app's own database.
- Neighbourhood: a join-by-code community announcement board — a
  genuinely separate multi-tenant concept from the estate-planning family
  structure, scoped to the user account. Found and fixed a real UX gap
  during testing: the create/join form was only reachable with zero
  existing neighbourhoods, with no way to join a second one.

Also found and fixed a real bug: the heir-notify and emergency-notify
status messages shared the same CSS class, breaking any script (including
the pre-existing e2e-per-member.cjs) that targeted '.notify-status'
without further filtering — gave each its own distinguishing class.

Covered by e2e-khairat-lifestyle.cjs (13/13) and e2e-emergency-notify.cjs
(5/5). Full regression: 316/316 across all suites (several transient
flakes under heavy mail-relay load during the sweep, all confirmed clean
on rerun — one led to the real class-collision fix above).
2026-08-14 14:25:26 +08:00
wmj 7ce1121b94 feat: add stickiness round — daily Sadaqah tracker + Family Tree social loop
Two features scoped from the stickiness brainstorm, sharing one digest
notification pipeline:

Sadaqah tracker (new tab): a private daily giving journal, not a payment
processor — the app logs, it never moves money. Streak tracking follows
the proven pattern from dedicated apps (Sidq, Daily Sadaqa). Family
visibility is strictly limited to streak counts via a SECURITY DEFINER
RPC (nf_family_sadaqah_streaks) — amounts, causes, and notes never cross
the member boundary, respecting the Islamic preference for giving
privately. Entries can be dedicated 'in memory of' a deceased person from
the Family Tree, with a memorial count (nf_memorial_sadaqah_count, count
only) surfacing on that person's card.

Family Tree social loop: a 'give sadaqah in memory of' link on every
deceased person's card (cross-tab jump via a small requestedTab store in
nav.js), plus completeness hints (missing birth date, missing photo,
no relationships linked) feeding directly into the existing Coverage
Dashboard recommendations engine rather than a new UI surface.

Daily digest Edge Function + pg_cron (07:00 UTC): batches into one email
per family member per day, sent only when there's real content — tree
activity in the last 24h, a birthday/death-anniversary today, or a
weekly sadaqah recap on Sundays. An empty day sends nothing, deliberately
avoiding the notification-spam failure mode the research flagged.
Protected by a shared secret header since it's cron-invoked, not
user-triggered. Verified with a live manual invocation before relying on
the schedule.

Found and fixed two real bugs in nf_family_sadaqah_streaks during E2E
testing: an ambiguous unqualified 'member_id' column reference colliding
with the function's OUT parameter (42702), and a bigint/int type mismatch
from count(*) (42804) — both would have 400'd on every call in production.

Covered by e2e-sadaqah-tree.cjs (12/12). Full regression: 280/280 across
all suites.
2026-08-14 13:42:36 +08:00
wmj 97a1d2ebe2 feat: close final competitive gap — professional review workflow + multi-country (MY/SG/UK)
Closes the 4th and final gap from the competitive benchmark: the
human-in-the-loop professional tier every commercial competitor pairs
with their software. Deliberately NOT a marketplace or payment
integration — a structured review-request status on each member's
Wassiyah (not_requested -> requested -> reviewed, with reviewer name
recorded), surfaced on the Mutawalli dashboard so nothing quietly ships
as final without the legally-required review being flagged.

Paired with a new whole-app jurisdiction setting since 'necessary in
certain countries' only makes sense per-jurisdiction:

- nf_families.jurisdiction (MY/SG/UK), owner-only to change — first
  UPDATE policy ever added to nf_families, which previously had none.
- New Settings-tab jurisdiction selector (JurisdictionSetting.svelte).
- Wassiyah tab and the draft will document now carry jurisdiction-
  specific legal notes: Wills Act 1959 + state Syariah (Malaysia),
  Wills Act 1838 + AMLA (Singapore), Wills Act 1837 (UK). Singapore
  added as a full third jurisdiction option, not just a stub.
- Zakat calculator's currency label and Nisab default now follow the
  family's jurisdiction (RM/SGD/GBP) instead of a hardcoded RM guess.

Covered by e2e-jurisdiction-review.cjs (13/13). Full regression:
253/253 across all suites (2 reruns confirmed as pre-existing
parallel-load flakes, not regressions).
2026-08-14 12:37:25 +08:00
wmj 71aa4ae47c feat: close 3 more competitive gaps — recommendations engine, multi-madhab Faraid, draft will PDF
Closes 3 of the remaining 4 gaps from the competitive benchmark:

- Coverage Dashboard now has a rule-based recommendations engine
  (recommendations.js) surfacing plain-language next steps from data
  already in the app — exposed assets, missing Wassiyah, unverified
  assets, unlinked liabilities, missing insurance/Zakat setup,
  insufficient attestors, no agent assigned, no Waqf configured.

- Faraid Calculator gains a madhab selector (Shafi'i/Hanafi/Maliki/
  Hanbali) encoding the one well-documented divergence this engine's
  existing rules touch: whether radd extends to a sole-heir spouse
  (Hanafi: yes; Shafi'i/Maliki/Hanbali: no, residue unallocated).
  Ja'fari (Shia) is honestly gated as unsupported rather than silently
  computed with Sunni rules, since it's a structurally different
  classification system, not a parameter tweak. faraid.test.js grows
  from 14 to 17 cases covering the divergence and the gating.

- Wassiyah tab gains a 'Generate draft will document' button producing
  a formatted, statutory-style DRAFT will (declaration/revocation,
  executor appointment, bequest schedule, witness attestation blocks)
  via browser print-to-PDF — no new PDF dependency. Clearly watermarked
  'DRAFT — NOT EXECUTED, requires physical signing and witnessing.'
  Not a claim of legal validity, a lawyer-reviewable starting point.

The 4th gap (human-in-the-loop professional tier) remains open — out
of scope for a self-serve prevention tool. COMPETITIVE_BENCHMARK.md
updated to reflect closed/partially-closed status on each item.

Covered by e2e-gaps-round2.cjs (14/14) plus 3 new faraid.test.js cases.
Full regression: 226/226 across all suites.
2026-08-14 12:15:54 +08:00
wmj 972ad7ff68 feat: add Zakat calculator (per-member, Nisab + 2.5% rate)
Closes the Zakat gap identified in the competitive benchmark against
Rafiq. Per-member module (nf_zakat_records, same author-owns/family-reads
RLS pattern as Insurance/Wassiyah/Waqf) computing 2.5% due on zakatable
wealth (cash, gold, silver, business assets, investments) once above a
user-entered Nisab threshold, minus deductible short-term liabilities.
Manual entry only, matching the app's existing philosophy — no live gold
price feed. Covered by e2e-zakat.cjs (7/7).
2026-08-14 11:55:59 +08:00
wmj 49706e0cd2 feat: add insurance/Takaful tracking, liabilities, and asset ownership verification
Per-member Insurance & Takaful tab (life/Takaful/medical/asset policies,
same author-owns/family-reads pattern as Wassiyah/Waqf), a family-shared
Liabilities section on the Asset Registry (kept distinct from assets since
Faraid requires debts settled before distribution), and dual-path asset
ownership verification (proof document + confirm by either the
mutawalli/agent or any other family member, since not every demo family
has an agent assigned). All proof documents share a new private
nf-asset-documents storage bucket with the same RLS pattern as person
photos. Mutawalli dashboard now surfaces each member's insurance policies.

Covered by e2e-insurance-verification.cjs (12/12); full regression sweep
233/233 across all suites.
2026-08-14 10:17:03 +08:00
wmj e7a111c387 Add mini family genealogy tree with per-person photos
Per explicit request: "mini family genealogy mapping with ability to add
photos on each family member." Scoped via clarifying questions: people are
lightweight records independent of login accounts (most real tree nodes —
grandparents, deceased relatives, young children — never sign up),
relationships support parent/child + spouse (enough to render a real
multi-generation tree), photos go to Supabase Storage (native to the
backend already in use, no new infrastructure).

Schema: nf_people (name, gender, birth/death dates, notes, optional
linked_user_id if the person also happens to have a real account,
photo_path) and nf_relationships (person_a/person_b + type: parent_of
directional, spouse_of symmetric). Both family-member-shared like the
Asset Registry — any owner/agent/member can view and edit, unlike the
per-author Wassiyah/Waqf model. New private Storage bucket
nf-people-photos, path convention {family_id}/{person_id}/{filename} so
RLS can check family membership straight from the path without a join.

New FamilyTree.svelte: add/edit/delete a person, upload/replace/remove
their photo (signed URLs, 1hr TTL, refreshed on every load since the
bucket is private), link/unlink relationships, and a recursive generational
tree render — roots are anyone with no recorded parent, spouses shown
inline next to their partner rather than as separate branches.

e2e-family-tree.cjs: 4-person 3-generation tree built end to end against
the live backend — add people, link parent/child + spouse relationships,
upload a real PNG to Supabase Storage and confirm it renders as the
avatar, remove it and confirm it reverts to the initial-letter fallback,
edit a person's notes, delete a relationship, and confirm a second family
under the same account sees none of this tree (isolation, same pattern
already proven for Wassiyah/Waqf). Found two real test-authoring bugs
along the way (not app bugs): a case-insensitive :has-text substring match
on "Spouse" was also matching the relationship-type select's own "is
spouse of" option text, and a person-summary click meant to inspect an
already-expanded card was instead toggling it closed. Both fixed by
selecting on DOM position/structure instead of loose text matching.
9/9 passing.

Full regression sweep: e2e-uat 32/32, e2e-fastpath 16/16, e2e-trust 12/12,
e2e-business 10/10, e2e-digital-vehicle 10/10 (one transient failure on
first run, passed clean on retry — consistent with earlier-observed
flakiness under heavy parallel test load, not a regression),
e2e-property 9/9, e2e-other 4/4, e2e-info 31/31, e2e-per-member 11/11,
e2e-family-tree 9/9 — 174/174 total.
2026-08-14 09:16:42 +08:00
wmj 5adbad6d53 Real SMTP heir notifications — no third-party signup needed
Per explicit direction to use existing VPS/Bitwarden/Gitea infrastructure
instead of waiting on a Resend signup. Bitwarden's MCP unlock/list both
hung (server-side issue, confirmed via direct CLI retry too — not
something to keep retrying), so this used the VPS and Gitea directly.

Found real, working infrastructure already in place: a documented but
never-deployed falah-ibaas email connector (SMTP wrapper) at
/opt/falah-ibaas/connectors/email, backed by a local Postfix relay that
Ghost already uses successfully in production on the same VPS
(mail__options__host=172.17.0.1:25, no auth needed internally).

Built nf-mail-relay: a small HTTP wrapper (Python stdlib, no deps) around
that connector, deployed as a Docker Swarm service on the existing
Traefik network at https://nfmailrelay.falahos.my, bind-mounting the
connector code read-only so it stays in sync with any future updates to
it. Shared-secret bearer auth (X-Relay-Secret) — verified a wrong secret
gets rejected with 401.

notify-heirs Edge Function rewired to call this relay instead of Resend.
Two real bugs found and fixed via actual testing, not code review:
- The function only took memberId, but a person can belong to multiple
  families — .maybeSingle() against multiple trigger rows failed closed
  (correctly, but silently, as "not triggered"). Function and both
  call sites (db.js notifyHeirs, MutawalliDashboard) now require and pass
  familyId too, matching the same composite-key fix already applied to
  the trigger tables themselves.
- No CORS/OPTIONS handling: a browser's preflight OPTIONS request has no
  body, and calling req.json() on it crashed the function before any
  headers were sent — surfaced in the browser as a generic "Failed to
  send a request" with no detail. Added an OPTIONS short-circuit and
  CORS headers on every response path.

Verified with a real send to a live inbox through the full chain
(browser -> Edge Function -> VPS relay -> Postfix -> SMTP), not just a
connectivity check.

Also fixed a stale e2e-trust.cjs assertion using the same
instant-isVisible()-after-fixed-wait pattern already fixed elsewhere in
this session — real app behavior was correct, only the test's timing
assumption was wrong.

e2e-per-member.cjs's heir-notification check now asserts an actual "Sent"
result via the real relay instead of accepting either Sent or a
not-configured failure. Full sweep: e2e-uat 32/32 (stable across 3 runs,
one earlier run's failure was a one-off network blip under heavy parallel
test load), e2e-fastpath 16/16, e2e-trust 12/12 (stable across 3 runs),
e2e-business 10/10, e2e-digital-vehicle 10/10, e2e-property 9/9,
e2e-other 4/4, e2e-info 31/31, e2e-per-member 11/11 — 165/165 total.
2026-08-14 07:59:44 +08:00
wmj 9eb2ce7ce3 Per-member estate model: each family member authors their own Wassiyah/Waqf,
mutawalli executes on any member's trigger, heir email notification

Per explicit product direction: "all members of the family can make their
own wassiyah or waqif. The mutawali or the trustee agent can access and
execute those wassiyah and waqif upon any event triggers. Warith or the
heir will be automatically notified via email."

Schema: nf_family_members.role now includes 'member' (authors own documents,
doesn't manage the family). nf_wassiyah_settings/nf_wassiyah_bequests/
nf_waqf_designations gained author_id — each is now per-author, not
per-family. Added recipient_email / beneficiary_email columns for warith
notification targets. New nf_member_triggers (composite PK family_id+
member_id) and nf_member_attestors: a per-member death trigger, separate
from the legacy family-wide nf_death_triggers (kept for backward
compatibility, still exercised by existing suites).

RLS: any family member can READ any other member's Wassiyah/Waqf (the
mutawalli needs full visibility to execute), but only the document's own
author can WRITE to it — not even the owner. Firing a member's trigger
requires the caller to have role agent/owner AND not be the member
themselves (enforced in the policy's WITH CHECK, not just the UI) — matches
"the mutawalli executes, never for themselves."

New UI: FamilyManagement gained a role selector (member vs agent) on
invites. New MutawalliDashboard.svelte — the trustee's execution surface:
pick any family member, see their Wassiyah/Waqf read-only, set up
attestors + death cert ref, fire their trigger (blocked for self both by
disabled UI and by RLS), then trigger heir email notifications.

Edge Function notify-heirs deployed (Deno, uses Resend): reads the
triggered member's Wassiyah recipients and Waqf beneficiaries wherever an
email was recorded, sends each a notice. Returns a clear 501 rather than
failing silently until RESEND_API_KEY is set as a project secret.

Three real bugs found via testing against the live backend, not visible
from code review alone:
- listFamilyMembers() never selected user_id — every member-scoped lookup
  on the new dashboard was silently keying off undefined.
- nf_member_triggers keyed by member_id alone: since a person can belong
  to multiple families, firing a trigger in one family marked them
  "triggered" in every other family they belong to. Fixed to composite
  (family_id, member_id) key.
- Classic Svelte 5 $state pitfall: (proxyObject[key] ??= []).push(item)
  mutates the plain array literal the ??= expression evaluates to, not
  the proxy-wrapped array Svelte actually tracks — so pushed items were
  silently invisible to the UI forever. Fixed by building on a plain
  object and assigning to the $state variable once. Also found and fixed
  the same design smell in the older WassiyahGenerator/FamilyWaqfDesignator
  authorId handling: it was snapshotted once via currentUser()?.id at
  mount instead of read live off the session store, which could silently
  break writes on a remount that happened before session hydration
  finished — now reads live and guards refresh() on it being present.

CoverageDashboard and DeathTrigger updated to check ANY family member's
Waqf corpus for coverage (not just one author's), since coverage is a
family-wide view even though authorship is per-member now.

e2e-per-member.cjs: new suite covering the full flow — owner invites a
member and an agent; member authors a private Wassiyah (invisible to
other members, confirming per-author isolation); mutawalli sees it on
their dashboard and fires the member's trigger; member cannot fire their
own; heir notification call completes with either Sent or a clear
"not configured" failure, never hangs. 11/11 passing.

Full regression sweep after these changes: e2e-uat 32/32 (stable across
3 consecutive runs), e2e-fastpath 16/16, e2e-trust 12/12, e2e-business
10/10, e2e-digital-vehicle 10/10, e2e-property 9/9, e2e-other 4/4,
e2e-info 31/31, e2e-family-agent 12/12 (updated for the new invite-form
role selector), e2e-per-member 11/11 — 178/178 total, no regressions.
2026-08-14 05:51:46 +08:00
wmj b9a98bd97a Restore all 8 pre-auth E2E suites with a sign-in prelude; fix bugs they found
New e2e-auth-helper.cjs: shared signInFreshFamily() prelude — signs in as
the confirmed test owner account and creates a uniquely-named family per
run, so accumulated data from a previous run's assets/hibah/etc. (now
persisted in Supabase, not wiped with the browser context like localStorage
was) can never bleed into another run's percentage/coverage assertions.
All 8 suites (e2e-uat, e2e-fastpath, e2e-trust, e2e-business,
e2e-digital-vehicle, e2e-property, e2e-other, e2e-info) now call it in
place of the old anonymous page.goto(BASE).

Restoring them surfaced two real product bugs, not just test staleness:

1. WassiyahGenerator.svelte was never migrated to Supabase in the earlier
   backend work — it still called the old local storage.js load()/save()
   for assets, bequests, and witnesses, so the one-third meter silently
   read an empty local cache and always showed 0. Migrated to family-scoped
   Supabase tables (new nf_wassiyah_bequests, nf_wassiyah_settings, with
   member-scoped RLS) matching the pattern used for Hibah/Nominations/etc.

2. storage.js's exportAll() did an unguarded JSON.parse on every
   "nf."-prefixed localStorage key, but family.js stores activeFamilyId as
   a raw string (not JSON-encoded) — one malformed parse threw and silently
   aborted the whole export before the file download fired. Made exportAll
   defensive: falls back to the raw string on a parse failure instead of
   throwing.

The remaining test failures were async-timing gaps inherent to the move
from synchronous localStorage reads to async Supabase fetches: several
assertions checked <select> option counts or newly-created rows immediately
after a fixed short wait, before the async load/refresh had actually
landed. Fixed by replacing blind isVisible()/fixed-timeout checks with
proper waitFor()/polling in the test helpers (selectByText, corpus-select
population, row-creation checks) — not a product bug, but worth fixing
since the old timing assumptions no longer hold now that data is live and
shared instead of instant and local.

Results: e2e-uat 32/32, e2e-fastpath 16/16, e2e-trust 12/12,
e2e-business 10/10, e2e-digital-vehicle 10/10, e2e-property 9/9,
e2e-other 4/4, e2e-info 31/31 — 124/124. Re-verified e2e-family-agent
(12/12) and e2e-smoke-authed (24/24) still pass after the
WassiyahGenerator migration. 160/160 total across all ten suites.
2026-08-13 21:30:46 +08:00
wmj a0c70e1411 Add estate agent delegation: real accounts, multi-tenant backend, RLS-enforced roles
Per explicit product direction: the app assumed a single user (head of
family) with everything in per-device localStorage. There was no way for
a family to delegate estate management to an agent (relative or
professional) without literally handing over the device. This required
real backend infrastructure, not a UI addition — added Supabase
(Postgres + Auth) as a multi-tenant backend.

Schema (nf_ prefixed to stay isolated from other tables in the reused
"Falah OS demo" project): nf_families, nf_family_members (role: owner/
agent, status: invited/active), and family-scoped versions of every
estate table — nf_assets, nf_trusted_contacts, nf_hibah_gifts,
nf_waqf_designations/nf_waqf_beneficiaries, nf_nominations,
nf_attestors, nf_death_triggers.

Permission model, enforced by RLS at the database level (not just
hidden in the UI): an agent can do everything an owner can — add/edit
assets, draft Hibah/Waqf/Nominations, set up the Death Trigger — except
fire it. nf_death_triggers' UPDATE/INSERT policies use a WITH CHECK that
only allows triggered=true when the caller has role='owner' on that
family. A professional agent can be invited to multiple families and
switches between them from their own dashboard.

New: auth.js, family.js, db.js, AuthScreen.svelte, FamilySwitcher.svelte,
FamilyManagement.svelte (new "Family" tab: invite agents, see members,
switch families). AssetRegistry, HibahTracker, FamilyWaqfDesignator,
NominationRegistry, CoverageDashboard, and DeathTrigger all migrated
from storage.js (localStorage) to db.js (Supabase), scoped to the
active family_id. App.svelte now gates on auth + family selection
before showing the main tab shell.

Three real bugs found and fixed via testing against the live backend
(not caught by the old localStorage-based suites, which had no
cross-client concurrency to expose them):
- RLS gap: pending-invite lookup joins nf_families(name), but the
  invitee isn't a family member yet, so the join was silently dropped —
  added a policy letting a pending invitee see just the family name.
- Attestor row race: lazy "create on first blur" could double-fire from
  two different code paths, creating duplicate rows and confirming the
  wrong one. Fixed by eagerly creating attestor rows on first load so
  every row always has a real id — no more create-or-update ambiguity.
- Out-of-order async clobber: three death-trigger setup fields each
  fired a full-snapshot upsert on every input; whichever request
  *finished* last (not fired last) won, silently reverting the other
  two fields to stale values. Fixed with per-field partial updates
  (updateDeathTriggerField) that can't clobber columns they don't touch.

e2e-family-agent.cjs: full owner/agent flow against the live Supabase
backend — invite, accept, shared live data, agent blocked from firing
the trigger (button stays disabled and a direct RLS-level attempt would
also fail), owner successfully fires it. 12/12 passing.
e2e-smoke-authed.cjs: post-auth-gate sweep confirming every existing tab
still renders and its info panel still opens under the new sign-in
requirement. 24/24 passing, zero console errors.

Known follow-up, not done here: the eight pre-auth E2E suites
(e2e-uat.cjs, e2e-fastpath.cjs, e2e-trust.cjs, e2e-business.cjs,
e2e-digital-vehicle.cjs, e2e-property.cjs, e2e-other.cjs, e2e-info.cjs)
assume an anonymous landing page and need a sign-in prelude added
before they're valid again — their detailed assertions were re-verified
functionally via the smoke test and manual review, not by running them
as-is.
2026-08-13 21:07:23 +08:00