Versie 0.3.3

This commit is contained in:
Atreides Assets committed 2026-10-30 12:28:48 +01:00
1 parent 1cf6eeed29
commit 6e741a51fe
57 files changed
+1362 -2135

No files matched your search

+2 -2
View File
@@ -1,4 +1,4 @@
// Generated, do not edit. sha256:88f1ff40757f8e5095efe909c3a45c2f437406121459c4091bd0b4d3c80eac8c
// Generated, do not edit. sha256:69057ab8eda9ab21e21cc877b9070be1e08c5b047758e64ec4c5cd772fa0ac07
import { localDate } from '../../shared/time.js';
const DAY_MS = 86400000;
@@ -53,7 +53,7 @@ export function heartbeat(items, todayIso, fromIso = null) {
const last = Math.max(...byDay.keys(), today);
const first = Math.min(from ?? Math.min(...byDay.keys(), today) - DAY_MS, last - DAY_MS);
const points = [];
for (let x = first; x <= last; x += DAY_MS) {
for (let x = first; x <= last + DAY_MS; x += DAY_MS) {
const day = byDay.get(x) || [];
const up = day.filter(i => i.dir > 0).length;
const down = day.filter(i => i.dir < 0).length;
+3 -1
View File
@@ -1,4 +1,4 @@
// Generated, do not edit. sha256:51b53dc3f01e74bb7f221f45ce7f2b9b6f6a164a12cedc96e33ad4f1b1b701a0
// Generated, do not edit. sha256:a36be83c24f4e2c2e73be5d6705dd33c4759901705776f21398caba72366f10a
import { currentNotifications } from '../data/notifications.js';
import { countAiToolCallsSince } from '../data/ai-audit.js';
import { getPref } from '../data/preferences.js';
@@ -12,6 +12,7 @@ import { CONFIG, TARGETS, NOTIFICATION_TARGETS, SOURCE_ENDPOINTS, targetHash, ta
import { registerAttention } from './attention.js';
import { appSetting, baseCurrency } from '../app-settings.js';
import { licenseChangedSinceSeen } from '../license.js';
import { placeAvailable } from '../routes.js';
async function integrationsNeedingKey() {
const rijen = await listProviders();
@@ -96,6 +97,7 @@ if (typeof document !== 'undefined') {
document.addEventListener('price-gaps-changed', () => { gapsCache = { at: 0, total: 0 }; });
}
async function priceGaps() {
if (!placeAvailable(TARGETS.PRICE_HISTORY)) return [];
if (Date.now() - gapsCache.at > GAPS_TTL_MS) {
try { gapsCache = { at: Date.now(), total: (await listPriceGaps()).total }; }
catch { return []; }
+10 -1
View File
@@ -1,4 +1,4 @@
// Generated, do not edit. sha256:3f4bf112e28f009a738272dd18bef14d5e6736a31c2f3e1f6e434733f909d6ea
// Generated, do not edit. sha256:93e679e11f5329a380a996f2d99d603c26410a0b331dfefcb1da78dc2058cc17
import { appSetting, setAppSetting } from '../app-settings.js';
import { t } from '../i18n/index.js';
import { EYE, EYE_OFF } from './icons.js';
@@ -37,6 +37,14 @@ async function setOn(on) {
function onActivity() { if (!isOn()) armIdle(); }
function onSpace(e) {
if (e.key !== ' ' || e.repeat || e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) return;
if (e.target !== document.body && e.target !== document.documentElement) return;
if (document.querySelector('.nav-edit-toggle.is-on')) return;
e.preventDefault();
setOn(!isOn());
}
export function initPrivacyBlur(hostEl) {
btn = document.createElement('button');
btn.type = 'button';
@@ -52,5 +60,6 @@ export function initPrivacyBlur(hostEl) {
for (const ev of ['pointerdown', 'keydown', 'wheel', 'touchstart']) {
document.addEventListener(ev, onActivity, { passive: true });
}
document.addEventListener('keydown', onSpace);
document.addEventListener('settings-changed', () => { if (!isOn()) armIdle(); });
}
+17 -1
View File
@@ -1,4 +1,4 @@
// Generated, do not edit. sha256:618e9d10819427320df9cea13ee26eca05db3121f4e69b541d2a8a431e85957a
// Generated, do not edit. sha256:2cb6efb613791caa2b77487d43b44b223a6a6e006b7a374e8ed1b4c6e8f50169
import { esc } from './escape.js';
const MOUNT = 'page-hint';
@@ -47,8 +47,24 @@ function watchFit(mount) {
panel.classList.remove('is-wide');
if (panel.scrollHeight > panel.clientHeight + 1) panel.classList.add('is-wide');
};
const close = () => {
const open = mount.querySelector('.hint.is-open');
open?.classList.remove('is-open');
open?.querySelector('.hint-btn')?.setAttribute('aria-expanded', 'false');
};
mount.addEventListener('pointerenter', fit);
mount.addEventListener('focusin', fit);
mount.addEventListener('click', (e) => {
const btn = e.target.closest?.('.hint-btn');
if (!btn) return;
const hint = btn.closest('.hint');
if (hint.classList.contains('is-open')) { close(); return; }
hint.classList.add('is-open');
btn.setAttribute('aria-expanded', 'true');
fit();
});
document.addEventListener('click', (e) => { if (!mount.contains(e.target)) close(); });
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') close(); });
}
function row(r) {
+3 -1
View File
@@ -1,4 +1,4 @@
// Generated, do not edit. sha256:06627f5d01d37034867aaf9d6b234f888eb4b99f4ecd0e53cb5df7c45bce718a
// Generated, do not edit. sha256:0e4fc635bcc50c839bba37ef206c2c659c8dcc17c03278176cb0be588b69506a
export const CHEVRON = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>';
export const CHEVRON_RIGHT = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 6 15 12 9 18"/></svg>';
@@ -36,4 +36,6 @@ export const CHECK = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none
export const REFRESH = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>';
export const IMAGE = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>';
export const COPY = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
-147
View File
@@ -1,147 +0,0 @@
// Generated, do not edit. sha256:e64fde93e62c826f1ddca6217aab270e6309de4394ce5cb0e258a0e0fdb72d5c
import { t } from '../i18n/index.js';
import { listPriceSource, setPriceSourceRank, clearPriceSourceCell, confirmPriceSourceCell } from '../data/price-source.js';
import { listProviders, getProvider } from '../data/providers.js';
import { rateSourceFor } from '../price/providers.js';
import { serviceCoverage } from '../data/market.js';
import { SOURCE_CAPABILITIES, SOURCE_ENDPOINTS } from '../../shared/config.js';
import { baseCurrency, appSetting } from '../app-settings.js';
import { assetLabelHtml } from './asset-label.js';
import { esc } from './escape.js';
import * as dialog from './dialog.js';
const endpointsUnder = (p, cap) => (p.endpoints || []).filter(e => SOURCE_ENDPOINTS[cap].includes(e.capability));
const answers = (p, cap) => endpointsUnder(p, cap).length > 0;
function quoteOf(p, cap) {
const e = endpointsUnder(p, cap).find(x => x.quoteCurrency && x.quoteCurrency !== baseCurrency());
return e ? e.quoteCurrency : null;
}
const currenciesOf = p => [...new Set(SOURCE_CAPABILITIES.map(c => quoteOf(p, c)).filter(Boolean))];
export async function loadMatrixContext() {
const alle = (await Promise.all((await listProviders()).map(p => getProvider(p.key)))).filter(Boolean);
const providers = alle.filter(p => !p.isSystem);
const cell = new Map();
for (const r of await listPriceSource()) {
if (r.rank == null) continue;
cell.set(`${r.assetId}|${r.capability}|${r.providerId}`,
{ rank: r.rank, proposed: r.status === 'proposed', own: r.status === 'manual' });
}
const rateSource = new Map();
for (const p of providers) {
for (const c of currenciesOf(p)) if (!rateSource.has(c)) rateSource.set(c, await rateSourceFor(c));
}
return { alle, providers, cell, rateSource, service: await serviceContext() };
}
async function serviceContext() {
let mode = 'off';
try { mode = appSetting('market_spot'); } catch { }
if (mode === 'off') return { mode, covered: new Set() };
try {
const r = await serviceCoverage();
return { mode, covered: new Set(r.status === 'ok' ? r.assetIds : []) };
} catch { return { mode, covered: new Set() }; }
}
export function serviceOrder(chosen, mode) {
const byRank = [...chosen].sort((x, y) => x.rank - y.rank);
const order = mode === 'first'
? [...byRank.filter(c => c.own), 'service', ...byRank.filter(c => !c.own)]
: [...byRank, 'service'];
const ranks = new Map();
let service = null;
order.forEach((c, i) => { if (c === 'service') service = i + 1; else ranks.set(c.providerId, i + 1); });
return { ranks, service };
}
const marker = (ctx, v) => `<span class="mx-cur${ctx.rateSource.get(v) ? '' : ' t-neg'}">${esc(v)}</span>`;
const offTitle = p => (p.isActive ? '' : ` title="${esc(t('integrations.inactiveNoEffect'))}"`);
function kopVan(a, stand, title = true) {
if (!title && !stand) return '';
return `<div class="mx-card-head">
${title ? `<div class="wealth-head"><strong>${assetLabelHtml(a)}</strong><span class="badge">${esc(t('enums.asset_kind.' + a.kind))}</span></div>` : ''}
${stand ? `<div class="mx-stand t-caption">${esc(stand)}</div>` : ''}
</div>`;
}
export function matrixCardHtml(a, ctx, { title = true } = {}) {
const cols = ctx.providers.filter(p => (p.coverage || []).includes(a.symbol));
const svc = ctx.service && ctx.service.mode !== 'off' && ctx.service.covered.has(a.id);
if (!cols.length && !svc) {
const systeem = ctx.alle.find(p => p.isSystem && (p.coverage || []).includes(a.symbol));
const tekst = systeem ? esc(t('integrations.systemSource').replace('{naam}', systeem.name)) : esc(t('integrations.noCoverage'));
return `<div class="card mx-card">${kopVan(a, '', title)}<div class="mx-empty">${tekst}</div></div>`;
}
const svcTitle = svc ? esc(t('integrations.mxServiceHint_' + ctx.service.mode)) : '';
const naamKop = `<tr><th class="mx-lab"></th>${cols.map(p =>
`<th class="mx-col${p.isActive ? '' : ' is-off'}" title="${esc(p.isActive ? p.name : `${p.name}: ${t('integrations.inactiveNoEffect')}`)}"><span class="mx-name">${esc(p.name)}</span></th>`).join('')}${
svc ? `<th class="mx-col" title="${svcTitle}"><span class="mx-name">Atreides Services</span></th>` : ''}</tr>`;
const muntKop = `<tr class="mx-cur-row"><th class="mx-lab"></th>${cols.map(p =>
`<th>${currenciesOf(p).map(v => marker(ctx, v)).join(' ')}</th>`).join('')}${svc ? '<th></th>' : ''}</tr>`;
const head = naamKop + muntKop;
const rank = (c, p, shown = c.rank) =>
`<span class="mx-rank mx-rank-${Math.min(shown, 2)}${c.proposed ? ' is-proposed' : ''}${p.isActive ? '' : ' is-off'}"${offTitle(p)}>${shown}</span>`;
let open = 0, voorstellen = 0;
const leeg = [];
const rows = SOURCE_CAPABILITIES.filter(cap => cols.some(p => answers(p, cap)) || (svc && cap === 'spot'));
const body = rows.map(cap => {
const metDienst = svc && cap === 'spot';
const volgorde = metDienst
? serviceOrder(cols.map(p => ({ providerId: p.id, ...ctx.cell.get(`${a.id}|${cap}|${p.id}`) }))
.filter(c => c.rank != null), ctx.service.mode)
: null;
if (!metDienst && !cols.some(p => ctx.cell.has(`${a.id}|${cap}|${p.id}`))) leeg.push(t('integrations.sourceCap.' + cap));
const dienstCel = svc
? (metDienst
? `<td class="mx-svc" title="${svcTitle}"><span class="mx-rank mx-rank-${Math.min(volgorde.service, 2)} is-service">${volgorde.service}</span></td>`
: `<td class="mx-na"></td>`)
: '';
const cells = cols.map(p => {
if (!answers(p, cap)) return `<td class="mx-na"></td>`;
const c = ctx.cell.get(`${a.id}|${cap}|${p.id}`);
const valuta = quoteOf(p, cap);
if (valuta && !ctx.rateSource.get(valuta) && !c) {
return `<td class="mx-na" title="${esc(t('integrations.noRateSourceHint', { valuta }))}"></td>`;
}
if (!c) open++;
else if (c.proposed) voorstellen++;
const inner = c ? rank(c, p, volgorde ? volgorde.ranks.get(p.id) : c.rank) : `<span class="mx-pick">+</span>`;
return `<td class="mx-cell" data-a="${a.id}" data-cap="${cap}" data-p="${p.id}" data-has="${c ? 1 : 0}" data-proposed="${c && c.proposed ? 1 : 0}">${inner}</td>`;
}).join('');
return `<tr><th class="mx-lab">${esc(t('integrations.sourceCap.' + cap))}</th>${cells}${dienstCel}</tr>`;
}).join('');
const stand = [
leeg.length ? t('integrations.mxToSetUp', { wat: leeg.join(' en ') }) : '',
voorstellen ? t(voorstellen === 1 ? 'integrations.mxProposalsOne' : 'integrations.mxProposalsMany', { n: voorstellen }) : '',
open ? t(open === 1 ? 'integrations.mxOpenOne' : 'integrations.mxOpenMany', { n: open }) : t('integrations.mxAllChosen'),
].filter(Boolean).join(' · ');
return `<div class="card mx-card">${kopVan(a, stand.charAt(0).toUpperCase() + stand.slice(1), title)}<div class="mx-wrap"><table class="mx-table"><thead>${head}</thead><tbody>${body}</tbody></table></div></div>`;
}
export function wireMatrix(host, redraw) {
host.querySelectorAll('.mx-cell').forEach(td => td.onclick = async () => {
const d = td.dataset;
const a = Number(d.a), p = Number(d.p);
try {
if (d.has === '1' && d.proposed === '1') await confirmPriceSourceCell(a, d.cap, p);
else if (d.has === '1') await clearPriceSourceCell(a, d.cap, p);
else await setPriceSourceRank(a, d.cap, p);
document.dispatchEvent(new CustomEvent('providers-changed'));
} catch (e) { dialog.alert({ title: t('dialog.error'), message: e.message }); }
await redraw();
});
}
export function matrixLegendRows(ctx) {
const uit = ctx.providers.some(p => !p.isActive)
? [{ sample: '<span class="mx-rank mx-rank-1 is-off" style="--mx-rank:1.1rem">1</span>', text: esc(t('integrations.inactiveHint')) }]
: [];
const munten = [...ctx.rateSource].map(([v, via]) => ({
sample: marker(ctx, v),
text: esc(via ? t('integrations.convertedVia', { naam: via.name, valuta: v, munt: baseCurrency() }) : t('integrations.noRateSource', { valuta: v })),
}));
return [...uit, ...munten];
}