pgkiln 0.83.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.
Files changed (126) hide show
  1. package/db/migrations/154_notifications.sql +186 -0
  2. package/docs/apex-feature-parity.md +3 -2
  3. package/docs/guide/03-builder.md +16 -5
  4. package/docs/guide/04-pages-and-regions.md +42 -0
  5. package/docs/guide/06-processing.md +22 -1
  6. package/docs/guide/07-dynamic-actions.md +1 -0
  7. package/docs/guide/09-reference.md +15 -0
  8. package/docs/guide/11-from-apex.md +1 -0
  9. package/docs/guide/12-development.md +13 -1
  10. package/package.json +1 -1
  11. package/public/app.css +29 -0
  12. package/public/app.js +78 -0
  13. package/public/sw.js +5 -1
  14. package/src/app.ts +11 -0
  15. package/src/builder/ai-agents.ts +86 -85
  16. package/src/builder/ai-builder.ts +30 -30
  17. package/src/builder/ai.ts +93 -92
  18. package/src/builder/api.ts +47 -47
  19. package/src/builder/app-groups.ts +33 -32
  20. package/src/builder/appfromfile.ts +57 -58
  21. package/src/builder/appsheets.ts +33 -32
  22. package/src/builder/appwizard.ts +65 -65
  23. package/src/builder/arrange.ts +23 -22
  24. package/src/builder/assistant.ts +39 -38
  25. package/src/builder/automations.ts +32 -31
  26. package/src/builder/availability.ts +24 -24
  27. package/src/builder/backups.ts +30 -31
  28. package/src/builder/blueprints.ts +62 -61
  29. package/src/builder/code-editor.ts +1 -1
  30. package/src/builder/components.ts +10 -6
  31. package/src/builder/create-page.ts +52 -50
  32. package/src/builder/cross-app.ts +37 -35
  33. package/src/builder/dataload.ts +2 -2
  34. package/src/builder/datasets.ts +107 -106
  35. package/src/builder/dependencies.ts +20 -19
  36. package/src/builder/designer.ts +36 -36
  37. package/src/builder/diagnostics.ts +58 -57
  38. package/src/builder/features.ts +39 -35
  39. package/src/builder/feedback.ts +23 -20
  40. package/src/builder/find-workspace.ts +28 -24
  41. package/src/builder/form-designer.ts +36 -35
  42. package/src/builder/gallery.ts +25 -29
  43. package/src/builder/globalization.ts +50 -49
  44. package/src/builder/i18n/de.ts +3461 -0
  45. package/src/builder/i18n/en.ts +3461 -0
  46. package/src/builder/i18n/es.ts +3461 -0
  47. package/src/builder/i18n/fr.ts +3461 -0
  48. package/src/builder/i18n/it.ts +3461 -0
  49. package/src/builder/i18n/ja.ts +3461 -0
  50. package/src/builder/i18n/ko.ts +3461 -0
  51. package/src/builder/i18n/nl.ts +3461 -0
  52. package/src/builder/i18n/pt.ts +3461 -0
  53. package/src/builder/i18n/zh-cn.ts +3461 -0
  54. package/src/builder/i18n/zh-tw.ts +3461 -0
  55. package/src/builder/i18n.ts +17 -3
  56. package/src/builder/json-sources.ts +63 -62
  57. package/src/builder/layouts.ts +12 -11
  58. package/src/builder/locks.ts +29 -28
  59. package/src/builder/login-page.ts +26 -25
  60. package/src/builder/mail.ts +148 -147
  61. package/src/builder/newapp.ts +15 -12
  62. package/src/builder/notification-settings.ts +27 -0
  63. package/src/builder/plugins.ts +47 -44
  64. package/src/builder/process-jobs.ts +11 -10
  65. package/src/builder/pwa.ts +22 -21
  66. package/src/builder/remote-databases.ts +55 -56
  67. package/src/builder/report-settings.ts +1 -1
  68. package/src/builder/reporter.ts +43 -42
  69. package/src/builder/rest-catalogs.ts +87 -86
  70. package/src/builder/rest.ts +9 -8
  71. package/src/builder/routes.ts +30 -27
  72. package/src/builder/search.ts +14 -13
  73. package/src/builder/static-files.ts +31 -32
  74. package/src/builder/subscriptions.ts +47 -45
  75. package/src/builder/supporting.ts +23 -18
  76. package/src/builder/team.ts +179 -179
  77. package/src/builder/telemetry.ts +59 -62
  78. package/src/builder/templates.ts +66 -65
  79. package/src/builder/themeroller.ts +60 -63
  80. package/src/builder/ui-defaults.ts +21 -20
  81. package/src/builder/ui.ts +2 -1
  82. package/src/builder/upgrade.ts +25 -24
  83. package/src/builder/utilities.ts +48 -47
  84. package/src/builder/vector-providers.ts +67 -66
  85. package/src/builder/websources.ts +43 -42
  86. package/src/builder/wizards.ts +95 -89
  87. package/src/builder/workflow-migrate.ts +30 -31
  88. package/src/builder/workflows.ts +40 -39
  89. package/src/builder/workingcopies.ts +41 -49
  90. package/src/builder/workspace-backups.ts +21 -20
  91. package/src/builder/workspace-mail.ts +17 -16
  92. package/src/builder/workspace-resources.ts +104 -112
  93. package/src/builder/workspace-transfer.ts +34 -26
  94. package/src/cli/replace.ts +2 -0
  95. package/src/i18n/ar.ts +9 -0
  96. package/src/i18n/cs.ts +9 -0
  97. package/src/i18n/da.ts +9 -0
  98. package/src/i18n/de.ts +9 -0
  99. package/src/i18n/el.ts +9 -0
  100. package/src/i18n/es.ts +9 -0
  101. package/src/i18n/fi.ts +9 -0
  102. package/src/i18n/fr.ts +9 -0
  103. package/src/i18n/he.ts +9 -0
  104. package/src/i18n/it.ts +9 -0
  105. package/src/i18n/ja.ts +9 -0
  106. package/src/i18n/ko.ts +9 -0
  107. package/src/i18n/nb.ts +9 -0
  108. package/src/i18n/pl.ts +9 -0
  109. package/src/i18n/pt.ts +9 -0
  110. package/src/i18n/ru.ts +9 -0
  111. package/src/i18n/sv.ts +9 -0
  112. package/src/i18n/tr.ts +9 -0
  113. package/src/i18n/uk.ts +9 -0
  114. package/src/i18n/zh.ts +9 -0
  115. package/src/i18n.ts +18 -0
  116. package/src/metadata.ts +9 -3
  117. package/src/runtime/engine.ts +2 -1
  118. package/src/runtime/feedback.ts +1 -1
  119. package/src/runtime/lists.ts +43 -11
  120. package/src/runtime/notifications.ts +237 -0
  121. package/src/runtime/processes.ts +45 -1
  122. package/src/runtime/regions.ts +11 -15
  123. package/src/runtime/render.ts +12 -3
  124. package/src/runtime/routes.ts +12 -4
  125. package/src/runtime/signin.ts +2 -1
  126. package/src/session.ts +13 -4
