pgkiln 0.84.0 → 0.85.0
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/docs/apex-feature-parity.md +2 -2
- package/docs/guide/03-builder.md +14 -5
- package/docs/guide/12-development.md +6 -1
- package/package.json +1 -1
- package/src/app.ts +9 -0
- package/src/builder/ai-agents.ts +86 -85
- package/src/builder/ai-builder.ts +30 -30
- package/src/builder/ai.ts +93 -92
- package/src/builder/api.ts +47 -47
- package/src/builder/app-groups.ts +33 -32
- package/src/builder/appfromfile.ts +57 -58
- package/src/builder/appsheets.ts +33 -32
- package/src/builder/appwizard.ts +65 -65
- package/src/builder/arrange.ts +23 -22
- package/src/builder/assistant.ts +39 -38
- package/src/builder/automations.ts +32 -31
- package/src/builder/availability.ts +24 -24
- package/src/builder/backups.ts +30 -31
- package/src/builder/blueprints.ts +62 -61
- package/src/builder/create-page.ts +52 -50
- package/src/builder/cross-app.ts +37 -35
- package/src/builder/dataload.ts +2 -2
- package/src/builder/datasets.ts +107 -106
- package/src/builder/dependencies.ts +20 -19
- package/src/builder/designer.ts +36 -36
- package/src/builder/diagnostics.ts +58 -57
- package/src/builder/features.ts +39 -35
- package/src/builder/feedback.ts +23 -20
- package/src/builder/find-workspace.ts +28 -24
- package/src/builder/form-designer.ts +36 -35
- package/src/builder/gallery.ts +25 -29
- package/src/builder/globalization.ts +50 -49
- package/src/builder/i18n/de.ts +3453 -0
- package/src/builder/i18n/en.ts +3453 -0
- package/src/builder/i18n/es.ts +3453 -0
- package/src/builder/i18n/fr.ts +3453 -0
- package/src/builder/i18n/it.ts +3453 -0
- package/src/builder/i18n/ja.ts +3453 -0
- package/src/builder/i18n/ko.ts +3453 -0
- package/src/builder/i18n/nl.ts +3453 -0
- package/src/builder/i18n/pt.ts +3453 -0
- package/src/builder/i18n/zh-cn.ts +3453 -0
- package/src/builder/i18n/zh-tw.ts +3453 -0
- package/src/builder/i18n.ts +17 -3
- package/src/builder/json-sources.ts +63 -62
- package/src/builder/layouts.ts +12 -11
- package/src/builder/locks.ts +29 -28
- package/src/builder/login-page.ts +26 -25
- package/src/builder/mail.ts +148 -147
- package/src/builder/newapp.ts +15 -12
- package/src/builder/plugins.ts +47 -44
- package/src/builder/process-jobs.ts +11 -10
- package/src/builder/pwa.ts +22 -21
- package/src/builder/remote-databases.ts +55 -56
- package/src/builder/report-settings.ts +1 -1
- package/src/builder/reporter.ts +43 -42
- package/src/builder/rest-catalogs.ts +87 -86
- package/src/builder/rest.ts +9 -8
- package/src/builder/routes.ts +27 -27
- package/src/builder/search.ts +14 -13
- package/src/builder/static-files.ts +31 -32
- package/src/builder/subscriptions.ts +47 -45
- package/src/builder/supporting.ts +23 -18
- package/src/builder/team.ts +179 -179
- package/src/builder/telemetry.ts +59 -62
- package/src/builder/templates.ts +66 -65
- package/src/builder/themeroller.ts +60 -63
- package/src/builder/ui-defaults.ts +21 -20
- package/src/builder/ui.ts +2 -1
- package/src/builder/upgrade.ts +25 -24
- package/src/builder/utilities.ts +48 -47
- package/src/builder/vector-providers.ts +67 -66
- package/src/builder/websources.ts +43 -42
- package/src/builder/wizards.ts +95 -89
- package/src/builder/workflow-migrate.ts +30 -31
- package/src/builder/workflows.ts +40 -39
- package/src/builder/workingcopies.ts +41 -49
- package/src/builder/workspace-backups.ts +21 -20
- package/src/builder/workspace-mail.ts +17 -16
- package/src/builder/workspace-resources.ts +104 -112
- package/src/builder/workspace-transfer.ts +34 -26
package/src/builder/arrange.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { owner } from '../db.ts';
|
|
|
3
3
|
import { saveState, type Session } from '../session.ts';
|
|
4
4
|
import { COMPONENTS } from './components.ts';
|
|
5
5
|
import { BASE, developer, flash, type Req } from './ui.ts';
|
|
6
|
+
import { scopedTx as tx } from './i18n.ts';
|
|
6
7
|
|
|
7
8
|
// Page designer layout: moving regions, items and buttons (drag and drop on
|
|
8
9
|
// the Layout tab, or the Move up / Move down / column span buttons), creating
|
|
@@ -142,7 +143,7 @@ async function targetRegion(c: Q, pid: number, value: string): Promise<number |
|
|
|
142
143
|
if (value === '') return null;
|
|
143
144
|
const id = int(value);
|
|
144
145
|
if (id === null || !(await c.query('select 1 from meta.region where id = $1 and page_id = $2', [id, pid])).rowCount)
|
|
145
|
-
throw new LayoutError(400, '
|
|
146
|
+
throw new LayoutError(400, tx('arr.that_region_is_not_on'));
|
|
146
147
|
return id;
|
|
147
148
|
}
|
|
148
149
|
|
|
@@ -165,8 +166,8 @@ export async function moveComponent(c: Q, pid: number, kind: LayoutKind, id: num
|
|
|
165
166
|
if (kind === 'region' && o.position !== undefined && !o.dir) place = o.position || 'body';
|
|
166
167
|
if (kind === 'button' && o.slot !== undefined && !o.dir) place = o.slot || 'region';
|
|
167
168
|
if (kind === 'button' && region === null) place = '';
|
|
168
|
-
if (kind === 'region' && !POSITIONS().includes(place)) throw new LayoutError(400, '
|
|
169
|
-
if (kind === 'button' && place && !SLOTS().includes(place)) throw new LayoutError(400, '
|
|
169
|
+
if (kind === 'region' && !POSITIONS().includes(place)) throw new LayoutError(400, tx('arr.unknown_position'));
|
|
170
|
+
if (kind === 'button' && place && !SLOTS().includes(place)) throw new LayoutError(400, tx('arr.unknown_button_position'));
|
|
170
171
|
const scope = kind === 'region' ? " and coalesce(position, 'body') = $2"
|
|
171
172
|
: kind === 'button' ? " and region_id is not distinct from $2 and (region_id is null or coalesce(slot, 'region') = $3)"
|
|
172
173
|
: ' and region_id is not distinct from $2';
|
|
@@ -178,13 +179,13 @@ export async function moveComponent(c: Q, pid: number, kind: LayoutKind, id: num
|
|
|
178
179
|
const now = list.indexOf(id);
|
|
179
180
|
at = Math.max(0, Math.min(others.length, now + (o.dir === 'up' ? -1 : 1)));
|
|
180
181
|
} else if (o.dir) {
|
|
181
|
-
throw new LayoutError(400, '
|
|
182
|
+
throw new LayoutError(400, tx('arr.unknown_direction'));
|
|
182
183
|
} else if (!o.before) {
|
|
183
184
|
at = others.length;
|
|
184
185
|
} else {
|
|
185
186
|
const before = int(o.before);
|
|
186
187
|
at = before === null ? -1 : others.indexOf(before);
|
|
187
|
-
if (at < 0) throw new LayoutError(400, '
|
|
188
|
+
if (at < 0) throw new LayoutError(400, tx('arr.the_place_to_move_to'));
|
|
188
189
|
}
|
|
189
190
|
others.splice(at, 0, id);
|
|
190
191
|
const stored = place === 'body' || place === 'region' || place === '' ? null : place;
|
|
@@ -198,9 +199,9 @@ export async function moveComponent(c: Q, pid: number, kind: LayoutKind, id: num
|
|
|
198
199
|
/** Sets a region's column span (1–12), or widens / narrows it by one column. */
|
|
199
200
|
export async function spanRegion(c: Q, pid: number, id: number, o: { columns?: string; delta?: string }) {
|
|
200
201
|
const row = (await c.query('select columns from meta.region where id = $1 and page_id = $2', [id, pid])).rows[0];
|
|
201
|
-
if (!row) throw new LayoutError(404, '
|
|
202
|
+
if (!row) throw new LayoutError(404, tx('arr.region_not_found_on_this'));
|
|
202
203
|
const n = o.delta === '1' ? row.columns + 1 : o.delta === '-1' ? row.columns - 1 : int(o.columns);
|
|
203
|
-
if (n === null || n < 1 || n > 12) throw new LayoutError(400, '
|
|
204
|
+
if (n === null || n < 1 || n > 12) throw new LayoutError(400, tx('arr.the_column_span_is_1'));
|
|
204
205
|
await c.query('update meta.region set columns = $3 where id = $1 and page_id = $2', [id, pid, n]);
|
|
205
206
|
return n;
|
|
206
207
|
}
|
|
@@ -208,10 +209,10 @@ export async function spanRegion(c: Q, pid: number, id: number, o: { columns?: s
|
|
|
208
209
|
/** A new component from the gallery, with a working default; placed like moveComponent. Returns its id. */
|
|
209
210
|
export async function createComponent(c: Q, pid: number, kind: LayoutKind, type: string, o: { region?: string; before?: string; position?: string; slot?: string }) {
|
|
210
211
|
const page = (await c.query('select page_no from meta.page where id = $1', [pid])).rows[0];
|
|
211
|
-
if (!page) throw new LayoutError(404, '
|
|
212
|
+
if (!page) throw new LayoutError(404, tx('arr.page_not_found'));
|
|
212
213
|
let id: number;
|
|
213
214
|
if (kind === 'region') {
|
|
214
|
-
if (!REGION_TYPES().includes(type)) throw new LayoutError(400, '
|
|
215
|
+
if (!REGION_TYPES().includes(type)) throw new LayoutError(400, tx('arr.unknown_region_type'));
|
|
215
216
|
id = (await c.query(`insert into meta.region (page_id, seq, title, type, source) values ($1, 999999, $2, $3, $4) returning id`,
|
|
216
217
|
[pid, `New ${REGION_LABELS[type]?.[0].toLowerCase() ?? type}`, type, NEW_REGION_SOURCE[type] ?? null])).rows[0].id;
|
|
217
218
|
// (140) a breadcrumb goes in the breadcrumb bar, without a frame of its own (unless it is dropped elsewhere)
|
|
@@ -222,14 +223,14 @@ export async function createComponent(c: Q, pid: number, kind: LayoutKind, type:
|
|
|
222
223
|
} else {
|
|
223
224
|
const region = await targetRegion(c, pid, o.region ?? '');
|
|
224
225
|
if (kind === 'item') {
|
|
225
|
-
if (!ITEM_TYPES().includes(type)) throw new LayoutError(400, '
|
|
226
|
+
if (!ITEM_TYPES().includes(type)) throw new LayoutError(400, tx('arr.unknown_item_type'));
|
|
226
227
|
const taken = new Set((await c.query('select name from meta.item where page_id = $1', [pid])).rows.map((r) => r.name));
|
|
227
228
|
const base = `P${page.page_no}_NEW`;
|
|
228
229
|
let name = base;
|
|
229
230
|
for (let i = 2; taken.has(name); i++) name = `${base}_${i}`;
|
|
230
231
|
id = (await c.query(`insert into meta.item (page_id, region_id, seq, name, label, type) values ($1, $2, 999999, $3, 'New item', $4) returning id`, [pid, region, name, type])).rows[0].id;
|
|
231
232
|
} else {
|
|
232
|
-
if (!BUTTON_ACTIONS().includes(type)) throw new LayoutError(400, '
|
|
233
|
+
if (!BUTTON_ACTIONS().includes(type)) throw new LayoutError(400, tx('arr.unknown_button_action'));
|
|
233
234
|
const taken = new Set((await c.query('select name from meta.button where page_id = $1', [pid])).rows.map((r) => r.name));
|
|
234
235
|
let name = 'NEW_BUTTON';
|
|
235
236
|
for (let i = 2; taken.has(name); i++) name = `NEW_BUTTON_${i}`;
|
|
@@ -268,7 +269,7 @@ export async function duplicateComponent(c: Q, pid: number, kind: LayoutKind, id
|
|
|
268
269
|
const row = (await c.query(`select * from ${table} where id = $1 and page_id = $2`, [id, pid])).rows[0];
|
|
269
270
|
if (!row) throw new LayoutError(404, `${COMPONENTS[kind].label} not found on this page.`);
|
|
270
271
|
const pages = (await c.query('select t.page_no from meta.page t join meta.page f on f.app_id = t.app_id where t.id = $1 and f.id = $2', [toPid, pid])).rows;
|
|
271
|
-
if (!pages.length) throw new LayoutError(400, '
|
|
272
|
+
if (!pages.length) throw new LayoutError(400, tx('arr.that_page_is_not_in'));
|
|
272
273
|
const same = toPid === pid;
|
|
273
274
|
const rename = (name: string) => (same ? name : name.replace(/^P\d+_/, `P${pages[0].page_no}_`));
|
|
274
275
|
const copyChild = async (k: 'item' | 'button', x: any, regionId: number | null) =>
|
|
@@ -336,24 +337,24 @@ export async function arrangeRoutes(app: FastifyInstance) {
|
|
|
336
337
|
route('move', async (s, pid, b) => {
|
|
337
338
|
const kind = b.kind;
|
|
338
339
|
const id = int(b.id);
|
|
339
|
-
if (!isKind(kind) || id === null) throw new LayoutError(400, '
|
|
340
|
+
if (!isKind(kind) || id === null) throw new LayoutError(400, tx('arr.unknown_component'));
|
|
340
341
|
await recorded(s, pid, `Move ${await describe(kind, id)}`, (c) => moveComponent(c, pid, kind, id, { dir: b.dir, region: b.region, before: b.before, position: b.position, slot: b.slot }));
|
|
341
342
|
return { sel: `${kind}-${id}`, message: `${COMPONENTS[kind].label} moved.` };
|
|
342
343
|
});
|
|
343
344
|
|
|
344
345
|
route('span', async (s, pid, b) => {
|
|
345
346
|
const id = int(b.id);
|
|
346
|
-
if (id === null) throw new LayoutError(400, '
|
|
347
|
+
if (id === null) throw new LayoutError(400, tx('arr.unknown_region'));
|
|
347
348
|
let n = 0;
|
|
348
349
|
await recorded(s, pid, `Resize ${await describe('region', id)}`, async (c) => {
|
|
349
350
|
n = await spanRegion(c, pid, id, { columns: b.columns, delta: b.delta });
|
|
350
351
|
});
|
|
351
|
-
return { sel: `region-${id}`, message:
|
|
352
|
+
return { sel: `region-${id}`, message: tx('arr.spans', { n }) };
|
|
352
353
|
});
|
|
353
354
|
|
|
354
355
|
route('create', async (s, pid, b) => {
|
|
355
356
|
const kind = b.kind;
|
|
356
|
-
if (!isKind(kind)) throw new LayoutError(400, '
|
|
357
|
+
if (!isKind(kind)) throw new LayoutError(400, tx('arr.unknown_component_type'));
|
|
357
358
|
const id = await owner.tx((c) => createComponent(c, pid, kind, b.type ?? '', { region: b.region, before: b.before, position: b.position, slot: b.slot }));
|
|
358
359
|
keep(s, { pid, undo: [], redo: [] }); // earlier layout steps no longer line up with the new component
|
|
359
360
|
return { sel: `${kind}-${id}`, message: `${COMPONENTS[kind].label} created; set its properties on the right.` };
|
|
@@ -365,7 +366,7 @@ export async function arrangeRoutes(app: FastifyInstance) {
|
|
|
365
366
|
route('comment', async (_s, pid, b) => {
|
|
366
367
|
const kind = b.kind ?? '';
|
|
367
368
|
const id = int(b.id);
|
|
368
|
-
if (!COMMENTABLE.has(kind) || id === null) throw new LayoutError(400, '
|
|
369
|
+
if (!COMMENTABLE.has(kind) || id === null) throw new LayoutError(400, tx('arr.unknown_component'));
|
|
369
370
|
const on = b.on !== 'false';
|
|
370
371
|
const done = await owner.tx(async (c) => {
|
|
371
372
|
if (on)
|
|
@@ -375,14 +376,14 @@ export async function arrangeRoutes(app: FastifyInstance) {
|
|
|
375
376
|
on conflict (app_id, name) do nothing`, [pid]);
|
|
376
377
|
return (await c.query(`update ${COMPONENTS[kind].table} set build_option = $3 where id = $1 and page_id = $2 and ${on ? 'true' : `build_option = 'COMMENTED_OUT'`}`, [id, pid, on ? 'COMMENTED_OUT' : null])).rowCount;
|
|
377
378
|
});
|
|
378
|
-
if (!done) throw new LayoutError(404, '
|
|
379
|
+
if (!done) throw new LayoutError(404, tx('arr.unknown_component'));
|
|
379
380
|
return { sel: `${kind}-${id}`, message: `${COMPONENTS[kind].label} ${on ? 'commented out: it stays in the page designer and is left out of the page (build option COMMENTED_OUT)' : 'back on the page'}.` };
|
|
380
381
|
});
|
|
381
382
|
|
|
382
383
|
route('duplicate', async (s, pid, b) => {
|
|
383
384
|
const kind = b.kind;
|
|
384
385
|
const id = int(b.id);
|
|
385
|
-
if (!isKind(kind) || id === null) throw new LayoutError(400, '
|
|
386
|
+
if (!isKind(kind) || id === null) throw new LayoutError(400, tx('arr.unknown_component'));
|
|
386
387
|
const copy = await owner.tx((c) => duplicateComponent(c, pid, kind, id));
|
|
387
388
|
keep(s, { pid, undo: [], redo: [] });
|
|
388
389
|
return { sel: `${kind}-${copy}`, message: `${COMPONENTS[kind].label} duplicated.` };
|
|
@@ -398,7 +399,7 @@ export async function arrangeRoutes(app: FastifyInstance) {
|
|
|
398
399
|
const id = int(b.id);
|
|
399
400
|
try {
|
|
400
401
|
if (pid === null || !(await owner.one('select 1 as ok from meta.page where id = $1', [pid]))) throw new LayoutError(404, 'Page not found');
|
|
401
|
-
if (!isKind(kind) || id === null || to === null) throw new LayoutError(400, '
|
|
402
|
+
if (!isKind(kind) || id === null || to === null) throw new LayoutError(400, tx('arr.unknown_component_or_page'));
|
|
402
403
|
const copy = await owner.tx((c) => duplicateComponent(c, pid, kind, id, to));
|
|
403
404
|
if (to === pid) keep(s, { pid, undo: [], redo: [] });
|
|
404
405
|
flash(s, `${COMPONENTS[kind].label} copied.`);
|
|
@@ -415,10 +416,10 @@ export async function arrangeRoutes(app: FastifyInstance) {
|
|
|
415
416
|
route(op, async (s, pid) => {
|
|
416
417
|
const h = history(s, pid);
|
|
417
418
|
const step = (op === 'undo' ? h.undo : h.redo).pop();
|
|
418
|
-
if (!step) return { sel: '', message:
|
|
419
|
+
if (!step) return { sel: '', message: op === 'undo' ? tx('arr.nothing_undo') : tx('arr.nothing_redo') };
|
|
419
420
|
await owner.tx((c) => restore(c, pid, op === 'undo' ? step.before : step.after));
|
|
420
421
|
(op === 'undo' ? h.redo : h.undo).push(step);
|
|
421
422
|
keep(s, h);
|
|
422
|
-
return { sel: '', message: `${op === 'undo' ? '
|
|
423
|
+
return { sel: '', message: `${op === 'undo' ? tx('arr.undone') : tx('arr.redone')}: ${step.label}.` };
|
|
423
424
|
});
|
|
424
425
|
}
|
package/src/builder/assistant.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { assistantProblems, type AssistantConfig } from '../runtime/assistant.ts
|
|
|
7
7
|
import type { Session } from '../session.ts';
|
|
8
8
|
import { agentOptions } from './ai-agents.ts';
|
|
9
9
|
import { back, BASE, csrf, developer, flash, type Req } from './ui.ts';
|
|
10
|
+
import { scopedTx as tx } from './i18n.ts';
|
|
10
11
|
|
|
11
12
|
// Page designer → an AI assistant region → Settings (service, prompts,
|
|
12
13
|
// context queries, tools, limits), and → a report region → "Ask in your own
|
|
@@ -29,7 +30,7 @@ async function serviceOptions(appId: number, current: unknown) {
|
|
|
29
30
|
};
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
const NO_SERVICE = html`<p class="muted"
|
|
33
|
+
const NO_SERVICE = () => html`<p class="muted">${tx.rich('asst.no_service', { link: html`<a href="${BASE}/ai">${tx('asst.workspace_utilities_ai_services')}</a>` })}</p>`;
|
|
33
34
|
|
|
34
35
|
const json = (v: unknown) => (v === undefined ? '' : JSON.stringify(v, null, 2));
|
|
35
36
|
|
|
@@ -40,41 +41,41 @@ export async function assistantSettingsForm(pageId: number, appId: number, r: {
|
|
|
40
41
|
const problems = assistantProblems(cfg);
|
|
41
42
|
const agents = await agentOptions(appId);
|
|
42
43
|
const agent = typeof cfg.agent === 'string' ? cfg.agent.toUpperCase() : '';
|
|
43
|
-
return html`<h3 class="u-mt15"
|
|
44
|
-
<p class="muted u-mt0"
|
|
45
|
-
${allowed.length ? '' : NO_SERVICE}
|
|
44
|
+
return html`<h3 class="u-mt15">${tx('asst.ai_assistant_settings')}</h3>
|
|
45
|
+
<p class="muted u-mt0">${tx('asst.a_chat_with_an_ai')}</p>
|
|
46
|
+
${allowed.length ? '' : NO_SERVICE()}
|
|
46
47
|
${problems.length ? html`<div class="alert alert-error" role="alert">${problems.join(' ')}</div>` : ''}
|
|
47
48
|
<form method="post" action="${BASE}/pages/${pageId}/region/${r.id}/assistant" class="component-form">${csrf(s)}
|
|
48
|
-
<fieldset class="prop-group"><legend
|
|
49
|
-
<div class="field"><label class="label" for="${id('agent')}"
|
|
50
|
-
<select id="${id('agent')}" name="agent">${opt('', '
|
|
51
|
-
<small class="help"
|
|
52
|
-
<div class="field"><label class="label" for="${id('service')}"
|
|
53
|
-
<select id="${id('service')}" name="service">${opt('', '
|
|
54
|
-
<div class="field"><label class="label" for="${id('max_rounds')}"
|
|
49
|
+
<fieldset class="prop-group"><legend>${tx('asst.assistant')}</legend><div class="form-grid">
|
|
50
|
+
<div class="field"><label class="label" for="${id('agent')}">${tx('asst.ai_agent')}</label>
|
|
51
|
+
<select id="${id('agent')}" name="agent">${opt('', tx('asst.none_the_service_prompt_and'), agent)}${agents.map((n) => opt(n, n, agent))}${agent && !agents.includes(agent) ? opt(agent, `${agent} (does not exist)`, agent) : ''}</select>
|
|
52
|
+
<small class="help">${tx.rich('asst.agent_help', { link: html`<a href="${BASE}/apps/${appId}/ai-agents">${tx('asst.shared_components_ai_agents')}</a>` })}</small></div>
|
|
53
|
+
<div class="field"><label class="label" for="${id('service')}">${tx('asst.ai_service')}</label>
|
|
54
|
+
<select id="${id('service')}" name="service">${opt('', tx('asst.choose'), cfg.service ?? '')}${options}</select></div>
|
|
55
|
+
<div class="field"><label class="label" for="${id('max_rounds')}">${tx('asst.tool_rounds_per_question')}</label>
|
|
55
56
|
<input id="${id('max_rounds')}" name="max_rounds" type="number" min="1" max="10" value="${cfg.max_rounds ?? ''}" placeholder="5"></div>
|
|
56
|
-
<div class="field"><label class="label" for="${id('max_turns')}"
|
|
57
|
+
<div class="field"><label class="label" for="${id('max_turns')}">${tx('asst.questions_per_conversation')}</label>
|
|
57
58
|
<input id="${id('max_turns')}" name="max_turns" type="number" min="1" max="100" value="${cfg.max_turns ?? ''}" placeholder="20"></div>
|
|
58
|
-
<div class="field" data-wide><label class="label" for="${id('system')}"
|
|
59
|
+
<div class="field" data-wide><label class="label" for="${id('system')}">${tx('asst.system_prompt')}</label>
|
|
59
60
|
<textarea id="${id('system')}" name="system" rows="4">${cfg.system ?? ''}</textarea>
|
|
60
|
-
<small class="help"
|
|
61
|
-
<div class="field" data-wide><label class="label" for="${id('welcome')}"
|
|
61
|
+
<small class="help">${tx.code('asst.who_the_assistant_is_and')}</small></div>
|
|
62
|
+
<div class="field" data-wide><label class="label" for="${id('welcome')}">${tx('asst.welcome_text')}</label>
|
|
62
63
|
<textarea id="${id('welcome')}" name="welcome" rows="2">${cfg.welcome ?? ''}</textarea></div>
|
|
63
|
-
<div class="field"><label class="label" for="${id('placeholder')}"
|
|
64
|
+
<div class="field"><label class="label" for="${id('placeholder')}">${tx('asst.placeholder')}</label>
|
|
64
65
|
<input id="${id('placeholder')}" name="placeholder" maxlength="200" value="${cfg.placeholder ?? ''}"></div>
|
|
65
|
-
<div class="field"><label class="label" for="${id('error_message')}"
|
|
66
|
-
<input id="${id('error_message')}" name="error_message" maxlength="300" value="${cfg.error_message ?? ''}" placeholder="
|
|
66
|
+
<div class="field"><label class="label" for="${id('error_message')}">${tx('asst.error_message')}</label>
|
|
67
|
+
<input id="${id('error_message')}" name="error_message" maxlength="300" value="${cfg.error_message ?? ''}" placeholder="${tx('asst.the_service_s_message')}"></div>
|
|
67
68
|
</div>
|
|
68
|
-
<div class="field"><label class="check"><input type="checkbox" name="public" value="true"${cfg.public ? raw(' checked') : ''}>
|
|
69
|
-
<fieldset class="prop-group"><legend
|
|
70
|
-
<div class="field"><label class="label" for="${id('context')}"
|
|
69
|
+
<div class="field"><label class="check"><input type="checkbox" name="public" value="true"${cfg.public ? raw(' checked') : ''}> ${tx('asst.also_for_users_who_are')}</label></div></fieldset>
|
|
70
|
+
<fieldset class="prop-group"><legend>${tx('asst.context_and_tools')}</legend>
|
|
71
|
+
<div class="field"><label class="label" for="${id('context')}">${tx('asst.context_queries_json')}</label>
|
|
71
72
|
<textarea id="${id('context')}" name="context" rows="5" class="code" spellcheck="false">${json(cfg.context)}</textarea>
|
|
72
|
-
<small class="help"
|
|
73
|
-
<div class="field"><label class="label" for="${id('tools')}"
|
|
73
|
+
<small class="help">${tx.code('asst.run_before_each_question_their')}</small></div>
|
|
74
|
+
<div class="field"><label class="label" for="${id('tools')}">${tx('asst.tools_json')}</label>
|
|
74
75
|
<textarea id="${id('tools')}" name="tools" rows="10" class="code" spellcheck="false">${json(cfg.tools)}</textarea>
|
|
75
|
-
<small class="help"
|
|
76
|
+
<small class="help">${tx.code('asst.the_model_calls_them_when')}</small></div>
|
|
76
77
|
</fieldset>
|
|
77
|
-
<div class="buttons"><button class="btn btn-hot"
|
|
78
|
+
<div class="buttons"><button class="btn btn-hot">${tx('asst.save_ai_assistant_settings')}</button></div>
|
|
78
79
|
</form>`;
|
|
79
80
|
}
|
|
80
81
|
|
|
@@ -84,9 +85,9 @@ async function appProblems(appId: number, cfg: AssistantConfig) {
|
|
|
84
85
|
const sources = new Set((await owner.query<{ name: string }>('select name from meta.rest_source where app_id = $1', [appId])).rows.map((x) => x.name));
|
|
85
86
|
const schemes = new Set((await owner.query<{ name: string }>('select name from meta.authz_scheme where app_id = $1', [appId])).rows.map((x) => x.name.toUpperCase()));
|
|
86
87
|
for (const t of Array.isArray(cfg.tools) ? cfg.tools : []) {
|
|
87
|
-
if (t?.type === 'rest' && typeof t.source === 'string' && !sources.has(t.source.toUpperCase())) out.push(
|
|
88
|
+
if (t?.type === 'rest' && typeof t.source === 'string' && !sources.has(t.source.toUpperCase())) out.push(tx('asst.no_rest_source', { tool: t.name, source: t.source.toUpperCase() }));
|
|
88
89
|
if (typeof t?.authz === 'string' && t.authz.trim() && !['MUST_NOT_BE_PUBLIC_USER'].includes(t.authz.replace(/^!/, '').trim().toUpperCase()) && !schemes.has(t.authz.replace(/^!/, '').trim().toUpperCase()))
|
|
89
|
-
out.push(
|
|
90
|
+
out.push(tx('asst.no_authz', { tool: t.name, scheme: t.authz }));
|
|
90
91
|
}
|
|
91
92
|
return out;
|
|
92
93
|
}
|
|
@@ -124,12 +125,12 @@ export async function mergeAssistantSettings(appId: number, config: Config, b: B
|
|
|
124
125
|
try {
|
|
125
126
|
out[k] = JSON.parse(v);
|
|
126
127
|
} catch {
|
|
127
|
-
errors.push(`${k === 'tools' ? '
|
|
128
|
+
errors.push(`${k === 'tools' ? tx('asst.tools') : tx('asst.context_queries')}: not valid JSON (kept as it was).`);
|
|
128
129
|
}
|
|
129
130
|
}
|
|
130
131
|
if (out.agent) {
|
|
131
132
|
for (const k of ['service', 'system', 'tools', 'max_rounds']) delete out[k];
|
|
132
|
-
if (!(await agentOptions(appId)).includes(out.agent)) errors.push(
|
|
133
|
+
if (!(await agentOptions(appId)).includes(out.agent)) errors.push(tx('asst.no_agent', { agent: out.agent }));
|
|
133
134
|
}
|
|
134
135
|
errors.push(...assistantProblems(out), ...(await appProblems(appId, out as AssistantConfig)));
|
|
135
136
|
return { config: out, errors };
|
|
@@ -140,17 +141,17 @@ export async function aiFilterSettingsForm(pageId: number, appId: number, r: { i
|
|
|
140
141
|
const cfg = (r.config?.ai_filter ?? {}) as Config;
|
|
141
142
|
const id = (n: string) => `raf_${r.id}_${n}`;
|
|
142
143
|
const { options } = await serviceOptions(appId, cfg.service);
|
|
143
|
-
return html`<h3 class="u-mt15"
|
|
144
|
-
<p class="muted u-mt0"
|
|
144
|
+
return html`<h3 class="u-mt15">${tx('asst.ask_in_your_own_words')}</h3>
|
|
145
|
+
<p class="muted u-mt0">${tx.rich('asst.filter_intro', { builtin: html`<i>${tx('asst.built_in_phrases')}</i>` })}</p>
|
|
145
146
|
<form method="post" action="${BASE}/pages/${pageId}/region/${r.id}/ai-filter" class="component-form">${csrf(s)}
|
|
146
147
|
<div class="form-grid">
|
|
147
|
-
<div class="field"><label class="label" for="${id('service')}"
|
|
148
|
-
<select id="${id('service')}" name="service">${opt('', '
|
|
149
|
-
<div class="field" data-wide><label class="label" for="${id('placeholder')}"
|
|
148
|
+
<div class="field"><label class="label" for="${id('service')}">${tx('asst.ai_service')}</label>
|
|
149
|
+
<select id="${id('service')}" name="service">${opt('', tx('asst.off'), r.config?.ai_filter ? '-' : '')}${opt('-', tx('asst.built_in_phrases_no_ai'), r.config?.ai_filter && !cfg.service ? '-' : '')}${options}</select></div>
|
|
150
|
+
<div class="field" data-wide><label class="label" for="${id('placeholder')}">${tx('asst.placeholder')}</label>
|
|
150
151
|
<input id="${id('placeholder')}" name="placeholder" maxlength="200" value="${cfg.placeholder ?? ''}"></div>
|
|
151
152
|
</div>
|
|
152
|
-
<div class="field"><label class="check"><input type="checkbox" name="public" value="true"${cfg.public ? raw(' checked') : ''}>
|
|
153
|
-
<div class="buttons"><button class="btn"
|
|
153
|
+
<div class="field"><label class="check"><input type="checkbox" name="public" value="true"${cfg.public ? raw(' checked') : ''}> ${tx('asst.also_for_users_who_are_2')}</label></div>
|
|
154
|
+
<div class="buttons"><button class="btn">${tx('asst.save')}</button></div>
|
|
154
155
|
</form>`;
|
|
155
156
|
}
|
|
156
157
|
|
|
@@ -170,7 +171,7 @@ export async function assistantBuilderRoutes(app: FastifyInstance) {
|
|
|
170
171
|
const { config, errors } = await mergeAssistantSettings(r.app_id, r.config ?? {}, (req.body ?? {}) as Body);
|
|
171
172
|
await owner.query('update meta.region set config = $2 where id = $1', [r.id, JSON.stringify(config)]);
|
|
172
173
|
if (errors.length) flash(s, `Saved, but: ${errors.join(' ')}`, 'error');
|
|
173
|
-
else flash(s, '
|
|
174
|
+
else flash(s, tx('asst.settings_saved'));
|
|
174
175
|
return back(reply, s, `${BASE}/pages/${pid}?c=region-${rid}`);
|
|
175
176
|
});
|
|
176
177
|
|
|
@@ -193,7 +194,7 @@ export async function assistantBuilderRoutes(app: FastifyInstance) {
|
|
|
193
194
|
if (errors.length) flash(s, errors.join(' '), 'error');
|
|
194
195
|
else {
|
|
195
196
|
await owner.query('update meta.region set config = $2 where id = $1', [r.id, JSON.stringify(config)]);
|
|
196
|
-
flash(s, '
|
|
197
|
+
flash(s, tx('asst.settings_saved'));
|
|
197
198
|
}
|
|
198
199
|
return back(reply, s, `${BASE}/pages/${pid}?c=region-${rid}`);
|
|
199
200
|
});
|
|
@@ -5,6 +5,7 @@ import { html, type Raw } from '../html.ts';
|
|
|
5
5
|
import { icon } from '../icons.ts';
|
|
6
6
|
import type { Session } from '../session.ts';
|
|
7
7
|
import { back, BASE, csrf, developer, flash, type Req } from './ui.ts';
|
|
8
|
+
import { scopedTx as tx } from './i18n.ts';
|
|
8
9
|
|
|
9
10
|
// Shared Components → Automations: the actions (add, edit, reorder),
|
|
10
11
|
// next run, Run now and the run history with the errors per row, under the
|
|
@@ -23,13 +24,13 @@ export function actionPrefill(automation: string, actions: any[]) {
|
|
|
23
24
|
/** Under an action's form: back to its automation. */
|
|
24
25
|
export function actionExtras(row: any, automations: any[]): Raw {
|
|
25
26
|
const a = automations.find((x) => x.name === row.automation_name);
|
|
26
|
-
return a ? html`<p class="u-mt1"><a class="btn" href="?c=automation-${a.id}">${icon('clock')}
|
|
27
|
+
return a ? html`<p class="u-mt1"><a class="btn" href="?c=automation-${a.id}">${icon('clock')} ${tx('au.back_to', { name: a.name })}</a></p>` : html``;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
function rowErrors(errors: RowError[] | null, failed: number | null): Raw {
|
|
30
31
|
if (!errors?.length) return html``;
|
|
31
|
-
return html`<details><summary>${failed ?? errors.length}
|
|
32
|
-
<ul class="small">${errors.map((e) => html`<li
|
|
32
|
+
return html`<details><summary>${tx('au.failed_rows', { n: failed ?? errors.length })}${failed && failed > errors.length ? ` ${tx('au.first', { n: errors.length })}` : ''}</summary>
|
|
33
|
+
<ul class="small">${errors.map((e) => html`<li>${tx.rich('au.row_action', { row: e.row, action: html`<b>${e.action}</b>` })}: ${e.message}${e.values ? html`<br><code>${e.values}</code>` : ''}</li>`)}</ul></details>`;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
/** Actions, next run, Run now and the last runs of an automation. */
|
|
@@ -43,44 +44,44 @@ export async function automationExtras(appId: number, row: { id: number; name: s
|
|
|
43
44
|
).rows;
|
|
44
45
|
let next: string;
|
|
45
46
|
try {
|
|
46
|
-
next = row.enabled ? when(nextRun(parseCron(row.schedule), row.time_zone, new Date())) : '
|
|
47
|
+
next = row.enabled ? when(nextRun(parseCron(row.schedule), row.time_zone, new Date())) : tx('au.not_scheduled');
|
|
47
48
|
} catch (e) {
|
|
48
|
-
next =
|
|
49
|
+
next = tx('au.never', { message: (e as Error).message });
|
|
49
50
|
}
|
|
50
|
-
const scheduler = process.env.AUTOMATIONS === 'off' ? html` <b
|
|
51
|
+
const scheduler = process.env.AUTOMATIONS === 'off' ? html` <b>${tx('au.scheduler_off')}</b>` : '';
|
|
51
52
|
const move = (id: number, dir: 'up' | 'down', label: string, disabled: boolean) =>
|
|
52
53
|
html`<form method="post" action="${BASE}/apps/${appId}/shared/automation_action/${id}/move" class="u-inline">${csrf(s)}<input type="hidden" name="dir" value="${dir}"><button class="link-button" aria-label="${label}"${disabled ? html` disabled` : ''}>${dir === 'up' ? '↑' : '↓'}</button></form>`;
|
|
53
|
-
const actionsHtml = html`<fieldset class="prop-group u-mt125"><legend
|
|
54
|
-
<p class="muted u-mt0"
|
|
54
|
+
const actionsHtml = html`<fieldset class="prop-group u-mt125"><legend>${tx('au.actions')}</legend>
|
|
55
|
+
<p class="muted u-mt0">${tx(!row.query?.trim() ? 'au.order_once' : row.error_handling === 'skip' ? 'au.order_rows_skip' : 'au.order_rows')}</p>
|
|
55
56
|
${actions.length
|
|
56
|
-
? html`<div class="table-wrap"><table class="report report-reflow"><thead><tr><th class="num"
|
|
57
|
+
? html`<div class="table-wrap"><table class="report report-reflow"><thead><tr><th class="num">${tx('au.seq')}</th><th>${tx('au.action')}</th><th>${tx('au.condition')}</th><th>${tx('au.order')}</th></tr></thead><tbody>
|
|
57
58
|
${actions.map((x, i) => html`<tr>
|
|
58
|
-
<td class="num" data-label="
|
|
59
|
-
<td data-label="
|
|
60
|
-
<td data-label="
|
|
61
|
-
<td data-label="
|
|
59
|
+
<td class="num" data-label="${tx('au.seq')}">${x.seq}</td>
|
|
60
|
+
<td data-label="${tx('au.action')}"><a href="?c=automation_action-${x.id}">${x.name}</a></td>
|
|
61
|
+
<td data-label="${tx('au.condition')}">${x.condition ? html`<code>${x.condition.length > 80 ? x.condition.slice(0, 80) + '…' : x.condition}</code>` : html`<span class="muted">${tx('au.always')}</span>`}</td>
|
|
62
|
+
<td data-label="${tx('au.order')}">${move(x.id, 'up', tx('au.move_up', { name: x.name }), i === 0)} ${move(x.id, 'down', tx('au.move_down', { name: x.name }), i === actions.length - 1)}</td></tr>`)}
|
|
62
63
|
</tbody></table></div>`
|
|
63
|
-
: html`<p class="muted"
|
|
64
|
-
<p><a class="btn" href="?new=automation_action&automation=${encodeURIComponent(row.name)}">+
|
|
64
|
+
: html`<p class="muted">${tx('au.no_actions')}</p>`}
|
|
65
|
+
<p><a class="btn" href="?new=automation_action&automation=${encodeURIComponent(row.name)}">+ ${tx('au.add_action')}</a></p>
|
|
65
66
|
</fieldset>`;
|
|
66
|
-
return html`${actionsHtml}<fieldset class="prop-group u-mt125"><legend
|
|
67
|
-
<p
|
|
68
|
-
<p class="muted"
|
|
67
|
+
return html`${actionsHtml}<fieldset class="prop-group u-mt125"><legend>${tx('au.runs')}</legend>
|
|
68
|
+
<p>${tx.rich('au.next_run', { when: html`<b>${next}</b>` })}${scheduler}</p>
|
|
69
|
+
<p class="muted">${tx.rich('au.from_code', { call: html`<code>select meta.run_automation('${row.name}')</code>` })}</p>
|
|
69
70
|
<form method="post" action="${BASE}/apps/${appId}/shared/automation/${row.id}/run">${csrf(s)}
|
|
70
|
-
<button class="btn">${icon('play')}
|
|
71
|
+
<button class="btn">${icon('play')} ${tx('au.run_now')}</button>
|
|
71
72
|
</form>
|
|
72
73
|
${log.length
|
|
73
|
-
? html`<div class="table-wrap u-mt075"><table class="report report-reflow"><thead><tr><th
|
|
74
|
+
? html`<div class="table-wrap u-mt075"><table class="report report-reflow"><thead><tr><th>${tx('au.started')}</th><th>${tx('au.took')}</th><th>${tx('au.by')}</th><th>${tx('au.status')}</th><th class="num">${tx('au.rows')}</th><th class="num">${tx('au.failed')}</th><th>${tx('au.message')}</th></tr></thead><tbody>
|
|
74
75
|
${log.map((l) => html`<tr>
|
|
75
|
-
<td data-label="
|
|
76
|
-
<td data-label="
|
|
77
|
-
<td data-label="
|
|
78
|
-
<td data-label="
|
|
79
|
-
<td class="num" data-label="
|
|
80
|
-
<td class="num" data-label="
|
|
81
|
-
<td data-label="
|
|
76
|
+
<td data-label="${tx('au.started')}">${when(l.started_at)}</td>
|
|
77
|
+
<td data-label="${tx('au.took')}">${l.finished_at ? `${((new Date(l.finished_at).getTime() - new Date(l.started_at).getTime()) / 1000).toFixed(1)} s` : '…'}</td>
|
|
78
|
+
<td data-label="${tx('au.by')}">${l.trigger}${l.run_by ? html` <span class="muted">(${l.run_by})</span>` : ''}</td>
|
|
79
|
+
<td data-label="${tx('au.status')}"><span class="tag${STATUS_TAG[l.status] ?? ''}">${l.status}</span></td>
|
|
80
|
+
<td class="num" data-label="${tx('au.rows')}">${l.rows ?? ''}</td>
|
|
81
|
+
<td class="num" data-label="${tx('au.failed')}">${l.rows_failed || ''}</td>
|
|
82
|
+
<td data-label="${tx('au.message')}">${l.message ?? ''}${rowErrors(l.errors, l.rows_failed)}</td></tr>`)}
|
|
82
83
|
</tbody></table></div>`
|
|
83
|
-
: html`<p class="muted"
|
|
84
|
+
: html`<p class="muted">${tx('au.no_runs')}</p>`}
|
|
84
85
|
</fieldset>`;
|
|
85
86
|
}
|
|
86
87
|
|
|
@@ -93,9 +94,9 @@ export async function automationRoutes(app: FastifyInstance) {
|
|
|
93
94
|
const a = /^\d+$/.test(id) && /^\d+$/.test(cid) ? await owner.one('select id from meta.automation where id = $1 and app_id = $2', [cid, id]) : undefined;
|
|
94
95
|
if (!a) return reply.code(404).send('Not found');
|
|
95
96
|
const r = await runAutomation(a.id, 'manual');
|
|
96
|
-
if (r.status === 'ok') flash(s,
|
|
97
|
-
else if (r.status === 'warning') flash(s,
|
|
98
|
-
else flash(s, r.message ?? '
|
|
97
|
+
if (r.status === 'ok') flash(s, r.rows ? tx('au.ran_rows', { n: r.rows }) : tx('au.ran'));
|
|
98
|
+
else if (r.status === 'warning') flash(s, tx('au.ran_errors', { message: r.message ?? '' }), 'error');
|
|
99
|
+
else flash(s, r.message ?? tx('au.failed_run'), 'error');
|
|
99
100
|
return back(reply, s, target);
|
|
100
101
|
});
|
|
101
102
|
|
|
@@ -4,18 +4,21 @@ import { html, raw } from '../html.ts';
|
|
|
4
4
|
import { clientIp, logActivity } from '../session.ts';
|
|
5
5
|
import { appOr404 } from './forms.ts';
|
|
6
6
|
import { appHeader, back, BASE, csrf, developer, flash, region, select, send, shell, type Req } from './ui.ts';
|
|
7
|
+
import { scopedTx as tx, type BuilderKey } from './i18n.ts';
|
|
7
8
|
|
|
8
9
|
// Application → Availability (APEX: Application Availability and Build Status,
|
|
9
10
|
// migration 083): who may use the running application, the message they see
|
|
10
11
|
// otherwise, and whether the builder may change it (Run only). Allowed on a
|
|
11
12
|
// Run only application (src/builder/locks.ts), logged as "availability".
|
|
12
13
|
|
|
13
|
-
const
|
|
14
|
-
['available', '
|
|
15
|
-
['developers', '
|
|
16
|
-
['restricted', '
|
|
17
|
-
['unavailable', '
|
|
14
|
+
const MODE_KEYS: [string, BuilderKey][] = [
|
|
15
|
+
['available', 'avl.mode.available'],
|
|
16
|
+
['developers', 'avl.mode.developers'],
|
|
17
|
+
['restricted', 'avl.mode.restricted'],
|
|
18
|
+
['unavailable', 'avl.mode.unavailable'],
|
|
18
19
|
];
|
|
20
|
+
/** The statuses with their labels in the developer's language. */
|
|
21
|
+
const modes = (): [string, string][] => MODE_KEYS.map(([v, k]) => [v, tx(k)]);
|
|
19
22
|
|
|
20
23
|
export async function availabilityRoutes(app: FastifyInstance) {
|
|
21
24
|
app.get(`${BASE}/apps/:id(^\\d+$)/availability`, async (req: Req, reply) => {
|
|
@@ -23,35 +26,32 @@ export async function availabilityRoutes(app: FastifyInstance) {
|
|
|
23
26
|
if (!s) return;
|
|
24
27
|
const a = await appOr404(req.params.id);
|
|
25
28
|
if (!a) return reply.code(404).send('Not found');
|
|
26
|
-
const body = region('
|
|
27
|
-
<p class="muted u-mt0">
|
|
28
|
-
(they see the developer toolbar). Pages, file downloads, REST modules and the REST API follow this setting; automations,
|
|
29
|
-
workflows, background jobs and queued mail keep running. It belongs to this installation: exports leave it out and an
|
|
30
|
-
import over the application keeps it.</p>
|
|
29
|
+
const body = region(tx('app.head.availability'), html`<form method="post" action="${BASE}/apps/${a.id}/availability">${csrf(s)}
|
|
30
|
+
<p class="muted u-mt0">${tx('avl.intro')}</p>
|
|
31
31
|
<div class="form-grid">
|
|
32
|
-
${select('availability', '
|
|
33
|
-
<div class="field" data-wide><label class="label" for="f_message"
|
|
32
|
+
${select('availability', tx('avl.status'), a.availability, modes())}
|
|
33
|
+
<div class="field" data-wide><label class="label" for="f_message">${tx('avl.message')}</label>
|
|
34
34
|
<textarea id="f_message" name="message" rows="3" maxlength="2000">${a.availability_message ?? ''}</textarea>
|
|
35
|
-
<small class="help"
|
|
36
|
-
<div class="field" data-wide><label class="label" for="f_users"
|
|
35
|
+
<small class="help">${tx('avl.message_help')}</small></div>
|
|
36
|
+
<div class="field" data-wide><label class="label" for="f_users">${tx('avl.users')}</label>
|
|
37
37
|
<textarea id="f_users" name="users" rows="2">${(a.availability_users ?? []).join(', ')}</textarea>
|
|
38
|
-
<small class="help"
|
|
39
|
-
${select('build_status', '
|
|
40
|
-
'
|
|
38
|
+
<small class="help">${tx('avl.users_help')}</small></div>
|
|
39
|
+
${select('build_status', tx('avl.build_status'), a.build_status, [['run_and_build', tx('avl.run_and_build')], ['run_only', tx('avl.run_only')]],
|
|
40
|
+
tx('avl.build_status_help'))}
|
|
41
41
|
</div>
|
|
42
|
-
<div class="buttons"><button class="btn btn-hot"
|
|
42
|
+
<div class="buttons"><button class="btn btn-hot">${tx('common.save')}</button></div></form>`);
|
|
43
43
|
const main = html`${appHeader(a, 'settings', s)}<div class="ide-body">${body}</div>`;
|
|
44
|
-
return send(reply, s, shell(s,
|
|
44
|
+
return send(reply, s, shell(s, tx('avl.page_title', { name: a.name }), [[tx('rail.app_builder'), BASE], [a.name, `${BASE}/apps/${a.id}`], [tx('app.head.availability')]], main));
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
app.post(`${BASE}/apps/:id(^\\d+$)/availability`, async (req: Req, reply) => {
|
|
48
48
|
const s = await developer(req, reply);
|
|
49
49
|
if (!s) return;
|
|
50
50
|
const b = req.body ?? {};
|
|
51
|
-
const mode =
|
|
51
|
+
const mode = MODE_KEYS.some(([v]) => v === b.availability) ? b.availability : 'available';
|
|
52
52
|
const users = [...new Set(String(b.users ?? '').split(/[\s,;]+/).map((u) => u.trim()).filter(Boolean))];
|
|
53
53
|
if (users.some((u) => u.length > 100 || /[:\s]/.test(u))) {
|
|
54
|
-
flash(s, '
|
|
54
|
+
flash(s, tx('avl.users_invalid'), 'error');
|
|
55
55
|
return back(reply, s, `${BASE}/apps/${req.params.id}/availability`);
|
|
56
56
|
}
|
|
57
57
|
const build = b.build_status === 'run_only' ? 'run_only' : 'run_and_build';
|
|
@@ -59,7 +59,7 @@ export async function availabilityRoutes(app: FastifyInstance) {
|
|
|
59
59
|
`update meta.app set availability = $2, availability_message = $3, availability_users = $4, build_status = $5, updated_at = now() where id = $1`,
|
|
60
60
|
[req.params.id, mode, String(b.message ?? '').trim().slice(0, 2000) || null, users, build]);
|
|
61
61
|
if (r.rowCount) await logActivity({ appId: Number(req.params.id), username: s.username, event: 'availability', ip: clientIp(req), detail: `${mode}, ${build}` });
|
|
62
|
-
flash(s, r.rowCount ? '
|
|
62
|
+
flash(s, r.rowCount ? tx('avl.saved') : tx('avl.not_found'), r.rowCount ? 'ok' : 'error');
|
|
63
63
|
return back(reply, s, `${BASE}/apps/${req.params.id}/availability`);
|
|
64
64
|
});
|
|
65
65
|
}
|
|
@@ -68,7 +68,7 @@ export async function availabilityRoutes(app: FastifyInstance) {
|
|
|
68
68
|
export const availabilityBadge = (a: { id: number; availability?: string; build_status?: string }) =>
|
|
69
69
|
a.availability && a.availability !== 'available' || a.build_status === 'run_only'
|
|
70
70
|
? html`<a class="tag tag-warning availability-badge" href="${BASE}/apps/${a.id}/availability">${[
|
|
71
|
-
a.availability && a.availability !== 'available' ?
|
|
72
|
-
a.build_status === 'run_only' ? '
|
|
71
|
+
a.availability && a.availability !== 'available' ? modes().find(([v]) => v === a.availability)?.[1] : null,
|
|
72
|
+
a.build_status === 'run_only' ? tx('avl.run_only') : null,
|
|
73
73
|
].filter(Boolean).join(' · ')}</a>`
|
|
74
74
|
: raw('');
|