consolidation-ui-logic.mjs
148 lines 6.8 KB
Raw
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor ⚠ breaking 2 days ago
1 /**
2 * Pure functions for consolidation UI — shared between hub.js (browser) and test runner (Node).
3 * These operate on plain data (no DOM dependency) for testability.
4 */
5
6 /**
7 * Given a settings response, populate a form-field map with daemon config values.
8 * @param {object} settings - GET /api/v1/settings response
9 * @param {object} form - { field_id: { value?, checked? } } map
10 * @returns {string} mode - 'daemon' | 'hosted' | 'off'
11 */
12 export function populateConsolSettingsForm(settings, form) {
13 if (!settings || !settings.daemon) return 'off';
14 const d = settings.daemon;
15 let mode = 'off';
16 if (d.enabled) mode = 'daemon';
17 else if (settings.hosted_delegating || (settings.vault_path_display || '').toLowerCase() === 'canister') mode = 'hosted';
18
19 if (form['consol-interval']) form['consol-interval'].value = d.interval_minutes ?? 120;
20 if (form['consol-idle-only']) form['consol-idle-only'].checked = d.idle_only !== false;
21 if (form['consol-idle-threshold']) form['consol-idle-threshold'].value = d.idle_threshold_minutes ?? 15;
22 if (form['consol-run-on-start']) form['consol-run-on-start'].checked = Boolean(d.run_on_start);
23 if (form['pass-consolidate']) form['pass-consolidate'].checked = d.passes?.consolidate !== false;
24 if (form['pass-verify']) form['pass-verify'].checked = d.passes?.verify !== false;
25 if (form['pass-discover']) form['pass-discover'].checked = Boolean(d.passes?.discover);
26 if (form['consol-llm-provider']) form['consol-llm-provider'].value = d.llm?.provider || '';
27 if (form['consol-llm-model']) form['consol-llm-model'].value = d.llm?.model || '';
28 if (form['consol-llm-base-url']) form['consol-llm-base-url'].value = d.llm?.base_url || '';
29 if (form['consol-lookback-hours']) form['consol-lookback-hours'].value = d.lookback_hours ?? 24;
30 if (form['consol-max-events']) form['consol-max-events'].value = d.max_events_per_pass ?? 200;
31 if (form['consol-max-topics']) form['consol-max-topics'].value = d.max_topics_per_pass ?? 10;
32 if (form['consol-llm-max-tokens']) form['consol-llm-max-tokens'].value = d.llm?.max_tokens ?? 1024;
33 if (form['consol-cost-cap']) form['consol-cost-cap'].value = d.max_cost_per_day_usd != null ? d.max_cost_per_day_usd : '';
34 if (form['consol-hosted-interval'] != null && d.interval_minutes != null) {
35 const v = String(d.interval_minutes);
36 const allowed = ['30', '60', '120', '360', '720', '1440', '10080'];
37 form['consol-hosted-interval'].value = allowed.includes(v) ? v : '120';
38 }
39
40 return mode;
41 }
42
43 /**
44 * Build a consolidation settings payload from a form-field map.
45 * @param {object} form - { field_id: { value?, checked? } }
46 * @param {string} mode - 'daemon' | 'hosted' | 'off'
47 * @returns {object} payload matching POST /api/v1/settings/consolidation schema
48 */
49 export function buildConsolSettingsPayload(form, mode) {
50 const intervalRaw =
51 mode === 'hosted' && form['consol-hosted-interval'] != null
52 ? form['consol-hosted-interval'].value
53 : form['consol-interval']?.value;
54 const llm = {
55 provider: form['consol-llm-provider']?.value || '',
56 model: form['consol-llm-model']?.value || '',
57 base_url: form['consol-llm-base-url']?.value || '',
58 };
59 const maxTokClamped = Math.max(
60 64,
61 Math.min(8192, Math.floor(Number(form['consol-llm-max-tokens']?.value) || 1024)),
62 );
63 if (mode === 'daemon') {
64 llm.max_tokens = maxTokClamped;
65 }
66 const payload = {
67 mode,
68 enabled: mode === 'daemon',
69 interval_minutes: Math.max(1, Math.floor(Number(intervalRaw) || 120)),
70 idle_only: Boolean(form['consol-idle-only']?.checked),
71 idle_threshold_minutes: Math.max(1, Math.floor(Number(form['consol-idle-threshold']?.value) || 15)),
72 run_on_start: Boolean(form['consol-run-on-start']?.checked),
73 passes: {
74 consolidate: Boolean(form['pass-consolidate']?.checked),
75 verify: Boolean(form['pass-verify']?.checked),
76 discover: Boolean(form['pass-discover']?.checked),
77 },
78 llm,
79 max_cost_per_day_usd: form['consol-cost-cap']?.value === '' ? null : Number(form['consol-cost-cap']?.value) || 0,
80 };
81 if (mode === 'daemon' || mode === 'hosted') {
82 payload.lookback_hours = Math.max(
83 1,
84 Math.min(8760, Math.floor(Number(form['consol-lookback-hours']?.value) || 24)),
85 );
86 payload.max_events_per_pass = Math.max(
87 1,
88 Math.min(10000, Math.floor(Number(form['consol-max-events']?.value) || 200)),
89 );
90 payload.max_topics_per_pass = Math.max(
91 1,
92 Math.min(500, Math.floor(Number(form['consol-max-topics']?.value) || 10)),
93 );
94 }
95 if (mode === 'hosted') {
96 payload.llm = { ...llm, max_tokens: maxTokClamped };
97 }
98 return payload;
99 }
100
101 /**
102 * Render consolidation history events into a container.
103 * @param {Array} events - consolidation-type memory events
104 * @param {{ innerHTML: string }} container - DOM-like object with innerHTML
105 * @returns {number} row count rendered
106 */
107 export function renderConsolidationHistory(events, container) {
108 if (!container) return 0;
109 if (!events || events.length === 0) {
110 container.innerHTML = '<p class="muted">No consolidation history found.</p>';
111 return 0;
112 }
113 let html = '<table class="consol-history-table"><thead><tr><th>Date</th><th>Topics</th><th>Events Merged</th><th>Cost</th><th>Status</th></tr></thead><tbody>';
114 events.forEach((ev) => {
115 const ts = ev.ts || ev.timestamp || ev.created_at;
116 const date = ts ? new Date(ts).toLocaleString() : '—';
117 const rawTopics = ev.data?.topics_count;
118 const topics = Array.isArray(rawTopics)
119 ? rawTopics.length
120 : (rawTopics ?? ev.data?.topics?.length ?? '—');
121 const merged = ev.data?.total_events ?? ev.data?.event_count ?? '—';
122 const cost = ev.data?.cost_usd != null ? '$' + Number(ev.data.cost_usd).toFixed(4) : '—';
123 const status = ev.data?.dry_run ? 'dry-run' : (ev.data?.error ? 'error' : 'complete');
124 html += `<tr><td>${esc(date)}</td><td>${esc(String(topics))}</td><td>${esc(String(merged))}</td><td>${esc(cost)}</td><td>${esc(status)}</td></tr>`;
125 });
126 html += '</tbody></table>';
127 container.innerHTML = html;
128 return events.length;
129 }
130
131 /**
132 * Compute cost meter display values.
133 * @param {number} costUsd - cost today in USD
134 * @param {number|null} capUsd - daily cap in USD (null = no cap)
135 * @returns {{ fillPercent: number, display: string, capLabel: string, showMeter: boolean }}
136 */
137 export function formatCostMeter(costUsd, capUsd) {
138 const cost = Math.max(0, Number(costUsd) || 0);
139 const cap = capUsd != null ? Math.max(0, Number(capUsd) || 0) : null;
140 const display = '$' + cost.toFixed(3) + ' today';
141 if (cap == null || cap === 0) return { fillPercent: 0, display, capLabel: '', showMeter: false };
142 const pct = Math.min(100, (cost / cap) * 100);
143 return { fillPercent: pct, display, capLabel: 'cap: $' + cap.toFixed(2), showMeter: true };
144 }
145
146 function esc(s) {
147 return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
148 }
File History 2 commits
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor 2 days ago
sha256:9103f98c89257ed2b01c237cea895dabb3e85ea337dccb1161c175e4422355b6 docs: accept Calendar Events v0 spec with Phase 0 security … Human 2 days ago