aimeat 3.3.0 → 3.3.2
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.
- package/README.md +1 -1
- package/dist/.env.example +10 -1
- package/dist/build-stamp.json +4 -4
- package/dist/locales/en.json +9263 -9225
- package/dist/locales/es.json +9255 -9217
- package/dist/locales/fi.json +9263 -9225
- package/dist/public/css/views/admin.css +935 -860
- package/dist/public/css/views/chat.css +393 -388
- package/dist/public/js/services/admin.js +12 -0
- package/dist/public/robots.node.txt +4 -0
- package/dist/public/views/admin/config-tab.js +333 -185
- package/dist/public/views/admin/usage-tab.js +373 -307
- package/dist/public/views/chat/parts.js +25 -6
- package/dist/public/views/profile/landing-page.cards.js +732 -700
- package/dist/public/views/profile/landing-page.js +6 -2
- package/dist/scripts/audit-mcp-schemas.js +22 -6
- package/dist/scripts/audit-mcp-schemas.js.map +1 -1
- package/dist/scripts/check-ai-disclosure.d.ts +1 -1
- package/dist/scripts/check-ai-disclosure.d.ts.map +1 -1
- package/dist/scripts/check-ai-disclosure.js +19 -3
- package/dist/scripts/check-ai-disclosure.js.map +1 -1
- package/dist/scripts/check-config-coverage.d.ts +2 -0
- package/dist/scripts/check-config-coverage.d.ts.map +1 -0
- package/dist/scripts/check-config-coverage.js +129 -0
- package/dist/scripts/check-config-coverage.js.map +1 -0
- package/dist/src/auth/ai-gate.d.ts +25 -0
- package/dist/src/auth/ai-gate.d.ts.map +1 -0
- package/dist/src/auth/ai-gate.js +21 -0
- package/dist/src/auth/ai-gate.js.map +1 -0
- package/dist/src/cli/connect/mcp/tools/apps.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/apps.js +102 -20
- package/dist/src/cli/connect/mcp/tools/apps.js.map +1 -1
- package/dist/src/cli/connect/tool-call-defs-agent.d.ts.map +1 -1
- package/dist/src/cli/connect/tool-call-defs-agent.js +38 -9
- package/dist/src/cli/connect/tool-call-defs-agent.js.map +1 -1
- package/dist/src/cli/connect/tool-call-defs-apps.d.ts.map +1 -1
- package/dist/src/cli/connect/tool-call-defs-apps.js +199 -103
- package/dist/src/cli/connect/tool-call-defs-apps.js.map +1 -1
- package/dist/src/cli/connect/tool-call-defs-core.d.ts.map +1 -1
- package/dist/src/cli/connect/tool-call-defs-core.js +149 -26
- package/dist/src/cli/connect/tool-call-defs-core.js.map +1 -1
- package/dist/src/cli/connect/tool-call-defs-organism.d.ts.map +1 -1
- package/dist/src/cli/connect/tool-call-defs-organism.js +20 -3
- package/dist/src/cli/connect/tool-call-defs-organism.js.map +1 -1
- package/dist/src/cli/connect/tool-call-defs-packages.d.ts +23 -0
- package/dist/src/cli/connect/tool-call-defs-packages.d.ts.map +1 -0
- package/dist/src/cli/connect/tool-call-defs-packages.js +45 -0
- package/dist/src/cli/connect/tool-call-defs-packages.js.map +1 -0
- package/dist/src/cli/connect/tool-call-defs-workflows.d.ts +14 -0
- package/dist/src/cli/connect/tool-call-defs-workflows.d.ts.map +1 -0
- package/dist/src/cli/connect/tool-call-defs-workflows.js +75 -0
- package/dist/src/cli/connect/tool-call-defs-workflows.js.map +1 -0
- package/dist/src/cli/connect/tool-call.d.ts.map +1 -1
- package/dist/src/cli/connect/tool-call.js +54 -1
- package/dist/src/cli/connect/tool-call.js.map +1 -1
- package/dist/src/config-types-ai.d.ts +13 -0
- package/dist/src/config-types-ai.d.ts.map +1 -1
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/config.js +2 -0
- package/dist/src/config.js.map +1 -1
- package/dist/src/generated/api-types.d.ts +196 -0
- package/dist/src/generated/api-types.d.ts.map +1 -1
- package/dist/src/mcp/annotations.d.ts.map +1 -1
- package/dist/src/mcp/annotations.js +8 -0
- package/dist/src/mcp/annotations.js.map +1 -1
- package/dist/src/mcp/catalog/definitions/organisms-workspaces-apps.d.ts.map +1 -1
- package/dist/src/mcp/catalog/definitions/organisms-workspaces-apps.js +25 -8
- package/dist/src/mcp/catalog/definitions/organisms-workspaces-apps.js.map +1 -1
- package/dist/src/mcp/catalog/definitions/packages.d.ts +17 -0
- package/dist/src/mcp/catalog/definitions/packages.d.ts.map +1 -0
- package/dist/src/mcp/catalog/definitions/packages.js +46 -0
- package/dist/src/mcp/catalog/definitions/packages.js.map +1 -0
- package/dist/src/mcp/catalog/definitions.d.ts.map +1 -1
- package/dist/src/mcp/catalog/definitions.js +2 -0
- package/dist/src/mcp/catalog/definitions.js.map +1 -1
- package/dist/src/mcp/catalog/scopes.d.ts.map +1 -1
- package/dist/src/mcp/catalog/scopes.js +2 -0
- package/dist/src/mcp/catalog/scopes.js.map +1 -1
- package/dist/src/mcp/catalog/surfaces.d.ts.map +1 -1
- package/dist/src/mcp/catalog/surfaces.js +2 -0
- package/dist/src/mcp/catalog/surfaces.js.map +1 -1
- package/dist/src/routes/admin-usage.d.ts.map +1 -1
- package/dist/src/routes/admin-usage.js +57 -0
- package/dist/src/routes/admin-usage.js.map +1 -1
- package/dist/src/routes/ai.d.ts.map +1 -1
- package/dist/src/routes/ai.js +4 -15
- package/dist/src/routes/ai.js.map +1 -1
- package/dist/src/routes/bootstrap.d.ts +2 -1
- package/dist/src/routes/bootstrap.d.ts.map +1 -1
- package/dist/src/routes/bootstrap.js +7 -18
- package/dist/src/routes/bootstrap.js.map +1 -1
- package/dist/src/routes/chat.d.ts +3 -0
- package/dist/src/routes/chat.d.ts.map +1 -1
- package/dist/src/routes/chat.js +20 -0
- package/dist/src/routes/chat.js.map +1 -1
- package/dist/src/routes/llm-proxy.d.ts +34 -0
- package/dist/src/routes/llm-proxy.d.ts.map +1 -0
- package/dist/src/routes/llm-proxy.js +258 -0
- package/dist/src/routes/llm-proxy.js.map +1 -0
- package/dist/src/routes/sitemaps.d.ts +22 -0
- package/dist/src/routes/sitemaps.d.ts.map +1 -0
- package/dist/src/routes/sitemaps.js +67 -0
- package/dist/src/routes/sitemaps.js.map +1 -0
- package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.js +2 -0
- package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
- package/dist/src/services/ai-completion.d.ts +76 -0
- package/dist/src/services/ai-completion.d.ts.map +1 -1
- package/dist/src/services/ai-completion.js +104 -68
- package/dist/src/services/ai-completion.js.map +1 -1
- package/dist/src/services/chat-session.d.ts +3 -0
- package/dist/src/services/chat-session.d.ts.map +1 -1
- package/dist/src/services/chat-session.js +4 -0
- package/dist/src/services/chat-session.js.map +1 -1
- package/dist/src/services/config-schema.d.ts +4 -0
- package/dist/src/services/config-schema.d.ts.map +1 -1
- package/dist/src/services/config-schema.js +33 -0
- package/dist/src/services/config-schema.js.map +1 -1
- package/dist/src/services/goose-acp.d.ts.map +1 -1
- package/dist/src/services/goose-acp.js +11 -0
- package/dist/src/services/goose-acp.js.map +1 -1
- package/dist/src/services/handbooks/appdev.d.ts +1 -1
- package/dist/src/services/handbooks/appdev.d.ts.map +1 -1
- package/dist/src/services/handbooks/appdev.js +7 -1
- package/dist/src/services/handbooks/appdev.js.map +1 -1
- package/dist/src/services/openrouter.d.ts +23 -0
- package/dist/src/services/openrouter.d.ts.map +1 -1
- package/dist/src/services/openrouter.js +32 -1
- package/dist/src/services/openrouter.js.map +1 -1
- package/dist/src/services/usage-summary.d.ts +15 -0
- package/dist/src/services/usage-summary.d.ts.map +1 -1
- package/dist/src/services/usage-summary.js +15 -0
- package/dist/src/services/usage-summary.js.map +1 -1
- package/dist/src/storage/providers/postgres-kysely/methods/wallet.d.ts.map +1 -1
- package/dist/src/storage/providers/postgres-kysely/methods/wallet.js +23 -4
- package/dist/src/storage/providers/postgres-kysely/methods/wallet.js.map +1 -1
- package/dist/src/storage/providers/sqlite/methods/work.d.ts +5 -0
- package/dist/src/storage/providers/sqlite/methods/work.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/methods/work.js +34 -22
- package/dist/src/storage/providers/sqlite/methods/work.js.map +1 -1
- package/package.json +4 -1
|
@@ -257,6 +257,18 @@ export const getUsageSummary = (report, from, to, owner) => {
|
|
|
257
257
|
return apiGet('/v1/admin/usage/summary?' + params.toString());
|
|
258
258
|
};
|
|
259
259
|
|
|
260
|
+
/**
|
|
261
|
+
* The money question: is the house key set, what does it grant, how much of the spend is the
|
|
262
|
+
* house's rather than people's own keys, and what is NOT metered here (the chat agent).
|
|
263
|
+
*/
|
|
264
|
+
export const getUsageHouse = (from, to) => {
|
|
265
|
+
const params = new URLSearchParams();
|
|
266
|
+
if (from) params.set('from', from);
|
|
267
|
+
if (to) params.set('to', to);
|
|
268
|
+
const qs = params.toString();
|
|
269
|
+
return apiGet('/v1/admin/usage/house' + (qs ? '?' + qs : ''));
|
|
270
|
+
};
|
|
271
|
+
|
|
260
272
|
// ── Extensions & Instances ──
|
|
261
273
|
export const getAvailableExtensions = () => apiGet('/v1/admin/extensions/available');
|
|
262
274
|
export const installBundledExtension = (name) => apiPost(`/v1/admin/extensions/available/${encodeURIComponent(name)}/install`);
|
|
@@ -112,4 +112,8 @@ Disallow: /
|
|
|
112
112
|
# /llms.txt is the curated index; /llms-full.txt is the full builder's manual.
|
|
113
113
|
# API entry point for AI agents: GET /?format=json
|
|
114
114
|
|
|
115
|
+
# The page sitemap, and the index over every published app's own sitemap. Each app lives on
|
|
116
|
+
# its own host and names its sitemap in its own robots.txt, which is what makes the
|
|
117
|
+
# cross-host entries in the index legitimate.
|
|
115
118
|
Sitemap: {{BASE_URL}}/sitemap.xml
|
|
119
|
+
Sitemap: {{BASE_URL}}/sitemap-index.xml
|
|
@@ -1,185 +1,333 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file config-tab.js
|
|
3
|
-
* @description Admin dashboard Config tab — renders the mutable node config
|
|
4
|
-
* schema (GET /v1/admin/config) with per-type editors and persists changes
|
|
5
|
-
* via PUT /v1/admin/config.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
<
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @file config-tab.js
|
|
3
|
+
* @description Admin dashboard Config tab — renders the mutable node config
|
|
4
|
+
* schema (GET /v1/admin/config) with per-type editors and persists changes
|
|
5
|
+
* via PUT /v1/admin/config.
|
|
6
|
+
*
|
|
7
|
+
* TWO THINGS THIS PAGE HAS TO SOLVE BEYOND SHOWING FIELDS.
|
|
8
|
+
*
|
|
9
|
+
* Finding one. There are over two hundred fields in forty-odd sections, so the page is longer
|
|
10
|
+
* than any screen and the browser's own find is the only navigation it had. The contents block at
|
|
11
|
+
* the top is grouped by domain rather than listing forty section names flat, because a flat list
|
|
12
|
+
* of forty is the same problem one scroll higher.
|
|
13
|
+
*
|
|
14
|
+
* Knowing you have unsaved work. Editing a field did nothing until you scrolled back to the top
|
|
15
|
+
* and pressed Save, and the banner that said so was itself at the top — off screen exactly when
|
|
16
|
+
* it mattered. A person who edited three fields and navigated away lost all three and had no
|
|
17
|
+
* reason to suspect it. The pending bar is STICKY now, so it is on screen wherever you are
|
|
18
|
+
* editing, it counts what is unsaved, it lists each change as `old → new`, and every edited row
|
|
19
|
+
* is marked so you can find them again. Nothing about the save itself changed: the fix is that
|
|
20
|
+
* the page stops hiding the one control that commits your work.
|
|
21
|
+
* @structure
|
|
22
|
+
* - DOMAINS / domainOf — the section-of-sections grouping behind the contents block
|
|
23
|
+
* - Toc — the contents block
|
|
24
|
+
* - PendingBar — the sticky unsaved-changes bar
|
|
25
|
+
* - FieldRow — one field's label, source badge and editor
|
|
26
|
+
* - ConfigTab (default)
|
|
27
|
+
* @version-history
|
|
28
|
+
* v1.4.0 -- 2026-08-16 -- A grouped table of contents, and a sticky pending-changes bar that
|
|
29
|
+
* names each change as old → new. Both for the same reason: the page had grown past the point
|
|
30
|
+
* where "it is at the top" is an answer.
|
|
31
|
+
* v1.3.0 -- 2026-06-02 -- Admin design unification: inline error <div>
|
|
32
|
+
* replaced with shared <ErrorBox>; success message moved to the
|
|
33
|
+
* adm-config-result-ok class (no inline color/background styles).
|
|
34
|
+
* v1.2.0 -- 2026-05-31 -- Fix: inputs were bound to the saved value, not the
|
|
35
|
+
* pending edit, so toggling a checkbox / typing snapped straight back on
|
|
36
|
+
* re-render (looked un-editable). All inputs now reflect pending[path].
|
|
37
|
+
* v1.1.0 -- 2026-05-31 -- Add an editor for array-of-strings config fields
|
|
38
|
+
* (e.g. agent.system_principles): one item per line in a textarea, split
|
|
39
|
+
* back to an array on save. Previously object/array fields were read-only.
|
|
40
|
+
*/
|
|
41
|
+
import { h } from 'preact';
|
|
42
|
+
import { useState } from 'preact/hooks';
|
|
43
|
+
import htm from 'htm';
|
|
44
|
+
const html = htm.bind(h);
|
|
45
|
+
import { t } from '/js/i18n.js';
|
|
46
|
+
import { escHtml } from '/js/utils.js';
|
|
47
|
+
import { Badge, Empty, ExpandableHelp, ErrorBox } from './shared.js';
|
|
48
|
+
import { saveConfig, deleteConfig } from '/js/services/admin.js';
|
|
49
|
+
|
|
50
|
+
// Map config source to Badge type for visual distinction
|
|
51
|
+
const SOURCE_BADGE = {
|
|
52
|
+
database: 'healthy',
|
|
53
|
+
env: 'info',
|
|
54
|
+
file: 'private',
|
|
55
|
+
consul: 'watch',
|
|
56
|
+
default: 'idle',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Sections of sections. A dot-path's first segment is its section (`ai`, `rate_limits`, `email`);
|
|
61
|
+
* this puts those forty sections under eight headings so the contents block can be read at a
|
|
62
|
+
* glance. A section nobody has classified lands in `other` and still appears — a missing entry
|
|
63
|
+
* here must never make a field unreachable.
|
|
64
|
+
*/
|
|
65
|
+
const DOMAINS = [
|
|
66
|
+
{ id: 'ai', groups: ['ai', 'agent', 'tasks', 'mcp', 'cortex', 'calibrator'] },
|
|
67
|
+
{ id: 'money', groups: ['morsel_policy', 'commerce', 'marketplace', 'work', 'economy', 'portfolio'] },
|
|
68
|
+
{ id: 'identity', groups: ['auth', 'totp', 'eudiw', 'consent', 'security', 'moderation'] },
|
|
69
|
+
{ id: 'node', groups: ['node', 'storage', 'database_url', 'sqlite_path', 'admin_password', 'setup', 'consul', 'stats', 'metrics'] },
|
|
70
|
+
{ id: 'limits', groups: ['quotas', 'rate_limits', 'extensions', 'realtime'] },
|
|
71
|
+
{ id: 'federation', groups: ['federation', 'sync', 'personal_nodes', 'genesis', 'tunnel', 'msm'] },
|
|
72
|
+
{ id: 'integrations', groups: ['email', 'push', 'indexing', 'cors', 'site', 'portal', 'cookie_consent', 'connections'] },
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
function domainOf(group) {
|
|
76
|
+
for (const d of DOMAINS) if (d.groups.includes(group)) return d.id;
|
|
77
|
+
return 'other';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const tr = (key, fallback) => { const v = t(key); return v && v !== key ? v : fallback; };
|
|
81
|
+
|
|
82
|
+
/** Translate a config path — try dashboard.cfg_<dotpath>, else the raw path. */
|
|
83
|
+
function label(path) {
|
|
84
|
+
const key = 'dashboard.cfg_' + path.replace(/\./g, '_');
|
|
85
|
+
const val = t(key);
|
|
86
|
+
return val !== key ? val : path;
|
|
87
|
+
}
|
|
88
|
+
function groupLabel(g) {
|
|
89
|
+
const key = 'dashboard.cfgGroup_' + g;
|
|
90
|
+
const val = t(key);
|
|
91
|
+
return val !== key ? val : g.charAt(0).toUpperCase() + g.slice(1).replace(/_/g, ' ');
|
|
92
|
+
}
|
|
93
|
+
function domainLabel(id) {
|
|
94
|
+
return tr('dashboard.cfgDomain_' + id, id.charAt(0).toUpperCase() + id.slice(1));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** A value as a person reads it in the change list: a secret is never one of them (see the API). */
|
|
98
|
+
function shown(v) {
|
|
99
|
+
if (v === '' || v === null || v === undefined) return tr('dashboard.cfgEmpty', '(empty)');
|
|
100
|
+
if (typeof v === 'boolean') return v ? t('dashboard.enabled') : t('dashboard.disabled');
|
|
101
|
+
const s = String(v);
|
|
102
|
+
return s.length > 60 ? s.slice(0, 57) + '…' : s;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The contents block: jump straight to a section, without reading forty names in a row. */
|
|
106
|
+
function Toc({ domains, onJump }) {
|
|
107
|
+
if (domains.length === 0) return null;
|
|
108
|
+
return html`
|
|
109
|
+
<details class="adm-card adm-cfg-toc" open>
|
|
110
|
+
<summary class="adm-cfg-toc-summary">${tr('dashboard.cfgToc', 'Sections')}</summary>
|
|
111
|
+
<div class="adm-cfg-toc-body">
|
|
112
|
+
${domains.map(([domain, groups]) => html`
|
|
113
|
+
<div class="adm-cfg-toc-domain" key=${domain}>
|
|
114
|
+
<div class="adm-cfg-toc-domain-name">${domainLabel(domain)}</div>
|
|
115
|
+
<div class="adm-cfg-toc-links">
|
|
116
|
+
${groups.map(([g, items]) => html`
|
|
117
|
+
<button type="button" class="adm-cfg-toc-link" key=${g} onClick=${() => onJump(g)}>
|
|
118
|
+
${groupLabel(g)} <span class="adm-cfg-toc-count">${items.length}</span>
|
|
119
|
+
</button>
|
|
120
|
+
`)}
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
`)}
|
|
124
|
+
</div>
|
|
125
|
+
</details>
|
|
126
|
+
`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The unsaved-changes bar.
|
|
131
|
+
*
|
|
132
|
+
* Sticky on purpose: this is the answer to "I changed four things and nothing happened". It says
|
|
133
|
+
* how many are pending, what each one would become, and carries the only two buttons that end the
|
|
134
|
+
* state it describes.
|
|
135
|
+
*/
|
|
136
|
+
function PendingBar({ paths, pending, schema, onSave, onCancel, saving }) {
|
|
137
|
+
if (paths.length === 0) return null;
|
|
138
|
+
return html`
|
|
139
|
+
<div class="adm-config-changes adm-cfg-pending" role="status">
|
|
140
|
+
<div class="adm-cfg-pending-head">
|
|
141
|
+
<strong>${tr('dashboard.cfgUnsaved', '{n} unsaved change(s)').replace('{n}', paths.length)}</strong>
|
|
142
|
+
<span class="adm-cfg-pending-hint">${tr('dashboard.cfgUnsavedHint', 'Nothing is stored until you save.')}</span>
|
|
143
|
+
<span class="adm-cfg-pending-actions">
|
|
144
|
+
<button class="adm-btn-action" onClick=${onSave} disabled=${saving}>${t('dashboard.saveChanges')}</button>
|
|
145
|
+
${' '}
|
|
146
|
+
<button class="adm-btn-action" onClick=${onCancel} disabled=${saving}>${t('dashboard.cancelLabel')}</button>
|
|
147
|
+
</span>
|
|
148
|
+
</div>
|
|
149
|
+
<ul class="adm-cfg-pending-list">
|
|
150
|
+
${paths.map(p => html`
|
|
151
|
+
<li key=${p}>
|
|
152
|
+
<code>${escHtml(p)}</code>
|
|
153
|
+
<span class="adm-cfg-pending-was">${escHtml(shown(schema[p] && schema[p].value))}</span>
|
|
154
|
+
<span aria-hidden="true">→</span>
|
|
155
|
+
<strong>${escHtml(shown(pending[p]))}</strong>
|
|
156
|
+
</li>
|
|
157
|
+
`)}
|
|
158
|
+
</ul>
|
|
159
|
+
</div>
|
|
160
|
+
`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** One field: what it is, where its value came from, and the editor for its type. */
|
|
164
|
+
function FieldRow({ path: p, entry: e, editable, pending, onChange, onReset }) {
|
|
165
|
+
const edited = p in pending;
|
|
166
|
+
const val = edited ? pending[p] : e.value;
|
|
167
|
+
return html`
|
|
168
|
+
<div class=${'adm-hrow' + (edited ? ' adm-cfg-row-edited' : '')} id=${'cfgf-' + p.replace(/\./g, '-')}>
|
|
169
|
+
<span class="adm-hmetric" title=${e.description}>
|
|
170
|
+
${label(p)}
|
|
171
|
+
${e.source && html` <span class="adm-badge adm-badge-${SOURCE_BADGE[e.source] || 'idle'}" style="font-size:.6rem;vertical-align:middle">${e.source}</span>`}
|
|
172
|
+
${edited && html` <span class="adm-cfg-edited-dot" title=${tr('dashboard.cfgEdited', 'Edited, not saved')}>●</span>`}
|
|
173
|
+
</span>
|
|
174
|
+
<span>
|
|
175
|
+
${!e.mutable
|
|
176
|
+
? (typeof e.value === 'boolean'
|
|
177
|
+
? html`${e.value ? html`<${Badge} type="healthy" /> ${t('dashboard.yesLabel')}` : html`<${Badge} type="critical" /> ${t('dashboard.noLabel')}`}`
|
|
178
|
+
: html`<code>${escHtml(String(e.value))}</code> <span class="adm-text-dim adm-text-xs">${t('dashboard.readOnly')}</span>`)
|
|
179
|
+
: e.type === 'boolean'
|
|
180
|
+
? html`<label style="cursor:pointer"><input type="checkbox" checked=${val} onChange=${ev => onChange(p, ev.target.checked)} disabled=${!editable} /> ${val ? t('dashboard.enabled') : t('dashboard.disabled')}</label>`
|
|
181
|
+
: e.type === 'integer'
|
|
182
|
+
? html`<input type="number" value=${val} style="width:120px" onInput=${ev => onChange(p, parseInt(ev.target.value))} disabled=${!editable} />${e.range ? html` <span class="adm-text-dim adm-text-xs">${escHtml(e.range)}</span>` : null}`
|
|
183
|
+
: e.type === 'float'
|
|
184
|
+
? html`<input type="number" step="0.01" value=${val} style="width:120px" onInput=${ev => onChange(p, parseFloat(ev.target.value))} disabled=${!editable} />${e.range ? html` <span class="adm-text-dim adm-text-xs">${escHtml(e.range)}</span>` : null}`
|
|
185
|
+
: e.type === 'string'
|
|
186
|
+
? html`<input type="text" value=${val || ''} style="width:250px" onInput=${ev => onChange(p, ev.target.value)} disabled=${!editable} />`
|
|
187
|
+
: e.type === 'object'
|
|
188
|
+
? (Array.isArray(e.value)
|
|
189
|
+
? html`<textarea class="adm-config-array-edit" rows="4" disabled=${!editable}
|
|
190
|
+
value=${pending[p] !== undefined ? pending[p] : e.value.join('\n')}
|
|
191
|
+
onInput=${ev => onChange(p, ev.target.value)}
|
|
192
|
+
placeholder=${t('dashboard.cfgOnePerLine')}></textarea>
|
|
193
|
+
<div class="adm-text-dim adm-text-xs">${t('dashboard.cfgOnePerLine')}</div>`
|
|
194
|
+
: html`<code style="font-size:.75rem">${escHtml(JSON.stringify(e.value)).substring(0, 100)}...</code>`)
|
|
195
|
+
: html`<code>${escHtml(String(e.value))}</code>`
|
|
196
|
+
}
|
|
197
|
+
${e.canReset && editable && html` <button class="adm-btn-sm" onClick=${() => onReset(p)}>${t('dashboard.cfgReset')}</button>`}
|
|
198
|
+
</span>
|
|
199
|
+
</div>
|
|
200
|
+
`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export default function ConfigTab({ data, reload }) {
|
|
204
|
+
// Hooks must run unconditionally before any early return (Rules of Hooks).
|
|
205
|
+
const [pending, setPending] = useState({});
|
|
206
|
+
const [result, setResult] = useState(null);
|
|
207
|
+
const [saving, setSaving] = useState(false);
|
|
208
|
+
|
|
209
|
+
const s = data.configSchema;
|
|
210
|
+
if (!s || !s.schema) return html`<${Empty} text=${t('dashboard.configNotAvailable')} />`;
|
|
211
|
+
|
|
212
|
+
const schema = s.schema;
|
|
213
|
+
const editable = s.editable !== false;
|
|
214
|
+
|
|
215
|
+
// Group by first path segment, then those groups by domain for the contents block.
|
|
216
|
+
const groups = {};
|
|
217
|
+
for (const path in schema) {
|
|
218
|
+
const group = path.split('.')[0];
|
|
219
|
+
if (!groups[group]) groups[group] = [];
|
|
220
|
+
groups[group].push({ path, entry: schema[path] });
|
|
221
|
+
}
|
|
222
|
+
const groupEntries = Object.entries(groups);
|
|
223
|
+
const byDomain = new Map();
|
|
224
|
+
for (const entry of groupEntries) {
|
|
225
|
+
const d = domainOf(entry[0]);
|
|
226
|
+
if (!byDomain.has(d)) byDomain.set(d, []);
|
|
227
|
+
byDomain.get(d).push(entry);
|
|
228
|
+
}
|
|
229
|
+
const domainOrder = [...DOMAINS.map(d => d.id), 'other'].filter(d => byDomain.has(d));
|
|
230
|
+
const tocDomains = domainOrder.map(d => [d, byDomain.get(d)]);
|
|
231
|
+
|
|
232
|
+
function jumpTo(group) {
|
|
233
|
+
const el = document.getElementById('cfg-' + group);
|
|
234
|
+
if (!el) return;
|
|
235
|
+
// A collapsed section still has to be reachable: scrolling to a closed <details> lands on a
|
|
236
|
+
// one-line summary and looks like the jump did nothing.
|
|
237
|
+
if (el instanceof HTMLDetailsElement) el.open = true;
|
|
238
|
+
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function onChange(path, value) {
|
|
242
|
+
setPending(prev => ({ ...prev, [path]: value }));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function save() {
|
|
246
|
+
const changes = Object.entries(pending).map(([path, value]) => {
|
|
247
|
+
const entry = schema[path];
|
|
248
|
+
// Array-of-strings fields (e.g. agent.system_principles) are edited as
|
|
249
|
+
// one item per line; split the raw text back into an array on save so it
|
|
250
|
+
// matches what the API validator expects.
|
|
251
|
+
if (entry && entry.type === 'object' && Array.isArray(entry.value) && typeof value === 'string') {
|
|
252
|
+
return { path, value: value.split('\n').map(l => l.trim()).filter(Boolean) };
|
|
253
|
+
}
|
|
254
|
+
return { path, value };
|
|
255
|
+
});
|
|
256
|
+
if (!changes.length) return;
|
|
257
|
+
setSaving(true);
|
|
258
|
+
try {
|
|
259
|
+
const r = await saveConfig(changes);
|
|
260
|
+
setResult({ ok: true, msg: t('dashboard.savedChanges').replace('{count}', r.data.applied.length) });
|
|
261
|
+
setPending({});
|
|
262
|
+
reload();
|
|
263
|
+
} catch (err) {
|
|
264
|
+
setResult({ ok: false, msg: err.message });
|
|
265
|
+
} finally {
|
|
266
|
+
setSaving(false);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function resetConfig(path) {
|
|
271
|
+
try {
|
|
272
|
+
await deleteConfig(path);
|
|
273
|
+
setResult({ ok: true, msg: t('dashboard.cfgResetDone').replace('{path}', path) });
|
|
274
|
+
reload();
|
|
275
|
+
} catch (err) {
|
|
276
|
+
setResult({ ok: false, msg: err.message });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function cancel() {
|
|
281
|
+
setPending({});
|
|
282
|
+
setResult(null);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const pendingKeys = Object.keys(pending);
|
|
286
|
+
|
|
287
|
+
return html`
|
|
288
|
+
<!-- Read-only banner for in-memory storage -->
|
|
289
|
+
${!editable && html`
|
|
290
|
+
<div class="adm-config-readonly-banner">
|
|
291
|
+
<p>${t('dashboard.cfgReadOnlyBanner')}</p>
|
|
292
|
+
<${ExpandableHelp} title=${t('dashboard.cfgReadOnlyHelpTitle')}>
|
|
293
|
+
<p>${t('dashboard.cfgReadOnlyHelpDetail')}</p>
|
|
294
|
+
</${ExpandableHelp}>
|
|
295
|
+
</div>
|
|
296
|
+
`}
|
|
297
|
+
|
|
298
|
+
${result && (result.ok
|
|
299
|
+
? html`<div class="adm-config-result-ok adm-mb-md adm-text-base">${escHtml(result.msg)}</div>`
|
|
300
|
+
: html`<div class="adm-mb-md"><${ErrorBox} message=${result.msg} /></div>`)}
|
|
301
|
+
|
|
302
|
+
${editable && html`<${PendingBar} paths=${pendingKeys} pending=${pending} schema=${schema}
|
|
303
|
+
onSave=${save} onCancel=${cancel} saving=${saving} />`}
|
|
304
|
+
|
|
305
|
+
<${Toc} domains=${tocDomains} onJump=${jumpTo} />
|
|
306
|
+
|
|
307
|
+
${domainOrder.map(domain => html`
|
|
308
|
+
<div class="adm-cfg-domain" key=${domain}>
|
|
309
|
+
<h3 class="adm-cfg-domain-head">${domainLabel(domain)}</h3>
|
|
310
|
+
${byDomain.get(domain).map(([g, items]) => {
|
|
311
|
+
const helpKey = 'dashboard.cfgHelp_' + g;
|
|
312
|
+
const helpText = t(helpKey);
|
|
313
|
+
const hasHelp = helpText !== helpKey;
|
|
314
|
+
const editedHere = items.filter(({ path }) => path in pending).length;
|
|
315
|
+
return html`
|
|
316
|
+
<details class="adm-card adm-mb-sm" id=${'cfg-' + g} key=${g} open>
|
|
317
|
+
<summary style="cursor:pointer;font-weight:600;font-size:.95rem;padding:8px 0">
|
|
318
|
+
${groupLabel(g)}
|
|
319
|
+
${editedHere > 0 && html` <span class="adm-cfg-edited-dot">● ${editedHere}</span>`}
|
|
320
|
+
</summary>
|
|
321
|
+
${hasHelp && html`<${ExpandableHelp} title=${t('dashboard.cfgHelpTitle')}><p>${helpText}</p></${ExpandableHelp}>`}
|
|
322
|
+
<div class=${!editable ? 'adm-config-readonly' : ''} style="padding:8px 0">
|
|
323
|
+
${items.map(({ path: p, entry: e }) => html`
|
|
324
|
+
<${FieldRow} key=${p} path=${p} entry=${e} editable=${editable}
|
|
325
|
+
pending=${pending} onChange=${onChange} onReset=${resetConfig} />
|
|
326
|
+
`)}
|
|
327
|
+
</div>
|
|
328
|
+
</details>
|
|
329
|
+
`})}
|
|
330
|
+
</div>
|
|
331
|
+
`)}
|
|
332
|
+
`;
|
|
333
|
+
}
|