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

+308
View File
@@ -0,0 +1,308 @@
// Generated, do not edit. sha256:1f57286ac82ef2aadfb0cdfa809fcb1f771cf74bba815b7e579bbb106a986dd4
import { t } from '../i18n/index.js';
import { listConnections, saveConnection, setActiveConnection, deleteConnection, recordConnectionStatus } from '../data/connections.js';
import { localDateTimeNl } from '../../shared/time.js';
import { CONFIG, ENUMS } from '../../shared/config.js';
import { originAllowedByCsp } from '../../shared/csp.js';
import * as dialog from '../ui/dialog.js';
import * as lnd from '../lightning/lnd.js';
import * as market from '../market-service.js';
import { appSetting, setAppSetting } from '../app-settings.js';
import { clearSpotCache } from '../price/providers.js';
import { fmtInt } from '../ui/format.js';
import { esc } from '../ui/escape.js';
import { locked } from '../license.js';
import { serviceState } from '../data/integration-log.js';
import { lastFailureHtml, openFailureLog } from '../integrations/failure-log.js';
const KINDS = ['btc_explorer', 'lnd'];
const KIND_META = {
btc_explorer: { label: 'connections.kindBtcExplorer', usedBy: 'connections.usedByBtcExplorer' },
lnd: { label: 'connections.kindLnd', usedBy: 'connections.usedByLnd' },
};
const explorerUrlsAtLoad = Promise.all([listConnections('btc_explorer')])
.then(([rows]) => rows.map(c => c.url)).catch(() => []);
const liveResult = new Map();
const fadeTimers = new Map();
function clearFade(kind) {
clearTimeout(fadeTimers.get(kind));
fadeTimers.delete(kind);
}
function scheduleFade(el, kind) {
clearFade(kind);
fadeTimers.set(kind, setTimeout(() => {
if (!isOnsPaneel(el)) { liveResult.delete(kind); return; }
const host = el.querySelector(`[data-result="${kind}"]`);
if (host) {
host.style.transitionDuration = `${CONFIG.CONNECTIONS.RESULT_FADE_MS}ms`;
host.classList.add('is-fading');
}
fadeTimers.set(kind, setTimeout(() => {
liveResult.delete(kind);
if (isOnsPaneel(el)) paintResult(el, kind);
}, CONFIG.CONNECTIONS.RESULT_FADE_MS));
}, CONFIG.CONNECTIONS.RESULT_HOLD_MS));
}
async function testConnection(conn) {
if (conn.kind === 'btc_explorer') {
if (!originAllowedByCsp(conn.url, location.origin, await explorerUrlsAtLoad)) {
throw new Error(t('connections.cspBlocked'));
}
const ctl = new AbortController();
const timer = setTimeout(() => ctl.abort(), CONFIG.CONNECTIONS.TEST_TIMEOUT_MS);
try {
const res = await fetch(`${conn.url}/api/blocks/tip/height`, { signal: ctl.signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const height = Number((await res.text()).trim());
if (!Number.isFinite(height) || height <= 0) throw new Error(t('connections.badResponse'));
return `${t('connections.blockHeight')} ${fmtInt(height)}`;
} finally { clearTimeout(timer); }
}
if (conn.kind === 'lnd') {
const r = await lnd.testLnd(conn.id);
const sync = t(r.synced ? 'connections.lndSynced' : 'connections.lndNotSynced');
return `${r.alias ? `${esc(r.alias)} · ` : ''}${sync} · ${t('connections.blockHeight')} ${fmtInt(r.blockHeight)}`;
}
throw new Error(t('connections.testUnsupported'));
}
async function runTest(el, conn, { footer = false } = {}) {
if (footer) { clearFade(conn.kind); liveResult.set(conn.kind, { pending: true }); paintResult(el, conn.kind); }
try {
const detail = await testConnection(conn);
await recordConnectionStatus(conn.id, true);
if (footer) liveResult.set(conn.kind, { ok: true, text: `${esc(conn.name)}: ${t('connections.statusOk')} · ${detail}` });
} catch (e) {
await recordConnectionStatus(conn.id, false);
if (footer) liveResult.set(conn.kind, { ok: false, text: `${esc(conn.name)}: ${t('connections.statusFail')}: ${esc(e.message)}` });
}
if (!isOnsPaneel(el)) return;
draw(el);
if (footer) scheduleFade(el, conn.kind);
}
function paintResult(el, kind) {
const host = el.querySelector(`[data-result="${kind}"]`);
if (!host) return;
host.style.transitionDuration = '';
const r = liveResult.get(kind);
if (!r) { host.className = 'conn-result'; host.innerHTML = ''; return; }
if (r.pending) { host.className = 'conn-result'; host.textContent = t('connections.testing'); return; }
host.className = `conn-result ${r.ok ? 'is-ok' : 'is-fail'}`;
host.innerHTML = r.text;
}
function dotHtml(c) {
const cls = c.lastStatus === 'ok' ? ' dot-ok' : c.lastStatus === 'fail' ? ' dot-fail' : '';
const label = c.lastStatus === 'ok' ? t('connections.statusOk')
: c.lastStatus === 'fail' ? t('connections.statusFail') : t('connections.notTested');
const when = c.lastCheckedAt ? ` · ${t('connections.lastChecked')} ${localDateTimeNl(c.lastCheckedAt)}` : '';
return `<span class="connp-dot${cls}" title="${esc(label + when)}"></span>`;
}
function tileHtml(c) {
const hover = c.isSystem ? '' : `<span class="connp-hover">
<button type="button" class="connp-mini" data-edit="${c.id}" title="${esc(t('connections.edit'))}">✎</button>
<button type="button" class="connp-mini" data-del="${c.id}" title="${esc(t('connections.delete'))}">✕</button>
</span>`;
return `<div class="connp${c.isActive ? ' is-active' : ''}" role="button" tabindex="0" data-select="${c.id}">
${dotHtml(c)}
<div class="connp-name">${esc(c.name)}</div>
<div class="connp-url" title="${esc(c.url)}">${esc(c.url)}</div>
${hover}
</div>`;
}
const KIND_FEATURE = Object.freeze({ btc_explorer: 'ownMempool', lnd: 'lightning', nwc: 'lightning' });
async function frameHtml(kind) {
const list = await listConnections(kind);
const opSlot = locked(KIND_FEATURE[kind]);
if (opSlot && !list.length) return '';
const tiles = list.map(tileHtml).join('');
return `<div class="card conn-frame" data-kind="${kind}">
<div class="t-label src-drawer-title">${t(KIND_META[kind].label)}</div>
<div class="t-body-sm t-muted conn-usedby">${t(KIND_META[kind].usedBy)}</div>
<div class="connp-row">${tiles}</div>
<div class="conn-foot">
${opSlot ? '' : `<button class="btn btn-ghost btn-md" data-new="${kind}" title="${esc(t('connections.newProfile'))}">+</button>`}
<button class="btn btn-primary btn-md" data-test-active="${kind}">${t('connections.test')}</button>
<div class="conn-result" data-result="${kind}"></div>
</div>
</div>`;
}
const MARKET = 'market';
let marketStatus = null;
function tokenLine(st) {
if (st.token_source === 'license') return t('connections.marketTokenLicense', { tier: t(`license.level_${st.tier}`) });
if (st.token_source === 'manual') return t('connections.marketTokenManual');
return t(st.can_register ? 'connections.marketTokenPending' : 'connections.marketTokenNoLicense');
}
async function marketFrameHtml() {
const st = await market.status();
let shown = marketStatus;
if (!shown && st.token_set) {
try {
const s = await serviceState();
if (s.ok != null) shown = { ok: s.ok, at: Date.parse(s.at) };
} catch { }
}
const cls = !st.token_set || !shown ? '' : shown.ok ? ' dot-ok' : ' dot-fail';
const label = !st.token_set ? t('connections.marketTokenUnset')
: !shown ? t('connections.notTested') : t(shown.ok ? 'connections.statusOk' : 'connections.statusFail');
const when = shown ? ` · ${t('connections.lastChecked')} ${localDateTimeNl(new Date(shown.at).toISOString())}` : '';
const lastFailure = st.token_set ? await lastFailureHtml('service').catch(() => '') : '';
const url = st.override ? `${st.url} (${t('connections.marketOverrideUrl')})` : '';
const stand = [tokenLine(st), t('connections.marketSpotShort_' + appSetting('market_spot'))].join(' · ');
return `<div class="card conn-frame" data-kind="${MARKET}">
<div class="t-label src-drawer-title">${t('connections.kindMarket')}</div>
<div class="t-body-sm t-muted conn-usedby">${t('connections.usedByMarket')}</div>
<div class="connp-row">
<div class="connp${st.token_set ? ' is-active' : ''}" role="button" tabindex="0" data-market-edit title="${esc(t('connections.marketConfigure'))}">
<span class="connp-dot${cls}" title="${esc(label + when)}"></span>
<div class="connp-name">Atreides Services</div>
${url ? `<div class="connp-url" title="${esc(url)}">${esc(url)}</div>` : ''}
</div>
</div>
<div class="t-body-sm t-muted">${esc(stand)}</div>
${lastFailure}
<div class="conn-foot">
<button class="btn btn-primary btn-md" data-market-test>${t('connections.test')}</button>
<div class="conn-result" data-result="${MARKET}"></div>
</div>
</div>`;
}
async function runMarketTest(el, { footer = false } = {}) {
if (footer) { clearFade(MARKET); liveResult.set(MARKET, { pending: true }); paintResult(el, MARKET); }
try {
const r = await market.test();
marketStatus = { ok: true, at: Date.now() };
const detail = t('connections.marketTestOk', { app: r.app, version: r.version || '', n: fmtInt(r.instruments) });
if (footer) liveResult.set(MARKET, { ok: true, text: `${t('connections.statusOk')} · ${esc(detail)}` });
} catch (e) {
marketStatus = { ok: false, at: Date.now() };
if (footer) liveResult.set(MARKET, { ok: false, text: `${t('connections.statusFail')}: ${esc(e.message)}` });
}
if (!isOnsPaneel(el)) return;
await draw(el);
if (footer) scheduleFade(el, MARKET);
}
async function marketDialog(el) {
const res = await dialog.form({
title: t('connections.marketDialogTitle'),
confirmLabel: t('connections.save'),
fields: [
{ key: 'spot', label: t('connections.marketSpot'), type: 'select', value: appSetting('market_spot'),
options: ENUMS.MARKET_SPOT.map(v => ({ value: v, label: t('connections.marketSpot_' + v) })) },
],
});
if (!res) return;
if (res.spot && res.spot !== appSetting('market_spot')) {
await setAppSetting('market_spot', res.spot);
clearSpotCache();
}
await draw(el);
runMarketTest(el, { footer: true });
}
const byId = async id => (await listConnections()).find(c => c.id === Number(id));
const activeOf = async kind => (await listConnections(kind)).find(c => c.isActive);
async function profileDialog(el, kind, c = null) {
const fields = [
{ key: 'name', label: t('connections.name'), value: c ? c.name : '' },
{ key: 'url', label: t(kind === 'lnd' ? 'connections.lndUrl' : 'connections.url'),
value: c ? c.url : '', placeholder: kind === 'lnd' ? '10.8.0.1:8080' : 'https://…' },
];
if (kind === 'lnd') {
const keep = c ? t('connections.secretKeepHint') : '';
fields.push(
{ key: 'macaroon', label: t('connections.lndMacaroon'), type: 'password', placeholder: keep || '0201036c6e64…' },
{ key: 'tls_cert', label: t('connections.lndTlsCert'), type: 'textarea', placeholder: keep || '-----BEGIN CERTIFICATE-----' },
);
}
const res = await dialog.form({
title: c ? t('connections.editProfile') : t('connections.newProfile'),
confirmLabel: t('connections.save'),
fields,
});
if (!res) return;
if (kind === 'lnd' && res.url && !/^[a-z]+:\/\//i.test(res.url.trim())) res.url = `https://${res.url.trim()}`;
const id = await saveConnection({ id: c ? c.id : null, kind, name: res.name, url: res.url });
if (kind === 'lnd') await lnd.setConfig(id, { url: res.url, macaroon: res.macaroon, tls_cert: res.tls_cert });
draw(el);
}
const isOnsPaneel = (el) => !!el && !!el.querySelector('[data-result]');
async function draw(el) {
const [marketFrame, frames] = await Promise.all([marketFrameHtml(), Promise.all(KINDS.map(frameHtml))]);
el.innerHTML = marketFrame + frames.join('');
for (const kind of [MARKET, ...KINDS]) paintResult(el, kind);
el.onclick = async e => {
try {
if (e.target.closest('[data-market-edit]')) { await marketDialog(el); return; }
if (e.target.closest('[data-market-test]')) { await runMarketTest(el, { footer: true }); return; }
const ilog = e.target.closest('[data-ilog-open]');
if (ilog) { await openFailureLog(ilog.dataset.ilogOpen); return; }
const edit = e.target.closest('[data-edit]');
if (edit) { const c = await byId(edit.dataset.edit); if (c) await profileDialog(el, c.kind, c); return; }
const del = e.target.closest('[data-del]');
if (del) {
const c = await byId(del.dataset.del);
if (!c) return;
if (!(await dialog.confirm({ message: t('connections.confirmDelete'), danger: true }))) return;
if (c.kind === 'lnd') await lnd.purgeConfig(c.id);
await deleteConnection(c.id);
draw(el);
if (c.isActive) { const a = await activeOf(c.kind); if (a) runTest(el, a); }
return;
}
const nieuw = e.target.closest('[data-new]');
if (nieuw) {
await profileDialog(el, nieuw.dataset.new);
return;
}
const testBtn = e.target.closest('[data-test-active]');
if (testBtn) { const a = await activeOf(testBtn.dataset.testActive); if (a) await runTest(el, a, { footer: true }); return; }
const tile = e.target.closest('[data-select]');
if (tile) {
const c = await byId(tile.dataset.select);
if (!c || c.isActive) return;
await setActiveConnection(c.id);
draw(el);
runTest(el, c);
return;
}
} catch (err) { dialog.alert({ title: t('dialog.error'), message: err.message }); }
};
el.onkeydown = e => {
if ((e.key === 'Enter' || e.key === ' ') && e.target.matches && e.target.matches('[data-select], [data-market-edit]')) {
e.preventDefault();
e.target.click();
}
};
}
export async function render(el) {
for (const kind of [MARKET, ...KINDS]) clearFade(kind);
liveResult.clear();
await draw(el);
const st = await market.status();
if (st.token_set || st.can_register) runMarketTest(el);
for (const kind of KINDS) {
for (const c of await listConnections(kind)) runTest(el, c);
}
}