6 Commits
Author SHA1 Message Date
wmj e87e329731 docs: resolve both governance gates (OPEN-01, Horizon 2 Phase 0)
OPEN-01 (scholarly-review-log.md): ruled NOT subject to the one-third
cap for an inter-vivos waqf made in health. The existing
FamilyWaqfDesignator logic already applied no health-state cap absent a
marad al-mawt trigger, so no behavior change — only the open-question
framing in the exported deed, InfoPanel copy, on-screen note, and source
comments moved from "open" to "resolved."

Horizon 2 Phase 0 (legal opinion + signed institutional partner
agreement): both now signed. This clears the PRD's gate to begin real
institutional-integration engineering, but that integration hasn't been
built yet — the Claims module (DigitalClaims.svelte, claims.js) still
ships as a local, non-custodial demonstration only, and the pilot banner
is updated to say so precisely rather than implying the module itself
is now production-ready.

Updated README.md and deploy/DEPLOY.md to match.
2026-10-23 10:40:26 +08:00
wmj e711783087 docs: add demo account UI screenshots 2026-10-16 15:35:09 +08:00
wmj a8f7024a21 Merge pull request 'feat: implement ui/ux dashboard improvements (search, layout, cta)' (#1) from ui-ux-dashboard-improvements into main 2026-10-13 12:20:14 +00:00
wmj 4961a07356 docs: revise RAMZ isolation decision to database-per-tenant
Schema-per-tenant was the original call, but falah-os-master's
infrastructure/postgres/init.sql already establishes database-per-service
as the platform convention (falahid, falahwallet, falahramz, falahmock,
falahistore). Database-per-tenant matches that pattern and is a stronger
isolation boundary than schemas would have been.
2026-08-18 15:55:17 +08:00
wmj 14b34ad532 docs: add architecture visuals and RAMZ tenant isolation decision
Clio/Stripe competitive benchmark, moslem03/04 backend reality check,
and the moslem04 sync API spec, plus the resolved schema-per-tenant
isolation decision that unblocks Phase 2 of the sync spec.
2026-08-18 13:12:05 +08:00
wmj 6941be23c3 feat: implement ui/ux dashboard improvements (search, layout, cta) 2026-08-14 11:56:05 +08:00
34 changed files with 1116 additions and 73 deletions
+23 -18
View File
@@ -12,28 +12,33 @@ system of the live `moslem03.falahos.my` (Nur Falah Muslim Companion) app.
| Asset Registry | 1 | Local-first, feeds every other module's estate total | | Asset Registry | 1 | Local-first, feeds every other module's estate total |
| Wassiyah Generator | 1 | 1/3 meter, heir-exclusion block, jurisdiction-aware export | | Wassiyah Generator | 1 | 1/3 meter, heir-exclusion block, jurisdiction-aware export |
| Hibah Tracker | 1 | Shared marad al-mawt guardrail | | Hibah Tracker | 1 | Shared marad al-mawt guardrail |
| Family Waqf Designator | 1 | **Built ahead of scholarly sign-off** — see below | | Family Waqf Designator | 1 | Scholarly sign-off received — see below |
| Digital Beneficial Claims | 2 | **Pre-pilot demo only** — see below | | Digital Beneficial Claims | 2 | Phase 0 complete; module itself still a demo — see below |
## Important: two governance gates were overridden for this build ## Governance gates — both now resolved
Per explicit product direction, this build proceeded past two gates stated in the Per explicit product direction, this build originally proceeded past two gates stated
project's own PRDs and review log: in the project's own PRDs and review log, before either was resolved. Both are now
closed:
1. **Family Waqf Designator** (Horizon 1 PRD §8.5) is gated on scholarly sign-off. 1. **Family Waqf Designator** (Horizon 1 PRD §8.5) was gated on scholarly sign-off.
`scholarly-review-log.md` (in the `digital-waqif` docs repo) tracks **OPEN-01** — `scholarly-review-log.md` (in the `digital-waqif` docs repo) tracked **OPEN-01** —
whether a healthy-state waqf is subject to the one-third cap — as unresolved. whether a healthy-state waqf is subject to the one-third cap. **Resolved: ruled
This module ships anyway; the in-app copy states the open question rather than NOT subject to the one-third cap.** This matches the module's existing
presenting invented certainty. implementation (no health-state cap applied absent a `marad al-mawt` trigger), so
2. **Horizon 2** (Digital Beneficial Claims) PRD states: *"No engineering work begins no logic change was needed — only the open-question framing in the UI and source
until Phase 0 (legal opinion + signed institutional partner agreement) is comments has been updated to reflect the ruling.
complete."* No confirmation of Phase 0 completion was available. The Claims module 2. **Horizon 2** (Digital Beneficial Claims) PRD required: *"No engineering work
ships as a local, non-custodial demonstration of the data model and transfer begins until Phase 0 (legal opinion + signed institutional partner agreement) is
restriction only — no real legal wrapper, no institutional integration, no real complete."* **Resolved: both the legal opinion and the institutional partner
claims issued. It carries a persistent in-app banner saying so. agreement are signed.** This clears the PRD's gate to begin real engineering on
institutional integration. It does **not** by itself mean the live Claims module
now issues real claims — no institutional integration has been built yet, so the
module still ships as a local, non-custodial demonstration of the data model and
transfer restriction only, pending that integration work.
Both are flagged here, in `deploy/DEPLOY.md`, and in the relevant module's source Resolution status is reflected here, in `deploy/DEPLOY.md`, and in the relevant
comments so this isn't silently presented as production-ready. module's source comments.
## Development ## Development
+10 -9
View File
@@ -58,12 +58,13 @@ docker service update --rollback nur-falah-prevention_web
a dedicated PDF pipeline. PRD §11 calls for "a single, reusable document-generation a dedicated PDF pipeline. PRD §11 calls for "a single, reusable document-generation
pipeline (PDF and plain text)" — plain text ships now, a real PDF renderer is a pipeline (PDF and plain text)" — plain text ships now, a real PDF renderer is a
follow-up. follow-up.
- **Horizon 2 claims module is a local, non-custodial demo only** — see the in-app - **Horizon 2 PRD's Phase 0 gate (legal opinion + signed institutional partner
banner and `src/lib/horizon2/claims.js`. It has no legal wrapper, no institutional agreement) is now resolved** — both are signed. This clears PRD §Phase-0 to begin
partner, and issues no real claim. Built ahead of the Horizon 2 PRD's stated Phase 0 real institutional-integration engineering. **The claims module itself remains a
gate per explicit product direction — do not represent this screen to any real user local, non-custodial demo** — see the in-app banner and `src/lib/horizon2/claims.js`
as a live programme. — since no institutional integration has actually been built yet. Do not represent
- **Family Waqf Designator ships ahead of scholarly sign-off** — `scholarly-review-log.md` this screen to any real user as a live programme until that integration ships.
OPEN-01 (the waqf one-third cap conflict) is still unresolved. Built per explicit - **Family Waqf Designator — `scholarly-review-log.md` OPEN-01 is resolved**: ruled
product direction; the in-app copy states this openly rather than presenting NOT subject to the one-third cap. Matches the module's existing implementation
invented certainty. (no health-state cap absent a `marad al-mawt` trigger); only documentation/in-app
copy updated, no logic change required.
@@ -0,0 +1,445 @@
<title>Falah OS — Competitive Benchmark: Clio &amp; Stripe</title>
<style>
:root {
--bg: #f6f5ef;
--bg-panel: #ffffff;
--ink: #1b2420;
--ink-soft: #4d5951;
--line: #dbd9cd;
--line-strong: #b9b6a6;
--accent: #2f6e52;
--accent-soft: #e3ede7;
--steal: #2f6e52;
--steal-bg: #e3ede7;
--build: #a6432e;
--build-bg: #f6e6e1;
--gap: #9a7327;
--gap-bg: #f2e9d4;
--mono-tint: #f0efe6;
}
:root[data-theme="dark"] {
--bg: #141815;
--bg-panel: #1b201c;
--ink: #e9e7dd;
--ink-soft: #a9b0a7;
--line: #333a34;
--line-strong: #48524a;
--accent: #6bbf94;
--accent-soft: #212b24;
--steal: #6bbf94;
--steal-bg: #1d2b22;
--build: #e08268;
--build-bg: #2c1e1a;
--gap: #e3bd6a;
--gap-bg: #2b2416;
--mono-tint: #1f2420;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #141815;
--bg-panel: #1b201c;
--ink: #e9e7dd;
--ink-soft: #a9b0a7;
--line: #333a34;
--line-strong: #48524a;
--accent: #6bbf94;
--accent-soft: #212b24;
--steal: #6bbf94;
--steal-bg: #1d2b22;
--build: #e08268;
--build-bg: #2c1e1a;
--gap: #e3bd6a;
--gap-bg: #2b2416;
--mono-tint: #1f2420;
}
}
* { box-sizing: border-box; }
html, body {
margin: 0;
background: var(--bg);
color: var(--ink);
font-family: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, "Source Serif Pro", serif;
}
body {
padding: clamp(1.5rem, 4vw, 4rem);
display: flex;
justify-content: center;
}
main {
width: 100%;
max-width: 920px;
display: flex;
flex-direction: column;
gap: clamp(2.5rem, 5vw, 3.5rem);
}
.masthead {
display: flex;
flex-direction: column;
gap: 0.6rem;
padding-bottom: 1.75rem;
border-bottom: 2px solid var(--ink);
}
.eyebrow {
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
font-size: 0.72rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--accent);
}
h1 {
margin: 0;
font-size: clamp(1.9rem, 4vw, 2.6rem);
font-weight: 600;
letter-spacing: -0.01em;
text-wrap: balance;
}
.dek {
margin: 0;
max-width: 62ch;
font-size: 1.02rem;
line-height: 1.55;
color: var(--ink-soft);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
}
section.bench {
display: flex;
flex-direction: column;
gap: 1.1rem;
}
.bench-head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
}
.bench-title {
display: flex;
align-items: baseline;
gap: 0.6rem;
}
.bench-title h2 {
margin: 0;
font-size: 1.4rem;
font-weight: 600;
}
.bench-title .vs {
font-family: ui-monospace, "SF Mono", Menlo, monospace;
font-size: 0.85rem;
color: var(--ink-soft);
}
.bench-note {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 0.82rem;
color: var(--ink-soft);
max-width: 40ch;
text-align: right;
line-height: 1.4;
}
.table-wrap {
overflow-x: auto;
border: 1px solid var(--line-strong);
border-radius: 3px;
background: var(--bg-panel);
}
table {
width: 100%;
min-width: 640px;
border-collapse: collapse;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
font-size: 0.88rem;
}
thead th {
text-align: left;
font-family: ui-monospace, "SF Mono", Menlo, monospace;
font-size: 0.68rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--ink-soft);
padding: 0.7rem 0.9rem;
border-bottom: 1px solid var(--line-strong);
background: var(--mono-tint);
}
tbody td {
padding: 0.75rem 0.9rem;
border-bottom: 1px solid var(--line);
vertical-align: top;
line-height: 1.45;
}
tbody tr:last-child td { border-bottom: none; }
tbody tr:hover { background: color-mix(in srgb, var(--accent) 5%, transparent); }
td.area {
font-weight: 600;
white-space: nowrap;
color: var(--ink);
}
td.detail { color: var(--ink-soft); }
td.verdict { width: 15%; }
.chip {
display: inline-block;
font-family: ui-monospace, "SF Mono", Menlo, monospace;
font-size: 0.68rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
padding: 0.22rem 0.55rem;
border-radius: 3px;
white-space: nowrap;
}
.chip.steal { color: var(--steal); background: var(--steal-bg); }
.chip.build { color: var(--build); background: var(--build-bg); }
.chip.gap { color: var(--gap); background: var(--gap-bg); }
.legend {
display: flex;
gap: 1.4rem;
flex-wrap: wrap;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 0.78rem;
color: var(--ink-soft);
align-items: center;
}
.legend span { display: inline-flex; align-items: center; gap: 0.4rem; }
.synth {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
padding: 1.5rem;
border: 1px solid var(--line-strong);
border-radius: 3px;
background: var(--bg-panel);
}
@media (max-width: 640px) {
.synth { grid-template-columns: 1fr; }
}
.synth-col h3 {
margin: 0 0 0.85rem 0;
font-family: ui-monospace, "SF Mono", Menlo, monospace;
font-size: 0.72rem;
letter-spacing: 0.1em;
text-transform: uppercase;
display: flex;
align-items: center;
gap: 0.5rem;
}
.synth-col.steal h3 { color: var(--steal); }
.synth-col.build h3 { color: var(--build); }
.synth ol {
margin: 0;
padding: 0 0 0 1.15rem;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 0.86rem;
line-height: 1.55;
color: var(--ink);
}
.synth ol li::marker {
font-family: ui-monospace, monospace;
color: var(--ink-soft);
}
.synth li + li { margin-top: 0.4rem; }
.divider {
grid-column: 1 / -1;
height: 1px;
background: var(--line);
display: none;
}
@media (max-width: 640px) {
.divider { display: block; }
}
footer {
padding-top: 1.5rem;
border-top: 1px solid var(--line);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 0.78rem;
color: var(--ink-soft);
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 0.5rem;
}
footer strong { color: var(--ink); }
</style>
<main>
<div class="masthead">
<span class="eyebrow">Falah OS — Nur Falah Admin Console</span>
<h1>Competitive benchmark: Clio &amp; Stripe Dashboard</h1>
<p class="dek">Two reference products mapped against EstateOS and the cross-tenant super-admin surface. Every row ends in a call: <strong style="color:var(--steal)">steal</strong> the pattern, <strong style="color:var(--build)">build</strong> it ourselves, or note it as a <strong style="color:var(--gap)">structural gap</strong> the reference product simply doesn't resolve.</p>
</div>
<section class="bench">
<div class="bench-head">
<div class="bench-title">
<h2>Clio</h2>
<span class="vs">vs. EstateOS</span>
</div>
<p class="bench-note">Legal practice management — case files, billing, client portal, co-counsel collaboration.</p>
</div>
<div class="table-wrap">
<table>
<thead>
<tr><th>Feature area</th><th>What Clio does</th><th>Verdict for EstateOS</th></tr>
</thead>
<tbody>
<tr>
<td class="area">Intake → billing</td>
<td class="detail">Clio Grow captures leads via custom forms, hands off to Clio Manage automatically once a matter opens.</td>
<td class="verdict"><span class="chip steal">Steal</span></td>
</tr>
<tr>
<td class="area">Document automation</td>
<td class="detail">Template library with variable-filled document generation, version control.</td>
<td class="verdict"><span class="chip steal">Steal</span></td>
</tr>
<tr>
<td class="area">Billing &amp; invoicing</td>
<td class="detail">Timers, hourly/contingency billing, AI-drafted invoices with error flagging.</td>
<td class="verdict"><span class="chip steal">Steal</span></td>
</tr>
<tr>
<td class="area">Trust accounting</td>
<td class="detail">Client trust ledgers, deposit/disbursement tracking, compliance reporting.</td>
<td class="verdict"><span class="chip steal">Steal</span></td>
</tr>
<tr>
<td class="area">Client portal</td>
<td class="detail">Encrypted messaging, document sharing, payments — but single-brand, one firm per instance.</td>
<td class="verdict"><span class="chip steal">Steal UX</span></td>
</tr>
<tr>
<td class="area">Faraid / Syariah workflows</td>
<td class="detail">No inheritance-share calculation, no Syariah Court filing templates — generic common-law matter types only.</td>
<td class="verdict"><span class="chip build">Build</span></td>
</tr>
<tr>
<td class="area">Panel-lawyer vetting</td>
<td class="detail">"Clio for Co-Counsel" assumes counsel is already chosen — it's a collaboration portal, not a credentialing tool.</td>
<td class="verdict"><span class="chip build">Build</span></td>
</tr>
<tr>
<td class="area">Per-tenant branding</td>
<td class="detail">Client portal is single-brand per firm — no white-label capability for a platform serving multiple firms.</td>
<td class="verdict"><span class="chip build">Build</span></td>
</tr>
<tr>
<td class="area">Multi-tenant backend</td>
<td class="detail">Clio is one firm, one instance — no shared-backend, multi-firm architecture exists to compare against.</td>
<td class="verdict"><span class="chip gap">Structural gap</span></td>
</tr>
<tr>
<td class="area">RAMZ row-level access</td>
<td class="detail">Firm-scoped permissions throughout — no concept of row-level, cross-tenant gating.</td>
<td class="verdict"><span class="chip gap">Structural gap</span></td>
</tr>
</tbody>
</table>
</div>
</section>
<section class="bench">
<div class="bench-head">
<div class="bench-title">
<h2>Stripe Dashboard</h2>
<span class="vs">vs. Falah OS super-admin</span>
</div>
<p class="bench-note">Connect platform admin — connected-account management, roles, audit, API access.</p>
</div>
<div class="table-wrap">
<table>
<thead>
<tr><th>Feature area</th><th>What Stripe does</th><th>Verdict for Falah OS</th></tr>
</thead>
<tbody>
<tr>
<td class="area">Admin role tiers</td>
<td class="detail">Admin vs. Super Administrator — Super Admin gates who can grant/revoke roles and manage environments.</td>
<td class="verdict"><span class="chip steal">Steal</span></td>
</tr>
<tr>
<td class="area">Audit logging</td>
<td class="detail">Activity Logs API — exportable, SOC 2/PCI-ready record of key changes, invitations, role changes.</td>
<td class="verdict"><span class="chip steal">Steal</span></td>
</tr>
<tr>
<td class="area">API key scoping</td>
<td class="detail">Restricted keys limited to specific permission sets per integration.</td>
<td class="verdict"><span class="chip steal">Steal</span></td>
</tr>
<tr>
<td class="area">Tenant status view</td>
<td class="detail">Connected-account list with onboarding and risk status at a glance.</td>
<td class="verdict"><span class="chip steal">Steal UX</span></td>
</tr>
<tr>
<td class="area">Onboarding flow</td>
<td class="detail">Hosted or embedded onboarding, incremental — collects only what's currently required.</td>
<td class="verdict"><span class="chip steal">Steal</span></td>
</tr>
<tr>
<td class="area">Cross-regime compliance routing</td>
<td class="detail">No concept of routing tenants through different regulatory rule sets (Syariah Court vs. JKPTG vs. govt case routing).</td>
<td class="verdict"><span class="chip build">Build</span></td>
</tr>
<tr>
<td class="area">Usage / billing-trigger events</td>
<td class="detail">Activity Logs are security-focused, not usage-metering. Stripe's actual metering product wasn't covered — separate follow-up needed.</td>
<td class="verdict"><span class="chip build">Build / unclear</span></td>
</tr>
<tr>
<td class="area">Tenant = product line</td>
<td class="detail">Every connected account is a roughly uniform merchant entity — no support for managing distinct SaaS products per tenant.</td>
<td class="verdict"><span class="chip gap">Structural gap</span></td>
</tr>
<tr>
<td class="area">RAMZ row-level access</td>
<td class="detail">Isolation happens at the account level (own object graph per tenant) — closer to separate-DB than row-level gating.</td>
<td class="verdict"><span class="chip gap">Structural gap</span></td>
</tr>
</tbody>
</table>
</div>
</section>
<div class="legend">
<span><span class="chip steal">Steal</span> pattern transfers directly</span>
<span><span class="chip build">Build</span> reference has nothing comparable</span>
<span><span class="chip gap">Structural gap</span> reference's whole model doesn't apply</span>
</div>
<section class="synth">
<div class="synth-col steal">
<h3>Combined — steal list</h3>
<ol>
<li>Intake → billing handoff (Clio Grow → Manage)</li>
<li>Document automation + template library</li>
<li>Trust ledger compliance reporting</li>
<li>Two-tier Admin / Super Admin roles</li>
<li>Exportable audit log, scoped API keys</li>
</ol>
</div>
<div class="divider"></div>
<div class="synth-col build">
<h3>Combined — build list</h3>
<ol>
<li>Faraid inheritance calculation engine</li>
<li>Panel-lawyer vetting / credentialing</li>
<li>Per-tenant white-labeled portal branding</li>
<li>RAMZ row-level, shared-schema isolation</li>
<li>Cross-regime compliance routing (Syariah / JKPTG / govt)</li>
</ol>
</div>
</section>
<footer>
<span>Sources: clio.com, docs.stripe.com, support.stripe.com — full citations in the prior research turns.</span>
<span><strong>Open:</strong> Core Ledger billing-event readiness still unconfirmed against Stripe's metering product.</span>
</footer>
</main>
@@ -0,0 +1,259 @@
<title>Falah OS — Backend Integration Reality Check</title>
<style>
:root {
--bg: #f6f5ef;
--bg-panel: #ffffff;
--ink: #1b2420;
--ink-soft: #4d5951;
--line: #dbd9cd;
--line-strong: #b9b6a6;
--accent: #2f6e52;
--accent-soft: #e3ede7;
--warn: #a6432e;
--warn-bg: #f6e6e1;
--future: #9a7327;
--future-bg: #f2e9d4;
--node-bg: #ffffff;
}
:root[data-theme="dark"] {
--bg: #141815;
--bg-panel: #1b201c;
--ink: #e9e7dd;
--ink-soft: #a9b0a7;
--line: #333a34;
--line-strong: #48524a;
--accent: #6bbf94;
--accent-soft: #212b24;
--warn: #e08268;
--warn-bg: #2c1e1a;
--future: #e3bd6a;
--future-bg: #2b2416;
--node-bg: #1f2420;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #141815;
--bg-panel: #1b201c;
--ink: #e9e7dd;
--ink-soft: #a9b0a7;
--line: #333a34;
--line-strong: #48524a;
--accent: #6bbf94;
--accent-soft: #212b24;
--warn: #e08268;
--warn-bg: #2c1e1a;
--future: #e3bd6a;
--future-bg: #2b2416;
--node-bg: #1f2420;
}
}
* { box-sizing: border-box; }
html, body { margin: 0; background: var(--bg); color: var(--ink); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; }
body { padding: clamp(1.25rem, 4vw, 3rem); display: flex; justify-content: center; }
main { width: 100%; max-width: 900px; display: flex; flex-direction: column; gap: 2.5rem; }
.masthead { display: flex; flex-direction: column; gap: 0.5rem; padding-bottom: 1.25rem; border-bottom: 2px solid var(--ink); }
.eyebrow { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 0.7rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--accent); }
h1 { margin: 0; font-size: clamp(1.5rem, 3.4vw, 2rem); font-weight: 700; letter-spacing: -0.01em; text-wrap: balance; }
.dek { margin: 0; max-width: 60ch; font-size: 0.98rem; line-height: 1.5; color: var(--ink-soft); }
h2.section-title {
display: flex; align-items: center; gap: 0.6rem;
font-size: 0.75rem; letter-spacing: 0.1em; text-transform: uppercase;
font-family: ui-monospace, "SF Mono", Menlo, monospace;
color: var(--ink-soft); margin: 0 0 1.1rem 0;
}
h2.section-title .n { color: var(--ink); font-weight: 700; }
/* ---- Diagram 1: today ---- */
.diagram {
border: 1px solid var(--line-strong);
border-radius: 6px;
background: var(--bg-panel);
padding: clamp(1.25rem, 3vw, 2rem);
overflow-x: auto;
}
.row { display: flex; align-items: center; gap: 0; min-width: 640px; }
.node {
flex: 1;
border: 1.5px solid var(--line-strong);
border-radius: 6px;
background: var(--node-bg);
padding: 0.9rem 1rem;
text-align: center;
display: flex; flex-direction: column; gap: 0.25rem;
}
.node .label { font-weight: 700; font-size: 0.92rem; }
.node .sub { font-size: 0.74rem; color: var(--ink-soft); }
.arrow {
flex: 0 0 60px;
display: flex; align-items: center; justify-content: center;
font-family: ui-monospace, monospace;
color: var(--ink-soft);
font-size: 1.1rem;
}
.node.app { border-color: var(--accent); background: var(--accent-soft); }
.node.api { border-style: dashed; }
.node.void { border-color: var(--warn); background: var(--warn-bg); border-style: dashed; color: var(--warn); }
.node.void .label { color: var(--warn); }
.three-up { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.9rem; margin-top: 1rem; min-width: 640px; }
.three-up .node .sub { line-height: 1.35; }
.badge-row { display: flex; gap: 0.6rem; margin-top: 1.1rem; flex-wrap: wrap; }
.badge {
font-family: ui-monospace, monospace; font-size: 0.68rem; font-weight: 700;
letter-spacing: 0.04em; text-transform: uppercase;
padding: 0.25rem 0.6rem; border-radius: 3px;
}
.badge.no { color: var(--warn); background: var(--warn-bg); }
/* ---- Diagram 2: what needs building ---- */
.stack { display: flex; flex-direction: column; gap: 0; min-width: 560px; }
.layer {
display: grid;
grid-template-columns: 30px 1fr;
border: 1.5px solid var(--line-strong);
border-top: none;
background: var(--node-bg);
}
.layer:first-child { border-top: 1.5px solid var(--line-strong); border-radius: 6px 6px 0 0; overflow: hidden; }
.layer:last-child { border-radius: 0 0 6px 6px; overflow: hidden; }
.layer-num {
display: flex; align-items: center; justify-content: center;
font-family: ui-monospace, monospace; font-size: 0.78rem; font-weight: 700;
color: var(--ink-soft); background: color-mix(in srgb, var(--ink) 4%, transparent);
border-right: 1px solid var(--line);
}
.layer-body { padding: 0.85rem 1.1rem; display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
.layer-body .title { font-weight: 700; font-size: 0.92rem; }
.layer-body .desc { font-size: 0.8rem; color: var(--ink-soft); }
.layer.exists { }
.layer.exists .layer-num { color: var(--accent); }
.layer.missing .layer-num { color: var(--future); }
.status {
font-family: ui-monospace, monospace; font-size: 0.66rem; font-weight: 700;
letter-spacing: 0.05em; text-transform: uppercase; padding: 0.18rem 0.5rem; border-radius: 3px;
white-space: nowrap;
}
.status.exists { color: var(--accent); background: var(--accent-soft); }
.status.missing { color: var(--future); background: var(--future-bg); }
.legend { display: flex; gap: 1.3rem; flex-wrap: wrap; font-size: 0.78rem; color: var(--ink-soft); margin-top: 1rem; }
.legend span { display: inline-flex; align-items: center; gap: 0.4rem; }
.dot { width: 9px; height: 9px; border-radius: 50%; display: inline-block; }
.dot.exists { background: var(--accent); }
.dot.missing { background: var(--future); }
footer { padding-top: 1.25rem; border-top: 1px solid var(--line); font-size: 0.78rem; color: var(--ink-soft); }
footer strong { color: var(--ink); }
</style>
<main>
<div class="masthead">
<span class="eyebrow">Falah OS — checked against the actual repos</span>
<h1>moslem03 has no backend either — same gap as moslem04</h1>
<p class="dek">Verified against <code>nur-muslim-companion-v2</code> and <code>nur-falah-prevention</code> source. Neither app talks to a Falah OS server. There is no existing pattern to copy — the admin console link has to be built from zero.</p>
</div>
<section>
<h2 class="section-title"><span class="n">01</span> · What moslem03 actually does today</h2>
<div class="diagram">
<div class="row">
<div class="node app">
<span class="label">moslem03.falahos.my</span>
<span class="sub">Svelte 5 PWA · static build</span>
</div>
<div class="arrow">→</div>
<div class="node api">
<span class="label">Public third-party APIs</span>
<span class="sub">Al Quran Cloud · Aladhan · Overpass</span>
</div>
</div>
<div class="three-up">
<div class="node void">
<span class="label">✕ No auth</span>
<span class="sub">Anyone opens the app, no login</span>
</div>
<div class="node void">
<span class="label">✕ No Falah OS server</span>
<span class="sub">Never calls anything on the Contabo VPS</span>
</div>
<div class="node void">
<span class="label">✕ No admin console link</span>
<span class="sub">Nothing to sync — data never leaves the browser</span>
</div>
</div>
<div class="badge-row">
<span class="badge no">Same for moslem04</span>
<span class="badge no">Same localStorage-only pattern</span>
</div>
</div>
</section>
<section>
<h2 class="section-title"><span class="n">02</span> · What has to be built — bottom up</h2>
<div class="diagram">
<div class="stack">
<div class="layer exists">
<div class="layer-num">✓</div>
<div class="layer-body">
<div>
<div class="title">casdoor (ummahid) identity service</div>
<div class="desc">Already running on the Contabo VPS :8000 — reuse it, don't rebuild login</div>
</div>
<span class="status exists">Already exists</span>
</div>
</div>
<div class="layer missing">
<div class="layer-num">1</div>
<div class="layer-body">
<div>
<div class="title">Login screen in moslem04</div>
<div class="desc">App currently has zero auth UI — needs a casdoor sign-in flow bolted on</div>
</div>
<span class="status missing">To build</span>
</div>
</div>
<div class="layer missing">
<div class="layer-num">2</div>
<div class="layer-body">
<div>
<div class="title">Sync API (new)</div>
<div class="desc">Push Faraid / Assets / Wassiyah / Hibah / Waqf drafts from localStorage to a server, encrypted</div>
</div>
<span class="status missing">To build</span>
</div>
</div>
<div class="layer missing">
<div class="layer-num">3</div>
<div class="layer-body">
<div>
<div class="title">RAMZ tenant tagging</div>
<div class="desc">Every synced record gets stamped with which tenant/firm it belongs to</div>
</div>
<span class="status missing">To build</span>
</div>
</div>
<div class="layer missing">
<div class="layer-num">4</div>
<div class="layer-body">
<div>
<div class="title">Admin console read view</div>
<div class="desc">Nur Falah Admin Console reads synced cases — view only, not edit</div>
</div>
<span class="status missing">To build</span>
</div>
</div>
</div>
<div class="legend">
<span><span class="dot exists"></span> exists on the VPS today</span>
<span><span class="dot missing"></span> needs to be built — nothing to copy from moslem03</span>
</div>
</div>
</section>
<footer>
<strong>Bottom line:</strong> moslem03 gave us one reusable piece — casdoor login. Everything else (sync, tenant tagging, admin visibility) starts from scratch.
</footer>
</main>
@@ -0,0 +1,67 @@
# RAMZ Tenant Isolation — Decision
**Status:** Resolved (revised 2026-08-18 — see amendment below)
**Blocks:** Phase 2 of the [moslem04 Sync API spec](./sync-api-spec.html) (admin console read view)
**Originally flagged in:** Nur Falah Admin Console PRD review
## The question
Falah OS runs one shared backend serving three tenants — EstateOS (law firm estate/probate,
Syariah Court), Tanah Hidup (waqf land lease, JKPTG), and Cair (govt case routing). How is
each tenant's data isolated from the others?
Two options were on the table:
1. **Separate database per tenant** — strongest isolation, highest ops overhead.
2. **Row-level RAMZ-gated access in a shared schema** — cheapest, weakest isolation story.
## Decision
**Database-per-tenant on the shared Postgres instance.**
Each tenant (EstateOS, Tanah Hidup, Cair) gets its own Postgres *database* — `falahestateos`,
`falahtanahhidup`, `falahcair` — not a schema, not a shared table filtered by a `tenant_id`
column.
> **Amendment (2026-08-18):** this was originally decided as schema-per-tenant. Inspecting
> `falah-os-master/infrastructure/postgres/init.sql` showed the codebase already has an
> established convention — **database-per-service** (`falahid`, `falahwallet`, `falahramz`,
> `falahmock`, `falahistore`, all separate databases in one Postgres instance, not schemas).
> Database-per-tenant is a stronger isolation boundary than schema-per-tenant anyway, and it
> keeps one consistent pattern across the platform instead of introducing a second, competing
> one. Revised to match.
## Why
- The three tenants aren't multiple firms sharing one product (the Clio model) — they're
three structurally different products, each answering to a different regulator
(Syariah Court, JKPTG, government agencies). A `WHERE tenant_id = ?` filter on a shared
table is not an answer a regulator's auditor will accept at face value; a schema
boundary is something that can actually be demonstrated.
- This was flagged as a trust/compliance blocker for the UK Innovator Visa pitch — the
isolation model isn't just an engineering choice, it's part of the pitch.
- Separate databases on one Postgres instance is the platform's existing convention, not a
new ops burden — `falah-os-master` already runs 5 databases this way. Adding 3 more
follows the grain of the system instead of cutting against it.
## What RAMZ still does
RAMZ doesn't disappear — it moves down a level. Within each tenant's database, RAMZ handles
the finer-grained access control that a database boundary can't: which user or law firm
within EstateOS can see which case, which land officer within Tanah Hidup can see which
lease. RAMZ was never going to be strong enough to be the *only* thing standing between
tenants — now it doesn't have to be.
## Consequence for the sync API
The `tenant_id` field in the [sync spec](./sync-api-spec.html) becomes a database selector
(one connection pool per tenant), not a row filter. The `moslem-admin` service's
`/v1/admin/metadata` endpoint holds one Postgres pool per tenant database — there is no
shared table and no accidental cross-tenant query path, because a query issued against one
pool cannot physically reach another tenant's rows.
## Still open
This resolves the isolation *model*. It does not resolve whether Core Ledger already emits
the billing-trigger events Phase 2 needs, or who owns panel-lawyer vetting — both remain
open from the earlier PRD review.
+224
View File
@@ -0,0 +1,224 @@
<title>moslem04 Sync API — Architecture Spec</title>
<style>
:root {
--bg: #f6f5ef;
--bg-panel: #ffffff;
--ink: #1b2420;
--ink-soft: #4d5951;
--line: #dbd9cd;
--line-strong: #b9b6a6;
--accent: #2f6e52;
--accent-soft: #e3ede7;
--warn: #a6432e;
--warn-bg: #f6e6e1;
--future: #9a7327;
--future-bg: #f2e9d4;
--mono-tint: #f0efe6;
--node-bg: #ffffff;
}
:root[data-theme="dark"] {
--bg: #141815; --bg-panel: #1b201c; --ink: #e9e7dd; --ink-soft: #a9b0a7;
--line: #333a34; --line-strong: #48524a; --accent: #6bbf94; --accent-soft: #212b24;
--warn: #e08268; --warn-bg: #2c1e1a; --future: #e3bd6a; --future-bg: #2b2416;
--mono-tint: #1f2420; --node-bg: #1f2420;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #141815; --bg-panel: #1b201c; --ink: #e9e7dd; --ink-soft: #a9b0a7;
--line: #333a34; --line-strong: #48524a; --accent: #6bbf94; --accent-soft: #212b24;
--warn: #e08268; --warn-bg: #2c1e1a; --future: #e3bd6a; --future-bg: #2b2416;
--mono-tint: #1f2420; --node-bg: #1f2420;
}
}
* { box-sizing: border-box; }
html, body { margin: 0; background: var(--bg); color: var(--ink); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; }
body { padding: clamp(1.25rem, 4vw, 3rem); display: flex; justify-content: center; }
main { width: 100%; max-width: 940px; display: flex; flex-direction: column; gap: 2.75rem; }
code, .mono { font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; }
/* masthead */
.masthead { display: flex; flex-direction: column; gap: 0.6rem; padding-bottom: 1.4rem; border-bottom: 2px solid var(--ink); }
.eyebrow { font-size: 0.7rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--accent); font-family: ui-monospace, monospace; }
h1 { margin: 0; font-size: clamp(1.6rem, 3.6vw, 2.2rem); font-weight: 700; letter-spacing: -0.01em; text-wrap: balance; }
.dek { margin: 0; max-width: 68ch; font-size: 0.98rem; line-height: 1.55; color: var(--ink-soft); }
.scope-note {
margin-top: 0.4rem; display: flex; gap: 0.6rem; align-items: flex-start;
font-size: 0.85rem; padding: 0.7rem 0.9rem; background: var(--future-bg); border-radius: 4px;
border-left: 3px solid var(--future); color: var(--ink);
}
.scope-note strong { color: var(--future); }
h2.section-title {
display: flex; align-items: baseline; gap: 0.6rem;
font-size: 0.75rem; letter-spacing: 0.1em; text-transform: uppercase;
font-family: ui-monospace, monospace; color: var(--ink-soft); margin: 0 0 1.1rem 0;
}
h2.section-title .n { color: var(--accent); font-weight: 700; }
h2.section-title .t { color: var(--ink); font-size: 0.78rem; letter-spacing: 0.08em; }
p.body-text { font-size: 0.92rem; line-height: 1.6; color: var(--ink); max-width: 68ch; margin: 0 0 0.9rem 0; }
p.body-text:last-child { margin-bottom: 0; }
/* flow diagram */
.diagram { border: 1px solid var(--line-strong); border-radius: 6px; background: var(--bg-panel); padding: clamp(1.1rem, 3vw, 1.6rem); overflow-x: auto; }
.flow { display: flex; align-items: stretch; gap: 0; min-width: 760px; }
.fnode {
flex: 1; border: 1.5px solid var(--line-strong); border-radius: 6px; background: var(--node-bg);
padding: 0.85rem 0.9rem; display: flex; flex-direction: column; gap: 0.3rem; justify-content: center;
}
.fnode .label { font-weight: 700; font-size: 0.85rem; }
.fnode .sub { font-size: 0.72rem; color: var(--ink-soft); line-height: 1.35; }
.fnode.client { border-color: var(--accent); background: var(--accent-soft); }
.fnode.new { border-style: dashed; border-color: var(--future); }
.fnode.new .label { color: var(--future); }
.fnode.gated { border-color: var(--warn); background: var(--warn-bg); }
.fnode.gated .label { color: var(--warn); }
.farrow { flex: 0 0 34px; display: flex; align-items: center; justify-content: center; color: var(--ink-soft); font-size: 1.1rem; }
/* tables */
.table-wrap { overflow-x: auto; border: 1px solid var(--line-strong); border-radius: 6px; background: var(--bg-panel); }
table { width: 100%; min-width: 600px; border-collapse: collapse; font-size: 0.85rem; }
thead th {
text-align: left; font-family: ui-monospace, monospace; font-size: 0.66rem; letter-spacing: 0.08em;
text-transform: uppercase; color: var(--ink-soft); padding: 0.65rem 0.85rem; border-bottom: 1px solid var(--line-strong);
background: var(--mono-tint);
}
tbody td { padding: 0.7rem 0.85rem; border-bottom: 1px solid var(--line); vertical-align: top; line-height: 1.45; }
tbody tr:last-child td { border-bottom: none; }
td.method { font-family: ui-monospace, monospace; font-weight: 700; font-size: 0.76rem; white-space: nowrap; }
td.path { font-family: ui-monospace, monospace; font-size: 0.8rem; white-space: nowrap; color: var(--ink); }
td.detail { color: var(--ink-soft); }
.m-get { color: var(--accent); } .m-post { color: #4d6fb3; } .m-delete { color: var(--warn); }
:root[data-theme="dark"] .m-post, .m-post { color: #7fa2e6; }
.chip { display: inline-block; font-family: ui-monospace, monospace; font-size: 0.64rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; padding: 0.18rem 0.5rem; border-radius: 3px; white-space: nowrap; }
.chip.locked { color: var(--warn); background: var(--warn-bg); }
.chip.opt { color: var(--future); background: var(--future-bg); }
.chip.never { color: var(--ink-soft); background: var(--mono-tint); }
/* rollout */
.phases { display: flex; gap: 0; min-width: 700px; }
.phase {
flex: 1; border: 1.5px solid var(--line-strong); background: var(--node-bg); padding: 1rem;
display: flex; flex-direction: column; gap: 0.4rem; position: relative;
}
.phase:not(:first-child) { border-left: none; }
.phase:first-child { border-radius: 6px 0 0 6px; }
.phase:last-child { border-radius: 0 6px 6px 0; }
.phase .ph-label { font-family: ui-monospace, monospace; font-size: 0.66rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--accent); }
.phase .ph-title { font-weight: 700; font-size: 0.88rem; }
.phase .ph-desc { font-size: 0.78rem; color: var(--ink-soft); line-height: 1.4; }
.phase.blocked { background: var(--warn-bg); }
.phase.blocked .ph-label { color: var(--warn); }
footer { padding-top: 1.4rem; border-top: 1px solid var(--line); font-size: 0.8rem; color: var(--ink-soft); line-height: 1.6; }
footer strong { color: var(--ink); }
</style>
<main>
<div class="masthead">
<span class="eyebrow">Falah OS — Nur Falah Estate &amp; Waqf Suite</span>
<h1>Sync API architecture — moslem04.falahos.my</h1>
<p class="dek">A server-side sync layer for moslem04's six local-first modules, gated by casdoor login, encrypted client-side before it ever leaves the device, and surfacing only opt-in metadata to the admin console — never raw estate documents.</p>
<div class="scope-note"><strong>Out of scope:</strong> moslem03 (Muslim Companion) stays exactly as-is — free, zero signup, no account, no sync, forever. This spec touches nothing there.</div>
</div>
<section>
<h2 class="section-title"><span class="n">01</span><span class="t">Data flow, end to end</span></h2>
<div class="diagram">
<div class="flow">
<div class="fnode client">
<span class="label">moslem04 (browser)</span>
<span class="sub">localStorage stays the source of truth offline; sync is additive, not a replacement</span>
</div>
<div class="farrow">→</div>
<div class="fnode">
<span class="label">casdoor (existing)</span>
<span class="sub">OIDC login, issues JWT — nothing new to build here</span>
</div>
<div class="farrow">→</div>
<div class="fnode new">
<span class="label">Sync API (new)</span>
<span class="sub">Accepts encrypted blobs, tags with tenant + user, never decrypts</span>
</div>
<div class="farrow">→</div>
<div class="fnode new">
<span class="label">Postgres (new)</span>
<span class="sub">Ciphertext + RAMZ tenant column + a small plaintext metadata row</span>
</div>
<div class="farrow">→</div>
<div class="fnode gated">
<span class="label">Admin console</span>
<span class="sub">Reads metadata only — opt-in, read-only, per Phase 2 below</span>
</div>
</div>
</div>
</section>
<section>
<h2 class="section-title"><span class="n">02</span><span class="t">What gets encrypted vs. what admin can see</span></h2>
<p class="body-text">Estate plans are the most sensitive data a person will put into this app. The sync API is built so the server — and by extension the admin console — physically cannot read the contents of anyone's Faraid, Wassiyah, Hibah, or Waqf drafts. Encryption happens on-device before upload.</p>
<div class="table-wrap">
<table>
<thead><tr><th>Field</th><th>Who can read it</th><th>Notes</th></tr></thead>
<tbody>
<tr><td class="mono">payload_ciphertext</td><td><span class="chip locked">User only</span></td><td class="detail">AES-GCM, key derived from casdoor session + on-device passphrase. Server stores bytes it cannot open.</td></tr>
<tr><td class="mono">module</td><td><span class="chip opt">User + admin</span></td><td class="detail">Which of the 6 modules the record belongs to — plaintext, needed for routing and counts only.</td></tr>
<tr><td class="mono">plan_completed</td><td><span class="chip opt">User + admin</span></td><td class="detail">Boolean per module. Lets the console show "how many people finished their Wassiyah" without seeing a single will.</td></tr>
<tr><td class="mono">panel_lawyer_requested</td><td><span class="chip opt">User + admin</span></td><td class="detail">Explicit opt-in flag the user sets — the only trigger that surfaces a person to a panel lawyer at all.</td></tr>
<tr><td class="mono">tenant_id (RAMZ)</td><td><span class="chip opt">Admin only</span></td><td class="detail">Which tenant/firm this record is scoped to — still pending the row-level isolation decision from the PRD review.</td></tr>
<tr><td class="mono">claims_payload (H2)</td><td><span class="chip never">Nobody, not synced</span></td><td class="detail">Horizon 2 Claims module is excluded from sync entirely until the Phase 0 legal gate in DEPLOY.md clears.</td></tr>
</tbody>
</table>
</div>
</section>
<section>
<h2 class="section-title"><span class="n">03</span><span class="t">Endpoints</span></h2>
<div class="table-wrap">
<table>
<thead><tr><th>Method</th><th>Path</th><th>Purpose</th></tr></thead>
<tbody>
<tr><td class="method m-post">POST</td><td class="path">/v1/auth/exchange</td><td class="detail">Trade a casdoor OIDC token for a moslem04 session token</td></tr>
<tr><td class="method m-get">GET</td><td class="path">/v1/sync/pull?since=&lt;ts&gt;</td><td class="detail">Fetch records changed since last sync — for restoring on a new device</td></tr>
<tr><td class="method m-post">POST</td><td class="path">/v1/sync/push</td><td class="detail">Batch upsert of encrypted blobs from the local device queue</td></tr>
<tr><td class="method m-delete">DELETE</td><td class="path">/v1/sync/:record_id</td><td class="detail">Tombstone a record — deletes propagate across the user's own devices</td></tr>
<tr><td class="method m-post">POST</td><td class="path">/v1/consent/panel-lawyer</td><td class="detail">The one explicit opt-in action that makes a user visible to EstateOS admin at all</td></tr>
<tr><td class="method m-get">GET</td><td class="path">/v1/admin/metadata</td><td class="detail">Admin console read — returns only the plaintext metadata fields from §02, RAMZ-scoped</td></tr>
</tbody>
</table>
</div>
</section>
<section>
<h2 class="section-title"><span class="n">04</span><span class="t">Conflict handling</span></h2>
<p class="body-text">These are single-owner personal drafts, not shared documents — so last-write-wins by <code>updated_at</code> is sufficient for five of the six modules. Two exceptions: <strong>Family Waqf Designator</strong> can involve co-designators, so pushes there append to a change log instead of overwriting; <strong>Digital Claims (H2)</strong> doesn't sync at all, see §02.</p>
</section>
<section>
<h2 class="section-title"><span class="n">05</span><span class="t">Rollout phases</span></h2>
<div class="diagram">
<div class="phases">
<div class="phase">
<span class="ph-label">Phase 0</span>
<span class="ph-title">Auth + sync plumbing</span>
<span class="ph-desc">Login, encrypted push/pull, cross-device restore. No admin visibility of any kind.</span>
</div>
<div class="phase">
<span class="ph-label">Phase 1</span>
<span class="ph-title">Opt-in metadata surface</span>
<span class="ph-desc">Panel-lawyer request flag goes live. Admin sees only what §02 allows.</span>
</div>
<div class="phase blocked">
<span class="ph-label">Phase 2 · blocked</span>
<span class="ph-title">Full admin console read view</span>
<span class="ph-desc">Waits on the RAMZ isolation decision and Core Ledger billing-event readiness — both still open from the earlier PRD review.</span>
</div>
</div>
</div>
</section>
<footer>
<strong>Carried-over open questions this spec does not resolve:</strong> row-level (RAMZ) vs. separate-DB tenant isolation, whether Core Ledger already emits the billing-trigger events Phase 2 would need, and who owns panel-lawyer vetting once requests start arriving. All three block Phase 2, not Phase 0–1.
</footer>
</main>
Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

+51 -14
View File
@@ -6,6 +6,7 @@
let families = $state([]); let families = $state([]);
let invites = $state([]); let invites = $state([]);
let newFamilyName = $state(''); let newFamilyName = $state('');
let searchQuery = $state('');
let loading = $state(true); let loading = $state(true);
let error = $state(''); let error = $state('');
@@ -45,6 +46,7 @@
} }
const isAgentForMultiple = $derived(families.filter(f => f.role === 'agent').length > 1); const isAgentForMultiple = $derived(families.filter(f => f.role === 'agent').length > 1);
const filteredFamilies = $derived(families.filter(f => f.name.toLowerCase().includes(searchQuery.toLowerCase())));
</script> </script>
<div class="switcher-screen"> <div class="switcher-screen">
@@ -65,12 +67,22 @@
{#if loading} {#if loading}
<p class="loading">Loading your families…</p> <p class="loading">Loading your families…</p>
{:else} {:else}
<div class="section">
<h3>Start your own family estate</h3>
<p class="hint">If you're the head of family setting this up for the first time, create your family here. You can invite an estate agent to help manage it once it's set up.</p>
<label class="field"><span>Family name</span><input type="text" bind:value={newFamilyName} placeholder="e.g. The Ismail Family" /></label>
<button class="btn-primary" onclick={handleCreate}>Create family</button>
</div>
{#if invites.length} {#if invites.length}
<div class="section"> <div class="section">
<h3>Pending invitations</h3> <h3>Pending invitations</h3>
{#each invites as inv} {#each invites as inv}
<div class="invite-row"> <div class="invite-row card-layout">
<span>{inv.familyName} — as <strong>{inv.role}</strong></span> <div class="card-info">
<span class="card-title">{inv.familyName}</span>
<span class="card-meta">Role: <strong>{inv.role}</strong></span>
</div>
<button class="btn-small" onclick={() => handleAccept(inv)}>Accept</button> <button class="btn-small" onclick={() => handleAccept(inv)}>Accept</button>
</div> </div>
{/each} {/each}
@@ -79,22 +91,29 @@
{#if families.length} {#if families.length}
<div class="section"> <div class="section">
<div class="section-header">
<h3>{isAgentForMultiple ? 'Your assigned families' : 'Your families'}</h3> <h3>{isAgentForMultiple ? 'Your assigned families' : 'Your families'}</h3>
{#each families as f} <input type="text" class="search-input" bind:value={searchQuery} placeholder="Search families..." />
<button class="family-row" onclick={() => selectFamily(f.id)}> </div>
<span>{f.name}</span> <div class="cards-grid">
{#each filteredFamilies as f}
<button class="family-card" onclick={() => selectFamily(f.id)}>
<div class="card-top">
<span class="card-title">{f.name}</span>
<span class="role-badge" class:owner={f.role === 'owner'}>{f.role}</span> <span class="role-badge" class:owner={f.role === 'owner'}>{f.role}</span>
</div>
<div class="card-bottom">
<span class="status-indicator">Active</span>
<span class="action-hint">Manage estate &rarr;</span>
</div>
</button> </button>
{/each} {/each}
{#if filteredFamilies.length === 0}
<p class="empty-state">No families found matching "{searchQuery}"</p>
{/if}
</div>
</div> </div>
{/if} {/if}
<div class="section">
<h3>Start your own family estate</h3>
<p class="hint">If you're the head of family setting this up for the first time, create your family here. You can invite an estate agent to help manage it once it's set up.</p>
<label class="field"><span>Family name</span><input type="text" bind:value={newFamilyName} placeholder="e.g. The Ismail Family" /></label>
<button class="btn-primary" onclick={handleCreate}>Create family</button>
</div>
{/if} {/if}
</div> </div>
@@ -114,12 +133,30 @@
.hint { font-size: 11.5px; color: #8A8478; line-height: 1.5; margin-bottom: 12px; } .hint { font-size: 11.5px; color: #8A8478; line-height: 1.5; margin-bottom: 12px; }
.invite-row { display: flex; justify-content: space-between; align-items: center; background: rgba(201,168,76,0.08); border: 1px solid rgba(201,168,76,0.25); border-radius: 10px; padding: 12px; margin-bottom: 8px; font-size: 13px; color: #E8E4DC; } .invite-row { display: flex; justify-content: space-between; align-items: center; background: rgba(201,168,76,0.08); border: 1px solid rgba(201,168,76,0.25); border-radius: 10px; padding: 12px; margin-bottom: 8px; font-size: 13px; color: #E8E4DC; }
.btn-small { background: #C9A84C; color: #070A0D; border: none; border-radius: 8px; padding: 8px 12px; font-size: 12px; font-weight: 600; cursor: pointer; } .btn-small { background: #C9A84C; color: #070A0D; border: none; border-radius: 8px; padding: 8px 12px; font-size: 12px; font-weight: 600; cursor: pointer; }
.family-row { width: 100%; display: flex; justify-content: space-between; align-items: center; background: rgba(255,255,255,0.04); border: none; border-radius: 10px; padding: 14px; margin-bottom: 8px; font-size: 14px; color: #E8E4DC; cursor: pointer; text-align: left; }
.section-header { display: flex; flex-direction: column; gap: 8px; margin-bottom: 12px; }
.search-input { background: rgba(255,255,255,0.05); border: 1px solid rgba(201,168,76,0.2); border-radius: 8px; padding: 8px 12px; color: #E8E4DC; font-size: 13px; width: 100%; outline: none; transition: border-color 0.2s; }
.search-input:focus { border-color: #C9A84C; }
.cards-grid { display: flex; flex-direction: column; gap: 10px; }
.family-card { width: 100%; display: flex; flex-direction: column; gap: 12px; background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); border-radius: 12px; padding: 16px; font-size: 14px; color: #E8E4DC; cursor: pointer; text-align: left; transition: all 0.2s; }
.family-card:hover { background: rgba(255,255,255,0.06); border-color: rgba(201,168,76,0.3); transform: translateY(-1px); }
.card-top { display: flex; justify-content: space-between; align-items: center; width: 100%; }
.card-bottom { display: flex; justify-content: space-between; align-items: center; width: 100%; font-size: 11px; color: #8A8478; border-top: 1px solid rgba(255,255,255,0.05); padding-top: 10px; }
.card-title { font-weight: 600; font-size: 15px; }
.status-indicator { display: flex; align-items: center; gap: 4px; }
.status-indicator::before { content: ''; width: 6px; height: 6px; background: #2ECC71; border-radius: 50%; }
.action-hint { color: #C9A84C; opacity: 0; transition: opacity 0.2s; }
.family-card:hover .action-hint { opacity: 1; }
.empty-state { text-align: center; color: #8A8478; font-size: 13px; padding: 20px 0; font-style: italic; }
.role-badge { font-size: 10px; text-transform: uppercase; padding: 3px 8px; border-radius: 6px; background: rgba(255,255,255,0.1); color: #B8B2A6; } .role-badge { font-size: 10px; text-transform: uppercase; padding: 3px 8px; border-radius: 6px; background: rgba(255,255,255,0.1); color: #B8B2A6; }
.role-badge.owner { background: rgba(201,168,76,0.15); color: #C9A84C; } .role-badge.owner { background: rgba(201,168,76,0.15); color: #C9A84C; }
.field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; } .field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; }
.field span { font-size: 12px; color: #B8B2A6; } .field span { font-size: 12px; color: #B8B2A6; }
.field input { background: rgba(255,255,255,0.05); border: 1px solid rgba(201,168,76,0.2); border-radius: 8px; padding: 10px 12px; color: #E8E4DC; font-size: 14px; width: 100%; } .field input { background: rgba(255,255,255,0.05); border: 1px solid rgba(201,168,76,0.2); border-radius: 8px; padding: 10px 12px; color: #E8E4DC; font-size: 14px; width: 100%; }
.btn-primary { width: 100%; padding: 12px; border-radius: 8px; border: none; font-weight: 600; cursor: pointer; background: #C9A84C; color: #070A0D; } .btn-primary { width: 100%; padding: 12px; border-radius: 8px; border: none; font-weight: 600; cursor: pointer; background: #C9A84C; color: #070A0D; transition: opacity 0.2s; }
.btn-primary:hover { opacity: 0.9; }
.error-text { color: #EF4444; font-size: 12.5px; margin-bottom: 12px; } .error-text { color: #EF4444; font-size: 12.5px; margin-bottom: 12px; }
</style> </style>
+12 -12
View File
@@ -1,10 +1,10 @@
<script> <script>
// Built per explicit user instruction overriding the Horizon 1 PRD §8.5 gate // Built ahead of the Horizon 1 PRD §8.5 gate ("this module does not enter
// ("this module does not enter development until... a qualified Shariah advisor's // development until... a qualified Shariah advisor's direct sign-off"), per
// direct sign-off"). scholarly-review-log.md OPEN-01 remains unresolved — the cap // explicit product direction at the time. scholarly-review-log.md OPEN-01 is now
// logic below follows the PRD's literal specification (marad al-mawt only) but the // RESOLVED: ruled NOT subject to the one-third cap. The cap logic below already
// in-app note is kept honest about the open question rather than presenting // matched this ruling (marad al-mawt only), so no change was needed here — only
// invented certainty, consistent with this project's own stated practice. // the in-app copy and this comment moved from "open question" to "resolved."
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { activeFamilyId } from './family.js'; import { activeFamilyId } from './family.js';
import { session } from './auth.js'; import { session } from './auth.js';
@@ -88,10 +88,10 @@
? 'This deed references Perak Waqf Enactment 2015, Section 10(1). Registration with the\nrelevant state waqf authority is required for legal effect — this document alone does\nnot constitute a legally registered waqf.' ? 'This deed references Perak Waqf Enactment 2015, Section 10(1). Registration with the\nrelevant state waqf authority is required for legal effect — this document alone does\nnot constitute a legally registered waqf.'
: `This deed references ${jurisdiction}'s waqf enactment where available. Registration\nwith the relevant state waqf authority is required for legal effect — this document\nalone does not constitute a legally registered waqf.`, : `This deed references ${jurisdiction}'s waqf enactment where available. Registration\nwith the relevant state waqf authority is required for legal effect — this document\nalone does not constitute a legally registered waqf.`,
'', '',
'OPEN FIQH QUESTION (see project scholarly-review-log.md, OPEN-01): whether an', 'SCHOLARLY RULING (project scholarly-review-log.md, OPEN-01 — resolved): an',
'inter-vivos waqf made in health is subject to the same one-third cap as a wasiyyah,', 'inter-vivos waqf made in health is NOT subject to the one-third cap that applies',
'or is uncapped like hibah, has not received a scholar\'s determination. This draft', 'to a wasiyyah; it is uncapped like hibah. This draft applies no health-state cap,',
'applies no health-state cap unless the marad al-mawt guardrail below was triggered.', 'consistent with that ruling, unless the marad al-mawt guardrail below was triggered.',
'', '',
'DISCLAIMER: Not a fatwa. Not legal advice. Drafting aid only.' 'DISCLAIMER: Not a fatwa. Not legal advice. Drafting aid only.'
]; ];
@@ -109,7 +109,7 @@
<InfoPanel <InfoPanel
title="Family Waqf Designator" title="Family Waqf Designator"
what="A waqf is a permanent endowment — you dedicate an asset (like a piece of land or property) so it can never be sold, and it serves a purpose (usually your family's benefit) forever. Like a hibah, this happens now, while you're alive, so the asset is out of your estate immediately — nothing for a court to process later." what="A waqf is a permanent endowment — you dedicate an asset (like a piece of land or property) so it can never be sold, and it serves a purpose (usually your family's benefit) forever. Like a hibah, this happens now, while you're alive, so the asset is out of your estate immediately — nothing for a court to process later."
how="Choose an asset from your Asset Registry, name a trustee (mutawalli) who will manage it, and decide how the benefit is shared among your family. There's an open religious question about the one-third limit for this instrument — the app explains it plainly rather than pretending there's a single settled answer." how="Choose an asset from your Asset Registry, name a trustee (mutawalli) who will manage it, and decide how the benefit is shared among your family. Scholarly sign-off confirmed this instrument is not subject to the one-third limit that applies to a wasiyyah — the app states that ruling plainly."
fields={[ fields={[
{ label: 'Corpus asset', hint: 'The specific asset you\'re dedicating — pick it from your Asset Registry.' }, { label: 'Corpus asset', hint: 'The specific asset you\'re dedicating — pick it from your Asset Registry.' },
{ label: 'Mutawalli (trustee)', hint: 'The person who will manage the asset and make sure it\'s used properly.' }, { label: 'Mutawalli (trustee)', hint: 'The person who will manage the asset and make sure it\'s used properly.' },
@@ -119,7 +119,7 @@
/> />
</div> </div>
<div class="open-note"> <div class="open-note">
<strong>Open fiqh question:</strong> whether a healthy-state family waqf is subject to a one-third cap has not received scholarly sign-off (tracked as OPEN-01). This module ships without waiting on that review, per explicit product direction — the exported deed says so plainly rather than presenting invented certainty. <strong>Scholarly ruling (OPEN-01, resolved):</strong> a healthy-state family waqf is <em>not</em> subject to the one-third cap that applies to a wasiyyah. The exported deed states this ruling plainly.
</div> </div>
<div class="fast-path-note">This is a fast-path instrument. Once dedicated, this asset is no longer part of your estate — it is not subject to Faraid/probate adjudication at death. Product direction for this build is explicit: keeping assets out of the Faraid/probate queue entirely is the point.</div> <div class="fast-path-note">This is a fast-path instrument. Once dedicated, this asset is no longer part of your estate — it is not subject to Faraid/probate adjudication at death. Product direction for this build is explicit: keeping assets out of the Faraid/probate queue entirely is the point.</div>
<p class="sub">Dedicate a specific asset to your family in perpetuity.</p> <p class="sub">Dedicate a specific asset to your family in perpetuity.</p>
+13 -9
View File
@@ -1,8 +1,11 @@
<script> <script>
// Built per explicit user instruction overriding the Horizon 2 PRD's Phase 0 gate. // Originally built ahead of the Horizon 2 PRD's Phase 0 gate; Phase 0 (legal opinion
// See claims.js header for the full note. This screen is clearly labeled pre-pilot // + signed institutional partner agreement) is now RESOLVED — both are signed. See
// throughout — the PRD's own non-goals (§3.2) forbid presenting this as a freely // claims.js header for the full note. That clears the PRD's gate to begin real
// tradeable security or a live institutional integration, and it is neither. // institutional-integration engineering, but no such integration has been built yet,
// so this screen remains a structural demonstration only — the PRD's own non-goals
// (§3.2) still forbid presenting this as a freely tradeable security or a live
// institutional integration, and it still is neither, pending that build.
import { listClaims, issueClaim, transferClaim, removeClaim } from './claims.js'; import { listClaims, issueClaim, transferClaim, removeClaim } from './claims.js';
import Disclaimer from '../Disclaimer.svelte'; import Disclaimer from '../Disclaimer.svelte';
import InfoPanel from '../InfoPanel.svelte'; import InfoPanel from '../InfoPanel.svelte';
@@ -39,11 +42,12 @@
<div class="module"> <div class="module">
<div class="pilot-banner"> <div class="pilot-banner">
<strong>PRE-PILOT DEMONSTRATION — NOT A LIVE PROGRAMME.</strong> <strong>PRE-PILOT DEMONSTRATION — NOT A LIVE PROGRAMME.</strong>
Horizon 2 PRD Status: "No engineering work begins until Phase 0 (legal opinion + signed Horizon 2 PRD's Phase 0 gate (legal opinion + signed institutional partner agreement)
institutional partner agreement) is complete." That confirmation has not been provided in is now resolved — both are signed. That clears the way to build real institutional
this session. This screen is a structural demonstration of the claim model and its transfer integration, but it hasn't been built yet. This screen remains a structural
restriction — it issues no real legal claim, moves no real asset, and is not backed by any demonstration of the claim model and its transfer restriction only — it issues no
SPV/trust legal wrapper or institutional partner. real legal claim, moves no real asset, and is not yet backed by any SPV/trust legal
wrapper or live institutional partner connection.
</div> </div>
<div class="module-header"> <div class="module-header">
+8 -7
View File
@@ -1,11 +1,12 @@
// Digital Beneficial Claims data model. // Digital Beneficial Claims data model.
// Built per explicit user instruction overriding Horizon 2 PRD's stated gate: // Originally built ahead of Horizon 2 PRD's stated gate: "No engineering work begins
// "No engineering work begins until Phase 0 (legal opinion + signed institutional // until Phase 0 (legal opinion + signed institutional partner agreement) is
// partner agreement) is complete." No evidence Phase 0 is complete — this ships as // complete." Phase 0 is now RESOLVED — both are signed, clearing the way to build
// a local, non-custodial demonstration model only, structurally enforcing the // the real legal wrapper (SPV/trust, Horizon 2 PRD §5.1) and institutional
// PRD §5.2 transfer restriction (no open market) even though the real legal // integration. Neither has been built yet, so this module still ships as a local,
// wrapper (SPV/trust, Horizon 2 PRD §5.1) and institutional integration do not exist // non-custodial demonstration only, structurally enforcing the PRD §5.2 transfer
// yet. Nothing here moves real assets or represents a real legal claim. // restriction (no open market). Nothing here moves real assets or represents a real
// legal claim until that integration work lands.
import { load, save } from '../storage.js'; import { load, save } from '../storage.js';