@waiaas/daemon 2.8.0-rc.1 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/README.md +17 -5
  2. package/dist/api/middleware/master-auth.d.ts +8 -1
  3. package/dist/api/middleware/master-auth.d.ts.map +1 -1
  4. package/dist/api/middleware/master-auth.js +8 -1
  5. package/dist/api/middleware/master-auth.js.map +1 -1
  6. package/dist/api/routes/actions.d.ts +3 -0
  7. package/dist/api/routes/actions.d.ts.map +1 -1
  8. package/dist/api/routes/actions.js +1 -1
  9. package/dist/api/routes/actions.js.map +1 -1
  10. package/dist/api/routes/admin.d.ts +3 -0
  11. package/dist/api/routes/admin.d.ts.map +1 -1
  12. package/dist/api/routes/admin.js +200 -1
  13. package/dist/api/routes/admin.js.map +1 -1
  14. package/dist/api/routes/defi-positions.d.ts +31 -0
  15. package/dist/api/routes/defi-positions.d.ts.map +1 -0
  16. package/dist/api/routes/defi-positions.js +182 -0
  17. package/dist/api/routes/defi-positions.js.map +1 -0
  18. package/dist/api/routes/openapi-schemas.d.ts +176 -5
  19. package/dist/api/routes/openapi-schemas.d.ts.map +1 -1
  20. package/dist/api/routes/openapi-schemas.js +43 -0
  21. package/dist/api/routes/openapi-schemas.js.map +1 -1
  22. package/dist/api/routes/transactions.d.ts +3 -0
  23. package/dist/api/routes/transactions.d.ts.map +1 -1
  24. package/dist/api/routes/transactions.js +2 -2
  25. package/dist/api/routes/transactions.js.map +1 -1
  26. package/dist/api/routes/wallets.d.ts +3 -0
  27. package/dist/api/routes/wallets.d.ts.map +1 -1
  28. package/dist/api/routes/wallets.js +3 -2
  29. package/dist/api/routes/wallets.js.map +1 -1
  30. package/dist/api/routes/x402.d.ts +3 -0
  31. package/dist/api/routes/x402.d.ts.map +1 -1
  32. package/dist/api/routes/x402.js +1 -1
  33. package/dist/api/routes/x402.js.map +1 -1
  34. package/dist/api/server.d.ts +3 -0
  35. package/dist/api/server.d.ts.map +1 -1
  36. package/dist/api/server.js +35 -18
  37. package/dist/api/server.js.map +1 -1
  38. package/dist/infrastructure/action/action-provider-registry.d.ts.map +1 -1
  39. package/dist/infrastructure/action/action-provider-registry.js +2 -0
  40. package/dist/infrastructure/action/action-provider-registry.js.map +1 -1
  41. package/dist/infrastructure/action/api-key-store.d.ts +5 -1
  42. package/dist/infrastructure/action/api-key-store.d.ts.map +1 -1
  43. package/dist/infrastructure/action/api-key-store.js +10 -4
  44. package/dist/infrastructure/action/api-key-store.js.map +1 -1
  45. package/dist/infrastructure/database/migrate.d.ts +1 -1
  46. package/dist/infrastructure/database/migrate.d.ts.map +1 -1
  47. package/dist/infrastructure/database/migrate.js +110 -2
  48. package/dist/infrastructure/database/migrate.js.map +1 -1
  49. package/dist/infrastructure/database/schema.d.ts +299 -0
  50. package/dist/infrastructure/database/schema.d.ts.map +1 -1
  51. package/dist/infrastructure/database/schema.js +31 -1
  52. package/dist/infrastructure/database/schema.js.map +1 -1
  53. package/dist/infrastructure/keystore/re-encrypt.d.ts +28 -0
  54. package/dist/infrastructure/keystore/re-encrypt.d.ts.map +1 -0
  55. package/dist/infrastructure/keystore/re-encrypt.js +134 -0
  56. package/dist/infrastructure/keystore/re-encrypt.js.map +1 -0
  57. package/dist/infrastructure/settings/setting-keys.d.ts +1 -1
  58. package/dist/infrastructure/settings/setting-keys.d.ts.map +1 -1
  59. package/dist/infrastructure/settings/setting-keys.js +8 -0
  60. package/dist/infrastructure/settings/setting-keys.js.map +1 -1
  61. package/dist/infrastructure/settings/settings-service.d.ts +6 -0
  62. package/dist/infrastructure/settings/settings-service.d.ts.map +1 -1
  63. package/dist/infrastructure/settings/settings-service.js +10 -4
  64. package/dist/infrastructure/settings/settings-service.js.map +1 -1
  65. package/dist/lifecycle/daemon.d.ts +8 -1
  66. package/dist/lifecycle/daemon.d.ts.map +1 -1
  67. package/dist/lifecycle/daemon.js +144 -3
  68. package/dist/lifecycle/daemon.js.map +1 -1
  69. package/dist/notifications/notification-service.d.ts.map +1 -1
  70. package/dist/notifications/notification-service.js +6 -3
  71. package/dist/notifications/notification-service.js.map +1 -1
  72. package/dist/notifications/templates/message-templates.d.ts.map +1 -1
  73. package/dist/notifications/templates/message-templates.js +32 -2
  74. package/dist/notifications/templates/message-templates.js.map +1 -1
  75. package/dist/pipeline/database-policy-engine.d.ts +30 -0
  76. package/dist/pipeline/database-policy-engine.d.ts.map +1 -1
  77. package/dist/pipeline/database-policy-engine.js +149 -2
  78. package/dist/pipeline/database-policy-engine.js.map +1 -1
  79. package/dist/pipeline/sign-only.d.ts.map +1 -1
  80. package/dist/pipeline/sign-only.js +1 -0
  81. package/dist/pipeline/sign-only.js.map +1 -1
  82. package/dist/pipeline/stages.d.ts.map +1 -1
  83. package/dist/pipeline/stages.js +7 -0
  84. package/dist/pipeline/stages.js.map +1 -1
  85. package/dist/services/autostop-service.d.ts +3 -3
  86. package/dist/services/autostop-service.d.ts.map +1 -1
  87. package/dist/services/autostop-service.js +8 -15
  88. package/dist/services/autostop-service.js.map +1 -1
  89. package/dist/services/defi/position-tracker.d.ts +61 -0
  90. package/dist/services/defi/position-tracker.d.ts.map +1 -0
  91. package/dist/services/defi/position-tracker.js +156 -0
  92. package/dist/services/defi/position-tracker.js.map +1 -0
  93. package/dist/services/defi/position-write-queue.d.ts +37 -0
  94. package/dist/services/defi/position-write-queue.d.ts.map +1 -0
  95. package/dist/services/defi/position-write-queue.js +92 -0
  96. package/dist/services/defi/position-write-queue.js.map +1 -0
  97. package/dist/services/monitoring/defi-monitor-service.d.ts +29 -0
  98. package/dist/services/monitoring/defi-monitor-service.d.ts.map +1 -0
  99. package/dist/services/monitoring/defi-monitor-service.js +62 -0
  100. package/dist/services/monitoring/defi-monitor-service.js.map +1 -0
  101. package/dist/services/monitoring/health-factor-monitor.d.ts +66 -0
  102. package/dist/services/monitoring/health-factor-monitor.d.ts.map +1 -0
  103. package/dist/services/monitoring/health-factor-monitor.js +247 -0
  104. package/dist/services/monitoring/health-factor-monitor.js.map +1 -0
  105. package/package.json +5 -5
  106. package/public/admin/assets/index-BpDnuS0k.css +1 -0
  107. package/public/admin/assets/index-CgtuiQGn.js +1 -0
  108. package/public/admin/index.html +2 -2
  109. package/public/admin/assets/index-DDqQR5T7.css +0 -1
  110. package/public/admin/assets/index-xzLnQ5sm.js +0 -1
