arc-1 0.9.18 → 0.9.20
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 +33 -30
- package/dist/adt/config.d.ts +1 -1
- package/dist/adt/config.d.ts.map +1 -1
- package/dist/adt/errors.d.ts.map +1 -1
- package/dist/adt/errors.js +186 -25
- package/dist/adt/errors.js.map +1 -1
- package/dist/adt/http.d.ts +1 -1
- package/dist/adt/http.d.ts.map +1 -1
- package/dist/adt/http.js.map +1 -1
- package/dist/authz/policy.d.ts +6 -0
- package/dist/authz/policy.d.ts.map +1 -1
- package/dist/authz/policy.js +20 -0
- package/dist/authz/policy.js.map +1 -1
- package/dist/cache/cache.d.ts +25 -0
- package/dist/cache/cache.d.ts.map +1 -1
- package/dist/cache/cache.js.map +1 -1
- package/dist/cache/caching-layer.d.ts +31 -2
- package/dist/cache/caching-layer.d.ts.map +1 -1
- package/dist/cache/caching-layer.js +102 -2
- package/dist/cache/caching-layer.js.map +1 -1
- package/dist/cache/memory.d.ts +2 -1
- package/dist/cache/memory.d.ts.map +1 -1
- package/dist/cache/memory.js +46 -0
- package/dist/cache/memory.js.map +1 -1
- package/dist/cache/sqlite.d.ts +2 -1
- package/dist/cache/sqlite.d.ts.map +1 -1
- package/dist/cache/sqlite.js +54 -0
- package/dist/cache/sqlite.js.map +1 -1
- package/dist/cli.js +21 -3
- package/dist/cli.js.map +1 -1
- package/dist/handlers/context.d.ts.map +1 -1
- package/dist/handlers/context.js +27 -4
- package/dist/handlers/context.js.map +1 -1
- package/dist/handlers/dispatch.d.ts +3 -0
- package/dist/handlers/dispatch.d.ts.map +1 -1
- package/dist/handlers/dispatch.js +71 -53
- package/dist/handlers/dispatch.js.map +1 -1
- package/dist/handlers/object-types.d.ts +1 -1
- package/dist/handlers/object-types.d.ts.map +1 -1
- package/dist/handlers/object-types.js +10 -2
- package/dist/handlers/object-types.js.map +1 -1
- package/dist/handlers/read.d.ts.map +1 -1
- package/dist/handlers/read.js +4 -1
- package/dist/handlers/read.js.map +1 -1
- package/dist/handlers/schemas.d.ts +8 -0
- package/dist/handlers/schemas.d.ts.map +1 -1
- package/dist/handlers/schemas.js +2 -0
- package/dist/handlers/schemas.js.map +1 -1
- package/dist/handlers/tool-registry.d.ts +4 -4
- package/dist/handlers/tool-registry.d.ts.map +1 -1
- package/dist/handlers/tool-registry.js +4 -0
- package/dist/handlers/tool-registry.js.map +1 -1
- package/dist/handlers/tools.d.ts.map +1 -1
- package/dist/handlers/tools.js +29 -23
- package/dist/handlers/tools.js.map +1 -1
- package/dist/handlers/write/create.d.ts.map +1 -1
- package/dist/handlers/write/create.js +89 -4
- package/dist/handlers/write/create.js.map +1 -1
- package/dist/plugins/manifest-interpreter.d.ts +25 -0
- package/dist/plugins/manifest-interpreter.d.ts.map +1 -0
- package/dist/plugins/manifest-interpreter.js +126 -0
- package/dist/plugins/manifest-interpreter.js.map +1 -0
- package/dist/public/define-tool.d.ts +9 -0
- package/dist/public/define-tool.d.ts.map +1 -0
- package/dist/public/define-tool.js +25 -0
- package/dist/public/define-tool.js.map +1 -0
- package/dist/public/index.d.ts +9 -0
- package/dist/public/index.d.ts.map +1 -0
- package/dist/public/index.js +10 -0
- package/dist/public/index.js.map +1 -0
- package/dist/public/testing.d.ts +27 -0
- package/dist/public/testing.d.ts.map +1 -0
- package/dist/public/testing.js +52 -0
- package/dist/public/testing.js.map +1 -0
- package/dist/public/types.d.ts +87 -0
- package/dist/public/types.d.ts.map +1 -0
- package/dist/public/types.js +4 -0
- package/dist/public/types.js.map +1 -0
- package/dist/public/ui/app.js +1044 -0
- package/dist/public/ui/arc-mark.svg +5 -0
- package/dist/public/ui/index.html +43 -0
- package/dist/public/ui/styles.css +563 -0
- package/dist/registry/tool-registry.d.ts +74 -0
- package/dist/registry/tool-registry.d.ts.map +1 -0
- package/dist/registry/tool-registry.js +59 -0
- package/dist/registry/tool-registry.js.map +1 -0
- package/dist/server/app-url.d.ts +31 -0
- package/dist/server/app-url.d.ts.map +1 -0
- package/dist/server/app-url.js +50 -0
- package/dist/server/app-url.js.map +1 -0
- package/dist/server/audit.d.ts +4 -0
- package/dist/server/audit.d.ts.map +1 -1
- package/dist/server/audit.js.map +1 -1
- package/dist/server/config.d.ts.map +1 -1
- package/dist/server/config.js +92 -0
- package/dist/server/config.js.map +1 -1
- package/dist/server/http.d.ts +17 -47
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +127 -376
- package/dist/server/http.js.map +1 -1
- package/dist/server/logger.d.ts +22 -0
- package/dist/server/logger.d.ts.map +1 -1
- package/dist/server/logger.js +22 -0
- package/dist/server/logger.js.map +1 -1
- package/dist/server/plugin-loader.d.ts +19 -0
- package/dist/server/plugin-loader.d.ts.map +1 -0
- package/dist/server/plugin-loader.js +162 -0
- package/dist/server/plugin-loader.js.map +1 -0
- package/dist/server/safe-http-client.d.ts +44 -0
- package/dist/server/safe-http-client.d.ts.map +1 -0
- package/dist/server/safe-http-client.js +198 -0
- package/dist/server/safe-http-client.js.map +1 -0
- package/dist/server/server.d.ts +2 -2
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +63 -9
- package/dist/server/server.js.map +1 -1
- package/dist/server/types.d.ts +24 -0
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js +6 -0
- package/dist/server/types.js.map +1 -1
- package/dist/server/ui-log-buffer.d.ts +29 -0
- package/dist/server/ui-log-buffer.d.ts.map +1 -0
- package/dist/server/ui-log-buffer.js +72 -0
- package/dist/server/ui-log-buffer.js.map +1 -0
- package/dist/server/ui-state.d.ts +32 -0
- package/dist/server/ui-state.d.ts.map +1 -0
- package/dist/server/ui-state.js +230 -0
- package/dist/server/ui-state.js.map +1 -0
- package/dist/server/ui.d.ts +20 -0
- package/dist/server/ui.d.ts.map +1 -0
- package/dist/server/ui.js +275 -0
- package/dist/server/ui.js.map +1 -0
- package/package.json +34 -14
- package/dist/adt/btp.d.ts +0 -140
- package/dist/adt/btp.d.ts.map +0 -1
- package/dist/adt/btp.js +0 -427
- package/dist/adt/btp.js.map +0 -1
- package/dist/server/oauth-state.d.ts +0 -92
- package/dist/server/oauth-state.d.ts.map +0 -1
- package/dist/server/oauth-state.js +0 -163
- package/dist/server/oauth-state.js.map +0 -1
- package/dist/server/stateless-client-store.d.ts +0 -173
- package/dist/server/stateless-client-store.d.ts.map +0 -1
- package/dist/server/stateless-client-store.js +0 -503
- package/dist/server/stateless-client-store.js.map +0 -1
- package/dist/server/xsuaa.d.ts +0 -188
- package/dist/server/xsuaa.d.ts.map +0 -1
- package/dist/server/xsuaa.js +0 -464
- package/dist/server/xsuaa.js.map +0 -1
|
@@ -0,0 +1,1044 @@
|
|
|
1
|
+
const endpoints = {
|
|
2
|
+
overview: '/ui/api/overview',
|
|
3
|
+
config: '/ui/api/config',
|
|
4
|
+
safety: '/ui/api/safety',
|
|
5
|
+
features: '/ui/api/features',
|
|
6
|
+
cacheStats: '/ui/api/cache/stats',
|
|
7
|
+
cacheSources: '/ui/api/cache/sources',
|
|
8
|
+
logs: '/ui/api/logs',
|
|
9
|
+
docs: '/ui/api/docs',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const state = {
|
|
13
|
+
token: sessionStorage.getItem('arc1.ui.token') || '',
|
|
14
|
+
tab: 'overview',
|
|
15
|
+
refreshTimer: undefined,
|
|
16
|
+
refreshInFlight: false,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const content = document.querySelector('#content');
|
|
20
|
+
const statusBox = document.querySelector('#status');
|
|
21
|
+
const tokenInput = document.querySelector('#token');
|
|
22
|
+
const subtitle = document.querySelector('#subtitle');
|
|
23
|
+
|
|
24
|
+
tokenInput.value = state.token;
|
|
25
|
+
|
|
26
|
+
document.querySelector('#save-token').addEventListener('click', () => {
|
|
27
|
+
state.token = tokenInput.value.trim();
|
|
28
|
+
sessionStorage.setItem('arc1.ui.token', state.token);
|
|
29
|
+
loadTab(state.tab);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
document.querySelector('#clear-token').addEventListener('click', () => {
|
|
33
|
+
state.token = '';
|
|
34
|
+
tokenInput.value = '';
|
|
35
|
+
sessionStorage.removeItem('arc1.ui.token');
|
|
36
|
+
loadTab(state.tab);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
for (const button of document.querySelectorAll('[data-tab]')) {
|
|
40
|
+
button.addEventListener('click', () => {
|
|
41
|
+
state.tab = button.dataset.tab;
|
|
42
|
+
document.querySelectorAll('[data-tab]').forEach((tab) => tab.classList.toggle('active', tab === button));
|
|
43
|
+
loadTab(state.tab);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
loadTab(state.tab);
|
|
48
|
+
|
|
49
|
+
async function apiGet(path) {
|
|
50
|
+
const headers = { Accept: 'application/json' };
|
|
51
|
+
if (state.token) headers.Authorization = `Bearer ${state.token}`;
|
|
52
|
+
const response = await fetch(path, { headers });
|
|
53
|
+
const body = await response.json().catch(() => ({}));
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
const message = body.error || body.reason || `${response.status} ${response.statusText}`;
|
|
56
|
+
const err = new Error(message);
|
|
57
|
+
err.status = response.status;
|
|
58
|
+
throw err;
|
|
59
|
+
}
|
|
60
|
+
return body;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function loadTab(tab) {
|
|
64
|
+
clearAutoRefresh();
|
|
65
|
+
showStatus('');
|
|
66
|
+
content.replaceChildren(panel('Loading', text('Fetching current state...')));
|
|
67
|
+
try {
|
|
68
|
+
if (tab === 'overview') renderOverview(await apiGet(endpoints.overview));
|
|
69
|
+
if (tab === 'config') renderConfig(await apiGet(endpoints.config));
|
|
70
|
+
if (tab === 'safety') renderSafety(await apiGet(endpoints.safety));
|
|
71
|
+
if (tab === 'features') renderFeatures(await apiGet(endpoints.features));
|
|
72
|
+
if (tab === 'cache') renderCache();
|
|
73
|
+
if (tab === 'logs') renderLogs();
|
|
74
|
+
if (tab === 'docs') renderDocs(await apiGet(endpoints.docs));
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (error.status === 401) showStatus('Authentication required.');
|
|
77
|
+
content.replaceChildren(panel('Request Failed', codeBlock(error.message || String(error))));
|
|
78
|
+
} finally {
|
|
79
|
+
scheduleAutoRefresh();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function scheduleAutoRefresh() {
|
|
84
|
+
clearAutoRefresh();
|
|
85
|
+
state.refreshTimer = window.setInterval(refreshActiveTab, 5000);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function clearAutoRefresh() {
|
|
89
|
+
if (state.refreshTimer) {
|
|
90
|
+
window.clearInterval(state.refreshTimer);
|
|
91
|
+
state.refreshTimer = undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function refreshActiveTab() {
|
|
96
|
+
if (state.refreshInFlight || document.hidden) return;
|
|
97
|
+
state.refreshInFlight = true;
|
|
98
|
+
try {
|
|
99
|
+
await preserveScroll(async () => {
|
|
100
|
+
if (state.tab === 'overview') renderOverview(await apiGet(endpoints.overview));
|
|
101
|
+
if (state.tab === 'config') renderConfig(await apiGet(endpoints.config));
|
|
102
|
+
if (state.tab === 'safety') renderSafety(await apiGet(endpoints.safety));
|
|
103
|
+
if (state.tab === 'features') renderFeatures(await apiGet(endpoints.features));
|
|
104
|
+
if (state.tab === 'cache') await refreshCache({ silent: true });
|
|
105
|
+
if (state.tab === 'logs') await refreshLogs({ silent: true });
|
|
106
|
+
});
|
|
107
|
+
} catch (error) {
|
|
108
|
+
if (error.status === 401) showStatus('Authentication required.');
|
|
109
|
+
} finally {
|
|
110
|
+
state.refreshInFlight = false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function preserveScroll(action) {
|
|
115
|
+
const top = window.scrollY;
|
|
116
|
+
await action();
|
|
117
|
+
window.requestAnimationFrame(() => {
|
|
118
|
+
const maxTop = Math.max(0, document.documentElement.scrollHeight - window.innerHeight);
|
|
119
|
+
window.scrollTo(0, Math.min(top, maxTop));
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function renderOverview(data) {
|
|
124
|
+
subtitle.textContent = `${data.app.version} - ${data.transport.type}`;
|
|
125
|
+
const safety = data.safety || {};
|
|
126
|
+
const auth = data.auth || {};
|
|
127
|
+
const sapAuth = auth.sap || {};
|
|
128
|
+
const cache = data.cache || {};
|
|
129
|
+
content.replaceChildren(
|
|
130
|
+
panel(
|
|
131
|
+
'Runtime',
|
|
132
|
+
metricGrid([
|
|
133
|
+
['Version', data.app.version, 'accent'],
|
|
134
|
+
['Uptime', `${data.app.uptimeSeconds}s`, 'info'],
|
|
135
|
+
['Transport', data.transport.type, 'info'],
|
|
136
|
+
['UI mode', data.transport.uiMode, data.transport.uiMode === 'local' ? 'ok' : 'info'],
|
|
137
|
+
['SAP auth', sapAuthLabel(sapAuth), hasSapAuth(sapAuth) ? 'ok' : 'warn'],
|
|
138
|
+
['Cache', cache.mode, cache.mode === 'none' ? 'warn' : 'ok'],
|
|
139
|
+
]),
|
|
140
|
+
detailsList([
|
|
141
|
+
['Started at', data.app.startedAt],
|
|
142
|
+
['Process ID', data.app.pid],
|
|
143
|
+
['Node runtime', data.app.node],
|
|
144
|
+
['HTTP address', data.transport.httpAddr],
|
|
145
|
+
['UI address', data.transport.uiAddr],
|
|
146
|
+
]),
|
|
147
|
+
),
|
|
148
|
+
panel(
|
|
149
|
+
'Safety Posture',
|
|
150
|
+
statusGrid(safetyStatusRows(safety)),
|
|
151
|
+
detailsList([
|
|
152
|
+
['Allowed packages', safety.allowedPackages],
|
|
153
|
+
['Allowed transports', safety.allowedTransports],
|
|
154
|
+
['Denied actions', safety.denyActions],
|
|
155
|
+
]),
|
|
156
|
+
),
|
|
157
|
+
panel(
|
|
158
|
+
'Authentication',
|
|
159
|
+
statusGrid(authStatusRows(auth)),
|
|
160
|
+
detailsList([
|
|
161
|
+
['API key profiles', auth.apiKeys?.profiles],
|
|
162
|
+
['OIDC issuer', auth.oidc?.issuer || 'none'],
|
|
163
|
+
['OIDC audience', auth.oidc?.audience || 'none'],
|
|
164
|
+
['XSUAA DCR TTL', auth.xsuaa?.dcrTtlSeconds],
|
|
165
|
+
['DCR signing secret', auth.xsuaa?.dcrSigningSecret],
|
|
166
|
+
]),
|
|
167
|
+
),
|
|
168
|
+
panel(
|
|
169
|
+
'Cache',
|
|
170
|
+
metricGrid([
|
|
171
|
+
['Mode', cache.mode || 'none', cache.mode === 'none' ? 'warn' : 'ok'],
|
|
172
|
+
['Warmup', cache.warmup ? 'enabled' : 'disabled', cache.warmup ? 'ok' : ''],
|
|
173
|
+
['Packages', cache.warmupPackages || 'default', 'info'],
|
|
174
|
+
]),
|
|
175
|
+
detailsList([
|
|
176
|
+
['Cache file', cache.file || 'none'],
|
|
177
|
+
['Warmup packages', cache.warmupPackages || 'default'],
|
|
178
|
+
]),
|
|
179
|
+
),
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function renderSafety(data) {
|
|
184
|
+
const safety = data || {};
|
|
185
|
+
content.replaceChildren(
|
|
186
|
+
panel(
|
|
187
|
+
'Safety Ceiling',
|
|
188
|
+
statusGrid(safetyStatusRows(safety)),
|
|
189
|
+
detailsList([
|
|
190
|
+
['Allowed packages', safety.allowedPackages],
|
|
191
|
+
['Allowed transports', safety.allowedTransports],
|
|
192
|
+
['Denied actions', safety.denyActions],
|
|
193
|
+
]),
|
|
194
|
+
),
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function renderConfig(data) {
|
|
199
|
+
const cfg = data.config || {};
|
|
200
|
+
const safety = cfg.safety || {};
|
|
201
|
+
const auth = cfg.auth || {};
|
|
202
|
+
const cache = cfg.cache || {};
|
|
203
|
+
const features = cfg.features || {};
|
|
204
|
+
|
|
205
|
+
content.replaceChildren(
|
|
206
|
+
panel(
|
|
207
|
+
'Configuration Summary',
|
|
208
|
+
metricGrid([
|
|
209
|
+
['Transport', cfg.transport || '', 'info'],
|
|
210
|
+
['UI mode', cfg.uiMode || '', cfg.uiMode === 'local' ? 'ok' : 'info'],
|
|
211
|
+
['Tool mode', cfg.toolMode || '', 'accent'],
|
|
212
|
+
['Cache', cache.mode || '', cache.mode === 'none' ? 'warn' : 'ok'],
|
|
213
|
+
['Writes', safety.allowWrites ? 'enabled' : 'blocked', safety.allowWrites ? 'warn' : 'ok'],
|
|
214
|
+
['API keys', auth.apiKeys?.count ?? 0, auth.apiKeys?.count ? 'ok' : ''],
|
|
215
|
+
]),
|
|
216
|
+
),
|
|
217
|
+
panel(
|
|
218
|
+
'Connection',
|
|
219
|
+
detailsList([
|
|
220
|
+
['SAP URL', cfg.url],
|
|
221
|
+
['SAP user', cfg.username],
|
|
222
|
+
['Client', cfg.client],
|
|
223
|
+
['Language', cfg.language],
|
|
224
|
+
['TLS verification', cfg.insecure ? 'skipped' : 'enabled'],
|
|
225
|
+
['Cookie file', cfg.cookieFile || 'none'],
|
|
226
|
+
['Cookie string', cfg.cookieString],
|
|
227
|
+
]),
|
|
228
|
+
),
|
|
229
|
+
panel(
|
|
230
|
+
'Safety Gates',
|
|
231
|
+
statusGrid(safetyStatusRows(safety)),
|
|
232
|
+
detailsList([
|
|
233
|
+
['Allowed packages', safety.allowedPackages],
|
|
234
|
+
['Allowed transports', safety.allowedTransports],
|
|
235
|
+
['Denied actions', safety.denyActions],
|
|
236
|
+
]),
|
|
237
|
+
),
|
|
238
|
+
panel(
|
|
239
|
+
'Feature Toggles',
|
|
240
|
+
statusGrid(Object.entries(features).map(([name, value]) => [featureLabel(name), value, value ? 'ok' : ''])),
|
|
241
|
+
),
|
|
242
|
+
panel(
|
|
243
|
+
'Auth & Principal Propagation',
|
|
244
|
+
statusGrid(authStatusRows(auth, cfg.principalPropagation)),
|
|
245
|
+
detailsList([
|
|
246
|
+
['API key profiles', auth.apiKeys?.profiles],
|
|
247
|
+
['OIDC issuer', auth.oidc?.issuer || 'none'],
|
|
248
|
+
['OIDC audience', auth.oidc?.audience || 'none'],
|
|
249
|
+
['XSUAA DCR TTL', auth.xsuaa?.dcrTtlSeconds],
|
|
250
|
+
['PP strict', cfg.principalPropagation?.strict],
|
|
251
|
+
['PP shared cookies', cfg.principalPropagation?.allowSharedCookies],
|
|
252
|
+
]),
|
|
253
|
+
),
|
|
254
|
+
panel(
|
|
255
|
+
'Runtime & Cache',
|
|
256
|
+
detailsList([
|
|
257
|
+
['HTTP address', cfg.httpAddr],
|
|
258
|
+
['UI address', cfg.uiAddr],
|
|
259
|
+
['Open UI on startup', cfg.uiOpen],
|
|
260
|
+
['Cache file', cache.file],
|
|
261
|
+
['Cache warmup', cache.warmup],
|
|
262
|
+
['Cache warmup packages', cache.warmupPackages],
|
|
263
|
+
['Max concurrent SAP requests', cfg.concurrency?.maxConcurrent],
|
|
264
|
+
['Auth rate limit', cfg.rateLimiting?.authRateLimit],
|
|
265
|
+
['MCP rate limit', cfg.rateLimiting?.mcpRateLimit],
|
|
266
|
+
['Allowed origins', cfg.browser?.allowedOrigins],
|
|
267
|
+
]),
|
|
268
|
+
),
|
|
269
|
+
panel('Config Sources', configSourceTable(data.sources || {})),
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function renderFeatures(data) {
|
|
274
|
+
const featureEntries = Object.entries(data || {}).filter(([, value]) => isFeatureStatus(value));
|
|
275
|
+
const available = featureEntries.filter(([, value]) => value.available).length;
|
|
276
|
+
const unavailable = featureEntries.length - available;
|
|
277
|
+
const authProbe = data?.authProbe || {};
|
|
278
|
+
|
|
279
|
+
if (data?.probed === false || featureEntries.length === 0) {
|
|
280
|
+
content.replaceChildren(
|
|
281
|
+
panel('Feature State', metricGrid([['Probed', 'no']]), detailsList([['Message', data?.message || 'No feature data yet.']])),
|
|
282
|
+
);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
content.replaceChildren(
|
|
287
|
+
panel(
|
|
288
|
+
'Feature Summary',
|
|
289
|
+
metricGrid([
|
|
290
|
+
['Available', available, 'ok'],
|
|
291
|
+
['Unavailable', unavailable, unavailable > 0 ? 'warn' : 'ok'],
|
|
292
|
+
['SAP release', data.abapRelease || 'unknown', 'accent'],
|
|
293
|
+
['System type', data.systemType || 'unknown', 'info'],
|
|
294
|
+
['Discovery endpoints', data.discovery?.endpointCount ?? 0, 'info'],
|
|
295
|
+
['Text search', data.textSearch?.available ? 'available' : 'unavailable', data.textSearch?.available ? 'ok' : 'warn'],
|
|
296
|
+
]),
|
|
297
|
+
chartGrid([
|
|
298
|
+
barChart('Feature Availability', [
|
|
299
|
+
['Available', available, 'ok'],
|
|
300
|
+
['Unavailable', unavailable, unavailable > 0 ? 'warn' : ''],
|
|
301
|
+
]),
|
|
302
|
+
barChart(
|
|
303
|
+
'Configured Modes',
|
|
304
|
+
countRows(featureEntries.map(([, value]) => value.mode || 'unknown')),
|
|
305
|
+
),
|
|
306
|
+
]),
|
|
307
|
+
),
|
|
308
|
+
panel(
|
|
309
|
+
'Feature Details',
|
|
310
|
+
table(
|
|
311
|
+
['Feature', 'Available', 'Mode', 'Message', 'Probed At'],
|
|
312
|
+
featureEntries.map(([name, value]) => [
|
|
313
|
+
featureLabel(name),
|
|
314
|
+
pill(value.available ? 'yes' : 'no', value.available ? 'ok' : 'warn'),
|
|
315
|
+
value.mode || '',
|
|
316
|
+
value.message || '',
|
|
317
|
+
value.probedAt || '',
|
|
318
|
+
]),
|
|
319
|
+
),
|
|
320
|
+
),
|
|
321
|
+
panel(
|
|
322
|
+
'Search & Authorization',
|
|
323
|
+
statusGrid([
|
|
324
|
+
['Text search', data.textSearch?.available, data.textSearch?.available ? 'ok' : 'warn'],
|
|
325
|
+
['Search auth', authProbe.searchAccess, authProbe.searchAccess ? 'ok' : 'warn'],
|
|
326
|
+
['Transport auth', authProbe.transportAccess, authProbe.transportAccess ? 'ok' : 'warn'],
|
|
327
|
+
]),
|
|
328
|
+
detailsList([
|
|
329
|
+
['Text search reason', data.textSearch?.reason || 'none'],
|
|
330
|
+
['Search auth reason', authProbe.searchReason || 'none'],
|
|
331
|
+
['Transport auth reason', authProbe.transportReason || 'none'],
|
|
332
|
+
]),
|
|
333
|
+
),
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function renderCache() {
|
|
338
|
+
const container = document.createElement('div');
|
|
339
|
+
container.className = 'content';
|
|
340
|
+
const statsResult = document.createElement('div');
|
|
341
|
+
statsResult.id = 'cache-stats-result';
|
|
342
|
+
|
|
343
|
+
const sourceResult = document.createElement('div');
|
|
344
|
+
sourceResult.id = 'cache-source-result';
|
|
345
|
+
const activityResult = document.createElement('div');
|
|
346
|
+
activityResult.id = 'cache-activity-result';
|
|
347
|
+
container.append(
|
|
348
|
+
panel('Cache Stats', statsResult),
|
|
349
|
+
panel('Source Metadata', cacheSourceControls()),
|
|
350
|
+
panel('Source Entries', sourceResult),
|
|
351
|
+
panel('Recent Cache Activity', activityResult),
|
|
352
|
+
);
|
|
353
|
+
content.replaceChildren(container);
|
|
354
|
+
await refreshCache();
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async function refreshCache(options = {}) {
|
|
358
|
+
await refreshCacheStats();
|
|
359
|
+
await refreshCacheSources(options);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async function refreshCacheStats() {
|
|
363
|
+
const target = document.querySelector('#cache-stats-result');
|
|
364
|
+
const activityTarget = document.querySelector('#cache-activity-result');
|
|
365
|
+
if (!target || !activityTarget) return;
|
|
366
|
+
try {
|
|
367
|
+
const stats = await apiGet(endpoints.cacheStats);
|
|
368
|
+
if (!stats.enabled) {
|
|
369
|
+
target.replaceChildren(
|
|
370
|
+
metricGrid([
|
|
371
|
+
['State', 'disabled', 'warn'],
|
|
372
|
+
['Configured mode', stats.mode || 'none', stats.mode === 'none' ? 'warn' : 'info'],
|
|
373
|
+
]),
|
|
374
|
+
detailsList([['Reason', 'No cache layer is attached to this process.']]),
|
|
375
|
+
);
|
|
376
|
+
activityTarget.replaceChildren(text('Cache is disabled.'));
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const activityCounts = stats.activity?.counts || {};
|
|
381
|
+
target.replaceChildren(
|
|
382
|
+
metricGrid([
|
|
383
|
+
['Backend', stats.backend?.effective || stats.mode, stats.backend?.persistent ? 'ok' : 'info'],
|
|
384
|
+
['Persistence', stats.backend?.persistent ? 'persistent' : 'ephemeral', stats.backend?.persistent ? 'ok' : 'info'],
|
|
385
|
+
['Nodes', stats.stats.nodeCount, 'info'],
|
|
386
|
+
['Edges', stats.stats.edgeCount, 'info'],
|
|
387
|
+
['APIs', stats.stats.apiCount, 'info'],
|
|
388
|
+
['Sources', stats.stats.sourceCount, stats.stats.sourceCount ? 'ok' : ''],
|
|
389
|
+
['Contracts', stats.stats.contractCount, stats.stats.contractCount ? 'ok' : ''],
|
|
390
|
+
['Warmup', stats.warmup?.available ? 'available' : 'not available', stats.warmup?.available ? 'ok' : 'warn'],
|
|
391
|
+
['Invalidations', activityCounts.source_invalidate || 0, activityCounts.source_invalidate ? 'warn' : ''],
|
|
392
|
+
['Evictions', activityCounts.source_evict || 0, activityCounts.source_evict ? 'warn' : ''],
|
|
393
|
+
['Cache hits', activityCounts.source_hit || 0, activityCounts.source_hit ? 'ok' : ''],
|
|
394
|
+
['Cache misses', activityCounts.source_miss || 0, activityCounts.source_miss ? 'warn' : ''],
|
|
395
|
+
[
|
|
396
|
+
'SAP loads',
|
|
397
|
+
(activityCounts.source_store || 0) + (activityCounts.source_refresh || 0),
|
|
398
|
+
activityCounts.source_store || activityCounts.source_refresh ? 'info' : '',
|
|
399
|
+
],
|
|
400
|
+
]),
|
|
401
|
+
chartGrid([
|
|
402
|
+
barChart('Source Types', objectRows(stats.sources?.byType || {})),
|
|
403
|
+
barChart('Source Versions', objectRows(stats.sources?.byVersion || {})),
|
|
404
|
+
barChart('Cache Activity', objectRows(activityCounts, cacheEventLabel)),
|
|
405
|
+
]),
|
|
406
|
+
detailsList([
|
|
407
|
+
['Mode', stats.mode],
|
|
408
|
+
['Cache file', stats.backend?.file || 'none'],
|
|
409
|
+
['Warmup configured', stats.warmup?.configured ? 'yes' : 'no'],
|
|
410
|
+
['Warmup packages', stats.warmup?.packages || 'all configured packages'],
|
|
411
|
+
['Inactive-list users', stats.inactiveLists?.userCount ?? 0],
|
|
412
|
+
['Inactive-list entries', stats.inactiveLists?.totalEntries ?? 0],
|
|
413
|
+
['Source inventory', sourceInventoryLabel(stats.sources)],
|
|
414
|
+
]),
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
const activityItems = stats.activity?.items || [];
|
|
418
|
+
activityTarget.replaceChildren(
|
|
419
|
+
table(
|
|
420
|
+
['Time', 'Event', 'Object', 'Version', 'Detail'],
|
|
421
|
+
activityItems.map((item) => [
|
|
422
|
+
item.timestamp,
|
|
423
|
+
item.event,
|
|
424
|
+
cacheObjectLabel(item),
|
|
425
|
+
item.version || '',
|
|
426
|
+
cacheActivityDetail(item),
|
|
427
|
+
]),
|
|
428
|
+
),
|
|
429
|
+
text(`${activityItems.length} of ${stats.activity?.total ?? 0} events`),
|
|
430
|
+
);
|
|
431
|
+
} catch (error) {
|
|
432
|
+
target.replaceChildren(codeBlock(error.message || String(error)));
|
|
433
|
+
activityTarget.replaceChildren(codeBlock(error.message || String(error)));
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function cacheSourceControls() {
|
|
438
|
+
const wrap = document.createElement('div');
|
|
439
|
+
wrap.className = 'filters';
|
|
440
|
+
wrap.append(
|
|
441
|
+
labeledInput('objectType', 'Type', 'CLAS'),
|
|
442
|
+
labeledInput('q', 'Name', 'ZCL_'),
|
|
443
|
+
labeledSelect('version', 'Version', [
|
|
444
|
+
['', 'Any'],
|
|
445
|
+
['active', 'Active'],
|
|
446
|
+
['inactive', 'Inactive'],
|
|
447
|
+
]),
|
|
448
|
+
labeledInput('limit', 'Limit', '50'),
|
|
449
|
+
actionButton('Refresh', () => refreshCacheSources()),
|
|
450
|
+
);
|
|
451
|
+
return wrap;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async function refreshCacheSources(options = {}) {
|
|
455
|
+
const target = document.querySelector('#cache-source-result');
|
|
456
|
+
if (!target) return;
|
|
457
|
+
if (!options.silent) target.replaceChildren(text('Loading cache source metadata...'));
|
|
458
|
+
try {
|
|
459
|
+
const params = new URLSearchParams();
|
|
460
|
+
for (const name of ['objectType', 'q', 'version', 'limit']) {
|
|
461
|
+
const value = document.querySelector(`#${name}`)?.value.trim();
|
|
462
|
+
if (value) params.set(name, value);
|
|
463
|
+
}
|
|
464
|
+
const data = await apiGet(`${endpoints.cacheSources}?${params.toString()}`);
|
|
465
|
+
if (data.enabled === false) {
|
|
466
|
+
target.replaceChildren(
|
|
467
|
+
detailsList([
|
|
468
|
+
['State', 'disabled'],
|
|
469
|
+
['Configured mode', data.mode || 'none'],
|
|
470
|
+
['Entries', data.total ?? 0],
|
|
471
|
+
]),
|
|
472
|
+
);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
target.replaceChildren(
|
|
476
|
+
table(
|
|
477
|
+
['Type', 'Name', 'Version', 'Hash', 'ETag', 'Cached At', 'Length'],
|
|
478
|
+
data.items.map((item) => [
|
|
479
|
+
item.objectType,
|
|
480
|
+
item.objectName,
|
|
481
|
+
item.version,
|
|
482
|
+
item.hash.slice(0, 12),
|
|
483
|
+
item.etagPresent ? 'yes' : 'no',
|
|
484
|
+
item.cachedAt,
|
|
485
|
+
item.sourceLength,
|
|
486
|
+
]),
|
|
487
|
+
),
|
|
488
|
+
text(`${data.items.length} of ${data.total} entries`),
|
|
489
|
+
);
|
|
490
|
+
} catch (error) {
|
|
491
|
+
target.replaceChildren(codeBlock(error.message || String(error)));
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function sourceInventoryLabel(sources) {
|
|
496
|
+
if (!sources) return 'unavailable';
|
|
497
|
+
const sampled = sources.sampled ? `, sampled ${sources.sampleSize}` : '';
|
|
498
|
+
const etags = `${sources.etagCount || 0} with ETag`;
|
|
499
|
+
const newest = sources.newestCachedAt ? `, newest ${sources.newestCachedAt}` : '';
|
|
500
|
+
return `${sources.total} entries${sampled}, ${etags}${newest}`;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function cacheObjectLabel(item) {
|
|
504
|
+
if (!item.objectType && !item.objectName) return '';
|
|
505
|
+
return `${item.objectType || ''} ${item.objectName || ''}`.trim();
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function cacheActivityDetail(item) {
|
|
509
|
+
const parts = [];
|
|
510
|
+
if (item.removed !== undefined) parts.push(`removed ${item.removed}`);
|
|
511
|
+
if (item.sourceLength !== undefined) parts.push(`${item.sourceLength} chars`);
|
|
512
|
+
if (item.etagPresent !== undefined) parts.push(item.etagPresent ? 'ETag' : 'no ETag');
|
|
513
|
+
if (item.hash) parts.push(`hash ${item.hash.slice(0, 12)}`);
|
|
514
|
+
if (item.detail) parts.push(item.detail);
|
|
515
|
+
return parts.join(', ');
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
async function renderLogs() {
|
|
519
|
+
const controls = document.createElement('div');
|
|
520
|
+
controls.className = 'filters';
|
|
521
|
+
controls.append(
|
|
522
|
+
labeledInput('log-event', 'Event', 'tool_call_end'),
|
|
523
|
+
labeledSelect('log-level', 'Level', [
|
|
524
|
+
['', 'Any'],
|
|
525
|
+
['debug', 'Debug'],
|
|
526
|
+
['info', 'Info'],
|
|
527
|
+
['warn', 'Warn'],
|
|
528
|
+
['error', 'Error'],
|
|
529
|
+
]),
|
|
530
|
+
labeledInput('log-limit', 'Limit', '100'),
|
|
531
|
+
actionButton('Refresh', () => refreshLogs()),
|
|
532
|
+
);
|
|
533
|
+
const summary = document.createElement('div');
|
|
534
|
+
summary.id = 'logs-summary-result';
|
|
535
|
+
const result = document.createElement('div');
|
|
536
|
+
result.id = 'logs-result';
|
|
537
|
+
content.replaceChildren(panel('Audit Events', controls), panel('Log Overview', summary), panel('Audit Event Stream', result));
|
|
538
|
+
await refreshLogs();
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
async function refreshLogs(options = {}) {
|
|
542
|
+
const target = document.querySelector('#logs-result');
|
|
543
|
+
const summaryTarget = document.querySelector('#logs-summary-result');
|
|
544
|
+
if (!target) return;
|
|
545
|
+
if (!options.silent) target.replaceChildren(text('Loading audit events...'));
|
|
546
|
+
try {
|
|
547
|
+
const params = new URLSearchParams();
|
|
548
|
+
const event = document.querySelector('#log-event')?.value.trim();
|
|
549
|
+
const level = document.querySelector('#log-level')?.value.trim();
|
|
550
|
+
const limit = document.querySelector('#log-limit')?.value.trim();
|
|
551
|
+
if (event) params.set('event', event);
|
|
552
|
+
if (level) params.set('level', level);
|
|
553
|
+
if (limit) params.set('limit', limit);
|
|
554
|
+
const summaryParams = new URLSearchParams();
|
|
555
|
+
summaryParams.set('limit', String(Math.max(200, Number.parseInt(limit || '100', 10) || 100)));
|
|
556
|
+
const [data, summaryData] = await Promise.all([
|
|
557
|
+
apiGet(`${endpoints.logs}?${params.toString()}`),
|
|
558
|
+
apiGet(`${endpoints.logs}?${summaryParams.toString()}`),
|
|
559
|
+
]);
|
|
560
|
+
if (summaryTarget) summaryTarget.replaceChildren(logSummary(summaryData, data));
|
|
561
|
+
target.replaceChildren(
|
|
562
|
+
table(
|
|
563
|
+
['Time', 'Level', 'Event', 'Request', 'Detail'],
|
|
564
|
+
data.items.map((item) => [
|
|
565
|
+
item.timestamp,
|
|
566
|
+
pill(item.level, statusForLabel(item.level)),
|
|
567
|
+
item.event,
|
|
568
|
+
item.requestId || '',
|
|
569
|
+
compactLogDetail(item),
|
|
570
|
+
]),
|
|
571
|
+
),
|
|
572
|
+
text(`${data.items.length} of ${data.total} events`),
|
|
573
|
+
);
|
|
574
|
+
} catch (error) {
|
|
575
|
+
target.replaceChildren(codeBlock(error.message || String(error)));
|
|
576
|
+
if (summaryTarget) summaryTarget.replaceChildren(codeBlock(error.message || String(error)));
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function renderDocs(data) {
|
|
581
|
+
const list = document.createElement('div');
|
|
582
|
+
list.className = 'kv';
|
|
583
|
+
for (const link of data.links) {
|
|
584
|
+
const label = document.createElement('div');
|
|
585
|
+
label.textContent = link.label;
|
|
586
|
+
const value = document.createElement('div');
|
|
587
|
+
const anchor = document.createElement('a');
|
|
588
|
+
anchor.href = link.href;
|
|
589
|
+
anchor.target = '_blank';
|
|
590
|
+
anchor.rel = 'noreferrer';
|
|
591
|
+
anchor.textContent = link.href;
|
|
592
|
+
value.append(anchor);
|
|
593
|
+
list.append(label, value);
|
|
594
|
+
}
|
|
595
|
+
content.replaceChildren(panel('Documentation', list));
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function panel(title, ...children) {
|
|
599
|
+
const section = document.createElement('section');
|
|
600
|
+
section.className = 'panel';
|
|
601
|
+
const heading = document.createElement('h2');
|
|
602
|
+
heading.textContent = title;
|
|
603
|
+
section.append(heading, ...children);
|
|
604
|
+
return section;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function metricGrid(items) {
|
|
608
|
+
const grid = document.createElement('div');
|
|
609
|
+
grid.className = 'grid';
|
|
610
|
+
for (const [label, value, status] of items) {
|
|
611
|
+
const item = document.createElement('div');
|
|
612
|
+
item.className = `metric ${status || ''}`.trim();
|
|
613
|
+
const strong = document.createElement('strong');
|
|
614
|
+
strong.textContent = String(value ?? '');
|
|
615
|
+
const span = document.createElement('span');
|
|
616
|
+
span.textContent = label;
|
|
617
|
+
item.append(strong, span);
|
|
618
|
+
grid.append(item);
|
|
619
|
+
}
|
|
620
|
+
return grid;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function chartGrid(charts) {
|
|
624
|
+
const grid = document.createElement('div');
|
|
625
|
+
grid.className = 'chart-grid';
|
|
626
|
+
grid.append(...charts);
|
|
627
|
+
return grid;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function barChart(title, rows) {
|
|
631
|
+
const chart = document.createElement('div');
|
|
632
|
+
chart.className = 'chart';
|
|
633
|
+
const heading = document.createElement('h3');
|
|
634
|
+
heading.textContent = title;
|
|
635
|
+
chart.append(heading);
|
|
636
|
+
|
|
637
|
+
const cleanRows = rows
|
|
638
|
+
.map(([label, value, status]) => [label, Number(value) || 0, status || ''])
|
|
639
|
+
.filter(([, value]) => value > 0)
|
|
640
|
+
.sort((a, b) => b[1] - a[1])
|
|
641
|
+
.slice(0, 10);
|
|
642
|
+
|
|
643
|
+
if (cleanRows.length === 0) {
|
|
644
|
+
const empty = document.createElement('p');
|
|
645
|
+
empty.textContent = 'No data yet.';
|
|
646
|
+
chart.append(empty);
|
|
647
|
+
return chart;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const max = Math.max(...cleanRows.map(([, value]) => value));
|
|
651
|
+
for (const [label, value, status] of cleanRows) {
|
|
652
|
+
const row = document.createElement('div');
|
|
653
|
+
row.className = 'bar-row';
|
|
654
|
+
const name = document.createElement('div');
|
|
655
|
+
name.className = 'bar-label';
|
|
656
|
+
name.textContent = String(label);
|
|
657
|
+
const track = document.createElement('div');
|
|
658
|
+
track.className = 'bar-track';
|
|
659
|
+
const fill = document.createElement('div');
|
|
660
|
+
fill.className = `bar-fill ${status || ''}`.trim();
|
|
661
|
+
fill.style.width = `${Math.max(4, Math.round((value / max) * 100))}%`;
|
|
662
|
+
track.append(fill);
|
|
663
|
+
const count = document.createElement('div');
|
|
664
|
+
count.className = 'bar-value';
|
|
665
|
+
count.textContent = String(value);
|
|
666
|
+
row.append(name, track, count);
|
|
667
|
+
chart.append(row);
|
|
668
|
+
}
|
|
669
|
+
return chart;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function statusGrid(items) {
|
|
673
|
+
const grid = document.createElement('div');
|
|
674
|
+
grid.className = 'status-grid';
|
|
675
|
+
for (const [label, value, status] of items) {
|
|
676
|
+
const item = document.createElement('div');
|
|
677
|
+
item.className = `status-item ${status || ''}`.trim();
|
|
678
|
+
const name = document.createElement('span');
|
|
679
|
+
name.textContent = label;
|
|
680
|
+
const rendered = typeof value === 'boolean' ? pill(value ? 'enabled' : 'disabled', value ? status || 'ok' : '') : renderInlineValue(value);
|
|
681
|
+
item.append(name, rendered);
|
|
682
|
+
grid.append(item);
|
|
683
|
+
}
|
|
684
|
+
return grid;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function detailsList(items) {
|
|
688
|
+
const wrap = document.createElement('div');
|
|
689
|
+
wrap.className = 'kv';
|
|
690
|
+
for (const [key, value] of items) {
|
|
691
|
+
const name = document.createElement('div');
|
|
692
|
+
name.textContent = key;
|
|
693
|
+
const val = document.createElement('div');
|
|
694
|
+
val.append(renderInlineValue(value));
|
|
695
|
+
wrap.append(name, val);
|
|
696
|
+
}
|
|
697
|
+
return wrap;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function renderInlineValue(value) {
|
|
701
|
+
if (value instanceof Node) return value;
|
|
702
|
+
if (value === null || value === undefined || value === '') return text('none');
|
|
703
|
+
if (typeof value === 'boolean') return pill(value ? 'yes' : 'no', value ? 'ok' : '');
|
|
704
|
+
if (typeof value === 'number') return text(String(value));
|
|
705
|
+
if (typeof value === 'string') return text(value);
|
|
706
|
+
if (Array.isArray(value)) return text(value.length ? value.join(', ') : 'none');
|
|
707
|
+
if (typeof value === 'object' && 'configured' in value) {
|
|
708
|
+
return pill(value.configured ? 'configured' : 'not configured', value.configured ? 'ok' : '');
|
|
709
|
+
}
|
|
710
|
+
if (typeof value === 'object') {
|
|
711
|
+
return text(
|
|
712
|
+
Object.entries(value)
|
|
713
|
+
.map(([key, nested]) => `${key}: ${formatPrimitive(nested)}`)
|
|
714
|
+
.join(', ') || 'none',
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
return text(String(value));
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function formatPrimitive(value) {
|
|
721
|
+
if (value === null || value === undefined || value === '') return 'none';
|
|
722
|
+
if (Array.isArray(value)) return value.join(', ') || 'none';
|
|
723
|
+
if (typeof value === 'object' && 'configured' in value) return value.configured ? 'configured' : 'not configured';
|
|
724
|
+
if (typeof value === 'object') return 'configured';
|
|
725
|
+
return String(value);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function table(headers, rows) {
|
|
729
|
+
const wrap = document.createElement('div');
|
|
730
|
+
wrap.className = 'table-wrap';
|
|
731
|
+
const tableElement = document.createElement('table');
|
|
732
|
+
const thead = document.createElement('thead');
|
|
733
|
+
const headRow = document.createElement('tr');
|
|
734
|
+
for (const header of headers) {
|
|
735
|
+
const th = document.createElement('th');
|
|
736
|
+
th.textContent = header;
|
|
737
|
+
headRow.append(th);
|
|
738
|
+
}
|
|
739
|
+
thead.append(headRow);
|
|
740
|
+
const tbody = document.createElement('tbody');
|
|
741
|
+
for (const row of rows) {
|
|
742
|
+
const tr = document.createElement('tr');
|
|
743
|
+
for (const cell of row) {
|
|
744
|
+
const td = document.createElement('td');
|
|
745
|
+
td.append(cell instanceof Node ? cell : text(String(cell ?? '')));
|
|
746
|
+
tr.append(td);
|
|
747
|
+
}
|
|
748
|
+
tbody.append(tr);
|
|
749
|
+
}
|
|
750
|
+
tableElement.append(thead, tbody);
|
|
751
|
+
wrap.append(tableElement);
|
|
752
|
+
return wrap;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function labeledInput(id, label, defaultValue = '') {
|
|
756
|
+
const wrap = document.createElement('label');
|
|
757
|
+
wrap.append(text(label));
|
|
758
|
+
const input = document.createElement('input');
|
|
759
|
+
input.id = id;
|
|
760
|
+
input.value = defaultValue;
|
|
761
|
+
input.spellcheck = false;
|
|
762
|
+
wrap.append(input);
|
|
763
|
+
return wrap;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function labeledSelect(id, label, options) {
|
|
767
|
+
const wrap = document.createElement('label');
|
|
768
|
+
wrap.append(text(label));
|
|
769
|
+
const select = document.createElement('select');
|
|
770
|
+
select.id = id;
|
|
771
|
+
for (const [value, textValue] of options) {
|
|
772
|
+
const option = document.createElement('option');
|
|
773
|
+
option.value = value;
|
|
774
|
+
option.textContent = textValue;
|
|
775
|
+
select.append(option);
|
|
776
|
+
}
|
|
777
|
+
wrap.append(select);
|
|
778
|
+
return wrap;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function actionButton(label, action) {
|
|
782
|
+
const button = document.createElement('button');
|
|
783
|
+
button.type = 'button';
|
|
784
|
+
button.textContent = label;
|
|
785
|
+
button.addEventListener('click', action);
|
|
786
|
+
return button;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function text(value) {
|
|
790
|
+
return document.createTextNode(value);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function codeBlock(value) {
|
|
794
|
+
const pre = document.createElement('pre');
|
|
795
|
+
pre.textContent = value;
|
|
796
|
+
return pre;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function pill(value, status) {
|
|
800
|
+
const span = document.createElement('span');
|
|
801
|
+
span.className = `pill ${status || ''}`.trim();
|
|
802
|
+
span.textContent = value;
|
|
803
|
+
return span;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function showStatus(message) {
|
|
807
|
+
statusBox.hidden = !message;
|
|
808
|
+
statusBox.textContent = message;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function sapAuthLabel(sap) {
|
|
812
|
+
if (sap.principalPropagation) return 'principal propagation';
|
|
813
|
+
if (sap.btpServiceKey) return 'btp service key';
|
|
814
|
+
if (sap.destination) return 'btp destination';
|
|
815
|
+
if (sap.cookieFile || sap.cookieString) return 'cookie';
|
|
816
|
+
if (sap.basic) return 'basic';
|
|
817
|
+
return 'none';
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function hasSapAuth(sap) {
|
|
821
|
+
return Boolean(sap.basic || sap.cookieFile || sap.cookieString || sap.btpServiceKey || sap.destination || sap.principalPropagation);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function compactLogDetail(item) {
|
|
825
|
+
const chips = [];
|
|
826
|
+
for (const key of [
|
|
827
|
+
'tool',
|
|
828
|
+
'status',
|
|
829
|
+
'durationMs',
|
|
830
|
+
'resultSize',
|
|
831
|
+
'method',
|
|
832
|
+
'path',
|
|
833
|
+
'statusCode',
|
|
834
|
+
'operation',
|
|
835
|
+
'reason',
|
|
836
|
+
'errorClass',
|
|
837
|
+
'errorMessage',
|
|
838
|
+
]) {
|
|
839
|
+
if (item[key] !== undefined && item[key] !== '') chips.push([detailLabel(key), formatPrimitive(item[key]), detailStatus(key, item[key])]);
|
|
840
|
+
}
|
|
841
|
+
if (chips.length === 0) {
|
|
842
|
+
const clone = { ...item };
|
|
843
|
+
for (const key of ['timestamp', 'level', 'event', 'requestId']) delete clone[key];
|
|
844
|
+
for (const [key, value] of Object.entries(clone)) {
|
|
845
|
+
chips.push([detailLabel(key), formatPrimitive(value), detailStatus(key, value)]);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
return detailChips(chips);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function logSummary(data, streamData) {
|
|
852
|
+
const items = data.items || [];
|
|
853
|
+
const toolCalls = items.filter((item) => item.event === 'tool_call_end');
|
|
854
|
+
const avgDuration = average(toolCalls.map((item) => item.durationMs).filter((value) => typeof value === 'number'));
|
|
855
|
+
const httpRequests = items.filter((item) => item.event === 'http_request');
|
|
856
|
+
const streamFilterLabel = streamData && streamData.total !== data.total ? `${streamData.items?.length || 0} filtered rows shown` : 'unfiltered stream';
|
|
857
|
+
const fragment = document.createDocumentFragment();
|
|
858
|
+
fragment.append(
|
|
859
|
+
metricGrid([
|
|
860
|
+
['Recent events', items.length, 'info'],
|
|
861
|
+
['Tool calls', toolCalls.length, 'accent'],
|
|
862
|
+
['Errors', toolCalls.filter((item) => item.status === 'error').length, toolCalls.some((item) => item.status === 'error') ? 'warn' : 'ok'],
|
|
863
|
+
['HTTP requests', httpRequests.length, 'info'],
|
|
864
|
+
['Avg tool duration', avgDuration === undefined ? 'n/a' : `${Math.round(avgDuration)}ms`, avgDuration && avgDuration > 1000 ? 'warn' : 'ok'],
|
|
865
|
+
['Stream filter', streamFilterLabel, streamFilterLabel === 'unfiltered stream' ? 'ok' : 'info'],
|
|
866
|
+
]),
|
|
867
|
+
chartGrid([
|
|
868
|
+
barChart('Tool Calls by Tool', countRows(toolCalls.map((item) => item.tool || 'unknown'), toolStatus)),
|
|
869
|
+
barChart('Tool Call Status', countRows(toolCalls.map((item) => item.status || 'unknown'), statusForLabel)),
|
|
870
|
+
barChart('Event Mix', countRows(items.map((item) => item.event || 'unknown'))),
|
|
871
|
+
barChart('Level Mix', countRows(items.map((item) => item.level || 'unknown'), statusForLabel)),
|
|
872
|
+
barChart('HTTP Status Codes', countRows(httpRequests.map((item) => item.statusCode || 'unknown'), httpStatus)),
|
|
873
|
+
]),
|
|
874
|
+
slowestCallsTable(toolCalls),
|
|
875
|
+
);
|
|
876
|
+
return fragment;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function slowestCallsTable(toolCalls) {
|
|
880
|
+
const rows = [...toolCalls]
|
|
881
|
+
.filter((item) => typeof item.durationMs === 'number')
|
|
882
|
+
.sort((a, b) => b.durationMs - a.durationMs)
|
|
883
|
+
.slice(0, 5)
|
|
884
|
+
.map((item) => [item.timestamp, item.tool || '', pill(item.status || '', statusForLabel(item.status)), `${item.durationMs}ms`, item.requestId || '']);
|
|
885
|
+
if (rows.length === 0) return text('No timed tool calls in the current log slice.');
|
|
886
|
+
return table(['Time', 'Tool', 'Status', 'Duration', 'Request'], rows);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function slowestToolLabel(toolCalls) {
|
|
890
|
+
const slowest = [...toolCalls].filter((item) => typeof item.durationMs === 'number').sort((a, b) => b.durationMs - a.durationMs)[0];
|
|
891
|
+
return slowest ? `${slowest.tool || 'unknown'} ${slowest.durationMs}ms` : 'n/a';
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function objectRows(obj, labeler = (label) => label) {
|
|
895
|
+
return Object.entries(obj || {}).map(([label, value]) => [labeler(label), Number(value) || 0, statusForLabel(label)]);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function countRows(values, statusMapper = () => '') {
|
|
899
|
+
const counts = {};
|
|
900
|
+
for (const value of values) {
|
|
901
|
+
const label = String(value || 'unknown');
|
|
902
|
+
counts[label] = (counts[label] || 0) + 1;
|
|
903
|
+
}
|
|
904
|
+
return Object.entries(counts).map(([label, value]) => [label, value, statusMapper(label)]);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
function average(values) {
|
|
908
|
+
if (!values.length) return undefined;
|
|
909
|
+
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function statusForLabel(label) {
|
|
913
|
+
const normalized = String(label).toLowerCase();
|
|
914
|
+
if (['success', 'info', 'available', 'ok'].includes(normalized)) return 'ok';
|
|
915
|
+
if (['error'].includes(normalized)) return 'error';
|
|
916
|
+
if (['warn', 'warning', 'unavailable', 'disabled'].includes(normalized)) return 'warn';
|
|
917
|
+
return '';
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
function toolStatus(tool) {
|
|
921
|
+
const normalized = String(tool).toLowerCase();
|
|
922
|
+
if (normalized.includes('read') || normalized.includes('search') || normalized.includes('navigate')) return 'info';
|
|
923
|
+
if (normalized.includes('query') || normalized.includes('context') || normalized.includes('diagnose')) return 'accent';
|
|
924
|
+
if (normalized.includes('write') || normalized.includes('activate') || normalized.includes('transport') || normalized.includes('git')) return 'warn';
|
|
925
|
+
return '';
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function httpStatus(code) {
|
|
929
|
+
const statusCode = Number(code);
|
|
930
|
+
if (statusCode >= 200 && statusCode < 300) return 'ok';
|
|
931
|
+
if (statusCode >= 400 && statusCode < 500) return 'warn';
|
|
932
|
+
if (statusCode >= 500) return 'error';
|
|
933
|
+
return '';
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
function cacheEventLabel(event) {
|
|
937
|
+
return (
|
|
938
|
+
{
|
|
939
|
+
source_miss: 'Source miss',
|
|
940
|
+
source_store: 'SAP source load',
|
|
941
|
+
source_hit: 'Source hit',
|
|
942
|
+
source_refresh: 'Source refresh',
|
|
943
|
+
source_invalidate: 'Invalidation',
|
|
944
|
+
source_evict: 'Eviction',
|
|
945
|
+
depgraph_hit: 'Dep graph hit',
|
|
946
|
+
depgraph_store: 'Dep graph store',
|
|
947
|
+
func_group_hit: 'Function group hit',
|
|
948
|
+
func_group_store: 'Function group store',
|
|
949
|
+
warmup_state: 'Warmup state',
|
|
950
|
+
}[event] || event
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function isFeatureStatus(value) {
|
|
955
|
+
return value && typeof value === 'object' && typeof value.available === 'boolean' && 'mode' in value;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function safetyStatusRows(safety) {
|
|
959
|
+
return [
|
|
960
|
+
['Writes', safety.allowWrites, safety.allowWrites ? 'warn' : 'ok'],
|
|
961
|
+
['Data preview', safety.allowDataPreview, safety.allowDataPreview ? 'warn' : 'ok'],
|
|
962
|
+
['Free SQL', safety.allowFreeSQL, safety.allowFreeSQL ? 'warn' : 'ok'],
|
|
963
|
+
['Transport writes', safety.allowTransportWrites, safety.allowTransportWrites ? 'warn' : 'ok'],
|
|
964
|
+
['Git writes', safety.allowGitWrites, safety.allowGitWrites ? 'warn' : 'ok'],
|
|
965
|
+
['Read-only default', safety.readOnlyDefault, safety.readOnlyDefault ? 'ok' : 'warn'],
|
|
966
|
+
];
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function authStatusRows(auth, principalPropagation = {}) {
|
|
970
|
+
return [
|
|
971
|
+
['Basic SAP auth', auth.sap?.basic, auth.sap?.basic ? 'ok' : ''],
|
|
972
|
+
['Cookie auth', auth.sap?.cookieFile || auth.sap?.cookieString, auth.sap?.cookieFile || auth.sap?.cookieString ? 'ok' : ''],
|
|
973
|
+
['BTP service key', auth.sap?.btpServiceKey, auth.sap?.btpServiceKey ? 'ok' : ''],
|
|
974
|
+
['Destination', auth.sap?.destination, auth.sap?.destination ? 'ok' : ''],
|
|
975
|
+
['OIDC', auth.oidc?.configured, auth.oidc?.configured ? 'ok' : ''],
|
|
976
|
+
['XSUAA', auth.xsuaa?.enabled, auth.xsuaa?.enabled ? 'ok' : ''],
|
|
977
|
+
['Principal propagation', principalPropagation.enabled ?? auth.sap?.principalPropagation, principalPropagation.enabled || auth.sap?.principalPropagation ? 'ok' : ''],
|
|
978
|
+
];
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
function featureLabel(name) {
|
|
982
|
+
const labels = {
|
|
983
|
+
abapGit: 'abapGit',
|
|
984
|
+
gcts: 'gCTS',
|
|
985
|
+
rap: 'RAP/CDS',
|
|
986
|
+
amdp: 'AMDP',
|
|
987
|
+
ui5: 'UI5/Fiori BSP',
|
|
988
|
+
ui5repo: 'UI5 repository',
|
|
989
|
+
flp: 'FLP customization',
|
|
990
|
+
hana: 'HANA',
|
|
991
|
+
transport: 'CTS transport',
|
|
992
|
+
};
|
|
993
|
+
return labels[name] || String(name).replace(/([a-z])([A-Z])/g, '$1 $2');
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
function configSourceTable(sources) {
|
|
997
|
+
const rows = Object.entries(sources || {}).map(([key, source]) => [key, configSourceLabel(source)]);
|
|
998
|
+
if (rows.length === 0) return text('No non-default configuration sources recorded.');
|
|
999
|
+
return table(['Option', 'Source'], rows);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
function configSourceLabel(source) {
|
|
1003
|
+
if (!source || typeof source !== 'object') return String(source || 'default');
|
|
1004
|
+
return Object.entries(source)
|
|
1005
|
+
.map(([kind, value]) => `${kind}: ${value}`)
|
|
1006
|
+
.join(', ');
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function detailChips(chips) {
|
|
1010
|
+
const wrap = document.createElement('div');
|
|
1011
|
+
wrap.className = 'detail-chips';
|
|
1012
|
+
for (const [label, value, status] of chips) {
|
|
1013
|
+
const chip = document.createElement('span');
|
|
1014
|
+
chip.className = `detail-chip ${status || ''}`.trim();
|
|
1015
|
+
const name = document.createElement('span');
|
|
1016
|
+
name.textContent = label;
|
|
1017
|
+
const val = document.createElement('strong');
|
|
1018
|
+
val.textContent = value;
|
|
1019
|
+
chip.append(name, val);
|
|
1020
|
+
wrap.append(chip);
|
|
1021
|
+
}
|
|
1022
|
+
return wrap;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function detailLabel(key) {
|
|
1026
|
+
return (
|
|
1027
|
+
{
|
|
1028
|
+
durationMs: 'duration',
|
|
1029
|
+
resultSize: 'result',
|
|
1030
|
+
statusCode: 'http',
|
|
1031
|
+
errorClass: 'class',
|
|
1032
|
+
errorMessage: 'error',
|
|
1033
|
+
}[key] || key
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
function detailStatus(key, value) {
|
|
1038
|
+
if (key === 'status') return statusForLabel(value);
|
|
1039
|
+
if (key === 'level') return statusForLabel(value);
|
|
1040
|
+
if (key === 'statusCode') return httpStatus(value);
|
|
1041
|
+
if (key === 'errorClass' || key === 'errorMessage' || key === 'reason') return 'warn';
|
|
1042
|
+
if (key === 'durationMs' && Number(value) > 1000) return 'warn';
|
|
1043
|
+
return '';
|
|
1044
|
+
}
|