SPB Git

Repository Public

alert-catalog.ts 23.6 KB 230 lines typescript

1// Alert catalogue — the ONE place where an alert kind, its default severity, its default switch and its FR/EN wording live.2// No import: this file is shared as-is by the API (rules, webhook), the web app (Alerts screen) and the service worker (push).3// An alert stores a kind and parameters, never a sentence; the sentence is produced where it is shown, in that device's language.4//5// `body` may use every parameter. `priv` is the lock-screen wording used when "show details" is off: it may only use the6// parameters listed in SAFE_PARAMS (names, networks, counts, durations) — never an amount, a price, an address or a reason text.78export type AlertSeverity = 'info' | 'warning' | 'critical';9export type AlertGroup = 'transfers' | 'agents' | 'derivatives' | 'security' | 'system';10export type AlertLocale = 'fr' | 'en';11export type AlertParams = Record<string, string | number | boolean | null>;12/** what an alert is about; the client turns it into a deep link */13export interface AlertSubject { type: 'proposal' | 'agent' | 'account' | 'network' | 'deposit' | 'plan' | 'none'; id?: string; network?: string }14type Pair = readonly [fr: string, en: string];15export interface AlertDef {16  kind: string; group: AlertGroup; severity: AlertSeverity;17  /** default state of the owner's switch */18  on: boolean;19  /** not switchable (the cap summary and the test notification) */20  fixed?: boolean;21  title: Pair; body: Pair; priv?: Pair;22}2324const d = (kind: string, group: AlertGroup, severity: AlertSeverity, on: boolean, title: Pair, body: Pair, priv?: Pair, fixed?: boolean): AlertDef =>25  ({ kind, group, severity, on, title, body, ...(priv ? { priv } : {}), ...(fixed ? { fixed } : {}) });2627export const ALERT_DEFS: readonly AlertDef[] = [28  // ---- transfers, swaps, bridges, deposits29  d('transfer.confirmed', 'transfers', 'info', true, ['Transfert confirmé', 'Transfer confirmed'],30    ['{amount} {symbol} envoyés sur {network}.', '{amount} {symbol} sent on {network}.'], ['Sur {network}.', 'On {network}.']),31  d('transfer.failed', 'transfers', 'warning', true, ['Transfert échoué', 'Transfer failed'],32    ['{amount} {symbol} sur {network} : {reason}', '{amount} {symbol} on {network}: {reason}'], ['Sur {network}. Rien de plus n’a été envoyé.', 'On {network}. Nothing more was sent.']),33  d('swap.confirmed', 'transfers', 'info', true, ['Échange confirmé', 'Swap confirmed'],34    ['{amount} {symbol} échangés contre {toSymbol} sur {network}.', '{amount} {symbol} swapped for {toSymbol} on {network}.'], ['Sur {network}.', 'On {network}.']),35  d('swap.failed', 'transfers', 'warning', true, ['Échange échoué', 'Swap failed'],36    ['{amount} {symbol} sur {network} : {reason}', '{amount} {symbol} on {network}: {reason}'], ['Sur {network}.', 'On {network}.']),37  d('bridge.confirmed', 'transfers', 'info', true, ['Transfert inter-réseaux terminé', 'Bridge transfer completed'],38    ['{amount} {symbol} arrivés sur {to} (depuis {network}).', '{amount} {symbol} arrived on {to} (from {network}).'], ['De {network} vers {to}.', 'From {network} to {to}.']),39  d('bridge.failed', 'transfers', 'warning', true, ['Transfert inter-réseaux à vérifier', 'Bridge transfer needs attention'],40    ['{network} → {to} : {reason}', '{network} → {to}: {reason}'], ['De {network} vers {to}.', 'From {network} to {to}.']),41  d('transfer.stuck', 'transfers', 'warning', true, ['Opération en attente', 'Operation still pending'],42    ['{what} sur {network} : toujours « {state} » après {minutes} min.', '{what} on {network}: still “{state}” after {minutes} min.'],43    ['{what} sur {network} : en attente depuis {minutes} min.', '{what} on {network}: pending for {minutes} min.']),44  d('deposit.received', 'transfers', 'info', true, ['Dépôt reçu', 'Deposit received'],45    ['{amount} {symbol} reçus sur {network}.', '{amount} {symbol} received on {network}.'], ['Sur {network}.', 'On {network}.']),4647  // ---- v7 any-to-any plans: a plan never approves anything itself — each step waits for the owner's passkey48  d('plan.step_ready', 'transfers', 'info', true, ['Plan : étape à approuver', 'Plan: step to approve'],49    ['{what} : l’étape {step} sur {steps} est prête sur {network}. Elle attend votre clé d’accès.', '{what}: step {step} of {steps} is ready on {network}. It waits for your passkey.'],50    ['Une étape d’un plan d’échange attend votre approbation sur {network}.', 'A step of a swap plan waits for your approval on {network}.']),51  d('plan.paused', 'transfers', 'warning', true, ['Plan en pause', 'Plan paused'],52    ['{what} : arrêté à l’étape {step} sur {steps}. {reason} Les fonds restent dans votre portefeuille.', '{what}: stopped at step {step} of {steps}. {reason} The funds stay in your wallet.'],53    ['Un plan d’échange est en pause sur {network}. Les fonds restent dans votre portefeuille.', 'A swap plan is paused on {network}. The funds stay in your wallet.']),54  d('plan.done', 'transfers', 'info', true, ['Plan terminé', 'Plan completed'],55    ['{what} : {amount} {symbol} reçus après {steps} étape(s).', '{what}: {amount} {symbol} received after {steps} step(s).'], ['Un plan d’échange est terminé.', 'A swap plan is completed.']),5657  // ---- trading agents58  d('agent.order_filled', 'agents', 'info', true, ['Ordre d’agent exécuté', 'Agent order filled'],59    ['{agent} : {count} ordre(s) exécuté(s), dernier {from} → {to}.', '{agent}: {count} order(s) filled, last {from} → {to}.'], ['{agent} : {count} ordre(s) exécuté(s).', '{agent}: {count} order(s) filled.']),60  d('agent.stopped_loss', 'agents', 'critical', true, ['Agent arrêté : limite de perte', 'Agent stopped: loss limit'],61    ['{agent} : {reason}', '{agent}: {reason}'], ['{agent} a atteint sa limite de perte et s’est arrêté.', '{agent} reached its loss limit and stopped.']),62  d('agent.stopped_mandate', 'agents', 'warning', true, ['Agent arrêté : mandat expiré', 'Agent stopped: mandate expired'],63    ['{agent} : le mandat a expiré ; approuvez-en un nouveau pour continuer.', '{agent}: the mandate expired; approve a new one to continue.'],64    ['{agent} : le mandat a expiré.', '{agent}: the mandate expired.']),65  d('agent.stopped', 'agents', 'warning', true, ['Agent arrêté', 'Agent stopped'], ['{agent} : {reason}', '{agent}: {reason}'], ['{agent} s’est arrêté.', '{agent} stopped.']),66  d('agent.mandate_revoked', 'agents', 'info', true, ['Mandat révoqué', 'Mandate revoked'],67    ['{agent} : le mandat a été révoqué ; l’agent ne peut plus signer.', '{agent}: the mandate was revoked; the agent can no longer sign.'],68    ['{agent} : mandat révoqué.', '{agent}: mandate revoked.']),69  d('agent.data_stale', 'agents', 'warning', true, ['Agent sans données fraîches', 'Agent without fresh data'],70    ['{agent} n’a pas décidé depuis {bars} barres ({interval}) : données périmées ou incohérentes. Il n’a rien négocié.', '{agent} has not decided for {bars} bars ({interval}): stale or inconsistent data. It did not trade.'],71    ['{agent} : aucune décision depuis {bars} barres.', '{agent}: no decision for {bars} bars.']),72  d('agent.signal_failures', 'agents', 'warning', true, ['Signal de stratégie en échec', 'Strategy signal failing'],73    ['{agent} : {count} signaux invalides de suite ; l’agent conserve ses positions.', '{agent}: {count} invalid signals in a row; the agent is holding.'],74    ['{agent} : {count} signaux invalides de suite.', '{agent}: {count} invalid signals in a row.']),75  // >>> v7 research runtime76  d('agent.research_submitted', 'agents', 'info', true, ['Recherche terminée : stratégie à approuver', 'Research done: strategy awaiting approval'],77    ['{name} : backtest serveur terminé ({count} essais comptés). À approuver ou à rejeter.', '{name}: server-side backtest done ({count} trials counted). Approve or dismiss it.'],78    ['{name} : une stratégie attend votre approbation.', '{name}: a strategy awaits your approval.']),79  d('agent.research_failed', 'agents', 'warning', true, ['Tâche de recherche interrompue', 'Research job stopped'],80    ['{name} : {state} ({reason})', '{name}: {state} ({reason})'], ['{name} : {state}.', '{name}: {state}.']),81  // <<< v7 research runtime82  d('mandate.expiring', 'agents', 'warning', true, ['Mandat bientôt expiré', 'Mandate about to expire'],83    ['Le mandat de {agent} expire dans moins de {hours} h.', 'The mandate of {agent} expires in less than {hours} h.'],84    ['Le mandat de {agent} expire dans moins de {hours} h.', 'The mandate of {agent} expires in less than {hours} h.']),8586  // ---- derivatives (paper = simulated, always said)87  d('deriv.paper.filled', 'derivatives', 'info', true, ['Ordre simulé exécuté', 'Simulated order filled'],88    ['{account} (papier, simulé) : {side} {size} {instrument} à {price}.', '{account} (paper, simulated): {side} {size} {instrument} at {price}.'],89    ['{account} (papier, simulé) : {instrument}.', '{account} (paper, simulated): {instrument}.']),90  d('deriv.live.filled', 'derivatives', 'info', true, ['Ordre réel exécuté', 'Live order filled'],91    ['{account} : {side} {size} {instrument} à {price}.', '{account}: {side} {size} {instrument} at {price}.'], ['{account} : {instrument}.', '{account}: {instrument}.']),92  d('deriv.order_rejected', 'derivatives', 'warning', true, ['Ordre refusé', 'Order rejected'],93    ['{account} ({mode}) : {reason}', '{account} ({mode}): {reason}'], ['{account} ({mode}) : {count} ordre(s) refusé(s).', '{account} ({mode}): {count} order(s) rejected.']),94  d('deriv.order_unknown', 'derivatives', 'critical', true, ['Ordre réel dans un état inconnu', 'Live order in an unknown state'],95    ['{account} : l’ordre {clientId} sur {instrument} est sans réponse de la plateforme depuis plus de {seconds} s. Vérifiez sur la plateforme.', '{account}: order {clientId} on {instrument} has had no answer from the venue for more than {seconds} s. Check on the venue.'],96    ['{account} : un ordre sur {instrument} est sans réponse depuis plus de {seconds} s.', '{account}: an order on {instrument} has had no answer for more than {seconds} s.']),97  d('deriv.position_opened', 'derivatives', 'info', false, ['Position ouverte', 'Position opened'],98    ['{account} ({mode}) : {instrument}, {size}.', '{account} ({mode}): {instrument}, {size}.'], ['{account} ({mode}) : {instrument}.', '{account} ({mode}): {instrument}.']),99  d('deriv.position_closed', 'derivatives', 'info', true, ['Position fermée', 'Position closed'],100    ['{account} ({mode}) : {instrument}, PnL réalisé {pnl}.', '{account} ({mode}): {instrument}, realized PnL {pnl}.'], ['{account} ({mode}) : {instrument}.', '{account} ({mode}): {instrument}.']),101  d('deriv.margin_warning', 'derivatives', 'warning', true, ['Alerte de marge', 'Margin warning'],102    ['{account} ({mode}) : {reason}', '{account} ({mode}): {reason}'], ['{account} ({mode}) : la marge de maintien approche des fonds propres.', '{account} ({mode}): maintenance margin is close to equity.']),103  d('deriv.paper.liquidation', 'derivatives', 'warning', true, ['Liquidation simulée', 'Simulated liquidation'],104    ['{account} (papier) : liquidation SIMULÉE, {positions} position(s) fermée(s). Aucun fonds réel touché.', '{account} (paper): SIMULATED liquidation, {positions} position(s) closed. No real funds involved.'],105    ['{account} (papier) : liquidation SIMULÉE. Aucun fonds réel touché.', '{account} (paper): SIMULATED liquidation. No real funds involved.']),106  d('deriv.live.liquidation', 'derivatives', 'critical', true, ['Liquidation', 'Liquidation'],107    ['{account} : la plateforme a liquidé {instrument} ({size} à {price}).', '{account}: the venue liquidated {instrument} ({size} at {price}).'], ['{account} : liquidation sur {instrument}.', '{account}: liquidation on {instrument}.']),108  d('deriv.funding', 'derivatives', 'info', true, ['Paiement de financement', 'Funding payment'],109    ['{account} ({mode}) : {amount} USD sur {instrument}.', '{account} ({mode}): {amount} USD on {instrument}.'], ['{account} ({mode}) : {instrument}.', '{account} ({mode}): {instrument}.']),110  d('deriv.loss_limit', 'derivatives', 'critical', true, ['Limite de perte atteinte', 'Loss limit reached'],111    ['{account} ({mode}) : limite de perte {scope} atteinte ; négociation arrêtée.', '{account} ({mode}): {scope} loss limit reached; trading halted.'],112    ['{account} ({mode}) : limite de perte {scope} atteinte ; négociation arrêtée.', '{account} ({mode}): {scope} loss limit reached; trading halted.']),113  d('deriv.halt', 'derivatives', 'critical', true, ['Arrêt de négociation', 'Trading halt set'],114    ['{account} : arrêt posé. {reason}', '{account}: halt set. {reason}'], ['{account} : arrêt de négociation posé.', '{account}: trading halt set.']),115  d('deriv.divergence', 'derivatives', 'critical', true, ['Écart avec la plateforme', 'Divergence with the venue'],116    ['{account} : l’état de la plateforme et les registres du portefeuille diffèrent. {reason}', '{account}: the venue’s state and the wallet’s records differ. {reason}'],117    ['{account} : l’état de la plateforme et les registres diffèrent ; nouveaux ordres arrêtés.', '{account}: venue state and records differ; new orders are stopped.']),118  d('deriv.divergence_cleared', 'derivatives', 'info', true, ['Écart résorbé', 'Divergence cleared'],119    ['{account} : la plateforme et les registres concordent de nouveau.', '{account}: the venue and the records agree again.'],120    ['{account} : la plateforme et les registres concordent de nouveau.', '{account}: the venue and the records agree again.']),121  d('deriv.asset_table_mismatch', 'derivatives', 'critical', true, ['Table d’actifs modifiée', 'Asset table mismatch'],122    ['La table d’actifs de la plateforme ne correspond plus à la table épinglée : négociation réelle suspendue, rien n’est signé.', 'The venue’s asset table no longer matches the pinned table: live trading is suspended, nothing is signed.'],123    ['Négociation réelle suspendue, rien n’est signé.', 'Live trading is suspended, nothing is signed.']),124125  // ---- security126  d('security.passkey_added', 'security', 'critical', true, ['Nouvelle clé d’accès', 'New passkey enrolled'],127    ['Une clé d’accès « {label} » a été ajoutée. Si ce n’est pas vous : arrêt d’urgence.', 'A passkey “{label}” was added. If this was not you: emergency halt.'],128    ['Une clé d’accès a été ajoutée. Si ce n’est pas vous : arrêt d’urgence.', 'A passkey was added. If this was not you: emergency halt.']),129  d('security.new_device', 'security', 'warning', true, ['Connexion depuis un nouvel appareil', 'Sign-in from a new device'],130    ['{device} ({ip}).', '{device} ({ip}).'], ['Un appareil jamais vu a ouvert une session.', 'A device never seen before signed in.']),131  d('security.login_failed', 'security', 'warning', true, ['Connexions refusées', 'Sign-in attempts refused'],132    ['{count} tentative(s) refusée(s), dernière depuis {ip}.', '{count} attempt(s) refused, last from {ip}.'], ['{count} tentative(s) refusée(s).', '{count} attempt(s) refused.']),133  d('security.signer_unreachable', 'security', 'critical', true, ['Signataire injoignable', 'Signer unreachable'],134    ['Le signataire ne répond plus depuis {minutes} min. Rien ne peut être signé.', 'The signer has not answered for {minutes} min. Nothing can be signed.'],135    ['Le signataire ne répond plus depuis {minutes} min. Rien ne peut être signé.', 'The signer has not answered for {minutes} min. Nothing can be signed.']),136  d('security.signer_back', 'security', 'info', true, ['Signataire de nouveau joignable', 'Signer reachable again'],137    ['Le signataire répond de nouveau.', 'The signer answers again.'], ['Le signataire répond de nouveau.', 'The signer answers again.']),138  d('security.signer_halted', 'security', 'critical', true, ['Signataire à l’arrêt', 'Signer halted'],139    ['L’arrêt d’urgence est actif : plus rien n’est signé.', 'The emergency halt is on: nothing is signed any more.'], ['L’arrêt d’urgence est actif.', 'The emergency halt is on.']),140  d('security.signer_unhalted', 'security', 'warning', true, ['Signataire réactivé', 'Signer resumed'],141    ['L’arrêt d’urgence a été levé sur la machine du signataire.', 'The emergency halt was lifted on the signer machine.'], ['L’arrêt d’urgence a été levé.', 'The emergency halt was lifted.']),142  d('security.signer_refused', 'security', 'warning', true, ['Demandes refusées par le signataire', 'Requests refused by the signer'],143    ['{count} refus, dernier : {reason}', '{count} refusal(s), last: {reason}'], ['{count} demande(s) refusée(s).', '{count} request(s) refused.']),144  d('security.pin_mismatch', 'security', 'critical', true, ['Contrat épinglé modifié', 'Pinned contract changed'],145    ['{what} : le code ne correspond plus à la version révisée. Les opérations concernées sont refusées. {reason}', '{what}: the code no longer matches the reviewed version. The operations concerned are refused. {reason}'],146    ['{what} : le code ne correspond plus à la version révisée ; opérations refusées.', '{what}: the code no longer matches the reviewed version; operations refused.']),147  d('security.audit_broken', 'security', 'critical', true, ['Journal d’audit altéré', 'Audit chain broken'],148    ['La chaîne d’audit est rompue à l’événement n° {seq}.', 'The audit chain is broken at event #{seq}.'], ['La chaîne d’audit est rompue.', 'The audit chain is broken.']),149  d('security.policy_changed', 'security', 'warning', true, ['Politique modifiée', 'Policy changed'],150    ['{what} : la politique de limites a changé ({detail}).', '{what}: the limits policy changed ({detail}).'], ['{what} : la politique de limites a changé.', '{what}: the limits policy changed.']),151152  // ---- system153  d('system.api_restarted', 'system', 'info', true, ['Service redémarré', 'Service restarted'],154    ['L’API du portefeuille a redémarré.', 'The wallet API restarted.'], ['L’API du portefeuille a redémarré.', 'The wallet API restarted.']),155  d('system.worker_failing', 'system', 'warning', true, ['Tâche de fond en échec', 'Background task failing'],156    ['« {loop} » échoue depuis {count} passes : {reason}', '“{loop}” has failed {count} passes in a row: {reason}'], ['« {loop} » échoue depuis {count} passes.', '“{loop}” has failed {count} passes in a row.']),157  d('system.backup_missing', 'system', 'warning', true, ['Sauvegarde absente', 'Backup missing'],158    ['Aucune sauvegarde de la base depuis {hours} h.', 'No database backup for {hours} h.'], ['Aucune sauvegarde de la base depuis {hours} h.', 'No database backup for {hours} h.']),159  d('system.disk_low', 'system', 'warning', true, ['Espace disque faible', 'Disk space low'],160    ['Il reste {pct} % d’espace libre sur la machine de l’API.', '{pct} % of disk space is left on the API host.'], ['Il reste {pct} % d’espace libre sur la machine de l’API.', '{pct} % of disk space is left on the API host.']),161  d('conn.down', 'system', 'warning', true, ['Source indisponible', 'Source unavailable'],162    ['{name} ne répond plus depuis {minutes} min.', '{name} has not answered for {minutes} min.'], ['{name} ne répond plus depuis {minutes} min.', '{name} has not answered for {minutes} min.']),163  d('conn.up', 'system', 'info', true, ['Source rétablie', 'Source back'],164    ['{name} répond de nouveau.', '{name} answers again.'], ['{name} répond de nouveau.', '{name} answers again.']),165  d('system.alerts_capped', 'system', 'info', true, ['Autres alertes', 'More alerts'],166    ['{count} autre(s) alerte(s) dans l’application (plafond horaire de notifications atteint).', '{count} more alert(s) in the app (hourly notification cap reached).'],167    ['{count} autre(s) alerte(s) dans l’application.', '{count} more alert(s) in the app.'], true),168  d('system.test', 'system', 'info', true, ['Notification de test', 'Test notification'],169    ['Les notifications fonctionnent sur cet appareil.', 'Notifications work on this device.'], ['Les notifications fonctionnent sur cet appareil.', 'Notifications work on this device.'], true),170];171172export const ALERT_KINDS: readonly string[] = ALERT_DEFS.map((x) => x.kind);173const BY_KIND = new Map(ALERT_DEFS.map((x) => [x.kind, x]));174export const alertDef = (kind: string): AlertDef | undefined => BY_KIND.get(kind);175176/** parameters that may appear on a lock screen and in a webhook without "details" */177export const SAFE_PARAMS: readonly string[] = ['agent', 'account', 'network', 'to', 'what', 'mode', 'scope', 'count', 'minutes', 'hours', 'seconds', 'bars', 'interval', 'instrument', 'name', 'loop', 'pct', 'positions', 'state'];178179/** parameter values of the form `@word` are vocabulary, translated where the alert is shown */180const VOCAB: Record<string, Pair> = {181  paper: ['papier, simulé', 'paper, simulated'], live: ['réel', 'live'], transfer: ['Transfert', 'Transfer'], swap: ['Échange', 'Swap'], bridge: ['Transfert inter-réseaux', 'Bridge transfer'],182  buy: ['achat', 'buy'], sell: ['vente', 'sell'], daily: ['quotidienne', 'daily'], total: ['totale', 'total'],183  router: ['Routeur d’échange', 'Swap router'], jupiter: ['Programme Jupiter', 'Jupiter program'], cctp: ['Contrats CCTP (Circle)', 'CCTP contracts (Circle)'],184  signer: ['Signataire', 'Signer'], derivatives: ['Compte de dérivés', 'Derivatives account'],185  signed: ['signé', 'signed'], broadcast: ['diffusé', 'broadcast'], confirming: ['en confirmation', 'confirming'], attesting: ['en attente d’attestation', 'awaiting attestation'], minting: ['en cours de réception', 'minting'],186  market: ['Données de marché', 'Market data'], rpc: ['RPC', 'RPC'], unknown: ['inconnu', 'unknown'],187  failed: ['échec', 'failed'], over_budget: ['budget dépassé', 'over budget'], // v7 research runtime188};189const word = (v: unknown, l: AlertLocale): string => {190  if (v === null || v === undefined || v === '') return '—';191  const s = String(v);192  if (!s.startsWith('@')) return s;193  const w = VOCAB[s.slice(1)];194  return w ? w[l === 'fr' ? 0 : 1] : s.slice(1);195};196const fill = (tpl: string, p: AlertParams, l: AlertLocale): string => tpl.replace(/\{([a-zA-Z]+)\}/g, (_m, k: string) => word(p[k], l)).replace(/\s+/g, ' ').trim();197198/** only the parameters that may leave the server without "show details" */199export function safeParams(p: AlertParams): AlertParams {200  const out: AlertParams = {};201  for (const k of SAFE_PARAMS) if (p[k] !== undefined) out[k] = p[k]!;202  return out;203}204205/** Title and body of an alert in one language. `detail: false` gives the lock-screen wording (no amount, price, address or reason). */206export function renderAlert(kind: string, params: AlertParams, locale: AlertLocale, detail = true): { title: string; body: string } {207  const def = BY_KIND.get(kind), i = locale === 'fr' ? 0 : 1;208  if (!def) return { title: kind, body: '' };209  const tpl = detail ? def.body : def.priv ?? (['', ''] as const);210  return { title: def.title[i], body: fill(tpl[i], detail ? params : safeParams(params), locale) };211}212213/** placeholders used by a template (tests check that `priv` only uses safe parameters) */214export const alertPlaceholders = (tpl: string): string[] => [...tpl.matchAll(/\{([a-zA-Z]+)\}/g)].map((m) => m[1]!);215216/** in-app path opened when the alert (or its notification) is tapped */217export function alertPath(kind: string, subject: AlertSubject | null | undefined): string {218  const g = BY_KIND.get(kind)?.group;219  if (subject?.type === 'agent' && subject.id) return `/agents/${subject.id}`;220  if (subject?.type === 'plan' && subject.id) return `/plan/${subject.id}`; // v7 plans: the plan's progress view221  if (subject?.type === 'account') return '/futures?v=accounts';222  if (subject?.type === 'proposal' || subject?.type === 'deposit') return '/activity';223  if (g === 'security') return '/security';224  if (g === 'system') return '/monitor';225  if (g === 'agents') return '/agents';226  if (g === 'derivatives') return '/futures?v=accounts';227  if (g === 'transfers') return '/activity';228  return '/alerts';229}230
Files main
Menu

Theme