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

+114
View File
@@ -0,0 +1,114 @@
// Generated, do not edit. sha256:958c567a3f8a5513bb292a105e1d9e3838bf252c71b7a83188de3f33183e7f66
import { DatabaseSync } from 'node:sqlite';
import { SCHEMA as BASELINE_SCHEMA } from '../../server/db/schema.js';
import { CONFIG } from '../../shared/config.js';
const NU = '2026-08-12T10:00:00.000Z';
export function openBaselineDb() {
const db = new DatabaseSync(':memory:', { enableForeignKeyConstraints: true });
db.exec(`PRAGMA foreign_keys = ON`);
db.exec(BASELINE_SCHEMA);
return db;
}
export function foreignKeysAan(db) {
return db.prepare(`PRAGMA foreign_keys`).get().foreign_keys === 1;
}
const FIXTURE = [
[`Asset`, `(symbol, name, searchName, description, unit, subunit, displayUnit, decimals, kind)
VALUES ('BTC','Bitcoin','bitcoin',NULL,'BTC','sat','subunit',8,'crypto')`],
[`Account`, `(name, searchName, description, kind, isOwnCustody, scanEnabled, isActive, closedAt)
VALUES ('Kluis','kluis',NULL,'wallet',1,0,1,NULL)`],
[`Label`, `(name, searchName, description) VALUES ('Werk','werk',NULL)`],
[`MetalProduct`, `(assetId, name, searchName, description, fineGrams, form)
VALUES (1,'Baar 1 kg','baar 1 kg',NULL,10000000000,'bar')`],
[`Provider`, `(key, name, searchName, description, baseUrl, authenticationHeaderName, authenticationParameterName,
authenticationKind, authenticationRequired, isActive, isSeed, isSystem, createdAt)
VALUES ('mempool','mempool','mempool',NULL,'https://voorbeeld.test',NULL,NULL,'none',0,1,1,0,'${NU}')`],
[`ProviderAuthenticationHmac`, `(providerId, messageParts, keyHeader, signatureHeader, timestampHeader,
windowHeader, windowMs, hashAlgorithm, encoding)
VALUES (1,'timestamp,method','K','S','T',NULL,NULL,'SHA-256','hex')`],
[`ProviderEndpoint`, `(providerId, pathTemplate, queryTemplate, capability, method)
VALUES (1,'/api/prijs',NULL,'spot','GET')`],
[`ProviderFixedRate`, `(providerId, fiatPrice) VALUES (1,1.0)`],
[`ProviderCallLimit`, `(providerId, callsPerMonth, createdAt) VALUES (1,100,'${NU}')`],
[`ProviderInputMap`, `(providerId, symbol, externalId, isCatchAll) VALUES (1,'XAU','gold',0)`],
[`ProviderOutputMap`, `(providerId, jsonPath, capability, internalField, transform)
VALUES (1,'EUR','spot','price','none')`],
[`ProviderCoverage`, `(providerId, symbol) VALUES (1,'BTC')`],
[`ApiCallMetric`, `(providerId, capability, bucketMinute, okCount, failCount, sumLatencyMs, maxLatencyMs)
VALUES (1,'spot','2026-08-12T10:00',1,0,120,120)`],
[`Connection`, `(name, searchName, description, url, kind, isActive, isSystem, lastStatus,
lastCheckedAt, createdAt, updatedAt)
VALUES ('Self-hosted','self-hosted',NULL,'https://knoop.test','btc_explorer',1,0,NULL,NULL,'${NU}','${NU}')`],
[`WalletSource`, `(accountId, connectionId, value, name, kind, scriptType, createdAt)
VALUES (1,NULL,'xpub6voorbeeld','Hoofdbron','xpub','p2tr','${NU}')`],
[`WalletAddress`, `(accountId, sourceId, address, automaticLabel, chainIndex, derivationIndex,
balanceSats, transactionCount, lastScannedAt)
VALUES (1,1,'bc1qvoorbeeld',NULL,0,0,1000,2,'${NU}')`],
[`WalletTransaction`, `(accountId, sourceId, labelId, layer, txid, address, valueSats, feeSats, blockHeight,
blockTime, direction, createdAt)
VALUES (1,1,1,'onchain','aabbcc','bc1qvoorbeeld',1000,10,900000,'${NU}','in','${NU}')`],
[`Event`, `(assetId, fromAccountId, toAccountId, productId, productCount, importReference, externalAddress,
note, amount, fiatPrice, fiatFee, unitFee, type, source, occurredAt, createdAt)
VALUES (1,1,1,1,1,NULL,NULL,NULL,10000000000,50000.0,NULL,NULL,'buy','manual','${NU}','${NU}')`],
[`Valuation`, `(assetId, accountId, balance, fiatRate, source, valuedOn, rateAt)
VALUES (1,1,1000,50000.0,'manual','2026-08-12','${NU}')`],
[`ScanLog`, `(accountId, addressesFound, balanceFound, proposalsCreated, rawData, status, scannedAt)
VALUES (1,2,1000,0,NULL,'success','${NU}')`],
[`ReconcileProposal`, `(accountId, assetId, walletTransactionId, resolvedEventId, transactionHash, amount,
direction, suggestedType, status, detectedAt)
VALUES (1,1,1,1,'aabbcc',1000,'in','transfer','pending','${NU}')`],
[`WalletTransactionEventLink`, `(walletTransactionId, eventId, method, createdAt) VALUES (1,1,'exact','${NU}')`],
[`ImportBatch`, `(accountId, filename, rowCount, rawData, origin, importedAt)
VALUES (1,'export.csv',3,NULL,'Bitvavo','${NU}')`],
[`PriceHistory`, `(assetId, providerId, fiatPrice, resolution, observedAt, updatedAt)
VALUES (1,1,50000.0,'day','${NU}','${NU}')`],
[`InterestSchedule`, `(accountId, principalAssetId, payoutAssetId, note, principalAmount, annualRate,
rateKind, creditCadence, creditWeekday, creditDayOfMonth, startsOn, endsOn, compounding, createdAt)
VALUES (1,1,1,NULL,100000,0.05,'fixed','weekly',1,NULL,'2026-01-01',NULL,0,'${NU}')`],
[`InterestPeriod`, `(scheduleId, startsOn, annualRate)
VALUES (1,'2026-04-01',0.037)`],
[`InterestCredit`, `(scheduleId, eventId, status, periodStart, periodEnd, createdAt)
VALUES (1,1,'booked','2026-08-01','2026-08-08','${NU}')`],
[`Notification`, `(count, highWater, kind, dismissedAt, createdAt)
VALUES (1,3,'reconcile',NULL,'${NU}')`],
[`PriceSource`, `(assetId, providerId, capability, rank, status)
VALUES (1,1,'spot',1,'confirmed')`],
[`DashboardProfile`, `(name, searchName, description, sortOrder, isActive, isSystem, createdAt, updatedAt)
VALUES ('Standaard','standaard',NULL,0,1,1,'${NU}','${NU}')`],
[`DashboardWidget`, `(profileId, assetId, instanceKey, gridX, gridY, spanWidth, spanHeight, type, settings)
VALUES (1,1,'spot:1',0,0,2,2,'spot',NULL)`],
[`Preference`, `(scope, key, value) VALUES ('ui','theme','"dark"')`],
[`AiToolCall`, `(sessionId, client, tool, arguments, fieldsShared, charactersOut, outcome, detail, occurredAt)
VALUES ('11111111-2222-3333-4444-555555555555','voorbeeld-client','connection_test','{}','[]',42,'ok',NULL,'${NU}')`],
[`AiToolCallMessage`, `(callId, kind, text, occurredAt)
VALUES (1,'request','{"note":"hallo"}','${NU}')`],
[`BusSignal`, `(name, occurredAt) VALUES ('providers-changed','${NU}')`],
[`Command`, `(kind, payload, state, expiresAt, createdAt)
VALUES ('navigate','{"route":"system","tab":"ai"}','open','${NU}','${NU}')`],
[`ChangeLog`, `(tableName, rowId, operation, origin, originId, changes, occurredAt)
VALUES ('Account',1,'update','manual',NULL,'{"name":["Oude naam","Nieuwe naam"]}','${NU}')`],
[`AiProposal`, `(kind, assetId, fiatPrice, observedAt, note, status, createdAt)
VALUES ('price_point',1,50000.0,'${NU}','premie volgens de muntwinkel','pending','${NU}')`],
[`HygieneAcceptance`, `(kind, fingerprint, summary, note, createdAt)
VALUES ('transfer_without_counterpart','abc123','Verplaatsing zonder tegenkant (1)',NULL,'${NU}')`],
[`IgnoredPriceGap`, `(assetId, fromDay, untilDay, createdAt) VALUES (1,'1994-01-01','1994-01-31','${NU}')`],
[`PriceAbsence`, `(assetId, providerId, day, createdAt) VALUES (1,1,'2025-12-26','${NU}')`],
[`IntegrationCall`, `(target, providerKey, operation, subject, outcome, httpStatus, errorKey, detail, latencyMs, requestId, occurredAt)
VALUES ('service',NULL,'/v1/series','crypto:BTC','fail',404,'marketService.rejected api.noSeries','No series',42,'a1b2c3d4-0000-4000-8000-000000000000','${NU}')`],
[`AssetNote`, `(assetId, body, createdAt) VALUES (1,'2026-09-26 14:32 geërfd van oma','${NU}')`],
[`Schema`, `(id, version) VALUES (1,${CONFIG.SCHEMA_VERSION})`],
[`Portfolio`, `(id, baseCurrency, createdAt) VALUES (1,'EUR','${NU}')`],
];
export function vulFixture(db) {
for (const [tabel, rest] of FIXTURE) db.exec(`INSERT INTO ${tabel} ${rest}`);
return FIXTURE.map(([tabel]) => tabel);
}
export const FIXTURE_TABELLEN = FIXTURE.map(([tabel]) => tabel);
export const FIXTURE_TIJD = NU;
+80
View File
@@ -0,0 +1,80 @@
// Generated, do not edit. sha256:82c1a99dd9bcb14921519651f00e41b83ebb6753c9cc496d2de5d3792c7a8814
import { DatabaseSync } from 'node:sqlite';
import { SCHEMA, createDatabase as echteCreateDatabase } from '../../server/db.js';
import { CONFIG, PREFERENCES } from '../../shared/config.js';
const bind = params => params.map(v => {
if (v === undefined) return null;
if (typeof v === 'boolean') return v ? 1n : 0n;
if (typeof v === 'number' && Number.isInteger(v)) return BigInt(v);
return v;
});
export function createDbHarness({ seedPreferences = false } = {}) {
let sqlite;
let calls;
function fresh() {
sqlite = new DatabaseSync(':memory:', { enableForeignKeyConstraints: true });
sqlite.exec(SCHEMA);
sqlite.prepare(`INSERT INTO Schema (version) VALUES (?)`).run(CONFIG.SCHEMA_VERSION);
sqlite.prepare(`INSERT INTO Portfolio (id, baseCurrency, createdAt) VALUES (1, ?, ?)`)
.run(CONFIG.PORTFOLIO.DEFAULT_BASE_CURRENCY, '2026-01-01T00:00:00.000Z');
calls = { run: 0, persist: 0, seedPriceSource: [] };
if (seedPreferences) seedPrefs();
}
function seedPrefs() {
for (const [scope, keys] of Object.entries(PREFERENCES)) {
for (const [key, spec] of Object.entries(keys)) {
if (spec.secret) continue;
api.run(`INSERT INTO Preference (scope, key, value) VALUES (?,?,?)`,
[scope, key, JSON.stringify(spec.initial)]);
}
}
}
const api = {
run(sql, params = []) {
calls.run++;
if (params.length === 0) sqlite.exec(sql);
else sqlite.prepare(sql).run(...bind(params));
},
all(sql, params = []) {
return sqlite.prepare(sql).all(...bind(params)).map(row => ({ ...row }));
},
one(sql, params = []) {
const rows = api.all(sql, params);
return rows.length ? rows[0] : null;
},
lastId() {
const r = api.one(`SELECT last_insert_rowid() AS id`);
return r ? r.id : null;
},
schemaVersion() {
const r = api.one(`SELECT version FROM Schema`);
return r ? r.version : null;
},
async persist() { calls.persist++; },
seedPriceSource(assetId, status) { calls.seedPriceSource.push([assetId, status]); },
async init() { return sqlite; },
exportBytes() { throw new Error('db-node-harness: exportBytes() niet ondersteund in tests.'); },
async reset() { throw new Error('db-node-harness: reset() niet ondersteund; gebruik harness.recreate().'); },
async importBytes() { throw new Error('db-node-harness: importBytes() niet ondersteund in tests.'); },
createDatabase: echteCreateDatabase,
async idbGet(key) { return idb.has(key) ? idb.get(key) : null; },
async idbPut(key, value) { idb.set(key, value); },
};
const idb = new Map();
fresh();
return {
exports: api,
recreate() { sqlite.close(); idb.clear(); fresh(); },
get sqlite() { return sqlite; },
get calls() { return calls; },
};
}
+459
View File
@@ -0,0 +1,459 @@
// Generated, do not edit. sha256:db0c8db151ef8689662b07ab525d02d912551c1706e34e778f8bcf4190dc7dc0
const VOID = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'source', 'track', 'wbr']);
const RAW_TEXT = new Set(['textarea', 'script', 'style', 'title']);
const SELF_CLOSING_OK = new Set(['svg', 'path', 'line', 'circle', 'rect', 'polyline', 'polygon', 'g', 'use']);
const ENTITIES = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'" };
export function decodeEntities(s) {
return s.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (all, name) => {
if (name[0] === '#') {
const code = name[1] === 'x' || name[1] === 'X' ? parseInt(name.slice(2), 16) : parseInt(name.slice(1), 10);
return Number.isFinite(code) ? String.fromCodePoint(code) : all;
}
return Object.prototype.hasOwnProperty.call(ENTITIES, name.toLowerCase()) ? ENTITIES[name.toLowerCase()] : all;
});
}
const escText = s => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
const escAttr = s => String(s).replace(/&/g, '&amp;').replace(/"/g, '&quot;');
const hidden = (obj, key, value) => Object.defineProperty(obj, key, { value, writable: true, enumerable: false, configurable: true });
class Node {
constructor(doc) { hidden(this, 'ownerDocument', doc); hidden(this, 'parentNode', null); this.childNodes = []; }
get parentElement() { return this.parentNode instanceof Element ? this.parentNode : null; }
get isConnected() {
let n = this;
while (n) { if (n === this.ownerDocument) return true; n = n.parentNode; }
return false;
}
appendChild(child) {
if (child.parentNode) child.parentNode._removeChild(child);
child.parentNode = this;
this.childNodes.push(child);
return child;
}
append(...children) { for (const c of children) this.appendChild(c); }
_removeChild(child) {
const i = this.childNodes.indexOf(child);
if (i >= 0) this.childNodes.splice(i, 1);
child.parentNode = null;
}
remove() { if (this.parentNode) this.parentNode._removeChild(this); }
contains(other) {
for (let n = other; n; n = n.parentNode) if (n === this) return true;
return false;
}
}
class Text extends Node {
constructor(doc, data) { super(doc); this.nodeType = 3; this.data = data; }
get textContent() { return this.data; }
set textContent(v) { this.data = String(v); }
}
class ClassList {
constructor(el) { this.el = el; }
_get() { return (this.el.getAttribute('class') || '').split(/\s+/).filter(Boolean); }
_set(list) { this.el.setAttribute('class', list.join(' ')); }
contains(c) { return this._get().includes(c); }
add(...cs) { const l = this._get(); for (const c of cs) if (c && !l.includes(c)) l.push(c); this._set(l); }
remove(...cs) { this._set(this._get().filter(c => !cs.includes(c))); }
toggle(c, force) {
const on = force === undefined ? !this.contains(c) : !!force;
if (on) this.add(c); else this.remove(c);
return on;
}
}
const kebab = s => s.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
const camel = s => s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
function makeStyle() {
const props = new Map();
return new Proxy({
setProperty(k, v) { props.set(k, String(v)); },
getPropertyValue(k) { return props.get(k) ?? ''; },
removeProperty(k) { props.delete(k); },
}, {
get(target, key) { return key in target ? target[key] : (props.get(kebab(String(key))) ?? ''); },
set(_t, key, v) { props.set(kebab(String(key)), String(v)); return true; },
});
}
export class Element extends Node {
constructor(doc, tag) {
super(doc);
this.nodeType = 1;
this.localName = tag.toLowerCase();
this.attrs = new Map();
hidden(this, 'style', makeStyle());
hidden(this, 'classList', new ClassList(this));
hidden(this, 'listeners', []);
this.scrollTop = 0;
this.scrollLeft = 0;
}
get tagName() { return this.localName.toUpperCase(); }
[Symbol.for('nodejs.util.inspect.custom')]() {
const attrs = [...this.attrs].map(([k, v]) => (v === '' ? ` ${k}` : ` ${k}="${v}"`)).join('');
return `<${this.localName}${attrs}>`;
}
getAttribute(n) { return this.attrs.has(n.toLowerCase()) ? this.attrs.get(n.toLowerCase()) : null; }
setAttribute(n, v) { this.attrs.set(n.toLowerCase(), String(v)); }
removeAttribute(n) { this.attrs.delete(n.toLowerCase()); }
hasAttribute(n) { return this.attrs.has(n.toLowerCase()); }
get id() { return this.getAttribute('id') || ''; }
set id(v) { this.setAttribute('id', v); }
get className() { return this.getAttribute('class') || ''; }
set className(v) { this.setAttribute('class', v); }
get title() { return this.getAttribute('title') || ''; }
set title(v) { this.setAttribute('title', v); }
get src() { return this.getAttribute('src') || ''; }
set src(v) { this.setAttribute('src', v); }
get type() { return this.localName === 'input' ? (this.getAttribute('type') || 'text').toLowerCase() : (this.getAttribute('type') || ''); }
set type(v) { this.setAttribute('type', v); }
get disabled() { return this.hasAttribute('disabled'); }
set disabled(v) { if (v) this.setAttribute('disabled', ''); else this.removeAttribute('disabled'); }
get dataset() {
const el = this;
return new Proxy({}, {
get(_t, key) { return typeof key === 'string' ? (el.getAttribute('data-' + kebab(key)) ?? undefined) : undefined; },
set(_t, key, v) { el.setAttribute('data-' + kebab(String(key)), v); return true; },
has(_t, key) { return el.hasAttribute('data-' + kebab(String(key))); },
ownKeys() { return [...el.attrs.keys()].filter(k => k.startsWith('data-')).map(k => camel(k.slice(5))); },
getOwnPropertyDescriptor(_t, key) {
const v = el.getAttribute('data-' + kebab(String(key)));
return v === null ? undefined : { value: v, enumerable: true, configurable: true };
},
});
}
get children() { return this.childNodes.filter(n => n instanceof Element); }
get value() {
if (this.localName === 'select') {
if (this._value !== undefined) return this._value;
const opts = this.querySelectorAll('option');
const sel = opts.find(o => o.hasAttribute('selected')) || opts[0];
return sel ? sel.value : '';
}
if (this.localName === 'option') return this.getAttribute('value') ?? this.textContent;
if (this.localName === 'textarea') return this._value !== undefined ? this._value : this.textContent;
return this._value !== undefined ? this._value : (this.getAttribute('value') ?? '');
}
set value(v) { this._value = String(v); }
get checked() { return this._checked !== undefined ? this._checked : this.hasAttribute('checked'); }
set checked(v) { this._checked = !!v; }
get selectionStart() { return this._selStart ?? this.value.length; }
get selectionEnd() { return this._selEnd ?? this.value.length; }
setSelectionRange(a, b) { this._selStart = a; this._selEnd = b; }
get offsetHeight() { return 0; }
get offsetWidth() { return 0; }
get scrollHeight() { return 0; }
get clientHeight() { return 0; }
get clientWidth() { return 0; }
getBoundingClientRect() { return { left: 0, top: 0, right: 0, bottom: 0, width: 0, height: 0 }; }
scrollIntoView(opts) { this.ownerDocument.scrolledIntoView.push({ el: this, opts }); }
focus() { this.ownerDocument.activeElement = this; }
blur() { if (this.ownerDocument.activeElement === this) this.ownerDocument.activeElement = this.ownerDocument.body; }
get innerHTML() { return this.childNodes.map(serialize).join(''); }
set innerHTML(html) {
for (const c of [...this.childNodes]) this._removeChild(c);
for (const c of parseHTML(this.ownerDocument, String(html), this.localName)) this.appendChild(c);
this._value = undefined;
}
get outerHTML() { return serialize(this); }
get textContent() { return this.childNodes.map(n => n.textContent).join(''); }
set textContent(v) {
for (const c of [...this.childNodes]) this._removeChild(c);
if (v !== '' && v != null) this.appendChild(new Text(this.ownerDocument, String(v)));
}
querySelectorAll(sel) {
const list = parseSelector(sel);
const out = [];
walk(this, el => { if (list.some(s => matchComplex(el, s, this))) out.push(el); });
return out;
}
querySelector(sel) { return this.querySelectorAll(sel)[0] || null; }
matches(sel) { return parseSelector(sel).some(s => matchComplex(this, s, null)); }
closest(sel) {
const list = parseSelector(sel);
for (let n = this; n instanceof Element; n = n.parentNode) if (list.some(s => matchComplex(n, s, null))) return n;
return null;
}
addEventListener(type, fn, opts) { this.listeners.push({ type, fn, capture: opts === true || !!(opts && opts.capture) }); }
removeEventListener(type, fn, opts) {
const capture = opts === true || !!(opts && opts.capture);
this.listeners = this.listeners.filter(l => !(l.type === type && l.fn === fn && l.capture === capture));
}
dispatchEvent(ev) { return dispatch(this, ev.type, { bubbles: ev.bubbles, ...pickInit(ev) }); }
}
function pickInit(ev) {
const init = {};
for (const k of ['key', 'ctrlKey', 'metaKey', 'shiftKey', 'altKey', 'relatedTarget', 'detail']) if (ev[k] !== undefined) init[k] = ev[k];
return init;
}
function walk(root, fn) {
for (const c of root.childNodes) {
if (c instanceof Element) { fn(c); walk(c, fn); }
}
}
function serialize(n) {
if (n instanceof Text) return escText(n.data);
if (!(n instanceof Element)) return '';
const attrs = [...n.attrs].map(([k, v]) => (v === '' ? ` ${k}` : ` ${k}="${escAttr(v)}"`)).join('');
if (VOID.has(n.localName)) return `<${n.localName}${attrs}>`;
return `<${n.localName}${attrs}>${n.childNodes.map(serialize).join('')}</${n.localName}>`;
}
export function parseHTML(doc, html) {
const root = new Element(doc, '#fragment');
const stack = [root];
const top = () => stack[stack.length - 1];
let i = 0;
while (i < html.length) {
if (html.startsWith('<!--', i)) {
const end = html.indexOf('-->', i + 4);
i = end < 0 ? html.length : end + 3;
continue;
}
if (html[i] === '<' && html[i + 1] === '/') {
const end = html.indexOf('>', i);
const name = html.slice(i + 2, end < 0 ? html.length : end).trim().toLowerCase();
i = end < 0 ? html.length : end + 1;
const at = stack.map(n => n.localName).lastIndexOf(name);
if (at > 0) stack.length = at;
continue;
}
if (html[i] === '<' && /[a-zA-Z]/.test(html[i + 1] || '')) {
let j = i + 1, quote = null;
for (; j < html.length; j++) {
const ch = html[j];
if (quote) { if (ch === quote) quote = null; continue; }
if (ch === '"' || ch === "'") { quote = ch; continue; }
if (ch === '>') break;
}
let body = html.slice(i + 1, j);
i = j + 1;
const selfClose = body.endsWith('/');
if (selfClose) body = body.slice(0, -1);
const name = body.match(/^[^\s/>]+/)[0].toLowerCase();
const el = new Element(doc, name);
const attrRe = /([^\s"'>\/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
const rest = body.slice(name.length);
let m;
while ((m = attrRe.exec(rest))) {
const v = m[2] ?? m[3] ?? m[4] ?? '';
el.setAttribute(m[1], decodeEntities(v));
}
top().appendChild(el);
if (VOID.has(name) || (selfClose && SELF_CLOSING_OK.has(name))) continue;
if (RAW_TEXT.has(name)) {
const close = html.toLowerCase().indexOf(`</${name}`, i);
const raw = html.slice(i, close < 0 ? html.length : close);
if (raw) el.appendChild(new Text(doc, decodeEntities(raw)));
const end = close < 0 ? html.length : html.indexOf('>', close);
i = end < 0 ? html.length : end + 1;
continue;
}
stack.push(el);
continue;
}
const next = html.indexOf('<', i + 1);
const text = html.slice(i, next < 0 ? html.length : next);
i = next < 0 ? html.length : next;
if (text) top().appendChild(new Text(doc, decodeEntities(text)));
}
const nodes = [...root.childNodes];
for (const n of nodes) root._removeChild(n);
return nodes;
}
const cache = new Map();
export function parseSelector(sel) {
if (cache.has(sel)) return cache.get(sel);
const list = splitTop(sel, ',').map(s => s.trim()).filter(Boolean).map(parseComplex);
if (!list.length) throw new Error(`mini-dom: empty selector "${sel}"`);
cache.set(sel, list);
return list;
}
function splitTop(s, sep) {
const out = [];
let depth = 0, quote = null, start = 0;
for (let i = 0; i < s.length; i++) {
const ch = s[i];
if (quote) { if (ch === quote) quote = null; continue; }
if (ch === '"' || ch === "'") quote = ch;
else if (ch === '(' || ch === '[') depth++;
else if (ch === ')' || ch === ']') depth--;
else if (ch === sep && depth === 0) { out.push(s.slice(start, i)); start = i + 1; }
}
out.push(s.slice(start));
return out;
}
function parseComplex(s) {
const parts = [];
let i = 0;
let combinator = null;
while (i < s.length) {
if (/\s/.test(s[i]) || s[i] === '>') {
let comb = ' ';
while (i < s.length && (/\s/.test(s[i]) || s[i] === '>')) { if (s[i] === '>') comb = '>'; i++; }
if (parts.length) combinator = comb;
continue;
}
const [compound, next] = parseCompound(s, i);
if (parts.length) parts[parts.length - 1].combinator = combinator || ' ';
parts.push({ compound, combinator: null });
combinator = null;
i = next;
}
return parts;
}
function parseCompound(s, i) {
const c = { tag: null, id: null, classes: [], attrs: [], nots: [] };
const ident = (k) => { const m = s.slice(k).match(/^[A-Za-z0-9_-]+/); if (!m) throw new Error(`mini-dom: unsupported selector "${s}"`); return m[0]; };
if (s[i] === '*') i++;
else if (/[A-Za-z]/.test(s[i])) { c.tag = ident(i).toLowerCase(); i += c.tag.length; }
while (i < s.length && !/[\s>]/.test(s[i])) {
const ch = s[i];
if (ch === '#') { const n = ident(i + 1); c.id = n; i += 1 + n.length; }
else if (ch === '.') { const n = ident(i + 1); c.classes.push(n); i += 1 + n.length; }
else if (ch === '[') {
const end = findClose(s, i, '[', ']');
const inner = s.slice(i + 1, end).trim();
const m = inner.match(/^([A-Za-z0-9_:-]+)\s*(?:=\s*(?:"([^"]*)"|'([^']*)'|([^\s\]]+)))?$/);
if (!m) throw new Error(`mini-dom: unsupported attribute selector "[${inner}]"`);
c.attrs.push({ name: m[1].toLowerCase(), value: m[2] ?? m[3] ?? m[4] ?? null });
i = end + 1;
} else if (s.startsWith(':not(', i)) {
const end = findClose(s, i + 4, '(', ')');
c.nots.push(parseSelector(s.slice(i + 5, end)));
i = end + 1;
} else {
throw new Error(`mini-dom: unsupported selector "${s}" at "${s.slice(i)}"`);
}
}
return [c, i];
}
function findClose(s, open, a, b) {
let depth = 0, quote = null;
for (let k = open; k < s.length; k++) {
const ch = s[k];
if (quote) { if (ch === quote) quote = null; continue; }
if (ch === '"' || ch === "'") quote = ch;
else if (ch === a) depth++;
else if (ch === b && --depth === 0) return k;
}
throw new Error(`mini-dom: unclosed "${a}" in selector "${s}"`);
}
function matchCompound(el, c) {
if (!(el instanceof Element)) return false;
if (c.tag && el.localName !== c.tag) return false;
if (c.id && el.getAttribute('id') !== c.id) return false;
for (const k of c.classes) if (!el.classList.contains(k)) return false;
for (const a of c.attrs) {
if (!el.hasAttribute(a.name)) return false;
if (a.value !== null && el.getAttribute(a.name) !== a.value) return false;
}
for (const list of c.nots) if (list.some(s => matchComplex(el, s, null))) return false;
return true;
}
function matchComplex(el, parts, _scope) {
const at = (k, node) => {
if (!matchCompound(node, parts[k].compound)) return false;
if (k === 0) return true;
const comb = parts[k - 1].combinator;
if (comb === '>') return node.parentNode instanceof Element && at(k - 1, node.parentNode);
for (let p = node.parentNode; p instanceof Element; p = p.parentNode) if (at(k - 1, p)) return true;
return false;
};
return at(parts.length - 1, el);
}
export function dispatch(target, type, init = {}) {
const doc = target.ownerDocument || target;
let stopped = false;
const ev = {
type, target, bubbles: init.bubbles !== false, defaultPrevented: false, currentTarget: null,
key: init.key, ctrlKey: !!init.ctrlKey, metaKey: !!init.metaKey, shiftKey: !!init.shiftKey, altKey: !!init.altKey,
relatedTarget: init.relatedTarget ?? null, detail: init.detail ?? null,
stopPropagation() { stopped = true; },
stopImmediatePropagation() { stopped = true; },
preventDefault() { this.defaultPrevented = true; },
};
const path = [];
for (let n = target; n; n = n.parentNode) path.push(n);
if (path[path.length - 1] !== doc) path.push(doc);
const run = (node, capture) => {
ev.currentTarget = node;
if (!capture && typeof node['on' + type] === 'function') node['on' + type](ev);
for (const l of [...(node.listeners || [])]) if (l.type === type && l.capture === capture) l.fn(ev);
};
for (let k = path.length - 1; k > 0 && !stopped; k--) run(path[k], true);
if (!stopped) { run(target, true); if (!stopped) run(target, false); }
if (ev.bubbles) for (let k = 1; k < path.length && !stopped; k++) run(path[k], false);
return !ev.defaultPrevented;
}
class Document extends Node {
constructor() {
super(null);
this.ownerDocument = this;
this.listeners = [];
this.documentElement = new Element(this, 'html');
this.head = new Element(this, 'head');
this.body = new Element(this, 'body');
this.appendChild(this.documentElement);
this.documentElement.appendChild(this.head);
this.documentElement.appendChild(this.body);
this.activeElement = this.body;
this.scrolledIntoView = [];
}
[Symbol.for('nodejs.util.inspect.custom')]() { return '#document'; }
createElement(tag) { return new Element(this, tag); }
createTextNode(t) { return new Text(this, t); }
querySelectorAll(sel) { return this.documentElement.querySelectorAll(sel); }
querySelector(sel) { return this.documentElement.querySelector(sel); }
getElementById(id) { return this.querySelector(`#${id}`); }
addEventListener(type, fn, opts) { Element.prototype.addEventListener.call(this, type, fn, opts); }
removeEventListener(type, fn, opts) { Element.prototype.removeEventListener.call(this, type, fn, opts); }
dispatchEvent(ev) { return dispatch(this, ev.type, { bubbles: ev.bubbles, ...pickInit(ev) }); }
}
export function installDom() {
const document = new Document();
const frames = [];
const win = {
innerWidth: 1280, innerHeight: 800, listeners: [],
addEventListener(type, fn, opts) { Element.prototype.addEventListener.call(this, type, fn, opts); },
removeEventListener(type, fn, opts) { Element.prototype.removeEventListener.call(this, type, fn, opts); },
};
globalThis.document = document;
globalThis.window = win;
globalThis.requestAnimationFrame = fn => { frames.push(fn); return frames.length; };
globalThis.getComputedStyle = el => ({ getPropertyValue: k => el.style.getPropertyValue(k) });
const fire = (el, type, init) => dispatch(el, type, init);
return {
document,
window: win,
fire,
click: el => fire(el, 'click'),
type(el, value) { el.value = value; fire(el, 'input'); fire(el, 'change'); },
choose(el, value) { el.value = value; fire(el, 'change'); },
key: (el, key, mods = {}) => fire(el, 'keydown', { key, ...mods }),
flushFrames() { while (frames.length) frames.shift()(); },
host() { const el = document.createElement('div'); document.body.appendChild(el); return el; },
};
}
+61
View File
@@ -0,0 +1,61 @@
// Generated, do not edit. sha256:3babd66c28c9f09e458e639e8c0ca81825c815be279b32e296b0e13ec48e11ca
import { services } from '../../server/registers.mjs';
function pubkeyUit(uri) {
const m = /^nostr\+?walletconnect:\/\/([^?/]+)/i.exec(String(uri || '').trim());
if (!m) throw new Error('Ongeldige NWC-URI (verwacht nostr+walletconnect://…?relay=…&secret=…).');
return m[1].toLowerCase();
}
export function createNwcApiFake() {
const opslag = new Map();
const calls = { setUri: [], purgeUri: [], validateUri: [], call: [] };
const fout = { setUri: null, purgeUri: null };
const exports = {
async validateNwcUri(uri) {
calls.validateUri.push(uri);
return { walletPubkey: pubkeyUit(uri), relays: ['wss://relay.test'] };
},
async setNwcUri(sourceId, uri) {
calls.setUri.push([Number(sourceId), uri]);
if (fout.setUri) throw new Error(fout.setUri);
const walletPubkey = pubkeyUit(uri);
opslag.set(Number(sourceId), String(uri));
return { source: Number(sourceId), uri_set: true, walletPubkey };
},
async deleteNwcUri(sourceId) {
calls.purgeUri.push(Number(sourceId));
if (fout.purgeUri) throw new Error(fout.purgeUri);
opslag.delete(Number(sourceId));
return { source: Number(sourceId), deleted: true };
},
async nwcStatus(sourceId) {
const uri = opslag.get(Number(sourceId));
return { source: Number(sourceId), uri_set: !!uri, walletPubkey: uri ? pubkeyUit(uri) : '' };
},
async nwcCall() { throw new Error('nwc-api-fake: een relay-call hoort niet in een datalaag-test.'); },
async getInfo() { return exports.nwcCall(); },
async getBalance() { return exports.nwcCall(); },
async listTransactions() { return exports.nwcCall(); },
async testNwc() { return exports.nwcCall(); },
};
const api = {
exports,
calls,
registreer() {
services.register({ id: 'nwc', feature: 'lightning', setNwcUri: exports.setNwcUri, deleteNwcUri: exports.deleteNwcUri });
return api;
},
fout,
geheim: sourceId => opslag.get(Number(sourceId)) ?? null,
get ids() { return [...opslag.keys()]; },
leeg() {
opslag.clear();
calls.setUri.length = 0; calls.purgeUri.length = 0; calls.validateUri.length = 0;
fout.setUri = null; fout.purgeUri = null;
},
};
return api;
}
+23
View File
@@ -0,0 +1,23 @@
// Generated, do not edit. sha256:17b1281e54996148b279858b457fd106d4d2e1b529ec53afb0ad51df4cdcbc22
const PREFS = new URL('../../client/data/preferences.js', import.meta.url).href;
const APP_SETTINGS = new URL('../../client/app-settings.js', import.meta.url).href;
async function laad() {
const { loadAppSettings } = await import(APP_SETTINGS);
await loadAppSettings();
}
export async function vasteRekenvaluta(mock, baseCurrency = 'EUR') {
mock.module(PREFS, { namedExports: {
getSettings: async () => ({ baseCurrency }),
getPref: async () => null,
setPref: async () => {},
} });
await laad();
}
export async function rekenvalutaUitDatabase(mock) {
const server = await import(new URL('../../server/data/preferences.js', import.meta.url).href);
mock.module(PREFS, { namedExports: server });
await laad();
}
+23
View File
@@ -0,0 +1,23 @@
// Generated, do not edit. sha256:d225f580a4545f4be7759feb5bf7808d1ded43a0baf1cbde8115e6901c6a4521
export function schemaVorm(db) {
const tabellen = db.prepare(
`SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name`
).all().map(r => r.name);
const uit = [];
for (const t of tabellen) {
for (const k of db.prepare(`PRAGMA table_info(${t})`).all()) {
uit.push(`${t}.${k.name}: ${k.type}${k.notnull ? ' NOT NULL' : ''}${k.pk ? ` PK${k.pk}` : ''}`
+ `${k.dflt_value == null ? '' : ` DEFAULT ${k.dflt_value}`}`);
}
for (const fk of db.prepare(`PRAGMA foreign_key_list(${t})`).all()) {
uit.push(`${t}.${fk.from} -> ${fk.table}.${fk.to} ON DELETE ${fk.on_delete}`);
}
for (const ix of db.prepare(`PRAGMA index_list(${t})`).all()) {
const kolommen = db.prepare(`PRAGMA index_info(${ix.name})`).all().map(k => k.name).join(',');
uit.push(`${ix.unique ? 'UNIQUE ' : ''}INDEX ${ix.name} ON ${t}(${kolommen})`
+ `${ix.partial ? ' PARTIAL' : ''}${ix.origin === 'u' ? ' [uit UNIQUE]' : ''}`);
}
}
return uit.sort();
}