@saltcorn/copilot 0.8.2 → 0.8.3

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.
@@ -1,102 +1,790 @@
1
- const Field = require("@saltcorn/data/models/field");
2
1
  const Table = require("@saltcorn/data/models/table");
3
- const Form = require("@saltcorn/data/models/form");
4
- const MetaData = require("@saltcorn/data/models/metadata");
5
2
  const View = require("@saltcorn/data/models/view");
6
3
  const Trigger = require("@saltcorn/data/models/trigger");
7
- const { findType } = require("@saltcorn/data/models/discovery");
8
- const { save_menu_items } = require("@saltcorn/data/models/config");
9
- const db = require("@saltcorn/data/db");
10
- const WorkflowRun = require("@saltcorn/data/models/workflow_run");
11
- const {
12
- localeDateTime,
13
- renderForm,
14
- mkTable,
15
- post_delete_btn,
16
- } = require("@saltcorn/markup");
4
+ const WorkflowStep = require("@saltcorn/data/models/workflow_step");
5
+ const Page = require("@saltcorn/data/models/page");
6
+ const MetaData = require("@saltcorn/data/models/metadata");
7
+ const { mkTable } = require("@saltcorn/markup");
17
8
  const {
18
9
  div,
19
10
  script,
20
11
  domReady,
21
12
  pre,
22
- code,
23
- input,
24
- h4,
25
- style,
26
- h5,
27
13
  button,
28
- text_attr,
29
14
  i,
30
15
  p,
16
+ a,
31
17
  span,
32
18
  small,
33
- form,
34
- textarea,
19
+ text,
35
20
  } = require("@saltcorn/markup/tags");
36
21
  const { getState } = require("@saltcorn/data/db/state");
37
- const renderLayout = require("@saltcorn/markup/layout");
22
+ const db = require("@saltcorn/data/db");
38
23
  const { viewname } = require("./common");
24
+ const { task_tool } = require("./tools");
25
+ const { PromptGenerator } = require("./prompt-generator");
39
26
 