@@ -0,0 +1,29 @@
1
+ /**
2
+ * DeFiMonitorService: Lifecycle orchestrator for DeFi monitors.
3
+ *
4
+ * Manages multiple IDeFiMonitor instances (HealthFactorMonitor, etc.)
5
+ * with a single start/stop/updateConfig entry point for DaemonLifecycle.
6
+ *
7
+ * Features:
8
+ * - Per-monitor error isolation (fail-soft)
9
+ * - register() / start() / stop() lifecycle
10
+ * - updateConfig() propagation
11
+ *
12
+ * Design source: m29-00 design doc section 9.
13
+ * @see LEND-04
14
+ */
15
+ import type { IDeFiMonitor } from '@waiaas/core';
16
+ export declare class DeFiMonitorService {
17
+ private readonly monitors;
18
+ /** Register a monitor instance. */
19
+ register(monitor: IDeFiMonitor): void;
20
+ /** Start all registered monitors (fail-soft per monitor). */
21
+ start(): void;
22
+ /** Stop all registered monitors (fail-soft per monitor). */
23
+ stop(): void;
24
+ /** Propagate configuration updates to all monitors (fail-soft per monitor). */
25
+ updateConfig(config: Record<string, unknown>): void;
26
+ /** Number of registered monitors. */
27
+ get monitorCount(): number;
28
+ }
29
+ //# sourceMappingURL=defi-monitor-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"defi-monitor-service.d.ts","sourceRoot":"","sources":["../../../src/services/monitoring/defi-monitor-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAMjD,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAmC;IAE5D,mCAAmC;IACnC,QAAQ,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI;IAIrC,6DAA6D;IAC7D,KAAK,IAAI,IAAI;IAUb,4DAA4D;IAC5D,IAAI,IAAI,IAAI;IAUZ,+EAA+E;IAC/E,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAUnD,qCAAqC;IACrC,IAAI,YAAY,IAAI,MAAM,CAEzB;CACF"}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * DeFiMonitorService: Lifecycle orchestrator for DeFi monitors.
3
+ *
4
+ * Manages multiple IDeFiMonitor instances (HealthFactorMonitor, etc.)
5
+ * with a single start/stop/updateConfig entry point for DaemonLifecycle.
6
+ *
7
+ * Features:
8
+ * - Per-monitor error isolation (fail-soft)
9
+ * - register() / start() / stop() lifecycle
10
+ * - updateConfig() propagation
11
+ *
12
+ * Design source: m29-00 design doc section 9.
13
+ * @see LEND-04
14
+ */
15
+ // ---------------------------------------------------------------------------
16
+ // DeFiMonitorService
17
+ // ---------------------------------------------------------------------------
18
+ export class DeFiMonitorService {
19
+ monitors = new Map();
20
+ /** Register a monitor instance. */
21
+ register(monitor) {
22
+ this.monitors.set(monitor.name, monitor);
23
+ }
24
+ /** Start all registered monitors (fail-soft per monitor). */
25
+ start() {
26
+ for (const monitor of this.monitors.values()) {
27
+ try {
28
+ monitor.start();
29
+ }
30
+ catch (err) {
31
+ console.warn(`DeFiMonitorService: failed to start monitor '${monitor.name}':`, err);
32
+ }
33
+ }
34
+ }
35
+ /** Stop all registered monitors (fail-soft per monitor). */
36
+ stop() {
37
+ for (const monitor of this.monitors.values()) {
38
+ try {
39
+ monitor.stop();
40
+ }
41
+ catch (err) {
42
+ console.warn(`DeFiMonitorService: failed to stop monitor '${monitor.name}':`, err);
43
+ }
44
+ }
45
+ }
46
+ /** Propagate configuration updates to all monitors (fail-soft per monitor). */
47
+ updateConfig(config) {
48
+ for (const monitor of this.monitors.values()) {
49
+ try {
50
+ monitor.updateConfig?.(config);
51
+ }
52
+ catch (err) {
53
+ console.warn(`DeFiMonitorService: failed to update config for '${monitor.name}':`, err);
54
+ }
55
+ }
56
+ }
57
+ /** Number of registered monitors. */
58
+ get monitorCount() {
59
+ return this.monitors.size;
60
+ }
61
+ }
62
+ //# sourceMappingURL=defi-monitor-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"defi-monitor-service.js","sourceRoot":"","sources":["../../../src/services/monitoring/defi-monitor-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,8EAA8E;AAC9E,qBAAqB;AACrB,8EAA8E;AAE9E,MAAM,OAAO,kBAAkB;IACZ,QAAQ,GAAG,IAAI,GAAG,EAAwB,CAAC;IAE5D,mCAAmC;IACnC,QAAQ,CAAC,OAAqB;QAC5B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAC3C,CAAC;IAED,6DAA6D;IAC7D,KAAK;QACH,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC;YAC7C,IAAI,CAAC;gBACH,OAAO,CAAC,KAAK,EAAE,CAAC;YAClB,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,OAAO,CAAC,IAAI,CAAC,gDAAgD,OAAO,CAAC,IAAI,IAAI,EAAE,GAAG,CAAC,CAAC;YACtF,CAAC;QACH,CAAC;IACH,CAAC;IAED,4DAA4D;IAC5D,IAAI;QACF,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC;YAC7C,IAAI,CAAC;gBACH,OAAO,CAAC,IAAI,EAAE,CAAC;YACjB,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,OAAO,CAAC,IAAI,CAAC,+CAA+C,OAAO,CAAC,IAAI,IAAI,EAAE,GAAG,CAAC,CAAC;YACrF,CAAC;QACH,CAAC;IACH,CAAC;IAED,+EAA+E;IAC/E,YAAY,CAAC,MAA+B;QAC1C,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC;YAC7C,IAAI,CAAC;gBACH,OAAO,CAAC,YAAY,EAAE,CAAC,MAAM,CAAC,CAAC;YACjC,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,OAAO,CAAC,IAAI,CAAC,oDAAoD,OAAO,CAAC,IAAI,IAAI,EAAE,GAAG,CAAC,CAAC;YAC1F,CAAC;QACH,CAAC;IACH,CAAC;IAED,qCAAqC;IACrC,IAAI,YAAY;QACd,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;IAC5B,CAAC;CACF"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * HealthFactorMonitor: Adaptive polling health factor monitor.
3
+ *
4
+ * Reads LENDING positions from defi_positions DB cache and evaluates
5
+ * health factor against configurable severity thresholds. Adjusts its
6
+ * own polling interval based on worst severity across all positions.
7
+ *
8
+ * Implements IDeFiMonitor for DeFiMonitorService orchestration.
9
+ *
10
+ * Features:
11
+ * - 4-level severity: SAFE(5min) -> WARNING(1min) -> DANGER(15s) -> CRITICAL(5s)
12
+ * - Recursive setTimeout (not setInterval) for dynamic interval changes
13
+ * - Cooldown: WARNING/DANGER 4-hour cooldown per walletId:positionId, no cooldown for CRITICAL
14
+ * - On-demand PositionTracker sync when entering DANGER/CRITICAL
15
+ * - Cooldown cleanup on recovery to SAFE
16
+ *
17
+ * Design source: m29-00 design doc section 10.1.
18
+ * @see LEND-05, LEND-06
19
+ */
20
+ import type { Database } from 'better-sqlite3';
21
+ import type { IDeFiMonitor, MonitorSeverity } from '@waiaas/core';
22
+ import type { NotificationService } from '../../notifications/notification-service.js';
23
+ import type { PositionTracker } from '../defi/position-tracker.js';
24
+ import type { SettingsService } from '../../infrastructure/settings/settings-service.js';
25
+ export interface HealthFactorMonitorConfig {
26
+ safeThreshold: number;
27
+ warningThreshold: number;
28
+ dangerThreshold: number;
29
+ cooldownHours: number;
30
+ maxLtvPct?: number;
31
+ }
32
+ export declare class HealthFactorMonitor implements IDeFiMonitor {
33
+ readonly name = "health-factor";
34
+ private readonly sqlite;
35
+ private readonly notificationService;
36
+ private readonly positionTracker;
37
+ private config;
38
+ private timer;
39
+ private currentSeverity;
40
+ private readonly cooldownMap;
41
+ constructor(opts: {
42
+ sqlite: Database;
43
+ notificationService?: NotificationService;
44
+ positionTracker?: PositionTracker;
45
+ config?: Partial<HealthFactorMonitorConfig>;
46
+ });
47
+ start(): void;
48
+ stop(): void;
49
+ updateConfig(config: Record<string, unknown>): void;
50
+ /** Load config overrides from Admin Settings (Phase 278). */
51
+ loadFromSettings(settingsService: SettingsService): void;
52
+ getCurrentSeverity(): MonitorSeverity;
53
+ getCooldownMapSize(): number;
54
+ private scheduleNext;
55
+ /**
56
+ * Check all active LENDING positions and evaluate health factors.
57
+ * Exposed for direct testing access.
58
+ */
59
+ checkAllPositions(): Promise<void>;
60
+ private emitAlert;
61
+ private classifySeverity;
62
+ private getThresholdForSeverity;
63
+ /** Higher rank = more severe. */
64
+ private severityRank;
65
+ }
66
+ //# sourceMappingURL=health-factor-monitor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"health-factor-monitor.d.ts","sourceRoot":"","sources":["../../../src/services/monitoring/health-factor-monitor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAqB,MAAM,cAAc,CAAC;AACrF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,6CAA6C,CAAC;AACvF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mDAAmD,CAAC;AAMzF,MAAM,WAAW,yBAAyB;IACxC,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,MAAM,CAAC;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAiCD,qBAAa,mBAAoB,YAAW,YAAY;IACtD,QAAQ,CAAC,IAAI,mBAAmB;IAEhC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAW;IAClC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAA6B;IACjE,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAyB;IACzD,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,KAAK,CAA8C;IAC3D,OAAO,CAAC,eAAe,CAA2B;IAClD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA6B;gBAE7C,IAAI,EAAE;QAChB,MAAM,EAAE,QAAQ,CAAC;QACjB,mBAAmB,CAAC,EAAE,mBAAmB,CAAC;QAC1C,eAAe,CAAC,EAAE,eAAe,CAAC;QAClC,MAAM,CAAC,EAAE,OAAO,CAAC,yBAAyB,CAAC,CAAC;KAC7C;IAWD,KAAK,IAAI,IAAI;IAIb,IAAI,IAAI,IAAI;IAOZ,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAkBnD,6DAA6D;IAC7D,gBAAgB,CAAC,eAAe,EAAE,eAAe,GAAG,IAAI;IAqBxD,kBAAkB,IAAI,eAAe;IAIrC,kBAAkB,IAAI,MAAM;IAQ5B,OAAO,CAAC,YAAY;IAapB;;;OAGG;IACG,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC;IAuExC,OAAO,CAAC,SAAS;IA4BjB,OAAO,CAAC,gBAAgB;IAOxB,OAAO,CAAC,uBAAuB;IAa/B,iCAAiC;IACjC,OAAO,CAAC,YAAY;CAYrB"}
@@ -0,0 +1,247 @@
1
+ /**
2
+ * HealthFactorMonitor: Adaptive polling health factor monitor.
3
+ *
4
+ * Reads LENDING positions from defi_positions DB cache and evaluates
5
+ * health factor against configurable severity thresholds. Adjusts its
6
+ * own polling interval based on worst severity across all positions.
7
+ *
8
+ * Implements IDeFiMonitor for DeFiMonitorService orchestration.
9
+ *
10
+ * Features:
11
+ * - 4-level severity: SAFE(5min) -> WARNING(1min) -> DANGER(15s) -> CRITICAL(5s)
12
+ * - Recursive setTimeout (not setInterval) for dynamic interval changes
13
+ * - Cooldown: WARNING/DANGER 4-hour cooldown per walletId:positionId, no cooldown for CRITICAL
14
+ * - On-demand PositionTracker sync when entering DANGER/CRITICAL
15
+ * - Cooldown cleanup on recovery to SAFE
16
+ *
17
+ * Design source: m29-00 design doc section 10.1.
18
+ * @see LEND-05, LEND-06
19
+ */
20
+ const DEFAULT_CONFIG = {
21
+ safeThreshold: 2.0,
22
+ warningThreshold: 1.5,
23
+ dangerThreshold: 1.2,
24
+ cooldownHours: 4,
25
+ };
26
+ /** Polling intervals per severity level (milliseconds). */
27
+ const SEVERITY_INTERVALS = {
28
+ SAFE: 300_000, // 5 min
29
+ WARNING: 60_000, // 1 min
30
+ DANGER: 15_000, // 15 sec
31
+ CRITICAL: 5_000, // 5 sec
32
+ };
33
+ // ---------------------------------------------------------------------------
34
+ // HealthFactorMonitor
35
+ // ---------------------------------------------------------------------------
36
+ export class HealthFactorMonitor {
37
+ name = 'health-factor';
38
+ sqlite;
39
+ notificationService;
40
+ positionTracker;
41
+ config;
42
+ timer = null;
43
+ currentSeverity = 'SAFE';
44
+ cooldownMap = new Map();
45
+ constructor(opts) {
46
+ this.sqlite = opts.sqlite;
47
+ this.notificationService = opts.notificationService ?? null;
48
+ this.positionTracker = opts.positionTracker ?? null;
49
+ this.config = { ...DEFAULT_CONFIG, ...opts.config };
50
+ }
51
+ // -----------------------------------------------------------------------
52
+ // IDeFiMonitor lifecycle
53
+ // -----------------------------------------------------------------------
54
+ start() {
55
+ this.scheduleNext();
56
+ }
57
+ stop() {
58
+ if (this.timer) {
59
+ clearTimeout(this.timer);
60
+ this.timer = null;
61
+ }
62
+ }
63
+ updateConfig(config) {
64
+ if (typeof config['health_factor_safe_threshold'] === 'number') {
65
+ this.config.safeThreshold = config['health_factor_safe_threshold'];
66
+ }
67
+ if (typeof config['health_factor_warning_threshold'] === 'number') {
68
+ this.config.warningThreshold = config['health_factor_warning_threshold'];
69
+ }
70
+ if (typeof config['health_factor_danger_threshold'] === 'number') {
71
+ this.config.dangerThreshold = config['health_factor_danger_threshold'];
72
+ }
73
+ if (typeof config['cooldown_hours'] === 'number') {
74
+ this.config.cooldownHours = config['cooldown_hours'];
75
+ }
76
+ if (typeof config['max_ltv_pct'] === 'number') {
77
+ this.config.maxLtvPct = config['max_ltv_pct'];
78
+ }
79
+ }
80
+ /** Load config overrides from Admin Settings (Phase 278). */
81
+ loadFromSettings(settingsService) {
82
+ try {
83
+ const threshold = settingsService.get('actions.aave_v3_health_factor_warning_threshold');
84
+ const parsed = Number(threshold);
85
+ if (!Number.isNaN(parsed) && parsed > 0) {
86
+ this.updateConfig({ health_factor_warning_threshold: parsed });
87
+ }
88
+ }
89
+ catch { /* fallback to default */ }
90
+ try {
91
+ const maxLtv = settingsService.get('actions.aave_v3_max_ltv_pct');
92
+ const parsed = Number(maxLtv);
93
+ if (!Number.isNaN(parsed) && parsed > 0 && parsed <= 1) {
94
+ this.updateConfig({ max_ltv_pct: parsed });
95
+ }
96
+ }
97
+ catch { /* fallback to default */ }
98
+ }
99
+ // -----------------------------------------------------------------------
100
+ // Testing accessors
101
+ // -----------------------------------------------------------------------
102
+ getCurrentSeverity() {
103
+ return this.currentSeverity;
104
+ }
105
+ getCooldownMapSize() {
106
+ return this.cooldownMap.size;
107
+ }
108
+ // -----------------------------------------------------------------------
109
+ // Core logic
110
+ // -----------------------------------------------------------------------
111
+ scheduleNext() {
112
+ const interval = SEVERITY_INTERVALS[this.currentSeverity];
113
+ this.timer = setTimeout(async () => {
114
+ try {
115
+ await this.checkAllPositions();
116
+ }
117
+ catch (err) {
118
+ console.warn('HealthFactorMonitor check error:', err);
119
+ }
120
+ this.scheduleNext(); // recursive reschedule
121
+ }, interval);
122
+ this.timer.unref();
123
+ }
124
+ /**
125
+ * Check all active LENDING positions and evaluate health factors.
126
+ * Exposed for direct testing access.
127
+ */
128
+ async checkAllPositions() {
129
+ // Read all ACTIVE LENDING positions from DB cache
130
+ const positions = this.sqlite
131
+ .prepare("SELECT id, wallet_id, provider, metadata, status FROM defi_positions WHERE category = 'LENDING' AND status = 'ACTIVE'")
132
+ .all();
133
+ let worstSeverity = 'SAFE';
134
+ const recoveredKeys = new Set();
135
+ // Track which keys we've seen to identify recoveries
136
+ const seenKeys = new Set();
137
+ for (const pos of positions) {
138
+ if (!pos.metadata)
139
+ continue;
140
+ let meta;
141
+ try {
142
+ meta = JSON.parse(pos.metadata);
143
+ }
144
+ catch {
145
+ continue;
146
+ }
147
+ const hf = meta.healthFactor;
148
+ if (typeof hf !== 'number')
149
+ continue;
150
+ const severity = this.classifySeverity(hf);
151
+ const key = `${pos.wallet_id}:${pos.id}`;
152
+ seenKeys.add(key);
153
+ // Track worst severity
154
+ if (this.severityRank(severity) > this.severityRank(worstSeverity)) {
155
+ worstSeverity = severity;
156
+ }
157
+ if (severity === 'SAFE') {
158
+ // Recovery: clear cooldown
159
+ if (this.cooldownMap.has(key)) {
160
+ recoveredKeys.add(key);
161
+ this.cooldownMap.delete(key);
162
+ }
163
+ continue;
164
+ }
165
+ // Non-SAFE: emit alert
166
+ const evaluation = {
167
+ walletId: pos.wallet_id,
168
+ positionId: pos.id,
169
+ severity,
170
+ value: hf,
171
+ threshold: this.getThresholdForSeverity(severity),
172
+ provider: pos.provider,
173
+ };
174
+ this.emitAlert(evaluation);
175
+ }
176
+ // Update current severity
177
+ this.currentSeverity = worstSeverity;
178
+ // On-demand sync for DANGER/CRITICAL
179
+ if (worstSeverity === 'DANGER' || worstSeverity === 'CRITICAL') {
180
+ void this.positionTracker?.syncCategory('LENDING');
181
+ }
182
+ }
183
+ // -----------------------------------------------------------------------
184
+ // Alert emission
185
+ // -----------------------------------------------------------------------
186
+ emitAlert(evaluation) {
187
+ const key = `${evaluation.walletId}:${evaluation.positionId}`;
188
+ const now = Date.now();
189
+ const cooldownMs = this.config.cooldownHours * 60 * 60 * 1000;
190
+ const vars = {
191
+ healthFactor: evaluation.value.toFixed(2),
192
+ threshold: evaluation.threshold.toFixed(2),
193
+ };
194
+ if (evaluation.severity === 'CRITICAL') {
195
+ // CRITICAL: no cooldown -- always send
196
+ void this.notificationService?.notify('LIQUIDATION_IMMINENT', evaluation.walletId, vars);
197
+ this.cooldownMap.set(key, now);
198
+ }
199
+ else {
200
+ // WARNING/DANGER: 4-hour cooldown
201
+ const lastAlert = this.cooldownMap.get(key);
202
+ if (lastAlert && now - lastAlert < cooldownMs) {
203
+ return; // cooldown active
204
+ }
205
+ void this.notificationService?.notify('LIQUIDATION_WARNING', evaluation.walletId, vars);
206
+ this.cooldownMap.set(key, now);
207
+ }
208
+ }
209
+ // -----------------------------------------------------------------------
210
+ // Severity classification
211
+ // -----------------------------------------------------------------------
212
+ classifySeverity(hf) {
213
+ if (hf < this.config.dangerThreshold)
214
+ return 'CRITICAL';
215
+ if (hf < this.config.warningThreshold)
216
+ return 'DANGER';
217
+ if (hf < this.config.safeThreshold)
218
+ return 'WARNING';
219
+ return 'SAFE';
220
+ }
221
+ getThresholdForSeverity(severity) {
222
+ switch (severity) {
223
+ case 'CRITICAL':
224
+ return this.config.dangerThreshold;
225
+ case 'DANGER':
226
+ return this.config.warningThreshold;
227
+ case 'WARNING':
228
+ return this.config.safeThreshold;
229
+ default:
230
+ return this.config.safeThreshold;
231
+ }
232
+ }
233
+ /** Higher rank = more severe. */
234
+ severityRank(severity) {
235
+ switch (severity) {
236
+ case 'SAFE':
237
+ return 0;
238
+ case 'WARNING':
239
+ return 1;
240
+ case 'DANGER':
241
+ return 2;
242
+ case 'CRITICAL':
243
+ return 3;
244
+ }
245
+ }
246
+ }
247
+ //# sourceMappingURL=health-factor-monitor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"health-factor-monitor.js","sourceRoot":"","sources":["../../../src/services/monitoring/health-factor-monitor.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@waiaas/daemon",
3
- "version": "2.8.0-rc.1",
3
+ "version": "2.8.0",
4
4
  "description": "WAIaaS daemon - self-hosted wallet service for AI agents",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -47,10 +47,10 @@
