Versie 0.2.0

This commit is contained in:
Atreides Assets committed 2026-10-29 11:57:29 +01:00
commit e77d8fd373
495 files changed
+81026

No files matched your search

+241
View File
@@ -0,0 +1,241 @@
// Generated, do not edit. sha256:7498a98f290e59aca597eb4a57338d467525bf6c2933429d69f5e8474930a0ac
import { aiProposalsStatus, acceptAiProposal, acceptAiProposals, rejectAiProposal, rejectAiProposals }
from '../data/ai-proposals.js';
import { CONFIG, PREFERENCES } from '../../shared/config.js';
import { appSetting, timerSetting, loadAppSettings } from '../app-settings.js';
import { t } from '../i18n/index.js';
import { assetLabelHtml } from '../ui/asset-label.js';
import * as dialog from '../ui/dialog.js';
import { esc } from '../ui/escape.js';
import { fmtBase, fmtBasePrice, fmtUnit, priceFromBase, val, currencyName } from '../ui/format.js';
import { taskCardHtml, nothingPendingHtml } from '../ui/task-card.js';
import { priceUnitLabel } from '../../shared/units.js';
import { localDateNl } from '../../shared/time.js';
const ONOMKEERBAAR = new Set(CONFIG.AI.DESTRUCTIVE_PROPOSAL_KINDS);
const assetVan = p => ({ asset_name: p.asset_name, asset_symbol: p.asset_symbol });
function detailTekst(detail) {
if (typeof detail !== 'string' || !detail.startsWith('{')) return detail;
try {
const o = JSON.parse(detail);
return o && o.key ? t(o.key, o.params) : detail;
} catch { return detail; }
}
const meta = p => ({
kind: p.asset_kind, decimals: p.asset_decimals, unit: p.asset_unit,
subunit: p.asset_subunit, displayUnit: p.asset_display_unit,
});
const eenheidIsSymbool = p => {
const asset = meta(p);
const getoond = asset.displayUnit === 'subunit' && asset.subunit ? asset.subunit : asset.unit;
return String(getoond || '').toUpperCase() === String(p.asset_symbol || '').toUpperCase();
};
const koers = p => {
const asset = meta(p);
return `${fmtBasePrice(priceFromBase(p.fiatPrice, asset))} ${t('aiProposals.perUnit')} ${priceUnitLabel(asset)}`;
};
const leesbareWaarde = (p, json) => {
try { return toon(p, JSON.parse(json)); }
catch { return String(json); }
};
const toon = (p, v) => {
const spec = PREFERENCES[p.scope]?.[p.key];
if (spec?.type === 'bool') return t(Number(v) ? 'aiProposals.preferenceOn' : 'aiProposals.preferenceOff');
return String(v);
};
const huidigeVoorkeur = (p) => {
try {
const v = p.scope === 'timers' ? timerSetting(p.key) : appSetting(p.key);
return v == null ? null : toon(p, v);
} catch { return null; }
};
const OMSCHRIJVING = {
price_point: p =>
`${t('aiProposals.kindPricePoint')}: ${assetLabelHtml(assetVan(p))} · ${val(koers(p))} `
+ `<span class="t-muted-cell">${esc(localDateNl(p.observedAt))}</span>`,
account: p =>
`${t('aiProposals.kindAccount')}: <strong>${esc(p.name)}</strong> `
+ `<span class="t-muted-cell">${esc(t('enums.account_kind.' + p.accountKind))} · `
+ `${esc(p.isOwnCustody ? t('aiProposals.ownCustody') : t('aiProposals.thirdParty'))}</span>`,
account_delete: p =>
`${t('aiProposals.kindAccountDelete')}: <strong>${esc(p.account_name)}</strong> `
+ `<span class="t-muted-cell">${esc(t('enums.account_kind.' + p.account_kind))}</span>`,
portfolio: p =>
`${t('aiProposals.kindPortfolio')}: <strong>${esc(p.name)}</strong> `
+ `<span class="t-muted-cell">${esc(t('aiProposals.portfolioIn'))} ${esc(currencyName(p.currency))} (${esc(p.currency)})</span><br>`
+ `<span class="t-muted-cell">${esc(t('aiProposals.portfolioNote'))}</span>`,
preference: p => {
const nu = huidigeVoorkeur(p);
return `${t('aiProposals.kindPreference')}: <strong>${esc(p.scope)}.${esc(p.key)}</strong> `
+ `→ ${val(leesbareWaarde(p, p.value))}`
+ (nu === null ? '' : ` <span class="t-muted-cell">(${esc(t('aiProposals.preferenceCurrent'))} ${esc(nu)})</span>`)
+ `<br><span class="t-muted-cell">${esc(t('aiProposals.preferenceNote'))}</span>`;
},
source_choice: p =>
`${t('aiProposals.kindSourceChoice')}: ${assetLabelHtml(assetVan(p))} · `
+ `${esc(t('integrations.sourceCap.' + p.scope))} · <strong>${esc(p.provider_name || p.key)}</strong> `
+ `${esc(t(p.value === 'first' ? 'aiProposals.sourceFirst' : 'aiProposals.sourceRemove'))}<br>`
+ `<span class="t-muted-cell">${esc(t('aiProposals.sourceChoiceNote'))}</span>`,
valuation: p => {
const asset = meta(p);
const grootboek = p.ledger_balance == null ? null : Number(p.ledger_balance);
const verschil = grootboek == null ? null : Number(p.balance) - grootboek;
const naast = grootboek == null ? '' :
` · ${t('aiProposals.ledger')} ${val(fmtUnit(grootboek, asset))}`
+ (verschil === 0 ? '' : ` · ${t('aiProposals.diff')} ${verschil > 0 ? '+' : '−'}${val(fmtUnit(Math.abs(verschil), asset))}`);
return `${t('aiProposals.kindValuation')}: ${val(fmtUnit(p.balance, asset))} `
+ `${assetLabelHtml(assetVan(p), { withoutSymbol: eenheidIsSymbool(p) })} `
+ `· <strong>${esc(p.account_name || '')}</strong>${naast}<br>`
+ `<span class="t-muted-cell">${esc(localDateNl(p.observedAt))}</span>`;
},
event: p => {
const waar = [p.from_name, p.to_name].filter(Boolean).map(esc).join(' → ');
const prijs = p.fiatPrice == null ? '' : ` · ${val(koers(p))}`;
const bedragen = [
p.fiatFee == null ? null : val(fmtBase(p.fiatFee)),
p.unitFee == null ? null : val(fmtUnit(p.unitFee, meta(p))),
].filter(Boolean);
const kosten = bedragen.length ? ` · ${t('aiProposals.fee')} ${bedragen.join(' + ')}` : '';
return `${esc(t('enums.event_type.' + p.eventType))}: ${val(fmtUnit(p.amount, meta(p)))} `
+ `${assetLabelHtml(assetVan(p), { withoutSymbol: eenheidIsSymbool(p) })}${prijs}${kosten}<br>`
+ `<span class="t-muted-cell">${waar} · ${esc(localDateNl(p.observedAt))}</span>`;
},
};
function regel(p) {
const maak = OMSCHRIJVING[p.kind];
const tekst = maak ? maak(p) : esc(t('aiProposals.unknownKind') + ` (${p.kind})`);
const notitie = p.note ? `<div class="t-body-sm t-muted">${esc(p.note)}</div>` : '';
const mislukt = p.detail ? `<div class="t-body-sm t-neg">${esc(t('aiProposals.failed'))}: ${esc(detailTekst(p.detail))}</div>` : '';
const weg = ONOMKEERBAAR.has(p.kind);
const waarschuwing = weg
? `<div class="t-body-sm t-neg">${esc(t('aiProposals.irreversible'))}</div>` : '';
const opening = p.kind === 'valuation' && p.events_before === 0
? `<div class="t-body-sm t-neg">${esc(t('aiProposals.openingBalance'))}</div>` : '';
return `<div class="rec-item" data-aiprop="${p.id}" data-aidanger="${weg ? 1 : 0}" data-aikind="${esc(p.kind)}">
<div class="rec-item-row">
<div class="rec-item-meta">${tekst}${notitie}${waarschuwing}${opening}${mislukt}</div>
<div class="row-actions rec-hover">
<button class="btn ${weg ? 'btn-danger' : 'btn-primary'} btn-xs" data-aiact="accept">${weg ? t('aiProposals.acceptDelete') : t('aiProposals.accept')}</button>
<button class="btn btn-ghost btn-xs" data-aiact="reject">${t('aiProposals.reject')}</button>
</div>
</div>
</div>`;
}
function redenTekst(u) {
if (!u.key) return u.reden;
try { return t(u.key, u.params); } catch { return u.reden; }
}
async function meldMislukt(uitkomsten) {
const stuk = uitkomsten.filter(u => !u.ok);
if (!stuk.length) return;
await dialog.alert({
title: t('aiProposals.partialTitle'),
message: `${t('aiProposals.partialBody')}\n\n${stuk.map(u => `- ${redenTekst(u)}`).join('\n')}`,
});
}
let live = null;
let liveGebonden = false;
function bindLive() {
if (liveGebonden) return;
liveGebonden = true;
document.addEventListener('ai-proposals-changed', async () => {
if (!live || !live.host || !live.host.isConnected) { live = null; return; }
try { await renderAiProposals(live.host, live.refresh); } catch { }
});
}
export async function renderAiProposals(host, refresh) {
live = { host, refresh };
bindLive();
const { enabled, pending } = await aiProposalsStatus();
if (!enabled) { host.innerHTML = ''; return null; }
if (!pending.length) {
host.innerHTML = taskCardHtml({ title: t('aiProposals.title'), body: nothingPendingHtml() });
return 0;
}
const teVerwijderen = pending.filter(p => ONOMKEERBAAR.has(p.kind)).length;
const bulkUitleg = teVerwijderen
? `<div class="t-body-sm t-neg" style="margin-bottom:0.6rem">${esc(t('aiProposals.bulkSkips').replace('{n}', teVerwijderen))}</div>`
: '';
host.innerHTML = taskCardHtml({
title: t('aiProposals.title'),
actions: `<div class="row-actions">
<button class="btn btn-secondary btn-xs" id="aiprop-accept-all">${t('aiProposals.acceptAll')}</button>
<button class="btn btn-ghost btn-xs" id="aiprop-reject-all">${t('aiProposals.rejectAll')}</button>
</div>`,
body: `<div class="t-body-sm t-muted" style="margin-bottom:0.6rem">${t('aiProposals.intro')}</div>
${bulkUitleg}
${pending.map(regel).join('')}`,
});
const ids = pending.filter(p => !ONOMKEERBAAR.has(p.kind)).map(p => p.id);
const alleIds = pending.map(p => p.id);
host.querySelector('#aiprop-accept-all').onclick = async () => {
if (!ids.length) {
await dialog.alert({ title: t('aiProposals.title'), message: t('aiProposals.bulkNothing') });
return;
}
if (!(await dialog.confirm({ message: t('aiProposals.acceptAllConfirm').replace('{n}', ids.length) }))) return;
await meldMislukt(await acceptAiProposals(ids));
if (pending.some(p => ids.includes(p.id) && p.kind === 'preference')) {
await loadAppSettings();
location.reload();
return;
}
refresh();
};
host.querySelector('#aiprop-reject-all').onclick = async () => {
if (!(await dialog.confirm({ message: t('aiProposals.rejectAllConfirm').replace('{n}', alleIds.length), danger: true }))) return;
await meldMislukt(await rejectAiProposals(alleIds, t('aiProposals.rejectedByUser')));
refresh();
};
host.querySelectorAll('[data-aiprop]').forEach(rij => {
const id = Number(rij.dataset.aiprop);
rij.querySelectorAll('[data-aiact]').forEach(knop => knop.onclick = async () => {
try {
if (knop.dataset.aiact === 'accept') {
if (rij.dataset.aidanger === '1'
&& !(await dialog.confirm({ message: t('aiProposals.acceptDeleteConfirm'), danger: true }))) return;
await acceptAiProposal(id);
if (rij.dataset.aikind === 'preference') {
await loadAppSettings();
location.reload();
return;
}
} else await rejectAiProposal(id, t('aiProposals.rejectedByUser'));
} catch (e) {
await dialog.alert({ title: t('dialog.error'), message: e.message });
}
refresh();
});
});
return pending.length;
}