Initial build: Horizon 1 Prevention Suite + Horizon 2 Unlock demo

Svelte 5 + Vite PWA, styled to match moslem03.falahos.my's design system.

Horizon 1: Faraid Calculator (shared calc core, 14 classical cases passing),
Asset Registry, Wassiyah Generator (1/3 meter + heir-exclusion block),
Hibah Tracker and Family Waqf Designator (shared marad al-mawt guardrail).

Horizon 2: Digital Beneficial Claims — local non-custodial demo of the claim
model and transfer restriction only.

Two governance gates in this project's own PRDs were overridden per explicit
product direction, and are flagged in-app and in README.md / deploy/DEPLOY.md
rather than silently shipped as production-ready:
- Family Waqf Designator ships ahead of scholarly sign-off (OPEN-01 in
  scholarly-review-log.md remains unresolved).
- Horizon 2 ships ahead of the PRD's stated Phase 0 gate (legal opinion +
  signed institutional partner) with no confirmation that gate is cleared.
This commit is contained in:
wmj
2026-08-13 16:55:58 +08:00
commit 6690696f7f
28 changed files with 7586 additions and 0 deletions
+165
View File
@@ -0,0 +1,165 @@
<script>
import { load, save, estateTotal } from './storage.js';
import Disclaimer from './Disclaimer.svelte';
const TYPES = ['Property', 'Cash / Bank', 'Vehicle', 'Business interest', 'Digital assets', 'Jewelry / valuables', 'Other'];
let assets = $state(load('assets', []));
let trustedContacts = $state(load('trustedContacts', []));
let form = $state(emptyForm());
let editingId = $state(null);
let contactForm = $state({ name: '', method: '' });
function emptyForm() {
return { type: TYPES[0], description: '', value: '', location: '', ownershipShare: 100 };
}
function persist() {
save('assets', assets);
}
function addOrUpdate() {
if (!form.description || !form.value) return;
if (editingId) {
assets = assets.map(a => a.id === editingId ? { ...form, id: editingId, value: Number(form.value) } : a);
editingId = null;
} else {
assets = [...assets, { ...form, id: crypto.randomUUID(), value: Number(form.value) }];
}
form = emptyForm();
persist();
}
function edit(a) {
editingId = a.id;
form = { ...a, value: String(a.value) };
}
function remove(id) {
assets = assets.filter(a => a.id !== id);
if (editingId === id) { editingId = null; form = emptyForm(); }
persist();
}
function addContact() {
if (!contactForm.name) return;
trustedContacts = [...trustedContacts, { ...contactForm, id: crypto.randomUUID() }];
contactForm = { name: '', method: '' };
save('trustedContacts', trustedContacts);
}
function removeContact(id) {
trustedContacts = trustedContacts.filter(c => c.id !== id);
save('trustedContacts', trustedContacts);
}
function exportSummary() {
const total = estateTotal(assets);
const lines = [
'NUR FALAH — ASSET REGISTRY SUMMARY',
`Generated: ${new Date().toISOString().slice(0, 10)}`,
`Total estate value: ${total.toLocaleString()}`,
'',
...assets.map(a => `${a.type}${a.description} — value ${Number(a.value).toLocaleString()}${a.ownershipShare}% owned — ${a.location || 'no location noted'}`),
'',
'This is a portable "what I own" summary. Not a fatwa, not legal advice.'
];
const blob = new Blob([lines.join('\n')], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = 'asset-registry-summary.txt'; a.click();
URL.revokeObjectURL(url);
}
const total = $derived(estateTotal(assets));
</script>
<div class="module">
<h2>Asset Registry</h2>
<p class="sub">Log what you own — feeds the Faraid Calculator, Wassiyah one-third meter, and Waqf corpus selector.</p>
<div class="total-card">
<span>Estate total</span>
<strong>{total.toLocaleString()}</strong>
</div>
<div class="form-card">
<label class="field"><span>Type</span>
<select bind:value={form.type}>
{#each TYPES as t}<option value={t}>{t}</option>{/each}
</select>
</label>
<label class="field"><span>Description</span><input type="text" bind:value={form.description} placeholder="e.g. Terrace house, Shah Alam" /></label>
<label class="field"><span>Estimated value</span><input type="number" min="0" bind:value={form.value} /></label>
<label class="field"><span>Location / jurisdiction</span><input type="text" bind:value={form.location} placeholder="e.g. Selangor, UK" /></label>
<label class="field"><span>Ownership share (%)</span><input type="number" min="0" max="100" bind:value={form.ownershipShare} /></label>
<button class="btn-primary" onclick={addOrUpdate}>{editingId ? 'Update asset' : 'Add asset'}</button>
</div>
<div class="list">
{#each assets as a (a.id)}
<div class="asset-row">
<div class="asset-info">
<span class="asset-type">{a.type}</span>
<span class="asset-desc">{a.description}</span>
<span class="asset-meta">{a.ownershipShare}% owned · {a.location || '—'}</span>
</div>
<div class="asset-value">{Number(a.value).toLocaleString()}</div>
<div class="asset-actions">
<button onclick={() => edit(a)} aria-label="Edit"></button>
<button onclick={() => remove(a.id)} aria-label="Remove"></button>
</div>
</div>
{:else}
<p class="empty">No assets logged yet.</p>
{/each}
</div>
<button class="btn-secondary" onclick={exportSummary}>Export "what I own" summary</button>
<div class="contacts-section">
<h3>Trusted contacts</h3>
<p class="note">Notified or given a read-only export on a triggering event — not an automated death-detection or legal-transfer mechanism.</p>
<div class="form-card">
<label class="field"><span>Name</span><input type="text" bind:value={contactForm.name} /></label>
<label class="field"><span>Contact method</span><input type="text" bind:value={contactForm.method} placeholder="email or phone" /></label>
<button class="btn-secondary" onclick={addContact}>Add trusted contact</button>
</div>
{#each trustedContacts as c (c.id)}
<div class="contact-row"><span>{c.name}{c.method}</span><button onclick={() => removeContact(c.id)}>✕</button></div>
{/each}
</div>
<Disclaimer text="Manual entry only — bank/brokerage account linking and live balance sync are explicitly out of scope for Horizon 1." />
</div>
<style>
.module { padding: 4px 0 40px; }
h2 { font-family: 'DM Serif Display', serif; font-size: 24px; color: #E8E4DC; margin-bottom: 4px; }
.sub { font-size: 13px; color: #8A8478; margin-bottom: 16px; }
.total-card { display: flex; justify-content: space-between; align-items: baseline; background: rgba(201,168,76,0.08); border: 1px solid rgba(201,168,76,0.25); border-radius: 12px; padding: 16px; margin-bottom: 18px; }
.total-card span { font-size: 12.5px; color: #B8B2A6; }
.total-card strong { font-size: 22px; color: #C9A84C; }
.form-card { background: rgba(255,255,255,0.03); border-radius: 12px; padding: 14px; margin-bottom: 16px; }
.field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; }
.field span { font-size: 12px; color: #B8B2A6; }
.field select, .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; }
.btn-primary, .btn-secondary { width: 100%; padding: 12px; border-radius: 8px; border: none; font-weight: 600; cursor: pointer; }
.btn-primary { background: #C9A84C; color: #070A0D; }
.btn-secondary { background: rgba(255,255,255,0.08); color: #E8E4DC; margin-bottom: 10px; }
.list { margin-bottom: 12px; }
.asset-row { display: flex; align-items: center; gap: 10px; padding: 12px 0; border-bottom: 1px solid rgba(255,255,255,0.06); }
.asset-info { display: flex; flex-direction: column; flex: 1; gap: 2px; }
.asset-type { font-size: 11px; color: #8A8478; text-transform: uppercase; letter-spacing: 0.4px; }
.asset-desc { font-size: 13.5px; color: #E8E4DC; }
.asset-meta { font-size: 11px; color: #8A8478; }
.asset-value { color: #2ECC71; font-weight: 600; font-size: 13px; }
.asset-actions button { background: none; border: none; color: #8A8478; cursor: pointer; padding: 4px 6px; }
.empty { font-size: 13px; color: #8A8478; text-align: center; padding: 20px 0; }
.contacts-section { margin-top: 24px; border-top: 1px solid rgba(201,168,76,0.15); padding-top: 16px; }
.contacts-section h3 { font-size: 15px; color: #C9A84C; margin-bottom: 4px; }
.note { font-size: 11.5px; color: #8A8478; margin-bottom: 12px; }
.contact-row { display: flex; justify-content: space-between; padding: 8px 0; font-size: 13px; color: #E8E4DC; border-bottom: 1px solid rgba(255,255,255,0.06); }
.contact-row button { background: none; border: none; color: #8A8478; cursor: pointer; }
</style>