40
- const errorList = async (req) => {
41
- const errs = await MetaData.find({
27
+ const doCreateErrorFixTask = async (errorMd, userId) => {
28
+ const currentMd = await MetaData.findOne({
29
+ id: errorMd.id,
42
30
  type: "CopilotConstructMgr",
43
31
  name: "error",
44
32
  });
45
- if (errs.length) {
46
- return div(
47
- { class: "mt-2" },
48
- mkTable(
49
- [
50
- { label: "Status", key: (m) => m.body.status },
51
- {
52
- label: "Error",
53
- key: (m) => pre(JSON.stringify(m.body.error, null, 2)),
33
+ if (!currentMd || currentMd.body.fixing) return;
34
+ await currentMd.update({ body: { ...currentMd.body, fixing: true } });
35
+ try {
36
+ const tables = await Table.find({});
37
+ const views = await View.find({});
38
+ const triggers = await Trigger.find({});
39
+ const pages = await Page.find({});
40
+ const errorText = JSON.stringify(errorMd.body.error, null, 2);
41
+
42
+ // Include the affected entity's config so the planning LLM can name exact broken values.
43
+ let entityConfigSection = "";
44
+ const errorUrl = errorMd.body.error?.url || "";
45
+ const mView = errorUrl.match(/\/view\/([^/?#]+)/);
46
+ const mPage = errorUrl.match(/\/page\/([^/?#]+)/);
47
+ if (mView) {
48
+ const viewName = decodeURIComponent(mView[1]);
49
+ const view = views.find((v) => v.name === viewName);
50
+ if (view)
51
+ entityConfigSection =
52
+ `\nThe error occurred while rendering view "${viewName}". ` +
53
+ `Current configuration:\n\`\`\`json\n` +
54
+ `${JSON.stringify(view.configuration, null, 2)}\n\`\`\`\n`;
55
+ } else if (mPage) {
56
+ const pageName = decodeURIComponent(mPage[1]);
57
+ const page = pages.find((p) => p.name === pageName);
58
+ if (page)
59
+ entityConfigSection =
60
+ `\nThe error occurred while rendering page "${pageName}". ` +
61
+ `Current configuration:\n\`\`\`json\n` +
62
+ `${JSON.stringify(page.layout, null, 2)}\n\`\`\`\n`;
63
+ }
64
+
65
+ // Workflow step errors: inject the trigger config and all its steps
66
+ const stack = errorMd.body.error?.stack || "";
67
+ if (!entityConfigSection && stack.includes("workflow_step")) {
68
+ // Try to identify the trigger from a WorkflowRun referenced in the error context
69
+ const runIdMatch =
70
+ stack.match(/workflow_run[^0-9]*(\d+)/i) ||
71
+ JSON.stringify(errorMd.body.error).match(/"run_id"\s*:\s*(\d+)/);
72
+ let matchedTrigger = null;
73
+ if (runIdMatch) {
74
+ const WorkflowRun = require("@saltcorn/data/models/workflow_run");
75
+ const run = await WorkflowRun.findOne({
76
+ id: parseInt(runIdMatch[1]),
77
+ }).catch(() => null);
78
+ if (run?.trigger_id) {
79
+ matchedTrigger = triggers.find((t) => t.id === run.trigger_id);
80
+ }
81
+ }
82
+ // Fall back: include all workflow triggers with steps
83
+ const workflowTriggers = matchedTrigger
84
+ ? [matchedTrigger]
85
+ : triggers.filter(
86
+ (t) => t.action === "Workflow" || t.action === "workflow"
87
+ );
88
+ if (workflowTriggers.length) {
89
+ const sections = await Promise.all(
90
+ workflowTriggers.map(async (t) => {
91
+ const steps = await WorkflowStep.find({ trigger_id: t.id });
92
+ return (
93
+ `Trigger "${t.name}" (table: ${t.table_id}, action: ${t.action}):\n` +
94
+ `Configuration: ${JSON.stringify(t.configuration, null, 2)}\n` +
95
+ `Steps (${steps.length}):\n` +
96
+ steps
97
+ .map(
98
+ (s) =>
99
+ ` - name: ${s.name}, action: ${s.action_name}, initial: ${s.initial_step}\n` +
100
+ ` configuration: ${JSON.stringify(
101
+ s.configuration,
102
+ null,
103
+ 2
104
+ )}`
105
+ )
106
+ .join("\n")
107
+ );
108
+ })
109
+ );
110
+ entityConfigSection =
111
+ `\nThe error occurred in a workflow step. Relevant trigger(s) and steps:\n\n` +
112
+ sections.join("\n\n") +
113
+ "\n";
114
+ }
115
+ }
116
+
117
+ // SQL column-not-found: likely a modify_row trigger with a table-qualified key
118
+ if (!entityConfigSection) {
119
+ const colErr = (errorMd.body.error?.message || "").match(
120
+ /column "([^"]+)" of relation "([^"]+)" does not exist/
121
+ );
122
+ if (colErr) {
123
+ const tableName = colErr[2];
124
+ const matchedTable = tables.find((t) => t.name === tableName);
125
+ const modifyTriggers = triggers.filter(
126
+ (t) => t.table_id === matchedTable?.id && t.action === "modify_row"
127
+ );
128
+ if (modifyTriggers.length) {
129
+ entityConfigSection =
130
+ `\nThe error is a SQL column-not-found on table "${tableName}". ` +
131
+ `This is typically caused by a modify_row trigger whose row_expr returns a table-qualified key ` +
132
+ `(e.g. {"${tableName}.some_field": value}) — the dot is stripped by SQL sanitization, ` +
133
+ `producing an invalid column name. Relevant modify_row triggers:\n\n` +
134
+ modifyTriggers
135
+ .map(
136
+ (t) =>
137
+ `Trigger "${t.name}" (when: ${t.when_trigger}):\n` +
138
+ `Configuration: ${JSON.stringify(t.configuration, null, 2)}`
139
+ )
140
+ .join("\n\n") +
141
+ "\n";
142
+ }
143
+ }
144
+ }
145
+
146
+ const cannot_fix_tool = {
147
+ type: "function",
148
+ function: {
149
+ name: "cannot_fix",
150
+ description:
151
+ "Use this when the error cannot be diagnosed or fixed from the available " +
152
+ "information — e.g. the error is too vague, the stack trace points to platform " +
153
+ "internals with no clear application-level fix, or no relevant entity configuration " +
154
+ "is available. Do NOT invent a task just to produce output.",
155
+ parameters: {
156
+ type: "object",
157
+ required: ["reason"],
158
+ properties: {
159
+ reason: {
160
+ type: "string",
161
+ description:
162
+ "One sentence explaining why a fix task cannot be created.",
163
+ },
54
164
  },
165
+ },
166
+ },
167
+ };
168
+
169
+ const generator = await PromptGenerator.createInstance();
170
+ if (!generator.spec) return;
171
+
172
+ const answer = await getState().functions.llm_generate.run(
173
+ generator.errorPrompt(errorText, entityConfigSection),
174
+ {
175
+ tools: [task_tool, cannot_fix_tool],
176
+ systemPrompt:
177
+ "You are a Saltcorn developer. Analyse the error and decide: can you produce a\n" +
178
+ "concrete, actionable fix? If yes, call plan_tasks. If not, call cannot_fix.",
179
+ }
180
+ );
181
+
182
+ const tc =
183
+ typeof answer.getToolCalls === "function"
184
+ ? answer.getToolCalls()?.[0]
185
+ : undefined;
186
+ if (!tc) return;
187
+
188
+ if (tc.tool_name === "cannot_fix") {
189
+ await currentMd.update({
190
+ body: { ...currentMd.body, cannot_fix_reason: tc.input.reason },
191
+ });
192
+ return;
193
+ }
194
+
195
+ if (tc.tool_name !== "plan_tasks" || !Array.isArray(tc.input.tasks)) return;
196
+
197
+ for (const task of tc.input.tasks)
198
+ await MetaData.create({
199
+ type: "CopilotConstructMgr",
200
+ name: "task",
201
+ body: { ...task, source: "error_fix", error_id: currentMd.id },
202
+ user_id: userId,
203
+ });
204
+
205
+ await currentMd.update({
206
+ body: { ...currentMd.body, fix_task_created: true },
207
+ });
208
+ } finally {
209
+ try {
210
+ const current = await MetaData.findOne({
211
+ id: currentMd.id,
212
+ type: "CopilotConstructMgr",
213
+ name: "error",
214
+ });
215
+ if (current) {
216
+ const { fixing, ...rest } = current.body;
217
+ await current.update({ body: rest });
218
+ }
219
+ } catch (_) {}
220
+ try {
221
+ getState().emitDynamicUpdate(db.getTenantSchema(), {
222
+ eval_js: [
223
+ "if(typeof copilotRefreshErrs==='function')copilotRefreshErrs();",
224
+ "if(typeof copilotRefreshTasks==='function')copilotRefreshTasks();",
225
+ ],
226
+ });
227
+ } catch (_) {}
228
+ }
229
+ };
230
+
231
+ /**
232
+ * Renders the self-healing toggle section and error table.
233
+ */
234
+ const errorList = async (req) => {
235
+ const settings = await MetaData.findOne({
236
+ type: "CopilotConstructMgr",
237
+ name: "settings",
238
+ });
239
+ const healEnabled = !!settings?.body?.error_heal;
240
+
241
+ const healSection = healEnabled
242
+ ? div(
243
+ {
244
+ class:
245
+ "alert alert-success d-flex align-items-center gap-3 py-2 mb-3",
246
+ },
247
+ i({ class: "fas fa-heartbeat" }),
248
+ span(
249
+ "Self-healing enabled — new errors will automatically generate fix tasks."
250
+ ),
251
+ button(
55
252
  {
56
- label: "Delete",
57
- key: (r) =>
58
- button(
59
- {
60
- class: "btn btn-outline-danger btn-sm",
61
- onclick: `view_post("${viewname}", "del_err", {id:${r.id}})`,
62
- },
63
- i({ class: "fas fa-trash-alt" }),
64
- ),
253
+ class: "btn btn-sm btn-outline-secondary ms-auto",
254
+ onclick: "copilotToggleErrorHealing()",
65
255
  },
66
- ],
67
- errs,
68
- ),
69
- button(
256
+ "Disable"
257
+ )
258
+ )
259
+ : div(
70
260
  {
71
- class: "btn btn-outline-danger",
72
- onclick: `view_post("${viewname}", "del_all_errs")`,
261
+ class:
262
+ "alert alert-secondary d-flex align-items-center gap-3 py-2 mb-3",
73
263
  },
74
- "Delete all",
75
- ),
76
- );
77
- } else {
78
- return div({ class: "mt-2" }, p("No errors"));
264
+ i({ class: "fas fa-heartbeat" }),
265
+ div(
266
+ span({ class: "d-block" }, "Self-healing is disabled."),
267
+ small(
268
+ { class: "text-muted" },
269
+ "When enabled, new errors automatically generate a fix task."
270
+ )
271
+ ),
272
+ button(
273
+ {
274
+ class: "btn btn-sm btn-primary ms-auto",
275
+ onclick: "copilotToggleErrorHealing()",
276
+ },
277
+ "Enable self-healing"
278
+ )
279
+ );
280
+
281
+ const errs = (
282
+ await MetaData.find({
283
+ type: "CopilotConstructMgr",
284
+ name: "error",
285
+ })
286
+ ).sort((a, b) => new Date(b.written_at) - new Date(a.written_at));
287
+
288
+ // Build error_id → most recent fix task map for run links
289
+ const allTasks = await MetaData.find({
290
+ type: "CopilotConstructMgr",
291
+ name: "task",
292
+ });
293
+ const fixTaskByErrorId = {};
294
+ for (const t of allTasks) {
295
+ if (t.body.source === "error_fix" && t.body.error_id != null) {
296
+ const eid = parseInt(t.body.error_id);
297
+ if (!fixTaskByErrorId[eid] || t.id > fixTaskByErrorId[eid].id)
298
+ fixTaskByErrorId[eid] = t;
299
+ }
79
300
  }
301
+
302
+ const errTable = errs.length
303
+ ? div(
304
+ mkTable(
305
+ [
306
+ {
307
+ label: "Source",
308
+ key: (m) =>
309
+ m.body.source === "constructor"
310
+ ? span(
311
+ {
312
+ class: "badge bg-secondary",
313
+ title: "Error in the app constructor itself",
314
+ },
315
+ "constructor"
316
+ )
317
+ : span(
318
+ {
319
+ class: "badge bg-primary",
320
+ title: "Error in the application being built",
321
+ },
322
+ "application"
323
+ ),
324
+ },
325
+ {
326
+ label: "When",
327
+ key: (m) => {
328
+ const d = m.written_at ? new Date(m.written_at) : null;
329
+ if (!d) return "";
330
+ return small(
331
+ { class: "text-muted", style: "white-space:nowrap" },
332
+ d.toLocaleDateString([], { month: "short", day: "numeric" }),
333
+ " ",
334
+ d.toLocaleTimeString([], {
335
+ hour: "2-digit",
336
+ minute: "2-digit",
337
+ })
338
+ );
339
+ },
340
+ },
341
+ {
342
+ label: "Error",
343
+ key: (m) => {
344
+ const err = m.body.error || {};
345
+ const msg = err.message || String(err) || "(no message)";
346
+ const urlLine = err.url
347
+ ? div(
348
+ { class: "text-muted", style: "font-size:0.75rem" },
349
+ text(String(err.url))
350
+ )
351
+ : "";
352
+ const stackLines = (err.stack || "").split("\n").slice(0, 5);
353
+ const stackPreview = stackLines.length
354
+ ? pre(
355
+ {
356
+ style:
357
+ "font-size:0.72rem;white-space:pre-wrap;overflow-wrap:break-word;margin:4px 0 0;",
358
+ },
359
+ text(stackLines.join("\n"))
360
+ )
361
+ : "";
362
+ return div(
363
+ { style: "word-break:break-word;min-width:0;" },
364
+ div({ class: "fw-semibold small" }, text(msg)),
365
+ urlLine,
366
+ stackPreview
367
+ );
368
+ },
369
+ },
370
+ {
371
+ label: "Status",
372
+ key: (r) => {
373
+ if (r.body.source === "constructor") return "";
374
+ if (r.body.fixing)
375
+ return span(
376
+ {
377
+ class: "badge bg-info text-dark",
378
+ "data-fixing-id": r.id,
379
+ },
380
+ i({ class: "fas fa-spinner fa-spin me-1" }),
381
+ "Creating fix task..."
382
+ );
383
+ if (r.body.cannot_fix_reason)
384
+ return div(
385
+ span(
386
+ { class: "badge bg-warning text-dark" },
387
+ "No fix found"
388
+ ),
389
+ div(
390
+ {
391
+ class: "text-muted mt-1",
392
+ style:
393
+ "font-size:0.75rem;max-width:220px;white-space:normal;",
394
+ },
395
+ r.body.cannot_fix_reason
396
+ )
397
+ );
398
+ if (r.body.fix_task_created)
399
+ return div(
400
+ { class: "d-flex align-items-center gap-1" },
401
+ span({ class: "badge bg-success" }, "Fix task created"),
402
+ fixTaskByErrorId[r.id]?.body?.run_id
403
+ ? a(
404
+ {
405
+ href: `/view/Saltcorn%20Agent%20copilot?run_id=${
406
+ fixTaskByErrorId[r.id].body.run_id
407
+ }`,
408
+ target: "_blank",
409
+ title: "View fix task run",
410
+ class: "text-muted",
411
+ },
412
+ i({ class: "fas fa-external-link-alt" })
413
+ )
414
+ : ""
415
+ );
416
+ return span(
417
+ { class: "badge bg-light text-dark border" },
418
+ "New"
419
+ );
420
+ },
421
+ },
422
+ {
423
+ label: "",
424
+ key: (r) => {
425
+ const iconRow = div(
426
+ { class: "d-flex align-items-center gap-1" },
427
+ button(
428
+ {
429
+ class: "btn btn-sm btn-outline-secondary",
430
+ onclick: "copilotShowErrDetail(this)",
431
+ title: "View full error",
432
+ "data-err": JSON.stringify(r.body.error),
433
+ },
434
+ i({ class: "fas fa-eye" })
435
+ ),
436
+ button(
437
+ {
438
+ class: "btn btn-sm btn-outline-danger",
439
+ onclick: `copilotDelErr(${r.id})`,
440
+ title: "Delete",
441
+ },
442
+ i({ class: "fas fa-trash-alt" })
443
+ )
444
+ );
445
+ let fixRow = "";
446
+ if (r.body.source !== "constructor" && !r.body.fixing) {
447
+ const fixTask = fixTaskByErrorId[r.id];
448
+ if (r.body.fix_task_created && fixTask) {
449
+ const taskStatus = fixTask.body.status || null;
450
+ const canRun =
451
+ taskStatus !== "Done" && taskStatus !== "Running";
452
+ fixRow = div(
453
+ { class: "d-flex flex-column gap-1 mt-1" },
454
+ button(
455
+ {
456
+ class: "btn btn-sm btn-outline-secondary",
457
+ onclick: `copilotViewFixTask(${r.id})`,
458
+ title: "View fix task",
459
+ },
460
+ i({ class: "fas fa-search me-1" }),
461
+ "View task"
462
+ ),
463
+ canRun
464
+ ? button(
465
+ {
466
+ id: `run-fix-btn-${fixTask.id}`,
467
+ class: "btn btn-sm btn-success",
468
+ onclick: `copilotRunFixTask(${fixTask.id}, ${r.id})`,
469
+ title: "Run fix task",
470
+ },
471
+ i({ class: "fas fa-play me-1" }),
472
+ "Run"
473
+ )
474
+ : ""
475
+ );
476
+ } else if (
477
+ !r.body.fix_task_created &&
478
+ !r.body.cannot_fix_reason
479
+ ) {
480
+ fixRow = div(
481
+ { class: "mt-1" },
482
+ button(
483
+ {
484
+ id: `fix-err-btn-${r.id}`,
485
+ class: "btn btn-sm btn-outline-primary",
486
+ onclick: `copilotFixError(${r.id})`,
487
+ },
488
+ "Create fix task"
489
+ )
490
+ );
491
+ }
492
+ }
493
+ return div({ style: "white-space:nowrap;" }, iconRow, fixRow);
494
+ },
495
+ },
496
+ ],
497
+ errs
498
+ ),
499
+ button(
500
+ {
501
+ class: "btn btn-outline-danger",
502
+ onclick: "copilotDelAllErrs()",
503
+ },
504
+ "Delete all"
505
+ )
506
+ )
507
+ : p("No errors");
508
+
509
+ return div({ class: "mt-2" }, healSection, errTable);
80
510
  };
81
511
 
82
- const del_err = async (table_id, viewname, config, body, { req, res }) => {
512
+ const del_err = async (table_id, vn, config, body, { req, res }) => {
83
513
  const r = await MetaData.findOne({
84
- id: body.id,
514
+ id: parseInt(body.id),
515
+ type: "CopilotConstructMgr",
516
+ name: "error",
85
517
  });
86
-
87
518
  if (!r) throw new Error("Error not found");
88
519
  await r.delete();
89
- return { json: { reload_page: true } };
520
+ return { json: { success: true } };
90
521
  };
91
- const del_all_errs = async (table_id, viewname, config, body, { req, res }) => {
522
+
523
+ const del_all_errs = async (table_id, vn, config, body, { req, res }) => {
92
524
  const rs = await MetaData.find({
93
525
  type: "CopilotConstructMgr",
94
526
  name: "error",
95
527
  });
96
528
  for (const r of rs) await r.delete();
97
- return { json: { reload_page: true } };
529
+ return { json: { success: true } };
98
530
  };
99
531
 
100
- const error_routes = { del_err, del_all_errs };
532
+ /** Route: toggles the error_heal setting. */
533
+ const toggle_error_healing = async (
534
+ table_id,
535
+ vn,
536
+ config,
537
+ body,
538
+ { req, res }
539
+ ) => {
540
+ const settings = await MetaData.findOne({
541
+ type: "CopilotConstructMgr",
542
+ name: "settings",
543
+ });
544
+ if (settings) {
545
+ await settings.update({
546
+ body: { ...settings.body, error_heal: !settings.body.error_heal },
547
+ });
548
+ } else {
549
+ await MetaData.create({
550
+ type: "CopilotConstructMgr",
551
+ name: "settings",
552
+ body: { error_heal: true },
553
+ });
554
+ }
555
+ return { json: { success: true } };
556
+ };
101
557
 
102
- module.exports = { errorList, error_routes };
558
+ /** Route: fires doCreateErrorFixTask for a single error record. */
559
+ const fix_error_task = async (table_id, vn, config, body, { req, res }) => {
560
+ const id = parseInt(body.id);
561
+ const errorMd = await MetaData.findOne({
562
+ id,
563
+ type: "CopilotConstructMgr",
564
+ name: "error",
565
+ });
566
+ if (!errorMd) return { json: { error: "Error record not found" } };
567
+ doCreateErrorFixTask(errorMd, req.user?.id).catch((e) =>
568
+ console.error("fix_error_task error", e)
569
+ );
570
+ return { json: { success: true } };
571
+ };
572
+
573
+ /** Route: returns whether a fix task is still being generated for the given error id. */
574
+ const fix_error_status = async (table_id, vn, config, body, { req, res }) => {
575
+ const id = parseInt(body.id);
576
+ const errorMd = await MetaData.findOne({
577
+ id,
578
+ type: "CopilotConstructMgr",
579
+ name: "error",
580
+ });
581
+ return { json: { fixing: !!errorMd?.body?.fixing } };
582
+ };
583
+
584
+ /** Route: returns task details for a fix task linked to an error. */
585
+ const get_fix_task = async (table_id, vn, config, body, { req, res }) => {
586
+ const errorId = parseInt(body.error_id);
587
+ const allTasks = await MetaData.find({
588
+ type: "CopilotConstructMgr",
589
+ name: "task",
590
+ });
591
+ const fixTask = allTasks
592
+ .filter(
593
+ (t) =>
594
+ t.body.source === "error_fix" && parseInt(t.body.error_id) === errorId
595
+ )
596
+ .sort((a, b) => b.id - a.id)[0];
597
+ if (!fixTask) return { json: { error: "Fix task not found" } };
598
+ return {
599
+ json: {
600
+ task: {
601
+ id: fixTask.id,
602
+ name: fixTask.body.name,
603
+ description: fixTask.body.description,
604
+ status: fixTask.body.status || null,
605
+ },
606
+ },
607
+ };
608
+ };
609
+
610
+ /** Route: returns the rendered error list HTML for AJAX refresh. */
611
+ const err_list_html = async (table_id, vn, config, body, { req, res }) => {
612
+ const html = await errorList(req);
613
+ return { json: { html } };
614
+ };
615
+
616
+ const error_routes = {
617
+ del_err,
618
+ del_all_errs,
619
+ toggle_error_healing,
620
+ fix_error_task,
621
+ fix_error_status,
622
+ get_fix_task,
623
+ err_list_html,
624
+ };
625
+
626
+ const errTableStaticHtml = `
627
+ <style>
628
+ #err-list-area table { table-layout: auto; width: 100%; }
629
+ #err-list-area table th:nth-child(1),
630
+ #err-list-area table td:nth-child(1),
631
+ #err-list-area table th:nth-child(2),
632
+ #err-list-area table td:nth-child(2),
633
+ #err-list-area table th:nth-child(5),
634
+ #err-list-area table td:nth-child(5) { width: 1px; white-space: nowrap; }
635
+ #err-list-area table th:nth-child(3),
636
+ #err-list-area table td:nth-child(3) { width: 100%; }
637
+ #err-list-area table th:nth-child(4),
638
+ #err-list-area table td:nth-child(4) { padding-right: 2rem; }
639
+ </style>
640
+ <div class="modal fade" id="err-detail-modal" tabindex="-1" aria-hidden="true">
641
+ <div class="modal-dialog modal-lg modal-dialog-scrollable">
642
+ <div class="modal-content">
643
+ <div class="modal-header">
644
+ <h5 class="modal-title">Error details</h5>
645
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
646
+ </div>
647
+ <div class="modal-body">
648
+ <pre id="err-detail-body" style="white-space:pre-wrap;overflow-wrap:break-word;font-size:0.8rem;"></pre>
649
+ </div>
650
+ </div>
651
+ </div>
652
+ </div>
653
+ <div class="modal fade" id="fix-task-modal" tabindex="-1" aria-hidden="true">
654
+ <div class="modal-dialog modal-lg modal-dialog-scrollable">
655
+ <div class="modal-content">
656
+ <div class="modal-header">
657
+ <h5 class="modal-title">Fix task: <span id="fix-task-name" class="text-monospace fw-normal"></span></h5>
658
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
659
+ </div>
660
+ <div class="modal-body">
661
+ <div class="mb-2" id="fix-task-status-row"></div>
662
+ <p id="fix-task-desc" style="white-space:pre-wrap;font-size:0.9rem;"></p>
663
+ </div>
664
+ <div class="modal-footer">
665
+ <button type="button" id="fix-task-run-btn" class="btn btn-success" style="display:none">
666
+ <i class="fas fa-play me-1"></i>Run fix task
667
+ </button>
668
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
669
+ </div>
670
+ </div>
671
+ </div>
672
+ </div>
673
+ <script>
674
+ (function () {
675
+ const _vn = ${JSON.stringify(viewname)};
676
+ function refreshErrArea() {
677
+ view_post(_vn, 'err_list_html', {}, (r) => {
678
+ const el = document.getElementById('err-list-area');
679
+ if (r && r.html && el) el.innerHTML = r.html;
680
+ });
681
+ }
682
+ function startFixPolling() {
683
+ if (window.dynamic_updates_cfg?.enabled) return;
684
+ document.querySelectorAll('[data-fixing-id]').forEach((el) => {
685
+ const id = parseInt(el.dataset.fixingId);
686
+ const poll = () => {
687
+ view_post(_vn, 'fix_error_status', { id }, (resp) => {
688
+ if (resp && !resp.fixing) {
689
+ refreshErrArea();
690
+ } else {
691
+ setTimeout(poll, 3000);
692
+ }
693
+ });
694
+ };
695
+ setTimeout(poll, 3000);
696
+ });
697
+ }
698
+ window.copilotRefreshErrs = refreshErrArea;
699
+ window.copilotToggleErrorHealing = () => {
700
+ view_post(_vn, 'toggle_error_healing', {}, () => refreshErrArea());
701
+ };
702
+ window.copilotFixError = (id) => {
703
+ const btn = document.getElementById('fix-err-btn-' + id);
704
+ if (btn) { btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>'; }
705
+ view_post(_vn, 'fix_error_task', { id }, () => {
706
+ if (window.dynamic_updates_cfg?.enabled) return;
707
+ const poll = () => {
708
+ view_post(_vn, 'fix_error_status', { id }, (resp) => {
709
+ if (resp && !resp.fixing) {
710
+ refreshErrArea();
711
+ } else {
712
+ setTimeout(poll, 3000);
713
+ }
714
+ });
715
+ };
716
+ setTimeout(poll, 3000);
717
+ });
718
+ };
719
+ window.copilotDelErr = (id) => {
720
+ view_post(_vn, 'del_err', { id }, () => refreshErrArea());
721
+ };
722
+ window.copilotDelAllErrs = () => {
723
+ view_post(_vn, 'del_all_errs', {}, () => refreshErrArea());
724
+ };
725
+ window.copilotShowErrDetail = (btn) => {
726
+ let err = {};
727
+ try { err = JSON.parse(btn.dataset.err || '{}'); } catch (e) {}
728
+ document.getElementById('err-detail-body').textContent = JSON.stringify(err, null, 2);
729
+ new bootstrap.Modal(document.getElementById('err-detail-modal')).show();
730
+ };
731
+ window.copilotViewFixTask = (errorId) => {
732
+ view_post(_vn, 'get_fix_task', { error_id: errorId }, (r) => {
733
+ if (!r || !r.task) return;
734
+ const t = r.task;
735
+ document.getElementById('fix-task-name').textContent = t.name || '';
736
+ document.getElementById('fix-task-desc').textContent = t.description || '';
737
+ const statusBadge =
738
+ t.status === 'Done' ? '<span class="badge bg-success">Done</span>' :
739
+ t.status === 'Running' ? '<span class="badge bg-info text-dark"><i class="fas fa-spinner fa-spin me-1"></i>Running</span>' :
740
+ t.status === 'Error' ? '<span class="badge bg-danger">Error</span>' :
741
+ '<span class="badge bg-secondary">Pending</span>';
742
+ document.getElementById('fix-task-status-row').innerHTML = statusBadge;
743
+ const runBtn = document.getElementById('fix-task-run-btn');
744
+ if (t.status === 'Done' || t.status === 'Running') {
745
+ runBtn.style.display = 'none';
746
+ } else {
747
+ runBtn.style.display = '';
748
+ runBtn.disabled = false;
749
+ runBtn.innerHTML = '<i class="fas fa-play me-1"></i>Run fix task';
750
+ runBtn.onclick = () => copilotRunFixTask(t.id, errorId);
751
+ }
752
+ new bootstrap.Modal(document.getElementById('fix-task-modal')).show();
753
+ });
754
+ };
755
+ window.copilotRunFixTask = (taskId, errorId) => {
756
+ const modalRunBtn = document.getElementById('fix-task-run-btn');
757
+ const rowRunBtn = document.getElementById('run-fix-btn-' + taskId);
758
+ [modalRunBtn, rowRunBtn].forEach((btn) => {
759
+ if (!btn) return;
760
+ btn.disabled = true;
761
+ btn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Running...';
762
+ });
763
+ const modal = bootstrap.Modal.getInstance(document.getElementById('fix-task-modal'));
764
+ if (modal) modal.hide();
765
+ view_post(_vn, 'run_task', { id: taskId }, () => {
766
+ const poll = () => {
767
+ view_post(_vn, 'get_fix_task', { error_id: errorId }, (r) => {
768
+ const status = r && r.task && r.task.status;
769
+ if (status === 'Done' || status === 'Error') {
770
+ refreshErrArea();
771
+ if (typeof copilotRefreshTasks === 'function') copilotRefreshTasks();
772
+ } else {
773
+ setTimeout(poll, 3000);
774
+ }
775
+ });
776
+ };
777
+ setTimeout(poll, 3000);
778
+ });
779
+ };
780
+ if (document.readyState !== 'loading') startFixPolling();
781
+ else document.addEventListener('DOMContentLoaded', () => startFixPolling());
782
+ })();
783
+ </script>`;
784
+
785
+ module.exports = {
786
+ errorList,
787
+ doCreateErrorFixTask,
788
+ error_routes,
789
+ errTableStaticHtml,
790
+ };