47
47
  "uuidv7": "^1.0.2",
48
48
  "viem": "^2.21.0",
49
49
  "zod": "^3.24.0",
50
- "@waiaas/adapter-evm": "2.8.0-rc.1",
51
- "@waiaas/actions": "2.8.0-rc.1",
52
- "@waiaas/adapter-solana": "2.8.0-rc.1",
53
- "@waiaas/core": "2.8.0-rc.1"
50
+ "@waiaas/actions": "2.8.0",
51
+ "@waiaas/adapter-evm": "2.8.0",
52
+ "@waiaas/adapter-solana": "2.8.0",
53
+ "@waiaas/core": "2.8.0"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@types/better-sqlite3": "^7.6.0",
@@ -0,0 +1 @@
1
+ *,*:before,*:after{box-sizing:border-box;margin:0;padding:0}:root{--color-primary: #2563eb;--color-primary-hover: #1d4ed8;--color-primary-light: #eff6ff;--color-bg: #ffffff;--color-bg-secondary: #f8fafc;--color-bg-tertiary: #f1f5f9;--color-border: #e2e8f0;--color-text: #0f172a;--color-text-secondary: #64748b;--color-text-muted: #94a3b8;--color-success: #16a34a;--color-success-bg: #f0fdf4;--color-warning: #d97706;--color-warning-bg: #fffbeb;--color-danger: #dc2626;--color-danger-bg: #fef2f2;--color-info: #2563eb;--color-info-bg: #eff6ff;--color-tier-instant: #16a34a;--color-tier-delay: #d97706;--color-tier-blocked: #dc2626;--space-1: .25rem;--space-2: .5rem;--space-3: .75rem;--space-4: 1rem;--space-6: 1.5rem;--space-8: 2rem;--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;--font-size-xs: .75rem;--font-size-sm: .875rem;--font-size-base: 1rem;--font-size-lg: 1.125rem;--font-size-xl: 1.25rem;--font-size-2xl: 1.5rem;--font-weight-normal: 400;--font-weight-medium: 500;--font-weight-semibold: 600;--font-weight-bold: 700;--line-height: 1.5;--radius-sm: .25rem;--radius-md: .375rem;--radius-lg: .5rem;--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / .05);--shadow-md: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--sidebar-width: 220px;--header-height: 56px;--content-max-width: 1200px}body{font-family:var(--font-family);font-size:var(--font-size-base);line-height:var(--line-height);background:var(--color-bg);color:var(--color-text);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}a{color:var(--color-primary);text-decoration:none}button{cursor:pointer;font-family:inherit}input,select,textarea{font-family:inherit;font-size:inherit}.layout{display:flex;min-height:100vh}.sidebar{width:var(--sidebar-width);background:var(--color-bg);border-right:1px solid var(--color-border);display:flex;flex-direction:column;position:fixed;top:0;left:0;bottom:0;z-index:10}.sidebar-brand{padding:var(--space-4) var(--space-4);font-size:var(--font-size-lg);font-weight:var(--font-weight-bold);color:var(--color-primary);border-bottom:1px solid var(--color-border)}.sidebar-nav{display:flex;flex-direction:column;padding:var(--space-2);gap:var(--space-1)}.sidebar-link{display:block;padding:var(--space-2) var(--space-3);border-radius:var(--radius-md);color:var(--color-text-secondary);font-size:var(--font-size-sm);font-weight:var(--font-weight-medium);transition:background .15s,color .15s;text-decoration:none}.sidebar-link:hover{background:var(--color-bg-tertiary);color:var(--color-text)}.sidebar-link.active{background:var(--color-primary-light);color:var(--color-primary)}.main{margin-left:var(--sidebar-width);flex:1;display:flex;flex-direction:column}.header{min-height:var(--header-height);border-bottom:1px solid var(--color-border);display:flex;align-items:center;justify-content:space-between;padding:var(--space-2) var(--space-6);background:var(--color-bg);position:sticky;top:0;z-index:5}.header-left{display:flex;flex-direction:column;justify-content:center;gap:0}.header-title{font-size:var(--font-size-lg);font-weight:var(--font-weight-semibold)}.header-subtitle{font-size:var(--font-size-xs);color:var(--color-text-secondary);font-weight:var(--font-weight-normal);margin:0}.btn-logout{padding:var(--space-1) var(--space-3);font-size:var(--font-size-sm);color:var(--color-text-secondary);background:none;border:1px solid var(--color-border);border-radius:var(--radius-md);cursor:pointer;transition:background .15s,color .15s}.btn-logout:hover{background:var(--color-danger-bg);color:var(--color-danger);border-color:var(--color-danger)}.content{padding:var(--space-6);max-width:var(--content-max-width);width:100%}.page h2{font-size:var(--font-size-xl);font-weight:var(--font-weight-semibold);margin-bottom:var(--space-4)}.table-container{overflow-x:auto}table{width:100%;border-collapse:collapse}table th{text-align:left;padding:var(--space-2) var(--space-3);font-size:var(--font-size-xs);font-weight:var(--font-weight-semibold);color:var(--color-text-secondary);text-transform:uppercase;letter-spacing:.05em;border-bottom:2px solid var(--color-border);background:var(--color-bg-secondary)}table td{padding:var(--space-2) var(--space-3);font-size:var(--font-size-sm);border-bottom:1px solid var(--color-border)}table tbody tr:hover{background:var(--color-bg-secondary)}table tbody tr.clickable{cursor:pointer}table .table-empty,table .table-loading{text-align:center;color:var(--color-text-muted);padding:var(--space-8) var(--space-4)}.form-field{margin-bottom:var(--space-4)}.form-field label{display:block;font-size:var(--font-size-sm);font-weight:var(--font-weight-medium);color:var(--color-text);margin-bottom:var(--space-1)}.form-field input,.form-field select,.form-field textarea{width:100%;padding:var(--space-2) var(--space-3);border:1px solid var(--color-border);border-radius:var(--radius-md);font-size:var(--font-size-sm);background:var(--color-bg);color:var(--color-text);outline:none;transition:border-color .15s}.form-field input:focus,.form-field select:focus,.form-field textarea:focus{border-color:var(--color-primary);box-shadow:0 0 0 2px var(--color-primary-light)}.form-field textarea{min-height:80px;resize:vertical}.form-field .form-error{display:block;font-size:var(--font-size-xs);color:var(--color-danger);margin-top:var(--space-1)}.form-field input[type=checkbox]{width:auto;margin-right:var(--space-2)}.form-field .form-description{display:block;font-size:var(--font-size-xs);color:var(--color-text-secondary);margin-top:var(--space-1)}.field-group{border:1px solid var(--color-border);border-radius:var(--radius-lg);padding:var(--space-4) var(--space-6);margin-bottom:var(--space-4)}.field-group-legend{font-size:var(--font-size-lg);font-weight:var(--font-weight-semibold);padding:0 var(--space-2)}.field-group-description{font-size:var(--font-size-sm);color:var(--color-text-secondary);margin-bottom:var(--space-3)}.field-group-body{display:flex;flex-direction:column;gap:var(--space-3)}.btn{display:inline-flex;align-items:center;justify-content:center;gap:var(--space-2);padding:var(--space-2) var(--space-4);font-size:var(--font-size-sm);font-weight:var(--font-weight-medium);border:1px solid transparent;border-radius:var(--radius-md);cursor:pointer;transition:background .15s,color .15s,border-color .15s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-sm{padding:var(--space-1) var(--space-3);font-size:var(--font-size-xs)}.btn-primary{background:var(--color-primary);color:#fff}.btn-primary:hover:not(:disabled){background:var(--color-primary-hover)}.btn-secondary{background:var(--color-bg);color:var(--color-text);border-color:var(--color-border)}.btn-secondary:hover:not(:disabled){background:var(--color-bg-secondary)}.btn-danger{background:var(--color-danger);color:#fff}.btn-danger:hover:not(:disabled){background:#b91c1c}.btn-ghost{background:transparent;color:var(--color-text-secondary)}.btn-ghost:hover:not(:disabled){background:var(--color-bg-tertiary);color:var(--color-text)}.badge{display:inline-flex;align-items:center;padding:.125rem var(--space-2);font-size:var(--font-size-xs);font-weight:var(--font-weight-medium);border-radius:9999px}.badge-success{background:var(--color-success-bg);color:var(--color-success)}.badge-warning{background:var(--color-warning-bg);color:var(--color-warning)}.badge-danger{background:var(--color-danger-bg);color:var(--color-danger)}.badge-info{background:var(--color-info-bg);color:var(--color-info)}.badge-neutral{background:var(--color-bg-tertiary);color:var(--color-text-secondary)}.modal-overlay{position:fixed;inset:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:100}.modal-card{background:var(--color-bg);border-radius:var(--radius-lg);box-shadow:var(--shadow-lg);width:100%;max-width:480px;max-height:90vh;overflow-y:auto}.modal-header{padding:var(--space-4) var(--space-6);border-bottom:1px solid var(--color-border);font-size:var(--font-size-lg);font-weight:var(--font-weight-semibold)}.modal-body{padding:var(--space-4) var(--space-6)}.modal-footer{padding:var(--space-3) var(--space-6);border-top:1px solid var(--color-border);display:flex;justify-content:flex-end;gap:var(--space-2)}.toast-container{position:fixed;top:var(--space-4);right:var(--space-4);z-index:200;display:flex;flex-direction:column;gap:var(--space-2);max-width:360px}.toast{padding:var(--space-3) var(--space-4);border-radius:var(--radius-md);font-size:var(--font-size-sm);box-shadow:var(--shadow-md);cursor:pointer;animation:toast-in .2s ease-out}@keyframes toast-in{0%{opacity:0;transform:translate(100%)}to{opacity:1;transform:translate(0)}}.toast-success{background:var(--color-success-bg);color:var(--color-success);border:1px solid var(--color-success)}.toast-error{background:var(--color-danger-bg);color:var(--color-danger);border:1px solid var(--color-danger)}.toast-info{background:var(--color-info-bg);color:var(--color-info);border:1px solid var(--color-info)}.toast-warning{background:var(--color-warning-bg);color:var(--color-warning);border:1px solid var(--color-warning)}.empty-state{text-align:center;padding:var(--space-8) var(--space-4);color:var(--color-text-muted)}.empty-state h3{font-size:var(--font-size-lg);font-weight:var(--font-weight-semibold);color:var(--color-text-secondary);margin-bottom:var(--space-2)}.empty-state p{font-size:var(--font-size-sm);margin-bottom:var(--space-4)}.stat-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:var(--space-4)}.stat-card{background:var(--color-bg);border:1px solid var(--color-border);border-radius:var(--radius-lg);padding:var(--space-4) var(--space-6);box-shadow:var(--shadow-sm)}.stat-label{font-size:var(--font-size-xs);font-weight:var(--font-weight-semibold);color:var(--color-text-secondary);text-transform:uppercase;letter-spacing:.05em;margin-bottom:var(--space-2)}.stat-value{font-size:var(--font-size-2xl);font-weight:var(--font-weight-bold);color:var(--color-text)}.stat-skeleton{height:2rem;background:var(--color-bg-tertiary);border-radius:var(--radius-md);animation:pulse 1.5s ease-in-out infinite}@keyframes pulse{0%,to{opacity:1}50%{opacity:.5}}.dashboard-error{display:flex;align-items:center;justify-content:space-between;padding:var(--space-3) var(--space-4);background:var(--color-danger-bg);color:var(--color-danger);border:1px solid var(--color-danger);border-radius:var(--radius-md);margin-bottom:var(--space-4);font-size:var(--font-size-sm)}.update-banner{display:flex;align-items:center;gap:var(--space-2);padding:var(--space-3) var(--space-4);background:var(--color-warning-bg);color:var(--color-warning);border:1px solid var(--color-warning);border-radius:var(--radius-md);margin-bottom:var(--space-4);font-size:var(--font-size-sm)}.update-banner-icon{font-size:1.1em}.update-banner code{background:#0000001a;padding:.1em .4em;border-radius:var(--radius-sm);font-size:.9em}.auto-provision-banner{display:flex;align-items:center;gap:var(--space-2);padding:var(--space-3) var(--space-4);background:var(--color-warning-bg);color:var(--color-warning);border:1px solid var(--color-warning);border-radius:var(--radius-md);margin-bottom:var(--space-4);font-size:var(--font-size-sm)}.auto-provision-banner-icon{font-size:1.1em}.auto-provision-link{color:var(--color-warning);text-decoration:underline;font-weight:600}.auto-provision-link:hover{opacity:.8}.prompt-card{background:var(--color-bg-secondary);border:1px solid var(--color-border);border-radius:var(--radius-lg);padding:var(--space-4) var(--space-6)}.prompt-card-desc{color:var(--color-text-secondary);font-size:var(--font-size-sm);margin-bottom:var(--space-3)}.prompt-preview{background:var(--color-bg-tertiary);border:1px solid var(--color-border);border-radius:var(--radius-md);padding:var(--space-3);font-size:var(--font-size-xs);line-height:1.6;white-space:pre-wrap;word-break:break-all;max-height:300px;overflow:auto}.page-actions{display:flex;justify-content:flex-end;margin-bottom:var(--space-4)}.inline-form{background:var(--color-bg-secondary);border:1px solid var(--color-border);border-radius:var(--radius-lg);padding:var(--space-4) var(--space-6);margin-bottom:var(--space-4)}.inline-form-actions{display:flex;gap:var(--space-2);margin-top:var(--space-2)}.back-link{display:inline-block;font-size:var(--font-size-sm);color:var(--color-text-secondary);margin-bottom:var(--space-4)}.back-link:hover{color:var(--color-primary)}.wallet-detail{margin-top:var(--space-4)}.detail-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--space-6)}.detail-name{font-size:var(--font-size-xl);font-weight:var(--font-weight-bold)}.detail-grid{display:grid;gap:0}.detail-row{display:flex;padding:var(--space-3) 0;border-bottom:1px solid var(--color-border)}.detail-row-label{width:160px;flex-shrink:0;font-size:var(--font-size-sm);font-weight:var(--font-weight-medium);color:var(--color-text-secondary)}.detail-row-value{font-size:var(--font-size-sm);display:flex;align-items:center;gap:var(--space-2);word-break:break-all}.inline-edit{display:flex;align-items:center;gap:var(--space-2)}.inline-edit-input{padding:var(--space-1) var(--space-2);border:1px solid var(--color-primary);border-radius:var(--radius-md);font-size:var(--font-size-base);outline:none}.session-controls{display:flex;align-items:flex-end;gap:var(--space-4);margin-bottom:var(--space-4)}.session-wallet-select{flex:1;max-width:400px}.session-wallet-select label{display:block;font-size:var(--font-size-sm);font-weight:var(--font-weight-medium);color:var(--color-text);margin-bottom:var(--space-1)}.session-wallet-select select{width:100%;padding:var(--space-2) var(--space-3);border:1px solid var(--color-border);border-radius:var(--radius-md);font-size:var(--font-size-sm);background:var(--color-bg);color:var(--color-text);outline:none}.session-wallet-select select:focus{border-color:var(--color-primary);box-shadow:0 0 0 2px var(--color-primary-light)}.token-warning{color:var(--color-warning);font-weight:var(--font-weight-semibold);font-size:var(--font-size-sm);margin-bottom:var(--space-3)}.token-display{background:var(--color-bg-tertiary);border:1px solid var(--color-border);border-radius:var(--radius-md);padding:var(--space-3);display:flex;align-items:center;gap:var(--space-2)}.token-value{flex:1;font-size:var(--font-size-xs);word-break:break-all;font-family:SF Mono,Fira Code,monospace}.policy-controls{display:flex;align-items:flex-end;gap:var(--space-4);margin-bottom:var(--space-4)}.policy-filter-select{flex:1;max-width:400px}.policy-filter-select label{display:block;font-size:var(--font-size-sm);font-weight:var(--font-weight-medium);color:var(--color-text);margin-bottom:var(--space-1)}.policy-filter-select select{width:100%;padding:var(--space-2) var(--space-3);border:1px solid var(--color-border);border-radius:var(--radius-md);font-size:var(--font-size-sm);background:var(--color-bg);color:var(--color-text);outline:none}.policy-filter-select select:focus{border-color:var(--color-primary);box-shadow:0 0 0 2px var(--color-primary-light)}.tier-bars{display:flex;flex-direction:column;gap:3px;min-width:180px}.tier-bar{display:flex;align-items:center;gap:var(--space-2);font-size:var(--font-size-xs)}.tier-bar-label{width:52px;flex-shrink:0;font-weight:var(--font-weight-medium)}.tier-bar-track{flex:1;height:6px;background:var(--color-bg-tertiary);border-radius:3px;overflow:hidden}.tier-bar-fill{height:100%;border-radius:3px;transition:width .3s ease}.tier-bar-value{font-size:var(--font-size-xs);color:var(--color-text-secondary);min-width:60px;text-align:right}.tier-bar-fill--instant{background:var(--color-tier-instant)}.tier-bar-fill--notify{background:var(--color-info)}.tier-bar-fill--delay{background:var(--color-tier-delay)}.tier-bar-fill--approval{background:var(--color-tier-blocked)}.spending-limit-summary{display:flex;flex-direction:column;gap:.5rem}.cumulative-limits{display:flex;flex-direction:column;gap:.125rem;padding-top:.25rem;border-top:1px solid var(--color-border)}.cumulative-limits-label{font-size:var(--font-size-xs);color:var(--color-text-secondary);font-weight:500}.cumulative-limit-row{display:flex;align-items:center;gap:.5rem;font-size:var(--font-size-xs)}.cumulative-limit-type{color:var(--color-text-secondary);min-width:72px}.cumulative-limit-value{color:var(--color-warning);font-weight:600}.rules-vis-text{font-size:var(--font-size-xs);color:var(--color-text-secondary)}.rules-vis-badges{display:flex;flex-wrap:wrap;gap:4px}.rules-summary{font-size:var(--font-size-xs);color:var(--color-text-secondary);max-width:250px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:SF Mono,Fira Code,monospace}.edit-rules-textarea textarea{min-height:120px;font-family:SF Mono,Fira Code,monospace;font-size:var(--font-size-xs)}.policy-type-readonly{font-size:var(--font-size-sm);font-weight:var(--font-weight-semibold);color:var(--color-text);padding:var(--space-2) 0;margin-bottom:var(--space-2)}.settings-section{background:var(--color-bg);border:1px solid var(--color-border);border-radius:var(--radius-lg);padding:var(--space-6);margin-bottom:var(--space-4)}.settings-section--danger{border-color:var(--color-danger)}.settings-section-header{margin-bottom:var(--space-4)}.settings-section-header h3{font-size:var(--font-size-lg);font-weight:var(--font-weight-semibold);margin-bottom:var(--space-1)}.settings-description{font-size:var(--font-size-sm);color:var(--color-text-secondary)}.settings-section-body{display:flex;align-items:center;gap:var(--space-4)}.ks-state-card{display:flex;align-items:center;gap:var(--space-4);flex:1}.ks-state-info{font-size:var(--font-size-sm);color:var(--color-text-secondary)}.shutdown-overlay{position:fixed;inset:0;background:#000c;display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:300;color:#fff}.shutdown-overlay h2{font-size:var(--font-size-2xl);margin-bottom:var(--space-4);color:#fff}.shutdown-overlay p{font-size:var(--font-size-base);color:var(--color-text-muted)}.shutdown-confirm-input{margin-top:var(--space-3)}.shutdown-confirm-input label{display:block;font-size:var(--font-size-sm);color:var(--color-text-secondary);margin-bottom:var(--space-1)}.shutdown-confirm-input input{width:100%;padding:var(--space-2) var(--space-3);border:1px solid var(--color-border);border-radius:var(--radius-md);font-size:var(--font-size-sm);background:var(--color-bg);color:var(--color-text);outline:none}.shutdown-confirm-input input:focus{border-color:var(--color-danger);box-shadow:0 0 0 2px var(--color-danger-bg)}.channel-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--space-4);margin-bottom:var(--space-6)}.channel-card{padding:var(--space-4);border:1px solid var(--color-border);border-radius:var(--radius-lg);background:var(--color-bg)}.channel-card-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--space-2)}.channel-card-name{font-weight:var(--font-weight-semibold);font-size:var(--font-size-base);text-transform:capitalize}.test-results{margin-top:var(--space-4)}.test-result-item{display:flex;align-items:center;gap:var(--space-2);padding:var(--space-2) 0}.test-result-success{color:var(--color-success)}.test-result-failure{color:var(--color-danger)}.config-guidance{margin-top:var(--space-6);padding:var(--space-4);background:var(--color-info-bg);border:1px solid var(--color-info);border-radius:var(--radius-md);font-size:var(--font-size-sm)}.config-guidance pre{margin-top:var(--space-2);padding:var(--space-3);background:var(--color-bg-tertiary);border-radius:var(--radius-sm);font-family:monospace;font-size:var(--font-size-xs);overflow-x:auto;white-space:pre}.config-guidance p{color:var(--color-text-secondary);margin-bottom:var(--space-2)}.breadcrumb{display:flex;align-items:center;gap:var(--space-1);font-size:var(--font-size-xs);color:var(--color-text-secondary);margin-bottom:var(--space-2)}.breadcrumb-page{background:none;border:none;color:var(--color-primary);cursor:pointer;font-size:inherit;padding:0;font-family:inherit}.breadcrumb-page:hover{text-decoration:underline}.breadcrumb-separator{color:var(--color-text-muted)}.breadcrumb-current{color:var(--color-text-secondary);font-weight:var(--font-weight-medium)}.tab-nav{display:flex;gap:var(--space-1);border-bottom:1px solid var(--color-border)}.tab-btn{padding:var(--space-2) var(--space-4);background:none;border:none;border-bottom:2px solid transparent;font-size:var(--font-size-sm);font-weight:500;color:var(--color-text-secondary);cursor:pointer;transition:color .15s,border-color .15s}.tab-btn:hover{color:var(--color-text)}.tab-btn.active{color:var(--color-primary);border-bottom-color:var(--color-primary)}.pagination{display:flex;justify-content:space-between;align-items:center;margin-top:var(--space-4)}.pagination-info{font-size:var(--font-size-sm);color:var(--color-text-secondary)}.pagination-buttons{display:flex;gap:var(--space-2)}.notif-disabled-banner{padding:var(--space-3) var(--space-4);background:var(--color-warning-bg);border:1px solid var(--color-warning);border-radius:var(--radius-md);margin-bottom:var(--space-4);font-size:var(--font-size-sm);color:var(--color-text)}.settings-save-bar{position:sticky;top:var(--header-height);z-index:4;display:flex;align-items:center;justify-content:space-between;padding:var(--space-3) var(--space-4);background:var(--color-warning-bg);border:1px solid var(--color-warning);border-radius:var(--radius-md);margin-bottom:var(--space-4);font-size:var(--font-size-sm);color:var(--color-text)}.settings-save-bar-actions{display:flex;gap:var(--space-2)}.settings-category{background:var(--color-bg);border:1px solid var(--color-border);border-radius:var(--radius-lg);margin-bottom:var(--space-4);overflow:hidden}.settings-category-header{padding:var(--space-4) var(--space-6);border-bottom:1px solid var(--color-border)}.settings-category-header h3{font-size:var(--font-size-lg);font-weight:var(--font-weight-semibold);margin-bottom:var(--space-1)}.settings-category-body{padding:var(--space-4) var(--space-6)}.settings-fields-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:var(--space-4)}.settings-fields-grid .form-field{margin-bottom:0}.settings-field-full{grid-column:1 / -1}.settings-subgroup{margin-bottom:var(--space-4)}.settings-subgroup-title{font-size:var(--font-size-sm);font-weight:var(--font-weight-semibold);color:var(--color-text-secondary);text-transform:uppercase;letter-spacing:.05em;margin-bottom:var(--space-3);padding-bottom:var(--space-2);border-bottom:1px solid var(--color-border)}.rpc-field-row{display:flex;gap:var(--space-2);align-items:flex-end}.rpc-field-row .form-field{flex:1;margin-bottom:0}.rpc-test-result{display:inline-flex;align-items:center;gap:var(--space-1);font-size:var(--font-size-xs);margin-top:var(--space-1)}.rpc-test-result--success{color:var(--color-success)}.rpc-test-result--failure{color:var(--color-danger)}.rpc-pool-network{border:1px solid var(--color-border);border-radius:var(--radius-md);margin-bottom:var(--space-2);overflow:hidden}.rpc-pool-network-header{display:flex;justify-content:space-between;align-items:center;padding:var(--space-2) var(--space-3);cursor:pointer;background:var(--color-bg-secondary);font-weight:500;font-size:var(--font-size-sm);user-select:none;list-style:none}.rpc-pool-network-header::-webkit-details-marker{display:none}.rpc-pool-network-header:before{content:"▶";margin-right:var(--space-2);font-size:.65em;transition:transform .15s}.rpc-pool-network[open]>.rpc-pool-network-header:before{transform:rotate(90deg)}.rpc-pool-url-count{font-size:var(--font-size-xs);color:var(--color-text-secondary);font-weight:400}.rpc-url-list{padding:var(--space-2) 0}.rpc-url-item{display:flex;align-items:center;gap:var(--space-2);padding:var(--space-1) var(--space-3);font-size:var(--font-size-sm);border-bottom:1px solid var(--color-border-light, rgba(0,0,0,.05))}.rpc-url-item:last-of-type{border-bottom:none}.rpc-url-item--builtin{color:var(--color-text-secondary)}.rpc-url-disabled{opacity:.5}.rpc-url-priority{font-size:var(--font-size-xs);color:var(--color-text-secondary);min-width:2em;text-align:right;font-weight:500}.rpc-url-item-url{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--font-mono, monospace);font-size:var(--font-size-xs)}.rpc-url-actions{display:flex;gap:2px;align-items:center;flex-shrink:0}.rpc-action-btn{padding:2px 6px!important;min-width:0!important;font-size:var(--font-size-xs)!important;line-height:1.2!important}.rpc-delete-btn{color:var(--color-danger)!important}.rpc-toggle-off{color:var(--color-text-secondary)!important;opacity:.6}.badge-builtin{font-size:.65rem;background:var(--color-info-bg, #e0f2fe);color:var(--color-info, #0369a1);padding:1px 6px;border-radius:var(--radius-sm);white-space:nowrap;flex-shrink:0}.rpc-add-url{display:flex;gap:var(--space-2);padding:var(--space-2) var(--space-3);align-items:center}.rpc-add-url-input{flex:1;padding:var(--space-1) var(--space-2);border:1px solid var(--color-border);border-radius:var(--radius-sm);font-size:var(--font-size-sm);font-family:var(--font-mono, monospace);background:var(--color-bg);color:var(--color-text)}.rpc-add-url-input::placeholder{color:var(--color-text-secondary);opacity:.6}.rpc-url-status{display:inline-flex;align-items:center;gap:var(--space-1);font-size:var(--font-size-xs);white-space:nowrap}.rpc-url-status-dot{width:8px;height:8px;border-radius:50%;display:inline-block;flex-shrink:0}.rpc-url-status-dot--available{background-color:var(--color-success)}.rpc-url-status-dot--cooldown{background-color:var(--color-warning)}.rpc-url-status-dot--unknown{background-color:var(--color-text-muted)}.rpc-url-cooldown-info{color:var(--color-warning);font-size:var(--font-size-xs)}.rpc-url-test-inline{display:inline-flex;align-items:center;gap:var(--space-1)}.settings-info-box{padding:var(--space-3) var(--space-4);background:var(--color-info-bg);border:1px solid var(--color-info);border-radius:var(--radius-md);font-size:var(--font-size-sm);color:var(--color-text-secondary);margin-top:var(--space-3)}.settings-info-box a{color:var(--color-primary);text-decoration:underline}.notif-test-section{margin-top:var(--space-4);padding-top:var(--space-4);border-top:1px solid var(--color-border)}.event-filter-groups{display:flex;flex-direction:column;gap:var(--space-2)}.event-filter-group{border:1px solid var(--color-border);border-radius:var(--radius-md);overflow:hidden}.event-filter-group[open]>summary{border-bottom:1px solid var(--color-border)}.event-filter-group-header{display:flex;justify-content:space-between;align-items:center;padding:var(--space-2) var(--space-3);cursor:pointer;background:var(--color-bg-secondary);font-weight:500;font-size:var(--font-size-sm);user-select:none;list-style:none}.event-filter-group-header::-webkit-details-marker{display:none}.event-filter-group-header:before{content:"▶";margin-right:var(--space-2);font-size:.65em;transition:transform .15s}.event-filter-group[open]>.event-filter-group-header:before{transform:rotate(90deg)}.event-filter-group-all{display:flex;align-items:center;gap:var(--space-1);font-size:var(--font-size-xs);font-weight:400;cursor:pointer}.event-filter-events{display:flex;flex-direction:column}.event-filter-event{display:flex;align-items:center;gap:var(--space-2);padding:var(--space-1) var(--space-3) var(--space-1) var(--space-5);font-size:var(--font-size-sm);cursor:pointer;border-bottom:1px solid var(--color-border-light, rgba(0,0,0,.05))}.event-filter-event:last-child{border-bottom:none}.event-filter-event:hover{background:var(--color-bg-secondary)}.event-filter-event code{font-size:var(--font-size-xs);min-width:180px;color:var(--color-text)}.event-desc{color:var(--color-text-secondary);font-size:var(--font-size-xs)}.event-broadcast{opacity:.7}.event-broadcast-badge{font-size:.65rem;background:var(--color-warning-bg, #fef3c7);color:var(--color-warning, #92400e);padding:1px 6px;border-radius:var(--radius-sm);margin-left:auto;white-space:nowrap}.settings-category-actions{display:flex;gap:var(--space-2);margin-top:var(--space-4);padding-top:var(--space-3);border-top:1px solid var(--color-border)}.dynamic-row-list{display:flex;flex-direction:column;gap:var(--space-2)}.dynamic-row{display:flex;align-items:flex-start;gap:var(--space-2)}.dynamic-row-fields{flex:1;display:flex;gap:var(--space-2);align-items:flex-start}.dynamic-row-fields .form-field{flex:1;margin-bottom:0}.dynamic-row-remove{flex-shrink:0;margin-top:var(--space-6);color:var(--color-danger)}.policy-form-section{margin-bottom:var(--space-4)}.policy-form-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--space-2)}.policy-form-header label{font-weight:var(--font-weight-semibold);font-size:var(--font-size-sm)}.json-toggle{font-size:var(--font-size-xs)}.policy-form-placeholder{color:var(--color-text-secondary);font-size:var(--font-size-sm);padding:var(--space-4);border:1px dashed var(--color-border);border-radius:var(--radius-md);text-align:center}.policy-form-fields{display:flex;flex-direction:column;gap:var(--space-3)}.policy-form-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:var(--space-3)}.policy-form-grid .form-field{margin-bottom:0}.currency-select{position:relative}.currency-select-trigger{width:100%;display:flex;justify-content:space-between;align-items:center;padding:var(--space-2) var(--space-3);border:1px solid var(--color-border);border-radius:var(--radius-md);background:var(--color-bg);cursor:pointer;font-size:var(--font-size-sm);font-family:var(--font-family);color:var(--color-text)}.currency-select-trigger:hover{border-color:var(--color-primary)}.currency-select-chevron{font-size:var(--font-size-xs);color:var(--color-text-secondary)}.currency-rate-preview{margin-top:var(--space-1);font-size:var(--font-size-xs);color:var(--color-text-secondary)}.currency-select-dropdown{position:absolute;top:100%;left:0;right:0;z-index:50;border:1px solid var(--color-border);border-radius:var(--radius-md);background:var(--color-bg);box-shadow:0 4px 12px #0000001a;margin-top:var(--space-1)}.currency-select-search{width:100%;padding:var(--space-2) var(--space-3);border:none;border-bottom:1px solid var(--color-border);outline:none;font-size:var(--font-size-sm);font-family:var(--font-family)}.currency-select-list{max-height:300px;overflow-y:auto}.currency-select-option{width:100%;display:flex;gap:var(--space-2);padding:var(--space-2) var(--space-3);border:none;background:none;cursor:pointer;text-align:left;font-size:var(--font-size-sm);font-family:var(--font-family);color:var(--color-text)}.currency-select-option:hover{background:var(--color-bg-secondary)}.currency-select-option--active{background:var(--color-primary-light);font-weight:var(--font-weight-semibold)}.currency-option-code{font-weight:var(--font-weight-semibold);min-width:3rem}.currency-option-name{flex:1}.currency-option-symbol{color:var(--color-text-secondary)}.currency-select-empty{padding:var(--space-3);text-align:center;color:var(--color-text-secondary);font-size:var(--font-size-sm)}.search-overlay{position:fixed;inset:0;background:#00000080;display:flex;align-items:flex-start;justify-content:center;padding-top:15vh;z-index:100}.search-popover{background:var(--color-bg);border-radius:var(--radius-lg);box-shadow:var(--shadow-lg);width:100%;max-width:560px;max-height:60vh;overflow:hidden;display:flex;flex-direction:column}.search-input-wrapper{padding:var(--space-3) var(--space-4);border-bottom:1px solid var(--color-border)}.search-input-wrapper input{width:100%;padding:var(--space-2) var(--space-3);border:1px solid var(--color-border);border-radius:var(--radius-md);font-size:var(--font-size-base);outline:none;background:var(--color-bg);color:var(--color-text)}.search-input-wrapper input:focus{border-color:var(--color-primary);box-shadow:0 0 0 2px var(--color-primary-light)}.search-results{overflow-y:auto;flex:1}.search-result-item{display:block;width:100%;padding:var(--space-3) var(--space-4);border:none;border-bottom:1px solid var(--color-border);background:none;text-align:left;cursor:pointer;font-family:inherit;color:var(--color-text)}.search-result-item:hover,.search-result-item.selected{background:var(--color-bg-secondary)}.search-result-label{font-size:var(--font-size-sm);font-weight:var(--font-weight-semibold);margin-bottom:2px}.search-result-desc{font-size:var(--font-size-xs);color:var(--color-text-secondary);margin-bottom:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.search-result-path{font-size:var(--font-size-xs);color:var(--color-text-muted)}.search-empty{padding:var(--space-6) var(--space-4);text-align:center;color:var(--color-text-muted);font-size:var(--font-size-sm)}.search-hint{padding:var(--space-2) var(--space-4);font-size:var(--font-size-xs);color:var(--color-text-muted);border-top:1px solid var(--color-border);display:flex;gap:var(--space-4)}.search-hint kbd{background:var(--color-bg-tertiary);border:1px solid var(--color-border);border-radius:var(--radius-sm);padding:0 var(--space-1);font-size:var(--font-size-xs);font-family:inherit}.header-actions{display:flex;align-items:center;gap:var(--space-2)}.btn-search{padding:var(--space-1) var(--space-2);font-size:var(--font-size-base);background:none;border:1px solid var(--color-border);border-radius:var(--radius-md);cursor:pointer;color:var(--color-text-secondary);transition:background .15s,color .15s;line-height:1}.btn-search:hover{background:var(--color-bg-tertiary);color:var(--color-text)}.form-field--highlight{animation:field-highlight 2s ease-out;border-radius:var(--radius-md)}@keyframes field-highlight{0%{background:var(--color-primary-light);box-shadow:0 0 0 2px var(--color-primary)}to{background:transparent;box-shadow:none}}.unsaved-dialog-footer{justify-content:flex-end;gap:var(--space-2)}.filter-bar{display:flex;flex-wrap:wrap;align-items:flex-end;gap:var(--space-3);margin-bottom:var(--space-4)}.filter-field{display:flex;flex-direction:column;gap:var(--space-1)}.filter-field label{font-size:var(--font-size-xs);font-weight:var(--font-weight-medium);color:var(--color-text-secondary);text-transform:uppercase;letter-spacing:.03em}.filter-field select,.filter-field input[type=date]{padding:var(--space-2) var(--space-3);border:1px solid var(--color-border);border-radius:var(--radius-md);font-size:var(--font-size-sm);background:var(--color-bg);color:var(--color-text);outline:none;transition:border-color .15s;min-width:140px}.filter-field select:focus,.filter-field input[type=date]:focus{border-color:var(--color-primary);box-shadow:0 0 0 2px var(--color-primary-light)}.filter-clear{align-self:flex-end}.search-input{position:relative;display:inline-flex;align-items:center;max-width:400px;width:100%;margin-bottom:var(--space-4)}.search-input input[type=text]{width:100%;padding:var(--space-2) var(--space-3);padding-right:var(--space-8);border:1px solid var(--color-border);border-radius:var(--radius-md);font-size:var(--font-size-sm);background:var(--color-bg);color:var(--color-text);outline:none;transition:border-color .15s}.search-input input[type=text]:focus{border-color:var(--color-primary);box-shadow:0 0 0 2px var(--color-primary-light)}.search-clear{position:absolute;right:var(--space-2);background:none;border:none;font-size:var(--font-size-sm);color:var(--color-text-muted);cursor:pointer;padding:var(--space-1) var(--space-2);border-radius:var(--radius-sm)}.search-clear:hover{color:var(--color-text);background:var(--color-bg-tertiary)}@media(max-width:768px){.channel-grid,.settings-fields-grid{grid-template-columns:1fr}.filter-bar{flex-direction:column;align-items:stretch}.filter-field select,.filter-field input[type=date]{min-width:0;width:100%}.search-input{max-width:100%}}