206 lines
7.8 KiB
JavaScript
206 lines
7.8 KiB
JavaScript
// 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';
|
|
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';
|
|
import { placeAvailable } from '../routes.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 (!placeAvailable(TARGETS.PRICE_HISTORY)) return [];
|
|
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);
|
|
}
|