Versie 0.2.0
This commit is contained in:
commit
e77d8fd373
495 files changed
+81026
No files matched your search
@@ -0,0 +1,203 @@
|
||||
// Generated, do not edit. sha256:51b53dc3f01e74bb7f221f45ce7f2b9b6f6a164a12cedc96e33ad4f1b1b701a0
|
||||
import { currentNotifications } from '../data/notifications.js';
|
||||
import { countAiToolCallsSince } from '../data/ai-audit.js';
|
||||
import { getPref } from '../data/preferences.js';
|
||||
import { listProviders, getProvider } from '../data/providers.js';
|
||||
import { listPriceSource } from '../data/price-source.js';
|
||||
import { listPriceGaps } from '../data/price-history.js';
|
||||
import { apiCallsByProvider } from '../data/api-metrics.js';
|
||||
import { rateSourceFor } from '../price/providers.js';
|
||||
import { t } from '../i18n/index.js';
|
||||
import { CONFIG, TARGETS, NOTIFICATION_TARGETS, SOURCE_ENDPOINTS, targetHash, targetTab } from '../../shared/config.js';
|
||||
import { registerAttention } from './attention.js';
|
||||
import { appSetting, baseCurrency } from '../app-settings.js';
|
||||
import { licenseChangedSinceSeen } from '../license.js';
|
||||
|
||||
async function integrationsNeedingKey() {
|
||||
const rijen = await listProviders();
|
||||
const configs = await Promise.all(rijen.map(r => getProvider(r.key)));
|
||||
const providers = configs
|
||||
.filter(p => p && p.isActive && p.authenticationRequired && p.authenticationKind !== 'none');
|
||||
const out = [];
|
||||
for (const p of providers) {
|
||||
let complete = false;
|
||||
try {
|
||||
const st = await (await fetch(`/api/keys/status?provider=${encodeURIComponent(p.key)}&authenticationKind=${encodeURIComponent(p.authenticationKind)}`)).json();
|
||||
complete = !!(st && st.complete);
|
||||
} catch { continue; }
|
||||
if (!complete) {
|
||||
out.push({
|
||||
id: `integration:${p.key}`,
|
||||
severity: 'problem',
|
||||
route: TARGETS.PROVIDER_KEYS.route,
|
||||
tab: targetTab(TARGETS.PROVIDER_KEYS),
|
||||
hash: targetHash(TARGETS.PROVIDER_KEYS),
|
||||
label: p.name + t('attention.integrationNoKeySuffix'),
|
||||
});
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
async function sourcesNearLimit() {
|
||||
const maand = `${new Date().toISOString().slice(0, 7)}-01T00:00`;
|
||||
const calls = new Map((await apiCallsByProvider(maand)).map(r => [r.providerId, r.calls]));
|
||||
const out = [];
|
||||
for (const r of await listProviders()) {
|
||||
if (!r.isActive || !calls.get(r.id)) continue;
|
||||
const p = await getProvider(r.key);
|
||||
if (!p || !p.callsPerMonth) continue;
|
||||
const n = calls.get(r.id);
|
||||
const deel = n / p.callsPerMonth;
|
||||
if (deel < CONFIG.API_USAGE.WARN_SHARE) continue;
|
||||
out.push({
|
||||
id: `usage:${p.key}`,
|
||||
severity: deel >= 1 ? 'problem' : 'attention',
|
||||
route: TARGETS.PROVIDER_KEYS.route,
|
||||
tab: targetTab(TARGETS.PROVIDER_KEYS),
|
||||
hash: targetHash(TARGETS.PROVIDER_KEYS),
|
||||
label: t(deel >= 1 ? 'attention.usageOver' : 'attention.usageNear', { naam: p.name, calls: n, limiet: p.callsPerMonth }),
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
async function sourcesWithoutRate() {
|
||||
const configs = new Map();
|
||||
for (const r of await listProviders()) configs.set(r.id, await getProvider(r.key));
|
||||
const wanted = new Map();
|
||||
for (const cel of await listPriceSource()) {
|
||||
const p = configs.get(cel.providerId);
|
||||
if (cel.rank == null || !p || !p.isActive) continue;
|
||||
for (const e of (p ? p.endpoints : []).filter(x => SOURCE_ENDPOINTS[cel.capability].includes(x.capability))) {
|
||||
const currency = e.quoteCurrency;
|
||||
if (!currency || currency === baseCurrency()) continue;
|
||||
wanted.set(`${p.key}|${currency}`, { provider: p, currency });
|
||||
}
|
||||
}
|
||||
const out = [];
|
||||
for (const { provider, currency } of wanted.values()) {
|
||||
if (await rateSourceFor(currency)) continue;
|
||||
out.push({
|
||||
id: `fx:${provider.key}:${currency}`,
|
||||
severity: 'problem',
|
||||
route: TARGETS.SOURCE_PROPOSALS.route,
|
||||
tab: targetTab(TARGETS.SOURCE_PROPOSALS),
|
||||
hash: targetHash(TARGETS.SOURCE_PROPOSALS),
|
||||
label: `${provider.name}: ${t('integrations.noRateSource', { valuta: currency })}`,
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const GAPS_TTL_MS = 10 * 60 * 1000;
|
||||
let gapsCache = { at: 0, total: 0 };
|
||||
if (typeof document !== 'undefined') {
|
||||
document.addEventListener('price-gaps-changed', () => { gapsCache = { at: 0, total: 0 }; });
|
||||
}
|
||||
async function priceGaps() {
|
||||
if (Date.now() - gapsCache.at > GAPS_TTL_MS) {
|
||||
try { gapsCache = { at: Date.now(), total: (await listPriceGaps()).total }; }
|
||||
catch { return []; }
|
||||
}
|
||||
if (!gapsCache.total) return [];
|
||||
return [{
|
||||
id: 'price:gaps',
|
||||
severity: 'attention',
|
||||
route: TARGETS.PRICE_HISTORY.route,
|
||||
tab: targetTab(TARGETS.PRICE_HISTORY),
|
||||
hash: targetHash(TARGETS.PRICE_HISTORY),
|
||||
label: t('attention.priceGaps', { n: gapsCache.total }),
|
||||
}];
|
||||
}
|
||||
|
||||
const ACTIE_LABEL = {
|
||||
reconcile: n => t('attention.actionsReconcilePrefix') + n + t('attention.actionsReconcileSuffix'),
|
||||
interest: n => t('attention.actionsInterestPrefix') + n,
|
||||
valuation: n => t('attention.actionsValuationPrefix') + n,
|
||||
ai_proposal: n => t('attention.actionsAiPrefix') + n,
|
||||
reservation: n => t('attention.actionsReservationPrefix') + n,
|
||||
};
|
||||
async function openActions() {
|
||||
const meldingen = await currentNotifications();
|
||||
return meldingen.filter(n => ACTIE_LABEL[n.kind] && n.count > 0).map(n => ({
|
||||
id: `actions:${n.kind}`,
|
||||
severity: 'attention',
|
||||
route: NOTIFICATION_TARGETS[n.kind].route,
|
||||
tab: targetTab(NOTIFICATION_TARGETS[n.kind]),
|
||||
hash: targetHash(NOTIFICATION_TARGETS[n.kind]),
|
||||
count: n.count,
|
||||
label: ACTIE_LABEL[n.kind](n.count),
|
||||
}));
|
||||
}
|
||||
|
||||
function backupReminder() {
|
||||
const last = appSetting('last_backup_at');
|
||||
const dayMs = 86400000;
|
||||
if (!last) {
|
||||
return [{ id: 'backup:never', route: null, severity: 'attention', hash: targetHash(TARGETS.PORTFOLIOS),
|
||||
label: t('attention.backupNever') }];
|
||||
}
|
||||
const days = Math.floor((Date.now() - new Date(last).getTime()) / dayMs);
|
||||
if (Number.isFinite(days) && days >= CONFIG.BACKUP.REMIND_AFTER_DAYS) {
|
||||
return [{ id: 'backup:stale', route: null, severity: 'attention', hash: targetHash(TARGETS.PORTFOLIOS),
|
||||
label: t('attention.backupStalePrefix') + days + t('attention.backupStaleSuffix') }];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
async function aiActivity() {
|
||||
const gezien = await getPref('ai', 'seen_call_id', 0);
|
||||
const { count } = await countAiToolCallsSince(Number(gezien) || 0);
|
||||
if (count <= 0) return [];
|
||||
return [{
|
||||
id: 'ai:activity',
|
||||
severity: 'attention',
|
||||
route: TARGETS.AI_ACTIVITY.route,
|
||||
tab: targetTab(TARGETS.AI_ACTIVITY),
|
||||
hash: targetHash(TARGETS.AI_ACTIVITY),
|
||||
count,
|
||||
label: t('attention.aiActivityPrefix') + count + t('attention.aiActivitySuffix'),
|
||||
}];
|
||||
}
|
||||
|
||||
async function licenseChanged() {
|
||||
if (!await licenseChangedSinceSeen()) return [];
|
||||
return [{
|
||||
id: 'license:changed',
|
||||
severity: 'attention',
|
||||
route: TARGETS.LICENSE.route,
|
||||
tab: targetTab(TARGETS.LICENSE),
|
||||
hash: targetHash(TARGETS.LICENSE),
|
||||
label: t('attention.licenseChanged'),
|
||||
}];
|
||||
}
|
||||
|
||||
let registered = false;
|
||||
function healthCheckReminder() {
|
||||
const last = appSetting('last_health_check_at');
|
||||
const days = last ? Math.floor((Date.now() - new Date(last).getTime()) / 86400000) : null;
|
||||
if (!last) {
|
||||
return [{ id: 'health:never', route: null, severity: 'attention', hash: targetHash(TARGETS.HEALTH),
|
||||
label: t('attention.healthNever') }];
|
||||
}
|
||||
if (Number.isFinite(days) && days >= CONFIG.HEALTH_CHECK.REMIND_AFTER_DAYS) {
|
||||
return [{ id: 'health:stale', route: null, severity: 'attention', hash: targetHash(TARGETS.HEALTH),
|
||||
label: t('attention.healthStalePrefix') + days + t('attention.healthStaleSuffix') }];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export function registerAttentionSources() {
|
||||
if (registered) return;
|
||||
registered = true;
|
||||
registerAttention(integrationsNeedingKey);
|
||||
registerAttention(sourcesWithoutRate);
|
||||
registerAttention(sourcesNearLimit);
|
||||
registerAttention(priceGaps);
|
||||
registerAttention(openActions);
|
||||
registerAttention(backupReminder);
|
||||
registerAttention(healthCheckReminder);
|
||||
registerAttention(aiActivity);
|
||||
registerAttention(licenseChanged);
|
||||
}
|
||||
Reference in new issue
Block a user