@@ -4,6 +4,7 @@ import { APP_MFA, EMAIL_CODE_MINUTES, EMAIL_CODES_PER_HOUR, type AppMfa } from '
4
4
  import { html, raw } from '../html.ts';
5
5
  import { secretKeyConfigured } from '../secrets.ts';
6
6
  import { back, BASE, developer, flash, type Req } from './ui.ts';
7
+ import { scopedTx as tx } from './i18n.ts';
7
8
 
8
9
  // Application settings → Sign-in methods (migration 087): the application's
9
10
  // own login page (APEX: the login page, usually 9999) and "Forgot password?".
@@ -33,47 +34,47 @@ export async function loginSettingsFields(a: AppRow) {
33
34
  from meta.app_mail_service ams join meta.mail_service s on s.id = ams.service_id where ams.app_id = $1`, [a.id],
34
35
  );
35
36
  const mailOk = !!(mail?.enabled && mail.from_address?.trim());
36
- const noMail = html`<strong>Not offered yet:</strong> the application needs an enabled mail service with a From address (Workspace utilities → Mail services).`;
37
+ const noMail = html`<strong>${tx('lgp.not_offered_yet')}</strong> the application needs an enabled mail service with a From address (Workspace utilities → Mail services).`;
37
38
  const opt = (v: string, label: string, cur: string) => html`<option value="${v}"${v === cur ? raw(' selected') : ''}>${label}</option>`;
38
39
  const current = a.login_page ? String(a.login_page) : '';
39
40
  const page = html`<div class="form-grid">
40
- <div class="field"><label class="label" for="f_login_page">Login page</label>
41
- <select id="f_login_page" name="login_page">${opt('', '- the built-in sign-in page -', current)}${pages.map((p) =>
42
- opt(String(p.page_no), `${p.page_no} ${p.name}${p.requires_auth ? ' (requires sign-in: not usable)' : ''}`, current))}</select>
43
- <small class="help">A public page with a <em>Login</em> region (APEX: the login page); page template <em>Minimal</em> hides the header and navigation.
44
- <button class="link-button" formaction="${BASE}/apps/${a.id}/login-page">Create a login page</button></small></div>
41
+ <div class="field"><label class="label" for="f_login_page">${tx('lgp.login_page')}</label>
42
+ <select id="f_login_page" name="login_page">${opt('', tx('lgp.the_built_in_sign_in'), current)}${pages.map((p) =>
43
+ opt(String(p.page_no), p.requires_auth ? tx('lgp.requires_signin', { page: `${p.page_no} ${p.name}` }) : `${p.page_no} ${p.name}`, current))}</select>
44
+ <small class="help">${tx.rich('lgp.page_help', { login: html`<em>${tx('lgp.login')}</em>`, minimal: html`<em>${tx('lgp.minimal')}</em>` })}
45
+ <button class="link-button" formaction="${BASE}/apps/${a.id}/login-page">${tx('lgp.create_a_login_page')}</button></small></div>
45
46
  </div>
46
- <div class="field"><label class="check"><input type="checkbox" name="password_reset" value="true"${a.password_reset ? raw(' checked') : ''}> Offer "Forgot password?"</label>
47
- <small class="help">Users with a password and an e-mail address in the user directory get a single-use link, valid for 30 minutes, to choose a new password. Only for the user directory's own passwords.
48
- ${mailOk ? html`Sent through mail service ${mail!.service} as ${mail!.from_address}.` : noMail}</small></div>`;
47
+ <div class="field"><label class="check"><input type="checkbox" name="password_reset" value="true"${a.password_reset ? raw(' checked') : ''}> ${tx('lgp.offer_forgot_password')}</label>
48
+ <small class="help">${tx('lgp.reset_help')}
49
+ ${mailOk ? tx('lgp.sent_through', { service: mail!.service, from: mail!.from_address ?? '' }) : noMail}</small></div>`;
49
50
  const twoFactor = html`<div class="form-grid">
50
- <div class="field"><label class="label" for="f_mfa">Second step</label>
51
- <select id="f_mfa" name="mfa" aria-describedby="f_mfa_help">${(APP_MFA as string[]).map((m) => opt(m, MFA_LABELS[m as AppMfa], a.mfa))}</select>
52
- <small class="help" id="f_mfa_help">After the password or an LDAP directory: a code from an authenticator app, a passkey or a recovery code, added on <em>My account</em> or while signing in.
53
- ${secretKeyConfigured() ? '' : html`<strong>Not available yet:</strong> the server needs <code>PGKILN_SECRET_KEY</code> to keep the apps' secrets.`}</small></div>
54
- <div class="field"><label class="label" for="f_mfa_roles">Required for roles</label>
55
- <input id="f_mfa_roles" name="mfa_roles" value="${(a.mfa_roles ?? []).join(', ')}" placeholder="admin, manager" aria-describedby="f_mfa_roles_help">
56
- <small class="help" id="f_mfa_roles_help">With <em>Required for some roles</em>: users with one of these roles must use it, others may.</small></div>
51
+ <div class="field"><label class="label" for="f_mfa">${tx('lgp.second_step')}</label>
52
+ <select id="f_mfa" name="mfa" aria-describedby="f_mfa_help">${(APP_MFA as string[]).map((m) => opt(m, MFA_LABELS()[m as AppMfa], a.mfa))}</select>
53
+ <small class="help" id="f_mfa_help">${tx.rich('lgp.mfa_help', { account: html`<em>${tx('lgp.my_account')}</em>` })}
54
+ ${secretKeyConfigured() ? '' : tx.rich('lgp.needs_key', { na: html`<strong>${tx('lgp.not_available_yet')}</strong>`, key: html`<code>PGKILN_SECRET_KEY</code>` })}</small></div>
55
+ <div class="field"><label class="label" for="f_mfa_roles">${tx('lgp.required_for_roles')}</label>
56
+ <input id="f_mfa_roles" name="mfa_roles" value="${(a.mfa_roles ?? []).join(', ')}" placeholder="${tx('lgp.admin_manager')}" aria-describedby="f_mfa_roles_help">
57
+ <small class="help" id="f_mfa_roles_help">${tx.rich('lgp.roles_help', { some: html`<em>${tx('lgp.required_for_some_roles')}</em>` })}</small></div>
57
58
  </div>
58
- <div class="field"><label class="check"><input type="checkbox" name="mfa_email" value="true"${a.mfa_email ? raw(' checked') : ''}> Offer a code by e-mail</label>
59
- <small class="help">Users who already have an app or a passkey may get a one-time code by e-mail instead (valid ${EMAIL_CODE_MINUTES} minutes, at most ${EMAIL_CODES_PER_HOUR} an hour); never as their only factor.
59
+ <div class="field"><label class="check"><input type="checkbox" name="mfa_email" value="true"${a.mfa_email ? raw(' checked') : ''}> ${tx('lgp.offer_a_code_by_e')}</label>
60
+ <small class="help">${tx('lgp.email_help', { minutes: EMAIL_CODE_MINUTES, per_hour: EMAIL_CODES_PER_HOUR })}
60
61
  ${mailOk ? '' : noMail}</small></div>
61
- <p class="muted small">Passkeys need the application at a host name, not an IP address. Identity providers ask for their own second factor (see the provider's <em>Multi-factor sign-in</em>). To ask again before a page opens (step-up), give its authorization scheme <em>Second step within (minutes)</em> in Shared Components.</p>`;
62
+ <p class="muted small">${tx.rich('lgp.passkeys_note', { mfa: html`<em>${tx('lgp.multi_factor_sign_in')}</em>`, within: html`<em>${tx('lgp.second_step_within_minutes')}</em>` })}</p>`;
62
63
  return { page, twoFactor };
63
64
  }
64
65
 
65
- const MFA_LABELS: Record<AppMfa, string> = { off: 'Off', optional: 'Optional (users may add an app)', required: 'Required', roles: 'Required for some roles' };
66
+ const MFA_LABELS = (): Record<AppMfa, string> => ({ off: tx('lgp.off'), optional: tx('lgp.optional_users_may_add_an'), required: tx('lgp.required'), roles: tx('lgp.required_for_some_roles') });
66
67
 
67
68
  /** Save them (with the rest of the settings form). */
68
69
  export async function saveLoginSettings(appId: string, b: Record<string, string | undefined>) {
69
70
  const page = b.login_page?.trim() ? Number(b.login_page) : null;
70
- if (page !== null && !(await owner.one('select 1 from meta.page where app_id = $1 and page_no = $2', [appId, page]))) throw new Error(`Login page: there is no page ${b.login_page}.`);
71
+ if (page !== null && !(await owner.one('select 1 from meta.page where app_id = $1 and page_no = $2', [appId, page]))) throw new Error(tx('lgp.no_page', { n: String(b.login_page) }));
71
72
  const mfa = (APP_MFA as string[]).includes(b.mfa ?? '') ? (b.mfa as AppMfa) : 'off';
72
73
  const roles = [...new Set((b.mfa_roles ?? '').split(',').map((r) => r.trim().toLowerCase()).filter(Boolean))];
73
- if (roles.some((r) => r.length > 100)) throw new Error('Two-factor sign-in: a role name is too long.');
74
- if (mfa === 'roles' && !roles.length) throw new Error('Two-factor sign-in: name the roles that must use it.');
74
+ if (roles.some((r) => r.length > 100)) throw new Error(tx('lgp.two_factor_sign_in_a'));
75
+ if (mfa === 'roles' && !roles.length) throw new Error(tx('lgp.two_factor_sign_in_name'));
75
76
  // without the instance key, users could not add an app: requiring one would lock them out
76
- if ((mfa === 'required' || mfa === 'roles') && !secretKeyConfigured()) throw new Error('Two-factor sign-in: the server needs PGKILN_SECRET_KEY before it can be required.');
77
+ if ((mfa === 'required' || mfa === 'roles') && !secretKeyConfigured()) throw new Error(tx('lgp.two_factor_sign_in_the'));
77
78
  await owner.query('update meta.app set login_page = $2, password_reset = $3, mfa = $4, mfa_roles = $5, mfa_email = $6 where id = $1',
78
79
  [appId, page, b.password_reset === 'true', mfa, roles, b.mfa_email === 'true']);
79
80
  }
@@ -102,7 +103,7 @@ export async function loginPageRoutes(app: FastifyInstance) {
102
103
  if (!a) return reply.code(404).send('Not found');
103
104
  try {
104
105
  const { page, no } = await owner.tx((c) => createLoginPage(c, a));
105
- flash(s, `Page ${no} "Login" created and made the login page. Add regions around the sign-in form in the Page Designer.`);
106
+ flash(s, tx('lgp.created', { n: no }));
106
107
  return back(reply, s, `${BASE}/pages/${page}`);
107
108
  } catch (e) {
108
109
  flash(s, (e as Error).message, 'error');