Versie 0.2.0
This commit is contained in:
commit
e77d8fd373
495 files changed
+81026
No files matched your search
@@ -0,0 +1,414 @@
|
||||
// Generated, do not edit. sha256:df37b3da57b8535420153ba9f7331337454235c42435f0fd9deff4f9829eb96b
|
||||
import { listProviders, getProvider } from '../data/providers.js';
|
||||
import { listPriceSource } from '../data/price-source.js';
|
||||
import { diagnoseSources } from '../../shared/source-diagnosis.js';
|
||||
import { serviceCoveredAssets } from './service-cover.mjs';
|
||||
import { apiMetricsTotals } from '../data/api-metrics.js';
|
||||
import { listConnections } from '../data/connections.js';
|
||||
import { listAssets } from '../data/assets.js';
|
||||
import { getBaseCurrency } from '../data/portfolio.js';
|
||||
import { listAccounts } from '../data/accounts.js';
|
||||
import { listLabels, labelUsage } from '../data/labels.js';
|
||||
import { counts } from '../data/counts.js';
|
||||
import { listMetalProducts } from '../data/metals.js';
|
||||
import { listProfiles, listWidgets } from '../data/dashboard.js';
|
||||
import { getPref } from '../data/preferences.js';
|
||||
import { hygieneReport } from '../data/hygiene.js';
|
||||
import { listScanLogs } from '../data/scan-logs.js';
|
||||
import { listIntegrationCalls } from '../data/integration-log.js';
|
||||
import { assertScope, mayScope } from '../ai/gate.js';
|
||||
import { limitedList, limitRows, omittedNote } from '../ai/limits.js';
|
||||
import { vertalerVoor } from './session.mjs';
|
||||
import { CONFIG, PREFERENCES, SOURCE_CAPABILITIES, TAX_CATEGORY_BY_ASSET_KIND, balanceCaptureOn } from '../../shared/config.js';
|
||||
import { fromBaseByFactor } from '../../shared/units.js';
|
||||
|
||||
const GEEN_BEDRAGEN = 'Zonder bedragen, saldi of adressen, en wijzigt niets.';
|
||||
|
||||
function sindsUren(uren) {
|
||||
return new Date(Date.now() - uren * 3600_000).toISOString().slice(0, 16);
|
||||
}
|
||||
|
||||
function pct(deel, geheel) {
|
||||
return geheel ? `${Math.round((deel / geheel) * 1000) / 10}%` : '0%';
|
||||
}
|
||||
|
||||
const VENSTER_UREN = CONFIG.AI.LIMITS.METRICS_WINDOW_HOURS;
|
||||
|
||||
function scanBlok(t) {
|
||||
const regels = [];
|
||||
for (const rekening of listAccounts()) {
|
||||
const logs = listScanLogs(rekening.id);
|
||||
if (!logs.length && !rekening.scanEnabled) continue;
|
||||
const veiligeRekening = t.toAi(rekening, { entity: 'account' });
|
||||
if (!logs.length) {
|
||||
regels.push(`- ${veiligeRekening.id}: staat op automatisch scannen maar is nog nooit gescand.`);
|
||||
continue;
|
||||
}
|
||||
const laatste = t.toAi({
|
||||
status: logs[0].status,
|
||||
scannedAt: logs[0].scannedAt,
|
||||
addressesFound: logs[0].addressesFound,
|
||||
proposalsCreated: logs[0].proposalsCreated,
|
||||
}, { entity: 'scanLog' });
|
||||
const mislukt = logs.filter(l => l.status === 'failed').length;
|
||||
regels.push(
|
||||
`- ${veiligeRekening.id}: laatste scan ${laatste.status} op ${laatste.scannedAt}`
|
||||
+ `, ${laatste.addressesFound ?? 0} adressen, ${laatste.proposalsCreated ?? 0} voorstellen`
|
||||
+ ` (${logs.length} scans in totaal, ${mislukt} mislukt).`);
|
||||
}
|
||||
return limitedList(regels, r => r).lines;
|
||||
}
|
||||
|
||||
const DIAGNOSE_TEKST = Object.freeze({
|
||||
noCoverage: () => 'geen enkele bron levert hier historie voor',
|
||||
allInactive: n => `de dekkende bronnen staan uit (${n})`,
|
||||
noChoice: () => 'geen bron gekozen voor de historie',
|
||||
choiceUnusable: n => `de gekozen bron werkt niet (${n})`,
|
||||
limitReached: n => `maandlimiet bereikt (${n})`,
|
||||
choiceInactive: n => `gekozen maar uitgezet (${n})`,
|
||||
});
|
||||
function bronkeuzeBlok(t, uitDienst = new Set()) {
|
||||
const maand = `${new Date().toISOString().slice(0, 7)}-01T00:00`;
|
||||
const providers = listProviders().map(p => getProvider(p.key)).filter(Boolean);
|
||||
const perId = new Map(providers.map(p => [p.id, p]));
|
||||
const usage = {};
|
||||
for (const p of providers) if (p.callsPerMonth) usage[p.id] = apiMetricsTotals(p.id, maand).calls;
|
||||
const cellen = listPriceSource().filter(c => c.rank != null).sort((a, b) => a.rank - b.rank);
|
||||
const regels = [];
|
||||
const basis = getBaseCurrency();
|
||||
for (const asset of listAssets()) {
|
||||
if (asset.symbol === basis) continue;
|
||||
const veilig = t.toAi(asset, { entity: 'asset' });
|
||||
const keten = cap => cellen.filter(c => c.assetId === asset.id && c.capability === cap)
|
||||
.map(c => perId.get(c.providerId)).filter(Boolean);
|
||||
const naam = p => `${t.toAi({ key: p.key }, { entity: 'provider' }).key}${p.isActive ? '' : ' (uit)'}`;
|
||||
const delen = SOURCE_CAPABILITIES.map(cap => {
|
||||
const k = keten(cap);
|
||||
return `${cap === 'spot' ? 'spot' : 'historie'}: ${k.length ? k.map(naam).join(', ') : 'geen'}`;
|
||||
});
|
||||
const gekozen = new Set(cellen.filter(c => c.assetId === asset.id).map(c => c.providerId));
|
||||
const vrij = providers.filter(p => !p.isSystem && (p.coverage || []).includes(asset.symbol) && !gekozen.has(p.id));
|
||||
if (vrij.length) delen.push(`dekt ook: ${vrij.map(naam).join(', ')}`);
|
||||
const dienst = uitDienst.has(asset.id);
|
||||
if (dienst) delen.push('historie uit Atreides Services');
|
||||
const bevindingen = diagnoseSources(asset, { providers, historyChain: keten('history').map(p => p.key), usage,
|
||||
serviceCovers: dienst });
|
||||
for (const b of bevindingen) {
|
||||
const namen = (b.names || []).join(', ');
|
||||
const tekst = (DIAGNOSE_TEKST[b.code] || (() => b.code))(namen);
|
||||
delen.push(b.proposal ? `${tekst}, voorstel: ${b.proposal.providerKey}` : tekst);
|
||||
}
|
||||
regels.push(`- ${veilig.symbol} (${veilig.kind}): ${delen.join('; ')}.`);
|
||||
}
|
||||
return limitedList(regels, r => r).lines;
|
||||
}
|
||||
|
||||
const ILOG = CONFIG.INTEGRATION_LOG;
|
||||
function foutenBlok(t, magAdministratie) {
|
||||
const rijen = listIntegrationCalls({ days: ILOG.WINDOW_DAYS, limit: ILOG.MAX_ROWS });
|
||||
const regels = rijen.slice(0, ILOG.AI_MAX_ROWS).map(r => {
|
||||
const v = t.toAi({
|
||||
occurredAt: r.occurredAt, providerKey: r.providerKey, operation: r.operation, outcome: r.outcome,
|
||||
httpStatus: r.httpStatus, errorKey: r.errorKey, requestId: r.requestId,
|
||||
...(magAdministratie ? { subject: r.subject } : {}),
|
||||
}, { entity: 'integrationCall' });
|
||||
const bron = r.target === 'service' ? 'atreides_services' : v.providerKey;
|
||||
return `- ${v.occurredAt} ${bron} ${v.operation}${v.subject ? ` ${v.subject}` : ''}: ${v.outcome}`
|
||||
+ `${v.httpStatus ? ` ${v.httpStatus}` : ''}${v.errorKey ? ` ${v.errorKey}` : ''}`
|
||||
+ `${v.requestId ? ` (request-id ${v.requestId})` : ''}`;
|
||||
});
|
||||
if (rijen.length > regels.length) regels.push(`- en nog ${rijen.length - regels.length} oudere.`);
|
||||
return regels;
|
||||
}
|
||||
|
||||
const integrationHealth = Object.freeze({
|
||||
name: 'integration_health',
|
||||
title: 'Gezondheid van de koppelingen',
|
||||
description:
|
||||
'Geeft de technische toestand van de externe koppelingen van AssetApp: per koersbron de geslaagde '
|
||||
+ 'en mislukte aanroepen van de afgelopen 24 uur met de gemiddelde en langste reactietijd, per '
|
||||
+ 'verbinding de laatst gemeten status, per automatisch gescand account de uitslag van de '
|
||||
+ 'laatste wallet-scan, per asset de bronkeuze voor spot en historie met wat eraan schort, en de recente '
|
||||
+ 'fouten met request-id. Zegt dus of een koersbron onbetrouwbaar is, een verbinding stuk staat of een '
|
||||
+ 'scan niet meer doorkomt. Verbindingen en accounts worden aangeduid met een verwijzing zoals '
|
||||
+ 'connection:7f3a9c, zodat twee koppelingen van dezelfde soort te onderscheiden zijn. '
|
||||
+ GEEN_BEDRAGEN,
|
||||
inputSchema: Object.freeze({ type: 'object', properties: {}, additionalProperties: false }),
|
||||
scope: 'none',
|
||||
|
||||
async run(_args, context) {
|
||||
assertScope('none');
|
||||
const t = await vertalerVoor(context);
|
||||
const sinds = sindsUren(VENSTER_UREN);
|
||||
|
||||
const regels = limitedList(listProviders(), p => {
|
||||
const veilig = t.toAi(p, { entity: 'provider' });
|
||||
const m = apiMetricsTotals(p.id, sinds);
|
||||
const status = veilig.isActive ? 'actief' : 'uitgeschakeld';
|
||||
return m.calls === 0
|
||||
? `- ${veilig.key} (${status}): geen aanroepen in de afgelopen ${VENSTER_UREN} uur.`
|
||||
: `- ${veilig.key} (${status}): ${m.calls} aanroepen, ${m.fail} mislukt (${pct(m.fail, m.calls)}), `
|
||||
+ `gemiddeld ${m.avg_latency_ms} ms, langste ${m.maxLatencyMs} ms.`;
|
||||
}).lines;
|
||||
|
||||
const verbindingen = limitedList(listConnections(), c => {
|
||||
const veilig = t.toAi(c, { entity: 'connection' });
|
||||
const uitslag = veilig.lastStatus
|
||||
? `${veilig.lastStatus} (gemeten ${veilig.lastCheckedAt})`
|
||||
: 'nog nooit getest';
|
||||
return `- ${veilig.id} (${veilig.kind}${veilig.isActive ? ', actief' : ''}): ${uitslag}`;
|
||||
}).lines;
|
||||
|
||||
const magAdministratie = mayScope('read');
|
||||
const scans = magAdministratie ? scanBlok(t) : [];
|
||||
const uitDienst = magAdministratie ? await serviceCoveredAssets() : new Set();
|
||||
const bronkeuze = magAdministratie ? bronkeuzeBlok(t, uitDienst) : [];
|
||||
const fouten = foutenBlok(t, magAdministratie);
|
||||
|
||||
return {
|
||||
text: [
|
||||
`Koersbronnen, afgelopen ${VENSTER_UREN} uur:`,
|
||||
regels.length ? regels.join('\n') : '- geen koersbronnen ingesteld.',
|
||||
'',
|
||||
'Verbindingen:',
|
||||
verbindingen.length ? verbindingen.join('\n') : '- geen verbindingen ingesteld.',
|
||||
'',
|
||||
'Wallet-scans:',
|
||||
!magAdministratie
|
||||
? '- niet opgevraagd: de AI-koppeling van AssetApp staat uit, en scans horen bij een account.'
|
||||
: (scans.length ? scans.join('\n') : '- geen account staat op automatisch scannen.'),
|
||||
'',
|
||||
'Bronkeuze per asset (in volgorde; wijzigen doet de gebruiker op Integraties, Bronkeuze):',
|
||||
!magAdministratie
|
||||
? '- niet opgevraagd: de AI-koppeling van AssetApp staat uit, en de assets horen bij de administratie.'
|
||||
: (bronkeuze.length ? bronkeuze.join('\n') : '- geen assets.'),
|
||||
'',
|
||||
`Fouten, afgelopen ${ILOG.WINDOW_DAYS} dagen (nieuwste eerst):`,
|
||||
fouten.length ? fouten.join('\n') : '- geen.',
|
||||
].join('\n'),
|
||||
fieldsShared: [
|
||||
'key', 'isActive', 'okCount', 'failCount', 'sumLatencyMs', 'maxLatencyMs',
|
||||
'kind', 'lastStatus', 'lastCheckedAt', 'id',
|
||||
...(magAdministratie ? ['scanEnabled', 'status', 'scannedAt', 'addressesFound', 'proposalsCreated',
|
||||
'symbol', 'capability', 'rank', 'callsPerMonth'] : []),
|
||||
...(uitDienst.size ? ['value'] : []),
|
||||
...(fouten.length ? ['occurredAt', 'providerKey', 'operation', 'outcome', 'httpStatus', 'errorKey', 'requestId',
|
||||
...(magAdministratie ? ['subject'] : [])] : []),
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const adminOverview = Object.freeze({
|
||||
name: 'admin_overview',
|
||||
title: 'Overzicht van de inrichting',
|
||||
description:
|
||||
'Geeft de inrichting van AssetApp: welke assets er zijn (met hun symbool, soort en eenheid), '
|
||||
+ 'hoeveel accounts er zijn en van welke soort, welke labels bestaan en op hoeveel on-chain '
|
||||
+ 'bewegingen elk label gebruikt wordt, en hoeveel gebeurtenissen '
|
||||
+ 'en balansen er zijn vastgelegd. Bedoeld voor vragen over de opzet en om '
|
||||
+ 'te zien wat er ontbreekt. Plus de configuratie: metaalproducten, dashboardprofielen, '
|
||||
+ 'de app-instellingen en de timers. '
|
||||
+ 'Accounts en labels krijgen een verwijzing zoals account:7f3a9c, '
|
||||
+ 'ook in een later gesprek dezelfde; met onbewerkt delen aan is dat het rij-id met de naam '
|
||||
+ 'erachter. ' + GEEN_BEDRAGEN,
|
||||
inputSchema: Object.freeze({ type: 'object', properties: {}, additionalProperties: false }),
|
||||
scope: 'read',
|
||||
|
||||
async run(_args, context) {
|
||||
assertScope(adminOverview.scope);
|
||||
const t = await vertalerVoor(context);
|
||||
|
||||
const alleAssets = listAssets();
|
||||
const alleRekeningen = listAccounts();
|
||||
const alleLabels = listLabels();
|
||||
|
||||
const gekozen = (waarde, afgeleid, bron) => (waarde != null ? `${waarde} (gekozen)` : `${afgeleid} (${bron})`);
|
||||
const assets = limitedList(alleAssets, a => {
|
||||
const v = t.toAi(a, { entity: 'asset' });
|
||||
return `- ${v.symbol} (${v.kind}, eenheid ${v.unit}${v.subunit ? `, ondereenheid ${v.subunit}` : ''}; `
|
||||
+ `categorie ${gekozen(v.taxCategory, TAX_CATEGORY_BY_ASSET_KIND[v.kind] ?? 'onbekend', 'volgt soort')}, `
|
||||
+ `belastingen ${gekozen(v.taxScope, 'all', 'standaard')}, `
|
||||
+ `saldo vastleggen ${gekozen(v.balanceCapture == null ? null : (v.balanceCapture ? 'aan' : 'uit'),
|
||||
balanceCaptureOn({ kind: v.kind }) ? 'aan' : 'uit', 'volgt soort')})`;
|
||||
}).lines;
|
||||
|
||||
const onbewerkt = t.precision.rawValues;
|
||||
const naamErbij = v => (onbewerkt && v.name ? ` (${v.name})` : '');
|
||||
|
||||
const rekeningen = limitedList(alleRekeningen, a => {
|
||||
const v = t.toAi(a, { entity: 'account' });
|
||||
const eigen = v.isOwnCustody ? 'zelf in bewaring' : 'bij een derde partij';
|
||||
return `- ${v.id}${naamErbij(v)}: soort ${v.kind}, ${eigen}, ${v.isActive ? 'actief' : 'afgesloten'}`
|
||||
+ `${v.scanEnabled ? ', wordt automatisch gescand' : ''}`
|
||||
+ `; categorie ${v.taxCategory ?? 'volgt asset'}, belastingen ${v.taxScope ?? 'volgt asset'}`
|
||||
+ `, vermogen ${v.wealthScope ?? 'eigen'}`;
|
||||
}).lines;
|
||||
|
||||
const gebruik = new Map(labelUsage().map(r => [r.labelId, Number(r.walletTransactions)]));
|
||||
const labels = limitedList(alleLabels, l => {
|
||||
const n = gebruik.get(l.id) ?? 0;
|
||||
const hoe = n === 0 ? 'nergens gebruikt'
|
||||
: n === 1 ? 'gebruikt op 1 beweging'
|
||||
: `gebruikt op ${n} bewegingen`;
|
||||
const v = t.toAi(l, { entity: 'label' });
|
||||
return `- ${v.id}${naamErbij(v)}: ${hoe}`;
|
||||
}).lines;
|
||||
const c = counts();
|
||||
|
||||
const alleProducten = listMetalProducts();
|
||||
const perAsset = new Map();
|
||||
for (const p of alleProducten) {
|
||||
const key = p.asset_symbol;
|
||||
if (!perAsset.has(key)) perAsset.set(key, new Map());
|
||||
const vormen = perAsset.get(key);
|
||||
vormen.set(p.form, (vormen.get(p.form) ?? 0) + 1);
|
||||
}
|
||||
const producten = onbewerkt
|
||||
? limitedList(alleProducten, p =>
|
||||
`- ${p.asset_symbol} (${p.name}): ${p.form}, ${fromBaseByFactor(p.fineGrams, 'g')} g fijn`).lines
|
||||
: [...perAsset].map(([symbool, vormen]) =>
|
||||
`- ${symbool}: ${[...vormen].map(([vorm, n]) => `${n}× ${vorm}`).join(', ')}`);
|
||||
|
||||
const profielen = listProfiles().map(p =>
|
||||
`- ${onbewerkt && p.name ? p.name : `profiel ${p.sortOrder}`}: `
|
||||
+ `${p.isActive ? 'actief' : 'niet actief'}, ${listWidgets(p.id).length} widgets`);
|
||||
|
||||
const instellingen = [];
|
||||
for (const scope of CONFIG.AI.SHARED_PREF_SCOPES) {
|
||||
for (const key of Object.keys(PREFERENCES[scope] ?? {})) {
|
||||
if (key === '*') continue;
|
||||
const v = t.toAi({ scope, key, value: getPref(scope, key) }, { table: 'Preference' });
|
||||
if (v.value === undefined) continue;
|
||||
const waarde = v.value === null || v.value === '' ? 'leeg' : v.value;
|
||||
instellingen.push(`- ${v.scope}.${v.key} = ${waarde}`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
text: [
|
||||
`Rekenvaluta: ${getBaseCurrency()}. Elke koers en elk bedrag in AssetApp staat in deze munt.`,
|
||||
'',
|
||||
`Assets (${alleAssets.length}):`,
|
||||
assets.length ? assets.join('\n') : '- geen.',
|
||||
'',
|
||||
`Accounts (${alleRekeningen.length}):`,
|
||||
rekeningen.length ? rekeningen.join('\n') : '- geen.',
|
||||
'',
|
||||
`Labels (${alleLabels.length}):`,
|
||||
labels.length ? labels.join('\n') : '- geen.',
|
||||
'',
|
||||
`Vastgelegd: ${c.events} gebeurtenissen en ${c.valuations} balansen.`,
|
||||
'',
|
||||
`Metaalproducten (${alleProducten.length}):`,
|
||||
producten.length ? producten.join('\n') : '- geen.',
|
||||
'',
|
||||
`Dashboardprofielen (${profielen.length}):`,
|
||||
profielen.length ? profielen.join('\n') : '- geen.',
|
||||
'',
|
||||
`Instellingen (${instellingen.length}):`,
|
||||
instellingen.length ? instellingen.join('\n') : '- geen.',
|
||||
].join('\n'),
|
||||
fieldsShared: ['baseCurrency', 'id', 'symbol', 'kind', 'unit', 'subunit', 'isOwnCustody', 'isActive', 'scanEnabled',
|
||||
'taxCategory', 'taxScope', 'wealthScope', 'balanceCapture',
|
||||
'labelId', 'form', 'sortOrder', 'profileId', 'scope', 'key', 'Preference.value',
|
||||
...(onbewerkt ? ['name', 'fineGrams'] : [])],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const BEVINDING_TEKST = Object.freeze({
|
||||
duplicate_events: 'gebeurtenissen die er twee of meer keer identiek in staan',
|
||||
assets_without_balance: 'assets waarin geboekt is maar waarvoor nooit een balans is vastgelegd',
|
||||
unreconciled_valuations: 'balansen die niet aansluiten op de boekingen eronder, en die nog niet zijn afgehandeld',
|
||||
stale_prices: 'assets waarvan de koershistorie achterloopt op de laatste boeking, of ontbreekt',
|
||||
unlinked_transactions: 'on-chain bewegingen zonder koppeling en zonder openstaand voorstel',
|
||||
late_price_history: 'assets waarvan de koershistorie later begint dan de eerste boeking',
|
||||
price_gaps: 'assets met een gat tussen twee koersen (dagen zonder koers, vanaf de eerste boeking), waardoor daar een oude koers wordt meegesleept',
|
||||
price_stopped: 'assets in bezit waarvan de laatste koers ouder is dan de doortrekgrens van hun soort, dus nu niet te waarderen',
|
||||
transfer_without_counterpart: 'verplaatsingen waarvan één kant ontbreekt zonder extern adres',
|
||||
fee_without_effect: 'boekingen met kosten die geen berekening leest (geld bij rente, eenheden bij aankoop of rente)',
|
||||
coverage_without_asset: 'koersbronnen die dekking claimen op een symbool dat hier geen asset is',
|
||||
price_unit_swap: 'koersrijen die een factor 31 (gram tegen troy ounce) van de rij ervoor liggen',
|
||||
price_outlier: 'koersrijen die een factor drie of meer van beide buren liggen, zoals een tikfout',
|
||||
price_sources_disagree: 'dagen waarop twee bronnen een koers geven die ver uit elkaar ligt',
|
||||
price_not_positive: 'koersrijen met een koers van nul of lager, die de app niet meetelt',
|
||||
price_in_future: 'koersrijen met een moment in de toekomst',
|
||||
booking_price_deviates: 'aan- en verkopen waarvan de prijs ver van de koers van die dag ligt (tikfout, datum of eenheid)',
|
||||
price_origin_derivable: 'boekingen zonder herkomst waarvan de prijs de dagkoers is, in te vullen op Gezondheid',
|
||||
});
|
||||
|
||||
const DAG_VELDEN = Object.freeze({
|
||||
stale_prices: Object.freeze(['occurredAt', 'observedAt']),
|
||||
unlinked_transactions: Object.freeze(['createdAt']),
|
||||
price_unit_swap: Object.freeze(['observedAt']),
|
||||
price_outlier: Object.freeze(['observedAt']),
|
||||
price_sources_disagree: Object.freeze(['observedAt']),
|
||||
price_not_positive: Object.freeze(['observedAt']),
|
||||
price_in_future: Object.freeze(['observedAt']),
|
||||
booking_price_deviates: Object.freeze(['occurredAt']),
|
||||
price_origin_derivable: Object.freeze(['occurredAt']),
|
||||
});
|
||||
|
||||
function opDag(waarde) {
|
||||
return typeof waarde === 'string' && waarde.length >= 10 ? waarde.slice(0, 10) : waarde;
|
||||
}
|
||||
|
||||
function grofPerBevinding(soort, rij) {
|
||||
const velden = DAG_VELDEN[soort];
|
||||
if (!velden) return rij;
|
||||
const uit = { ...rij };
|
||||
for (const veld of velden) if (veld in uit) uit[veld] = opDag(uit[veld]);
|
||||
return uit;
|
||||
}
|
||||
|
||||
const dataHealth = Object.freeze({
|
||||
name: 'data_health',
|
||||
title: 'Controle op de eigen administratie',
|
||||
description:
|
||||
'Zoekt inconsistenties in de administratie van AssetApp en geeft per soort het aantal en de '
|
||||
+ 'betrokken rijen terug: dubbele boekingen, assets zonder vastgelegde balans, balansen die niet '
|
||||
+ 'aansluiten op de boekingen eronder, koershistorie die achterloopt, on-chain bewegingen die '
|
||||
+ 'nergens aan hangen, en verwijzingen die nergens heen wijzen. Elke bevinding is een aantal of '
|
||||
+ 'een verwijzing zoals account:a1, stabiel binnen dit gesprek. ' + GEEN_BEDRAGEN,
|
||||
inputSchema: Object.freeze({ type: 'object', properties: {}, additionalProperties: false }),
|
||||
scope: 'read',
|
||||
|
||||
async run(_args, context) {
|
||||
assertScope(dataHealth.scope);
|
||||
const t = await vertalerVoor(context);
|
||||
t.beginCall();
|
||||
const rapport = hygieneReport();
|
||||
|
||||
const blokken = rapport.findings.map(f => {
|
||||
const kop = `${BEVINDING_TEKST[f.kind] || f.kind} (${f.kind}, ${f.severity}): ${f.total}`;
|
||||
if (!f.total) return `${kop}.`;
|
||||
const { rows: zichtbaar, omitted, capped } = limitRows(f.rows);
|
||||
const grof = zichtbaar.map(r => grofPerBevinding(f.kind, r.row));
|
||||
const veilig = t.toAi(grof);
|
||||
const rijen = veilig.map((r, i) => {
|
||||
const delen = Object.entries(r).map(([k, v]) => `${k}=${v}`);
|
||||
for (const [k, v] of Object.entries(zichtbaar[i].counts || {})) delen.push(`${k}=${v}`);
|
||||
return ` - ${delen.join(', ')}`;
|
||||
});
|
||||
if (capped) rijen.push(omittedNote(omitted, CONFIG.AI.LIMITS.MAX_ROWS_PER_LIST));
|
||||
return `${kop}:\n${rijen.join('\n')}`;
|
||||
});
|
||||
|
||||
return {
|
||||
text: [
|
||||
rapport.total === 0
|
||||
? 'Er zijn geen inconsistenties gevonden.'
|
||||
: `Er zijn ${rapport.total} bevindingen, verdeeld over ${rapport.findings.filter(f => f.total).length} soorten.`,
|
||||
'',
|
||||
...blokken,
|
||||
].join('\n'),
|
||||
fieldsShared: [...new Set([
|
||||
...t.fieldsShared(),
|
||||
...rapport.findings.filter(f => f.rows.length).flatMap(f => f.countsFrom || []),
|
||||
])].sort(),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const READ_TOOLS = Object.freeze([dataHealth, integrationHealth, adminOverview]);
|
||||
Reference in new issue
Block a user