@symerian/symi 2.6.32 → 2.6.34
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/dist/build-info.json +3 -3
- package/dist/canvas-host/a2ui/.bundle.hash +1 -1
- package/dist/control-ui/css/style.css +441 -1
- package/dist/control-ui/index.html +1 -0
- package/dist/control-ui/js/app.js +4 -0
- package/dist/control-ui/js/menu.js +41 -1
- package/dist/control-ui/js/settings.js +681 -0
- package/extensions/bluebubbles/package.json +1 -1
- package/extensions/copilot-proxy/package.json +1 -1
- package/extensions/diagnostics-otel/package.json +1 -1
- package/extensions/discord/package.json +1 -1
- package/extensions/feishu/package.json +1 -1
- package/extensions/google-antigravity-auth/package.json +1 -1
- package/extensions/google-gemini-cli-auth/package.json +1 -1
- package/extensions/googlechat/package.json +1 -1
- package/extensions/imessage/package.json +1 -1
- package/extensions/irc/package.json +1 -1
- package/extensions/learning-loop/package.json +1 -1
- package/extensions/line/package.json +1 -1
- package/extensions/llm-task/package.json +1 -1
- package/extensions/matrix/CHANGELOG.md +12 -0
- package/extensions/matrix/package.json +1 -1
- package/extensions/mattermost/package.json +1 -1
- package/extensions/memory-core/package.json +1 -1
- package/extensions/memory-lancedb/package.json +1 -1
- package/extensions/minimax-portal-auth/package.json +1 -1
- package/extensions/msteams/CHANGELOG.md +12 -0
- package/extensions/msteams/package.json +1 -1
- package/extensions/nextcloud-talk/package.json +1 -1
- package/extensions/nostr/CHANGELOG.md +12 -0
- package/extensions/nostr/package.json +1 -1
- package/extensions/open-prose/package.json +1 -1
- package/extensions/outlook/package.json +1 -1
- package/extensions/pipeline/package.json +1 -1
- package/extensions/signal/package.json +1 -1
- package/extensions/slack/package.json +1 -1
- package/extensions/telegram/package.json +1 -1
- package/extensions/tlon/package.json +1 -1
- package/extensions/twitch/CHANGELOG.md +12 -0
- package/extensions/twitch/package.json +1 -1
- package/extensions/voice-call/CHANGELOG.md +12 -0
- package/extensions/voice-call/package.json +1 -1
- package/extensions/whatsapp/package.json +1 -1
- package/extensions/zalo/CHANGELOG.md +12 -0
- package/extensions/zalo/package.json +1 -1
- package/extensions/zalouser/CHANGELOG.md +12 -0
- package/extensions/zalouser/package.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
// ── Native Settings Panels ────────────────────────────────────────────
|
|
2
|
+
// Renders Config, Debug, and Logs natively inside the page overlay.
|
|
3
|
+
// No iframe, no SPA proxy — uses gateway RPC over WebSocket.
|
|
4
|
+
|
|
5
|
+
// ── Shared helpers ────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
function escapeSettingsHtml(s) {
|
|
8
|
+
if (!s) {
|
|
9
|
+
return "";
|
|
10
|
+
}
|
|
11
|
+
return String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Create or get the native content container inside the page overlay. */
|
|
15
|
+
function getSettingsContainer() {
|
|
16
|
+
let el = document.getElementById("native-settings-container");
|
|
17
|
+
if (!el) {
|
|
18
|
+
const overlay = document.querySelector("body > .page-overlay");
|
|
19
|
+
if (!overlay) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
el = document.createElement("div");
|
|
23
|
+
el.id = "native-settings-container";
|
|
24
|
+
overlay.appendChild(el);
|
|
25
|
+
}
|
|
26
|
+
return el;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Show native container, hide iframe. */
|
|
30
|
+
function showSettingsContainer() {
|
|
31
|
+
const container = getSettingsContainer();
|
|
32
|
+
const frame = document.getElementById("page-overlay-frame");
|
|
33
|
+
if (frame) {
|
|
34
|
+
frame.style.display = "none";
|
|
35
|
+
}
|
|
36
|
+
if (container) {
|
|
37
|
+
container.style.display = "flex";
|
|
38
|
+
}
|
|
39
|
+
return container;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Hide native container, restore iframe. */
|
|
43
|
+
function hideSettingsContainer() {
|
|
44
|
+
const container = document.getElementById("native-settings-container");
|
|
45
|
+
if (container) {
|
|
46
|
+
container.style.display = "none";
|
|
47
|
+
container.innerHTML = "";
|
|
48
|
+
}
|
|
49
|
+
const frame = document.getElementById("page-overlay-frame");
|
|
50
|
+
if (frame) {
|
|
51
|
+
frame.style.display = "";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
56
|
+
// CONFIG PANEL
|
|
57
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
58
|
+
|
|
59
|
+
let configData = null;
|
|
60
|
+
let configBaseHash = null;
|
|
61
|
+
let configEditMode = false;
|
|
62
|
+
|
|
63
|
+
async function fetchConfig() {
|
|
64
|
+
if (!window.gateway?.connected) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const result = await window.gateway.rpc("config.get", {});
|
|
69
|
+
return result;
|
|
70
|
+
} catch (err) {
|
|
71
|
+
console.error("[settings:config] fetch error:", err);
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function renderConfigValue(key, value, depth) {
|
|
77
|
+
const indent = depth * 1;
|
|
78
|
+
if (value === null || value === undefined) {
|
|
79
|
+
return `<span class="cfg-null">null</span>`;
|
|
80
|
+
}
|
|
81
|
+
if (typeof value === "boolean") {
|
|
82
|
+
return `<span class="cfg-bool">${value}</span>`;
|
|
83
|
+
}
|
|
84
|
+
if (typeof value === "number") {
|
|
85
|
+
return `<span class="cfg-num">${value}</span>`;
|
|
86
|
+
}
|
|
87
|
+
if (typeof value === "string") {
|
|
88
|
+
if (value.length > 120) {
|
|
89
|
+
return `<span class="cfg-str">"${escapeSettingsHtml(value.slice(0, 120))}..."</span>`;
|
|
90
|
+
}
|
|
91
|
+
return `<span class="cfg-str">"${escapeSettingsHtml(value)}"</span>`;
|
|
92
|
+
}
|
|
93
|
+
if (Array.isArray(value)) {
|
|
94
|
+
if (value.length === 0) {
|
|
95
|
+
return `<span class="cfg-bracket">[]</span>`;
|
|
96
|
+
}
|
|
97
|
+
const items = value
|
|
98
|
+
.map(
|
|
99
|
+
(v, i) =>
|
|
100
|
+
`<div class="cfg-row" style="padding-left:${indent + 1}em">${renderConfigValue(i, v, depth + 1)}</div>`,
|
|
101
|
+
)
|
|
102
|
+
.join("");
|
|
103
|
+
return `<span class="cfg-bracket">[</span>${items}<div style="padding-left:${indent}em"><span class="cfg-bracket">]</span></div>`;
|
|
104
|
+
}
|
|
105
|
+
if (typeof value === "object") {
|
|
106
|
+
const entries = Object.entries(value);
|
|
107
|
+
if (entries.length === 0) {
|
|
108
|
+
return `<span class="cfg-bracket">{}</span>`;
|
|
109
|
+
}
|
|
110
|
+
const rows = entries
|
|
111
|
+
.map(
|
|
112
|
+
([k, v]) =>
|
|
113
|
+
`<div class="cfg-row" style="padding-left:${indent + 1}em"><span class="cfg-key">${escapeSettingsHtml(k)}</span><span class="cfg-colon">: </span>${renderConfigValue(k, v, depth + 1)}</div>`,
|
|
114
|
+
)
|
|
115
|
+
.join("");
|
|
116
|
+
return `<span class="cfg-bracket">{</span>${rows}<div style="padding-left:${indent}em"><span class="cfg-bracket">}</span></div>`;
|
|
117
|
+
}
|
|
118
|
+
return escapeSettingsHtml(String(value));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function renderConfigSection(key, value, isRedacted) {
|
|
122
|
+
const isExpanded = typeof value === "object" && value !== null;
|
|
123
|
+
const icon = isExpanded ? "▾" : "▸";
|
|
124
|
+
const redactedBadge = isRedacted ? '<span class="cfg-redacted">REDACTED</span>' : "";
|
|
125
|
+
|
|
126
|
+
return `
|
|
127
|
+
<div class="settings-section" data-key="${escapeSettingsHtml(key)}">
|
|
128
|
+
<div class="settings-section-header" onclick="this.parentElement.classList.toggle('collapsed')">
|
|
129
|
+
<span class="settings-section-arrow">${icon}</span>
|
|
130
|
+
<span class="settings-section-key">${escapeSettingsHtml(key)}</span>
|
|
131
|
+
${redactedBadge}
|
|
132
|
+
</div>
|
|
133
|
+
<div class="settings-section-body">
|
|
134
|
+
${renderConfigValue(key, value, 0)}
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function openConfigPanel() {
|
|
141
|
+
const container = showSettingsContainer();
|
|
142
|
+
if (!container) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
container.innerHTML = `
|
|
147
|
+
<div class="settings-panel">
|
|
148
|
+
<div class="settings-toolbar">
|
|
149
|
+
<div class="settings-toolbar-left">
|
|
150
|
+
<span class="settings-toolbar-label">SYSTEM CONFIGURATION</span>
|
|
151
|
+
</div>
|
|
152
|
+
<div class="settings-toolbar-right">
|
|
153
|
+
<button class="settings-btn" id="config-refresh-btn" title="Refresh">Refresh</button>
|
|
154
|
+
<button class="settings-btn settings-btn--primary" id="config-edit-btn" title="Edit raw JSON">Edit</button>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
<div class="settings-content" id="config-content">
|
|
158
|
+
<div class="settings-loading">Loading configuration...</div>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
`;
|
|
162
|
+
|
|
163
|
+
// Wire refresh
|
|
164
|
+
document
|
|
165
|
+
.getElementById("config-refresh-btn")
|
|
166
|
+
.addEventListener("click", () => void loadConfigView());
|
|
167
|
+
document.getElementById("config-edit-btn").addEventListener("click", () => toggleConfigEditor());
|
|
168
|
+
|
|
169
|
+
await loadConfigView();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function loadConfigView() {
|
|
173
|
+
const content = document.getElementById("config-content");
|
|
174
|
+
if (!content) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
content.innerHTML = '<div class="settings-loading">Loading...</div>';
|
|
179
|
+
const result = await fetchConfig();
|
|
180
|
+
|
|
181
|
+
if (!result || !result.config) {
|
|
182
|
+
content.innerHTML =
|
|
183
|
+
'<div class="settings-empty">Unable to load configuration. Gateway may not be connected.</div>';
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
configData = result.config;
|
|
188
|
+
configBaseHash = result.hash;
|
|
189
|
+
configEditMode = false;
|
|
190
|
+
|
|
191
|
+
// Render tree view
|
|
192
|
+
const redacted = new Set(result.redactedPaths || []);
|
|
193
|
+
const sections = Object.entries(configData);
|
|
194
|
+
|
|
195
|
+
if (sections.length === 0) {
|
|
196
|
+
content.innerHTML = '<div class="settings-empty">Configuration is empty.</div>';
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
content.innerHTML = sections
|
|
201
|
+
.map(([key, value]) => {
|
|
202
|
+
const isRedacted = redacted.has(key);
|
|
203
|
+
return renderConfigSection(key, value, isRedacted);
|
|
204
|
+
})
|
|
205
|
+
.join("");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function toggleConfigEditor() {
|
|
209
|
+
const content = document.getElementById("config-content");
|
|
210
|
+
const editBtn = document.getElementById("config-edit-btn");
|
|
211
|
+
if (!content) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (configEditMode) {
|
|
216
|
+
// Switch back to tree view
|
|
217
|
+
configEditMode = false;
|
|
218
|
+
editBtn.textContent = "Edit";
|
|
219
|
+
editBtn.classList.remove("settings-btn--active");
|
|
220
|
+
void loadConfigView();
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Switch to editor
|
|
225
|
+
configEditMode = true;
|
|
226
|
+
editBtn.textContent = "View";
|
|
227
|
+
editBtn.classList.add("settings-btn--active");
|
|
228
|
+
|
|
229
|
+
const json = JSON.stringify(configData, null, 2);
|
|
230
|
+
content.innerHTML = `
|
|
231
|
+
<div class="config-editor-wrap">
|
|
232
|
+
<textarea class="config-editor" id="config-editor-textarea" spellcheck="false">${escapeSettingsHtml(json)}</textarea>
|
|
233
|
+
<div class="config-editor-actions">
|
|
234
|
+
<span class="config-editor-hint">Edit the JSON above, then save to apply changes.</span>
|
|
235
|
+
<button class="settings-btn settings-btn--danger" id="config-save-btn">Save & Apply</button>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
`;
|
|
239
|
+
|
|
240
|
+
document.getElementById("config-save-btn").addEventListener("click", saveConfig);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function saveConfig() {
|
|
244
|
+
const textarea = document.getElementById("config-editor-textarea");
|
|
245
|
+
const saveBtn = document.getElementById("config-save-btn");
|
|
246
|
+
if (!textarea || !saveBtn) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
let parsed;
|
|
251
|
+
try {
|
|
252
|
+
parsed = JSON.parse(textarea.value);
|
|
253
|
+
} catch (err) {
|
|
254
|
+
alert("Invalid JSON: " + err.message);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
saveBtn.textContent = "Saving...";
|
|
259
|
+
saveBtn.disabled = true;
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
await window.gateway.rpc("config.apply", {
|
|
263
|
+
raw: parsed,
|
|
264
|
+
baseHash: configBaseHash,
|
|
265
|
+
});
|
|
266
|
+
saveBtn.textContent = "Saved!";
|
|
267
|
+
setTimeout(() => {
|
|
268
|
+
configEditMode = false;
|
|
269
|
+
const editBtn = document.getElementById("config-edit-btn");
|
|
270
|
+
if (editBtn) {
|
|
271
|
+
editBtn.textContent = "Edit";
|
|
272
|
+
editBtn.classList.remove("settings-btn--active");
|
|
273
|
+
}
|
|
274
|
+
void loadConfigView();
|
|
275
|
+
}, 800);
|
|
276
|
+
} catch (err) {
|
|
277
|
+
saveBtn.textContent = "Save & Apply";
|
|
278
|
+
saveBtn.disabled = false;
|
|
279
|
+
alert("Save failed: " + err.message);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
284
|
+
// DEBUG PANEL
|
|
285
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
286
|
+
|
|
287
|
+
let debugEntries = [];
|
|
288
|
+
let debugPaused = false;
|
|
289
|
+
const DEBUG_MAX = 500;
|
|
290
|
+
|
|
291
|
+
function openDebugPanel() {
|
|
292
|
+
const container = showSettingsContainer();
|
|
293
|
+
if (!container) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
container.innerHTML = `
|
|
298
|
+
<div class="settings-panel">
|
|
299
|
+
<div class="settings-toolbar">
|
|
300
|
+
<div class="settings-toolbar-left">
|
|
301
|
+
<span class="settings-toolbar-label">DEBUG CONSOLE</span>
|
|
302
|
+
<span class="settings-toolbar-count" id="debug-count">0 events</span>
|
|
303
|
+
</div>
|
|
304
|
+
<div class="settings-toolbar-right">
|
|
305
|
+
<button class="settings-btn" id="debug-pause-btn">Pause</button>
|
|
306
|
+
<button class="settings-btn" id="debug-clear-btn">Clear</button>
|
|
307
|
+
<button class="settings-btn" id="debug-export-btn">Export</button>
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
<div class="settings-content debug-scroll" id="debug-content">
|
|
311
|
+
<div class="settings-empty">Listening for events...</div>
|
|
312
|
+
</div>
|
|
313
|
+
</div>
|
|
314
|
+
`;
|
|
315
|
+
|
|
316
|
+
debugEntries = [];
|
|
317
|
+
debugPaused = false;
|
|
318
|
+
|
|
319
|
+
document.getElementById("debug-pause-btn").addEventListener("click", () => {
|
|
320
|
+
debugPaused = !debugPaused;
|
|
321
|
+
document.getElementById("debug-pause-btn").textContent = debugPaused ? "Resume" : "Pause";
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
document.getElementById("debug-clear-btn").addEventListener("click", () => {
|
|
325
|
+
debugEntries = [];
|
|
326
|
+
renderDebugEntries();
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
document.getElementById("debug-export-btn").addEventListener("click", () => {
|
|
330
|
+
const text = debugEntries.map((e) => `${e.time} [${e.type}] ${e.summary}`).join("\n");
|
|
331
|
+
const blob = new Blob([text], { type: "text/plain" });
|
|
332
|
+
const a = document.createElement("a");
|
|
333
|
+
a.href = URL.createObjectURL(blob);
|
|
334
|
+
a.download = `symi-debug-${new Date().toISOString().slice(0, 19)}.txt`;
|
|
335
|
+
a.click();
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// Start capturing events
|
|
339
|
+
window.__debugPanelCapture = function (type, payload) {
|
|
340
|
+
if (debugPaused) {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const time = new Date().toLocaleTimeString("en-US", { hour12: false });
|
|
344
|
+
let summary = "";
|
|
345
|
+
try {
|
|
346
|
+
summary =
|
|
347
|
+
typeof payload === "object"
|
|
348
|
+
? JSON.stringify(payload).slice(0, 200)
|
|
349
|
+
: String(payload).slice(0, 200);
|
|
350
|
+
} catch {
|
|
351
|
+
summary = "[unserializable]";
|
|
352
|
+
}
|
|
353
|
+
debugEntries.unshift({ time, type, summary, payload });
|
|
354
|
+
if (debugEntries.length > DEBUG_MAX) {
|
|
355
|
+
debugEntries.length = DEBUG_MAX;
|
|
356
|
+
}
|
|
357
|
+
renderDebugEntries();
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function renderDebugEntries() {
|
|
362
|
+
const content = document.getElementById("debug-content");
|
|
363
|
+
const count = document.getElementById("debug-count");
|
|
364
|
+
if (!content) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
if (count) {
|
|
368
|
+
count.textContent = `${debugEntries.length} events`;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (debugEntries.length === 0) {
|
|
372
|
+
content.innerHTML = '<div class="settings-empty">Listening for events...</div>';
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const html = debugEntries
|
|
377
|
+
.map((e) => {
|
|
378
|
+
const typeClass =
|
|
379
|
+
e.type === "error"
|
|
380
|
+
? "debug-type--error"
|
|
381
|
+
: e.type === "chat"
|
|
382
|
+
? "debug-type--chat"
|
|
383
|
+
: e.type === "agent"
|
|
384
|
+
? "debug-type--agent"
|
|
385
|
+
: "debug-type--other";
|
|
386
|
+
return `<div class="debug-entry-row">
|
|
387
|
+
<span class="debug-time">${e.time}</span>
|
|
388
|
+
<span class="debug-type ${typeClass}">${escapeSettingsHtml(e.type)}</span>
|
|
389
|
+
<span class="debug-summary">${escapeSettingsHtml(e.summary)}</span>
|
|
390
|
+
</div>`;
|
|
391
|
+
})
|
|
392
|
+
.join("");
|
|
393
|
+
|
|
394
|
+
content.innerHTML = html;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function closeDebugPanel() {
|
|
398
|
+
window.__debugPanelCapture = null;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
402
|
+
// LOGS PANEL (replaces the dead-code logs.js)
|
|
403
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
404
|
+
|
|
405
|
+
const LOGS_POLL_MS = 3000;
|
|
406
|
+
const LOGS_LIMIT = 500;
|
|
407
|
+
const LOGS_MAX_BYTES = 250000;
|
|
408
|
+
|
|
409
|
+
const LEVEL_COLORS = {
|
|
410
|
+
TRACE: "#6b7280",
|
|
411
|
+
DEBUG: "#8b5cf6",
|
|
412
|
+
INFO: "#60a5fa",
|
|
413
|
+
WARN: "#f59e0b",
|
|
414
|
+
ERROR: "#ef4444",
|
|
415
|
+
FATAL: "#dc2626",
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
const LEVEL_ORDER = ["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"];
|
|
419
|
+
|
|
420
|
+
let logsEntries = [];
|
|
421
|
+
let logsCursor = null;
|
|
422
|
+
let logsTimer = null;
|
|
423
|
+
let logsAutoFollow = true;
|
|
424
|
+
let logsLevelFilters = new Set(LEVEL_ORDER);
|
|
425
|
+
let logsFilterText = "";
|
|
426
|
+
|
|
427
|
+
function parseLogLine(line) {
|
|
428
|
+
try {
|
|
429
|
+
const obj = JSON.parse(line);
|
|
430
|
+
const level = obj._meta?.logLevelName || "INFO";
|
|
431
|
+
const time = obj.time || obj._meta?.date || "";
|
|
432
|
+
const msg = obj["1"] || obj.msg || obj.message || "";
|
|
433
|
+
const sub = obj["0"] || "";
|
|
434
|
+
return { level, time, msg, sub, raw: line, parsed: obj };
|
|
435
|
+
} catch {
|
|
436
|
+
return { level: "INFO", time: "", msg: line, sub: "", raw: line, parsed: null };
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function getVisibleLogEntries() {
|
|
441
|
+
return logsEntries.filter((e) => {
|
|
442
|
+
if (!logsLevelFilters.has(e.level)) {
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
if (logsFilterText && !e.raw.toLowerCase().includes(logsFilterText)) {
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
return true;
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function renderLogEntries() {
|
|
453
|
+
const el = document.getElementById("logs-entries");
|
|
454
|
+
if (!el) {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const visible = getVisibleLogEntries();
|
|
459
|
+
const html = visible
|
|
460
|
+
.map((e) => {
|
|
461
|
+
const color = LEVEL_COLORS[e.level] || "#999";
|
|
462
|
+
const timeStr = e.time ? new Date(e.time).toLocaleTimeString("en-US", { hour12: false }) : "";
|
|
463
|
+
const escapedMsg = escapeSettingsHtml(e.msg).substring(0, 500);
|
|
464
|
+
const escapedSub = escapeSettingsHtml(e.sub);
|
|
465
|
+
return `<div class="log-row">
|
|
466
|
+
<span class="log-time">${timeStr}</span>
|
|
467
|
+
<span class="log-level" style="color:${color}">${e.level.padEnd(5)}</span>
|
|
468
|
+
${escapedSub ? `<span class="log-sub">${escapedSub}</span>` : ""}
|
|
469
|
+
<span class="log-msg">${escapedMsg}</span>
|
|
470
|
+
</div>`;
|
|
471
|
+
})
|
|
472
|
+
.join("");
|
|
473
|
+
|
|
474
|
+
el.innerHTML = html || '<div class="settings-empty">No log entries.</div>';
|
|
475
|
+
|
|
476
|
+
if (logsAutoFollow) {
|
|
477
|
+
const scroll = document.getElementById("logs-scroll-area");
|
|
478
|
+
if (scroll) {
|
|
479
|
+
scroll.scrollTop = scroll.scrollHeight;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
async function fetchLogs() {
|
|
485
|
+
if (!window.gateway?.connected) {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
try {
|
|
489
|
+
const params = { limit: LOGS_LIMIT, maxBytes: LOGS_MAX_BYTES };
|
|
490
|
+
if (logsCursor != null) {
|
|
491
|
+
params.cursor = logsCursor;
|
|
492
|
+
}
|
|
493
|
+
const result = await window.gateway.rpc("logs.tail", params);
|
|
494
|
+
if (!result) {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (result.reset) {
|
|
499
|
+
logsEntries = [];
|
|
500
|
+
logsCursor = null;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (result.lines && result.lines.length > 0) {
|
|
504
|
+
const newEntries = result.lines.map(parseLogLine);
|
|
505
|
+
logsEntries.push(...newEntries);
|
|
506
|
+
if (logsEntries.length > 2000) {
|
|
507
|
+
logsEntries = logsEntries.slice(logsEntries.length - 2000);
|
|
508
|
+
}
|
|
509
|
+
renderLogEntries();
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (typeof result.cursor === "number") {
|
|
513
|
+
logsCursor = result.cursor;
|
|
514
|
+
}
|
|
515
|
+
} catch (err) {
|
|
516
|
+
console.error("[settings:logs] fetch error:", err);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function startLogsPolling() {
|
|
521
|
+
logsCursor = null;
|
|
522
|
+
logsEntries = [];
|
|
523
|
+
|
|
524
|
+
if (!window.gateway?.connected) {
|
|
525
|
+
const waitIv = setInterval(() => {
|
|
526
|
+
if (window.gateway?.connected) {
|
|
527
|
+
clearInterval(waitIv);
|
|
528
|
+
void fetchLogs();
|
|
529
|
+
if (logsTimer) {
|
|
530
|
+
clearInterval(logsTimer);
|
|
531
|
+
}
|
|
532
|
+
logsTimer = setInterval(fetchLogs, LOGS_POLL_MS);
|
|
533
|
+
}
|
|
534
|
+
}, 500);
|
|
535
|
+
setTimeout(() => clearInterval(waitIv), 30000);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
void fetchLogs();
|
|
540
|
+
if (logsTimer) {
|
|
541
|
+
clearInterval(logsTimer);
|
|
542
|
+
}
|
|
543
|
+
logsTimer = setInterval(fetchLogs, LOGS_POLL_MS);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function stopLogsPolling() {
|
|
547
|
+
if (logsTimer) {
|
|
548
|
+
clearInterval(logsTimer);
|
|
549
|
+
logsTimer = null;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function openLogsPanel() {
|
|
554
|
+
const container = showSettingsContainer();
|
|
555
|
+
if (!container) {
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
container.innerHTML = `
|
|
560
|
+
<div class="settings-panel">
|
|
561
|
+
<div class="settings-toolbar">
|
|
562
|
+
<div class="settings-toolbar-left">
|
|
563
|
+
<span class="settings-toolbar-label">SYSTEM LOGS</span>
|
|
564
|
+
<span class="settings-toolbar-count" id="logs-count">0 entries</span>
|
|
565
|
+
</div>
|
|
566
|
+
<div class="settings-toolbar-right">
|
|
567
|
+
<div class="logs-level-filters">
|
|
568
|
+
${LEVEL_ORDER.map(
|
|
569
|
+
(l) => `
|
|
570
|
+
<label class="logs-level-chip" data-level="${l}">
|
|
571
|
+
<input type="checkbox" checked data-level="${l}">
|
|
572
|
+
<span style="color:${LEVEL_COLORS[l]}">${l}</span>
|
|
573
|
+
</label>
|
|
574
|
+
`,
|
|
575
|
+
).join("")}
|
|
576
|
+
</div>
|
|
577
|
+
<input type="text" class="settings-search" id="logs-filter-input" placeholder="Filter..." />
|
|
578
|
+
<label class="settings-checkbox-label">
|
|
579
|
+
<input type="checkbox" id="logs-follow-cb" checked> Follow
|
|
580
|
+
</label>
|
|
581
|
+
<button class="settings-btn" id="logs-export-btn">Export</button>
|
|
582
|
+
</div>
|
|
583
|
+
</div>
|
|
584
|
+
<div class="settings-content logs-scroll" id="logs-scroll-area">
|
|
585
|
+
<div id="logs-entries">
|
|
586
|
+
<div class="settings-loading">Connecting to log stream...</div>
|
|
587
|
+
</div>
|
|
588
|
+
</div>
|
|
589
|
+
</div>
|
|
590
|
+
`;
|
|
591
|
+
|
|
592
|
+
// Wire level filters
|
|
593
|
+
container.querySelectorAll(".logs-level-chip input").forEach((cb) => {
|
|
594
|
+
cb.addEventListener("change", () => {
|
|
595
|
+
const level = cb.dataset.level;
|
|
596
|
+
if (cb.checked) {
|
|
597
|
+
logsLevelFilters.add(level);
|
|
598
|
+
} else {
|
|
599
|
+
logsLevelFilters.delete(level);
|
|
600
|
+
}
|
|
601
|
+
renderLogEntries();
|
|
602
|
+
});
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
// Wire filter text
|
|
606
|
+
document.getElementById("logs-filter-input").addEventListener("input", (e) => {
|
|
607
|
+
logsFilterText = e.target.value.toLowerCase();
|
|
608
|
+
renderLogEntries();
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
// Wire auto-follow
|
|
612
|
+
document.getElementById("logs-follow-cb").addEventListener("change", (e) => {
|
|
613
|
+
logsAutoFollow = e.target.checked;
|
|
614
|
+
if (logsAutoFollow) {
|
|
615
|
+
const scroll = document.getElementById("logs-scroll-area");
|
|
616
|
+
if (scroll) {
|
|
617
|
+
scroll.scrollTop = scroll.scrollHeight;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
// Wire export
|
|
623
|
+
document.getElementById("logs-export-btn").addEventListener("click", () => {
|
|
624
|
+
const visible = getVisibleLogEntries();
|
|
625
|
+
const text = visible.map((e) => e.raw).join("\n");
|
|
626
|
+
const blob = new Blob([text], { type: "text/plain" });
|
|
627
|
+
const a = document.createElement("a");
|
|
628
|
+
a.href = URL.createObjectURL(blob);
|
|
629
|
+
a.download = `symi-logs-${new Date().toISOString().slice(0, 19)}.txt`;
|
|
630
|
+
a.click();
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
startLogsPolling();
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function closeLogsPanel() {
|
|
637
|
+
stopLogsPolling();
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
641
|
+
// PUBLIC API — called by menu.js
|
|
642
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
643
|
+
|
|
644
|
+
let activeSettingsPanel = null;
|
|
645
|
+
|
|
646
|
+
window.openNativeSettings = function (page) {
|
|
647
|
+
// Close previous panel cleanup
|
|
648
|
+
closeActiveSettingsPanel();
|
|
649
|
+
|
|
650
|
+
activeSettingsPanel = page;
|
|
651
|
+
|
|
652
|
+
switch (page) {
|
|
653
|
+
case "config":
|
|
654
|
+
void openConfigPanel();
|
|
655
|
+
break;
|
|
656
|
+
case "debug":
|
|
657
|
+
openDebugPanel();
|
|
658
|
+
break;
|
|
659
|
+
case "logs":
|
|
660
|
+
openLogsPanel();
|
|
661
|
+
break;
|
|
662
|
+
default:
|
|
663
|
+
console.warn("[settings] unknown page:", page);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
|
|
668
|
+
window.closeNativeSettings = function () {
|
|
669
|
+
closeActiveSettingsPanel();
|
|
670
|
+
hideSettingsContainer();
|
|
671
|
+
activeSettingsPanel = null;
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
function closeActiveSettingsPanel() {
|
|
675
|
+
if (activeSettingsPanel === "debug") {
|
|
676
|
+
closeDebugPanel();
|
|
677
|
+
}
|
|
678
|
+
if (activeSettingsPanel === "logs") {
|
|
679
|
+
closeLogsPanel();
|
|
680
|
+
}
|
|
681
|
+
}
|