@saltcorn/copilot 0.8.0 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,41 +8,13 @@ const { findType } = require("@saltcorn/data/models/discovery");
8
8
  const { save_menu_items } = require("@saltcorn/data/models/config");
9
9
  const db = require("@saltcorn/data/db");
10
10
  const WorkflowRun = require("@saltcorn/data/models/workflow_run");
11
+ const User = require("@saltcorn/data/models/user");
11
12
  const { viewname } = require("./common");
12
13
 
13
- const runNextTask = async (alwaysRun) => {
14
- if (!alwaysRun) {
15
- const settings = await MetaData.findOne({
16
- type: "CopilotConstructMgr",
17
- name: "settings",
18
- });
19
- if (!settings?.body?.running) return;
20
- }
21
- const tasks = await MetaData.find(
22
- {
23
- type: "CopilotConstructMgr",
24
- name: "task",
25
- },
26
- { orderBy: "id" }
27
- );
28
- const todos = tasks.filter(
29
- (t) => !t.body.status || t.body.status === "To do"
30
- );
31
- const done = tasks.filter((t) => t.body.status === "Done");
32
- const done_names = new Set(done.map((t) => t.body.name));
33
-
34
- const startable = todos.filter((t) =>
35
- t.body.depends_on.every((nm) => done_names.has(nm))
36
- );
37
-
38
- if (startable[0]) {
39
- console.log("running task", startable[0]);
40
-
41
- return await runTask(startable[0].id, {});
42
- }
43
- //not done
44
- };
45
-
14
+ /**
15
+ * @param {number} md_id - MetaData id of the task to run
16
+ * @param {object} req - Express request (may be empty `{}` from scheduler)
17
+ */
46
18
  const runTask = async (md_id, req) => {
47
19
  const md = await MetaData.findOne({
48
20
  id: md_id,
@@ -59,15 +31,19 @@ const runTask = async (md_id, req) => {
59
31
  when_trigger: "Never",
60
32
  configuration: {
61
33
  viewname: viewname,
62
- sys_prompt: "",
34
+ sys_prompt:
35
+ "Each task creates exactly one view or one page. " +
36
+ "Never create more than one view or page per task, even if the description mentions multiple. " +
37
+ "Call the view or page tool exactly once and then stop.",
63
38
  prompt: "{{prompt}}",
64
39
  skills: [
65
40
  { skill_type: "Generate Page", yoloMode: true },
66
41
  { skill_type: "Database design", yoloMode: true },
67
42
  { skill_type: "Generate Workflow", yoloMode: true },
43
+ { skill_type: "Generate trigger", yoloMode: true },
68
44
  { skill_type: "Generate View", yoloMode: true },
69
45
  { skill_type: "Install Plugin", yoloMode: true },
70
- { skill_type: "AppConstructor Context" },
46
+ { skill_type: "Registry editor", yoloMode: true },
71
47
  ],
72
48
  },
73
49
  });
@@ -83,46 +59,118 @@ Important: The database schema is already fully implemented. Do NOT use generate
83
59
 
84
60
  Important: Some fields are non-stored (virtual) calculated fields — they have no database column and are computed on-the-fly by Saltcorn. Never include such fields in modify_row, SQL UPDATE statements, or recalculate_stored_fields calls. Only fields that exist as actual database columns (regular fields and stored calculated fields) can be written. If a calculated field needs updating, it will refresh automatically when the fields it depends on change.
85
61
 
62
+ Important: The "users" table is built-in. Passwords are platform-managed — never add a password field to a view. Signup uses the built-in page at /auth/signup, login at /auth/login. Do NOT create triggers for registration or email verification — the platform handles this natively.
63
+
64
+ Important: On landing pages, place Log in / Create account buttons in no more than two locations (e.g. navbar and one hero call-to-action). Do not repeat them in a third "Get started" section or anywhere else. For links that take an already-authenticated user to their dashboard, use href="/" — not /auth/login.
65
+
66
+ Important: Do not name any page or view "Admin dashboard" — that name is reserved by the Saltcorn platform. For pages intended for role 1 (admin), use a name like "App admin dashboard" or prefix it with the application name (e.g. "Law Firm admin dashboard").
67
+
68
+ Important: When creating a page or view, always set min_role based on the intended audience: 1 for admin-only, 40 for staff and above, 80 for logged-in users and above, 100 for public. Never default to public (100) unless the page or view is explicitly intended for unauthenticated users (e.g. a landing page). A dashboard or view for clients/users is role 80, a staff page or view is role 40, an admin page or view is role 1.
69
+
70
+ Important: Two-factor authentication (2FA/TOTP) is fully built into the platform. To configure it, call set_entity directly with entity_type "system-configuration-value" and entity_name "twofa_policy_by_role". The entity_definition must be the plain JSON object itself — for example: {"1": "Mandatory", "100": "Disabled"}. Do NOT wrap it in {"type": "json", "value": ...} or any other envelope. Read the current value first with get_entity and merge rather than overwrite. Do NOT create a workflow or trigger to do this.
71
+
72
+ Important: To set a page as the home page for a role, call set_entity directly with entity_type "system-configuration-value" and entity_name "home_page_by_role". The value is a JSON object mapping role IDs to page names — Role IDs: public=100, user=80, staff=40, admin=1. The entity_definition must be the plain JSON object itself — for example: {"100": "landing", "80": "client_dashboard"}. Do NOT wrap it in {"type": "json", "value": ...} or any other envelope. Read the current value first with get_entity so you can merge rather than overwrite. Do NOT create a workflow or trigger to do this — use set_entity directly.
73
+
74
+ Important: If the task description mentions adding a viewlink, linking rows to another view, or a button that opens another view from a list — that viewlink column MUST be present in the finished view. Do not skip it. Viewlinks require calling get_relation_paths first to obtain the relation string before generating the layout.
75
+
76
+ Important: Before creating or updating any view or page that embeds, links to, or opens another view (including viewlinks, action buttons, and ajax_modal calls), call list_entities (entity_type "view") to get all existing view names. Only reference views that appear in that list — never invent a name or assume a view exists. If a view is not in the list, omit the reference entirely. Do the same for pages: call list_entities (entity_type "page") before linking to any page by name.
77
+
86
78
  Your task now is:
87
79
  ${md.body.description}`;
80
+ const safeReq = req?.__ ? req : { ...req, __: (s) => s, user: req?.user };
88
81
 
89
82
  await md.update({ body: { ...md.body, status: "Running" } });
90
- const actionres = await agent_action.runWithoutRow({
91
- row: { prompt },
92
- req,
93
- user: req?.user,
94
- });
95
- //console.log("actionres", actionres);
96
- const run_id = actionres.json.run_id;
97
- const run = await WorkflowRun.findOne({ id: run_id });
98
- await agent_action.runWithoutRow({
99
- row: {
100
- prompt:
101
- "Write a description of what you did, for the purposes of a progress report. Write 1-4 sentences. Do not use any tools or write any code",
102
- },
103
- req,
104
- run,
105
- user: req?.user,
106
- });
107
- const lastInteraction =
108
- run.context.interactions[run.context.interactions.length - 1];
109
- const lastText =
110
- typeof lastInteraction.content === "string"
111
- ? lastInteraction.content
112
- : lastInteraction.content.text
83
+ try {
84
+ const actionres = await agent_action.runWithoutRow({
85
+ row: { prompt },
86
+ req: safeReq,
87
+ user: safeReq.user,
88
+ });
89
+ const run_id = actionres.json.run_id;
90
+ const run = await WorkflowRun.findOne({ id: run_id });
91
+ await agent_action.runWithoutRow({
92
+ row: {
93
+ prompt:
94
+ "Write a description of what you did, for the purposes of a progress report. Write 1-4 sentences. Do not use any tools or write any code",
95
+ },
96
+ req: safeReq,
97
+ run,
98
+ user: safeReq.user,
99
+ });
100
+ const updatedRun = await WorkflowRun.findOne({ id: run_id });
101
+ const lastInteraction =
102
+ updatedRun.context.interactions[
103
+ updatedRun.context.interactions.length - 1
104
+ ];
105
+ const lastText =
106
+ typeof lastInteraction.content === "string"
107
+ ? lastInteraction.content
108
+ : lastInteraction.content.text
113
109
  ? lastInteraction.content.text
114
110
  : Array.isArray(lastInteraction.content)
115
- ? lastInteraction.content[0].text
116
- : lastInteraction.content;
117
- await MetaData.create({
118
- type: "CopilotConstructMgr",
119
- name: "progress",
120
- body: { text: lastText, run_id, task_id: md.id },
121
- user_id: req?.user?.id,
122
- });
111
+ ? lastInteraction.content[0].text
112
+ : lastInteraction.content;
113
+ await MetaData.create({
114
+ type: "CopilotConstructMgr",
115
+ name: "progress",
116
+ body: { text: lastText, run_id, task_id: md.id },
117
+ user_id: req?.user?.id,
118
+ });
119
+ await md.update({ body: { ...md.body, status: "Done", run_id } });
120
+ } catch (e) {
121
+ await md.update({ body: { ...md.body, status: "To do" } });
122
+ throw e;
123
+ }
124
+ };
125
+
126
+ /**
127
+ * Run the next startable task
128
+ * @param {boolean} [once=false] - true: run one task and stop, false: iterate all tasks
129
+ */
130
+ const runNextTask = async (once = false) => {
131
+ if (!once) {
132
+ const settings = await MetaData.findOne({
133
+ type: "CopilotConstructMgr",
134
+ name: "settings",
135
+ });
136
+ if (!settings?.body?.running) return;
137
+ }
138
+ const tasks = await MetaData.find(
139
+ {
140
+ type: "CopilotConstructMgr",
141
+ name: "task",
142
+ },
143
+ { orderBy: "id" }
144
+ );
145
+ if (tasks.some((t) => t.body.status === "Running")) return;
146
+ const todos = tasks.filter(
147
+ (t) => !t.body.status || t.body.status === "To do"
148
+ );
149
+ const done = tasks.filter((t) => t.body.status === "Done");
150
+ const done_names = new Set(done.map((t) => t.body.name));
151
+ const all_task_names = new Set(tasks.map((t) => t.body.name).filter(Boolean));
123
152
 
124
- //console.log("run", run);
125
- await md.update({ body: { ...md.body, status: "Done", run_id } });
153
+ const startable = todos.filter((t) =>
154
+ (t.body.depends_on || []).every(
155
+ (nm) => done_names.has(nm) || !all_task_names.has(nm)
156
+ )
157
+ );
158
+
159
+ if (startable[0]) {
160
+ console.log("running task", startable[0]);
161
+ const taskUser = startable[0].user_id
162
+ ? await User.findOne({ id: startable[0].user_id })
163
+ : null;
164
+ await runTask(startable[0].id, { user: taskUser, __: (s) => s });
165
+ if (!once) await runNextTask();
166
+ } else if (!once) {
167
+ const settings = await MetaData.findOne({
168
+ type: "CopilotConstructMgr",
169
+ name: "settings",
170
+ });
171
+ if (settings?.body?.running)
172
+ await settings.update({ body: { ...settings.body, running: false } });
173
+ }
126
174
  };
127
175
 
128
176
  module.exports = { runTask, runNextTask };
@@ -39,6 +39,7 @@ const { viewname, tool_choice } = require("./common");
39
39
  const { requirements_tool } = require("./tools");
40
40
  const { saltcorn_description, existing_tables_list } = require("./prompts");
41
41
  const GenerateTables = require("../actions/generate-tables");
42
+ const { buildMermaidMarkup } = GenerateTables;
42
43
  const GenerateTablesSkill = require("../agent-skills/database-design");
43
44
 
44
45
  const showSchema = async (req) => {
@@ -48,15 +49,94 @@ const showSchema = async (req) => {
48
49
  });
49
50
 
50
51
  if (schema) {
51
- const preview = GenerateTables.render_html(
52
- { tables: schema.body.tables },
53
- true
52
+ const newTableDefs = schema.body.tables || [];
53
+ const newTableInstances = GenerateTables.process_tables(newTableDefs);
54
+ const reusedMd = await MetaData.findOne({
55
+ type: "CopilotConstructMgr",
56
+ name: "reused_schema",
57
+ });
58
+ const reusedNames = reusedMd?.body?.table_names || [];
59
+ const reusedInstances = reusedNames
60
+ .map((n) => Table.findOne({ name: n }))
61
+ .filter(Boolean);
62
+ const allTables = [...newTableInstances, ...reusedInstances];
63
+ const mmdia = buildMermaidMarkup(allTables);
64
+ const implemented = !!schema.body.implemented;
65
+
66
+ const newNames = newTableDefs.map((t) => t.table_name).filter(Boolean);
67
+
68
+ const colorMap = Object.fromEntries([
69
+ ...newNames.map((n) => [n, "#198754"]),
70
+ ...reusedNames.map((n) => [n, "#6c757d"]),
71
+ ]);
72
+ const colorScript = script(
73
+ domReady(`
74
+ const colors = ${JSON.stringify(colorMap)};
75
+ const pre = document.querySelector('.schema-mermaid');
76
+ if (!pre) return;
77
+
78
+ const doRender = () => {
79
+ mermaid.run({ nodes: [pre], suppressErrors: true, postRenderCallback: () => {
80
+ for (const g of pre.querySelectorAll('g[id^="entity-"]')) {
81
+ const name = g.id.replace(/^entity-/, '').replace(/-\\d+$/, '');
82
+ const color = colors[name];
83
+ if (color) {
84
+ const p = g.querySelector('path');
85
+ if (p) p.setAttribute('fill', color);
86
+ }
87
+ }
88
+ for (const el of pre.querySelectorAll('g.label.name .nodeLabel')) {
89
+ el.style.color = 'white';
90
+ el.style.fontWeight = 'bold';
91
+ }
92
+ }});
93
+ };
94
+
95
+ // Defer render until tab is visible, then colorize nodes.
96
+ const pane = pre.closest('.tab-pane');
97
+ if (pane && !pane.classList.contains('active')) {
98
+ const link = document.querySelector('[href="#' + pane.id + '"]');
99
+ if (link) link.addEventListener('shown.bs.tab', doRender, { once: true });
100
+ else {
101
+ const o = new MutationObserver(() => {
102
+ if (pane.classList.contains('active')) { o.disconnect(); doRender(); }
103
+ });
104
+ o.observe(pane, { attributes: true, attributeFilter: ['class'] });
105
+ }
106
+ } else doRender();
107
+ `)
108
+ );
109
+
110
+ const legend = div(
111
+ { class: "mt-3 d-flex flex-wrap gap-3 align-items-start" },
112
+ newNames.length
113
+ ? div(
114
+ { class: "d-flex flex-wrap align-items-center gap-1" },
115
+ span(
116
+ { class: "me-1 text-muted small" },
117
+ implemented ? "Was created:" : "Will be created:"
118
+ ),
119
+ ...newNames.map((n) => span({ class: "badge bg-success" }, n))
120
+ )
121
+ : "",
122
+ reusedNames.length
123
+ ? div(
124
+ { class: "d-flex flex-wrap align-items-center gap-1" },
125
+ span(
126
+ { class: "me-1 text-muted small" },
127
+ implemented ? "Already existed:" : "Already exists:"
128
+ ),
129
+ ...reusedNames.map((n) => span({ class: "badge bg-secondary" }, n))
130
+ )
131
+ : ""
54
132
  );
55
133
 
56
134
  return div(
57
135
  { class: "mt-2" },
58
- preview,
59
- !schema.body.implemented &&
136
+ pre({ class: "schema-mermaid" }, mmdia),
137
+ colorScript,
138
+ legend,
139
+ !implemented &&
60
140
  div(
61
141
  { class: "mb-4 d-block mt-3" },
62
142
  button(
@@ -75,37 +155,71 @@ const showSchema = async (req) => {
75
155
  )
76
156
  )
77
157
  );
78
- } else {
158
+ }
159
+
160
+ const generating = await MetaData.findOne({
161
+ type: "CopilotConstructMgr",
162
+ name: "generating_schema",
163
+ });
164
+ if (generating) {
79
165
  return div(
80
166
  { class: "mt-2" },
81
- p("Schema not found"),
82
- button(
83
- {
84
- class: "btn btn-primary",
85
- onclick: `press_store_button(this);view_post("${viewname}", "gen_schema")`,
86
- },
87
- "Generate schema"
167
+ p(
168
+ i({ class: "fas fa-spinner fa-spin me-2" }),
169
+ "Generating schema, please wait..."
170
+ ),
171
+ script(
172
+ domReady(`
173
+ const poll = () => {
174
+ view_post(${JSON.stringify(viewname)}, 'schema_status', {}, (resp) => {
175
+ if (resp && !resp.generating) location.reload();
176
+ else setTimeout(poll, 3000);
177
+ });
178
+ };
179
+ setTimeout(poll, 3000);
180
+ `)
88
181
  )
89
182
  );
90
183
  }
184
+
185
+ return div(
186
+ { class: "mt-2", id: "schema-gen-area" },
187
+ p("Schema not found"),
188
+ button(
189
+ { class: "btn btn-primary", onclick: `copilotGenSchema()` },
190
+ "Generate schema"
191
+ ),
192
+ script(
193
+ domReady(`
194
+ window.copilotGenSchema = () => {
195
+ document.getElementById('schema-gen-area').innerHTML =
196
+ '<p><i class="fas fa-spinner fa-spin me-2"></i>Generating schema, please wait...</p>';
197
+ view_post(${JSON.stringify(viewname)}, 'gen_schema', {}, () => {});
198
+ const poll = () => {
199
+ view_post(${JSON.stringify(viewname)}, 'schema_status', {}, (resp) => {
200
+ if (resp && !resp.generating) location.reload();
201
+ else setTimeout(poll, 3000);
202
+ });
203
+ };
204
+ setTimeout(poll, 3000);
205
+ };
206
+ `)
207
+ )
208
+ );
91
209
  };
92
210
 
93
- const gen_schema = async (table_id, viewname, config, body, { req, res }) => {
94
- const spec = await MetaData.findOne({
95
- type: "CopilotConstructMgr",
96
- name: "spec",
97
- });
98
- if (!spec) throw new Error("Specification not found");
99
- const rs = await MetaData.find({
211
+ const doGenSchema = async (spec, rs, userId) => {
212
+ const generatingMd = await MetaData.create({
100
213
  type: "CopilotConstructMgr",
101
- name: "requirement",
214
+ name: "generating_schema",
215
+ body: {},
216
+ user_id: userId,
102
217
  });
103
- if (!rs.length) throw new Error("No requirements found");
104
-
105
- const databaseDesignTool = new GenerateTablesSkill({}).provideTools();
106
- const existing_tables = await Table.find({});
107
- const answer = await getState().functions.llm_generate.run(
108
- `Generate the database schema for this application:
218
+ try {
219
+ const databaseDesignTool = new GenerateTablesSkill({}).provideTools();
220
+ const existing_tables = await Table.find({});
221
+ const answer = await getState().functions.llm_generate.run(
222
+ `Generate the database schema for this application:
109
223
 
110
224
  Description: ${spec.body.description}
111
225
  Audience: ${spec.body.audience}
@@ -113,7 +227,7 @@ Core features: ${spec.body.core_features}
113
227
  Out of scope: ${spec.body.out_of_scope}
114
228
  Visual style: ${spec.body.visual_style}
115
229
 
116
- These are the requirements of the application:
230
+ These are the requirements of the application:
117
231
 
118
232
  ${rs.map((r) => `* ${r.body.requirement}`).join("\n")}
119
233
 
@@ -123,6 +237,10 @@ ${existing_tables_list(existing_tables)}
123
237
 
124
238
  Design a complete database schema that covers ALL requirements listed above. Every distinct entity in the application must have its own table. Do not produce a minimal or partial schema — all tables needed to implement every requirement must be included in this single call. Do not leave any tables for a later step.
125
239
 
240
+ The tables listed above already exist in the database. Do NOT modify or extend them — treat them as fixed. Handle them as follows:
241
+ - If an existing table is already complete and used as-is: add its name to reused_table_names. Do NOT define its fields again in the tables array.
242
+ - New tables not yet in the database: include them in the tables array with all their fields as usual.
243
+
126
244
  For every field that must be unique (e.g. unique email, unique slug, unique combination keys expressed as individual unique fields), set unique=true on that field.
127
245
  For every field that must not be empty, set not_null=true.
128
246
  Do NOT leave uniqueness or required constraints for a later step — express them fully in this schema.
@@ -130,34 +248,78 @@ Do NOT leave uniqueness or required constraints for a later step — express the
130
248
  Note: ownership configuration (automatically populating a FK-to-users field from the logged-in user) is a VIEW-level concern and cannot be expressed in the schema. Do not attempt to annotate fields as "ownership fields" here — simply define the foreign key field normally. Ownership will be configured when the Edit views are generated.
131
249
 
132
250
  Now use the ${
133
- databaseDesignTool.function.name
134
- } tool to generate the complete database schema for this software application
251
+ databaseDesignTool.function.name
252
+ } tool to generate the complete database schema for this software application
135
253
  `,
136
- {
137
- tools: [databaseDesignTool],
138
- ...tool_choice(databaseDesignTool.function.name),
139
- systemPrompt:
140
- "You are a database designer. The user wants to build an application, and you must analyse their application description and requirements and design a complete schema. Every entity needed by any requirement must have its own table. Never produce a partial schema.",
141
- }
142
- );
254
+ {
255
+ tools: [databaseDesignTool],
256
+ ...tool_choice(databaseDesignTool.function.name),
257
+ systemPrompt:
258
+ "You are a database designer. The user wants to build an application, and you must analyse their application description and requirements and design a complete schema. Every entity needed by any requirement must have its own table. Never produce a partial schema.",
259
+ }
260
+ );
261
+
262
+ const tc = answer.getToolCalls()[0];
143
263
 
144
- const tc = answer.getToolCalls()[0];
264
+ const noNewTables = !tc.input.tables || tc.input.tables.length === 0;
265
+ await MetaData.create({
266
+ type: "CopilotConstructMgr",
267
+ name: "schema",
268
+ body: { tables: tc.input.tables || [], implemented: noNewTables },
269
+ user_id: userId,
270
+ });
145
271
 
146
- await MetaData.create({
272
+ const reusedNames = tc.input.reused_table_names || [];
273
+ if (reusedNames.length) {
274
+ await MetaData.create({
275
+ type: "CopilotConstructMgr",
276
+ name: "reused_schema",
277
+ body: { table_names: reusedNames },
278
+ user_id: userId,
279
+ });
280
+ }
281
+ } finally {
282
+ await generatingMd.delete();
283
+ }
284
+ };
285
+
286
+ const gen_schema = async (table_id, viewname, config, body, { req, res }) => {
287
+ const spec = await MetaData.findOne({
147
288
  type: "CopilotConstructMgr",
148
- name: "schema",
149
- body: { tables: tc.input.tables, implemented: false },
150
- user_id: req.user?.id,
289
+ name: "spec",
151
290
  });
152
- return { json: { reload_page: true } };
291
+ if (!spec) throw new Error("Specification not found");
292
+ const rs = await MetaData.find({
293
+ type: "CopilotConstructMgr",
294
+ name: "requirement",
295
+ });
296
+ if (!rs.length) throw new Error("No requirements found");
297
+
298
+ doGenSchema(spec, rs, req.user?.id).catch((e) =>
299
+ console.error("gen_schema error", e)
300
+ );
301
+ return { json: { success: true } };
153
302
  };
154
303
 
155
- const del_schema = async (table_id, viewname, config, body, { req, res }) => {
156
- const rs = await MetaData.find({
304
+ const schema_status = async (
305
+ table_id,
306
+ viewname,
307
+ config,
308
+ body,
309
+ { req, res }
310
+ ) => {
311
+ const generating = await MetaData.findOne({
157
312
  type: "CopilotConstructMgr",
158
- name: "schema",
313
+ name: "generating_schema",
159
314
  });
160
- for (const r of rs) await r.delete();
315
+ return { json: { generating: !!generating } };
316
+ };
317
+
318
+ const del_schema = async (table_id, viewname, config, body, { req, res }) => {
319
+ for (const name of ["schema", "reused_schema", "generating_schema"]) {
320
+ const rs = await MetaData.find({ type: "CopilotConstructMgr", name });
321
+ for (const r of rs) await r.delete();
322
+ }
161
323
  return { json: { reload_page: true } };
162
324
  };
163
325
 
@@ -174,13 +336,29 @@ const implement_schema = async (
174
336
  });
175
337
 
176
338
  const { apply_copilot_tables } = new GenerateTablesSkill({}).userActions;
177
- await apply_copilot_tables({ tables: md.body.tables, user: req.user });
339
+ const existingNames = new Set((await Table.find({})).map((t) => t.name));
340
+ const newTables = md.body.tables.filter((t) => {
341
+ if (existingNames.has(t.name)) {
342
+ getState().log(
343
+ 2,
344
+ `AppConstructor: skipping table "${t.name}" — already exists in database`
345
+ );
346
+ return false;
347
+ }
348
+ return true;
349
+ });
350
+ await apply_copilot_tables({ tables: newTables, user: req.user });
178
351
  md.body.implemented = true;
179
352
  await md.update({ body: md.body });
180
353
 
181
354
  return { json: { reload_page: true } };
182
355
  };
183
356
 
184
- const schema_routes = { gen_schema, del_schema, implement_schema };
357
+ const schema_routes = {
358
+ gen_schema,
359
+ schema_status,
360
+ del_schema,
361
+ implement_schema,
362
+ };
185
363
 
186
364
  module.exports = { showSchema, schema_routes };