Commit Graph

4 Commits

Author SHA1 Message Date
wmj 33a821e61d refactor: restructure navigation into 5 grouped hubs, off the 22-item flat strip
Implements the researched IA fix: mobile nav UX consensus caps primary
destinations at 4-5 (uxpin.com, fintech/banking 2026 UX research), and
this app had grown to 22 tabs in one horizontally-scrolling row —
exactly the anti-pattern that research flags for choice paralysis and
slower task completion.

New structure — 5 bottom-nav hubs, grouped by what the user is actually
trying to do, not build order:
- Home: Coverage (unchanged, still the landing screen)
- Estate: Assets, Faraid, Insurance, Wassiyah, Hibah, Family Waqf,
  Nominate, Claims (H2)
- Giving: Zakat, Sadaqah, Khairat
- Family: Tree, Trigger, Mutawalli, Manage (was 'Family', renamed to
  avoid colliding with the hub's own label), Neighbourhood
- Daily: Prayer Times, Qibla, Quran, Locate

Each hub reveals its own sub-nav one tap in, instead of every tab
competing for space in a single row. Settings moved out of the tab
strip entirely into a header gear icon, matching the banking-app pattern
of keeping settings out of primary thumb-reach real estate. The bottom
nav is now fixed (thumb-zone), sub-nav keeps the old sticky-top position.

Cross-component navigation (nav.js requestedTab, used by the Family
Tree's 'give sadaqah in memory' link) now resolves a tab label to its
owning (hub, sub-tab) pair instead of a flat index — verified live.

This touches every E2E suite: a single click on a tab's old selector no
longer reaches it (hub, then sub-tab). Added a shared gotoTab(page, label)
helper to e2e-auth-helper.cjs encapsulating the two-step navigation, and
migrated all ~22 affected test files off direct nav-button selectors —
mechanical substitution followed by manual fixes for local clickTab
wrappers, template-literal selectors, and active-state assertions that
needed to target the new .hub-tab/.subnav structure specifically.

Full regression after migration: every suite passes (one isolated
Family Tree flake confirmed clean on rerun, unrelated to navigation).
2026-08-14 14:55:09 +08:00
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 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