@saltcorn/server 0.8.6-beta.1 → 0.8.6-beta.10

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/routes/fields.js CHANGED
@@ -83,6 +83,13 @@ const fieldForm = async (req, fkey_opts, existing_names, id, hasData) => {
83
83
  if (!s || s === "") return req.__("Missing label");
84
84
  if (!id && existing_names.includes(Field.labelToName(s)))
85
85
  return req.__("Column %s already exists", s);
86
+ if (Field.labelToName(s) === "row")
87
+ return req.__("Not a valid field name");
88
+ try {
89
+ new Function(Field.labelToName(s), "return;");
90
+ } catch {
91
+ return req.__("Not a valid field name");
92
+ }
86
93
  },
87
94
  }),
88
95
  // description
@@ -464,14 +471,11 @@ const fieldFlow = (req) =>
464
471
  },
465
472
  {
466
473
  name: req.__("Default"),
467
- onlyWhen: async (context) => {
468
- if (!context.required || context.id || context.calculated)
469
- return false;
474
+ onlyWhen: async (context) => context.required && !context.calculated,
475
+
476
+ form: async (context) => {
470
477
  const table = await Table.findOne({ id: context.table_id });
471
478
  const nrows = await table.countRows();
472
- return nrows > 0;
473
- },
474
- form: async (context) => {
475
479
  const formfield = new Field({
476
480
  name: "default",
477
481
  label: req.__("Default"),
@@ -484,12 +488,28 @@ const fieldFlow = (req) =>
484
488
  },
485
489
  });
486
490
  await formfield.fill_fkey_options();
487
- return new Form({
488
- blurb: req.__(
489
- "A default value is required when adding required fields to nonempty tables"
490
- ),
491
- fields: [formfield],
491
+ const defaultOptional = nrows === 0 || context.id;
492
+ if (defaultOptional) formfield.showIf = { set_default: true };
493
+
494
+ const form = new Form({
495
+ blurb: defaultOptional
496
+ ? req.__("Set a default value for missing data")
497
+ : req.__(
498
+ "A default value is required when adding required fields to nonempty tables"
499
+ ),
500
+ fields: [
501
+ ...(defaultOptional
502
+ ? [{ name: "set_default", label: "Set Default", type: "Bool" }]
503
+ : []),
504
+ formfield,
505
+ ],
492
506
  });
507
+ if (
508
+ typeof context.default !== "undefined" &&
509
+ context.default !== null
510
+ )
511
+ form.values.set_default = true;
512
+ return form;
493
513
  },
494
514
  },
495
515
  ],
@@ -717,7 +737,7 @@ router.post(
717
737
  error_catcher(async (req, res) => {
718
738
  const { tableName, fieldName, fieldview } = req.params;
719
739
  const table = await Table.findOne({ name: tableName });
720
- const role = req.user && req.user.id ? req.user.role_id : 10;
740
+ const role = req.user && req.user.id ? req.user.role_id : 100;
721
741
 
722
742
  const fields = await table.getFields();
723
743
  let row = { ...req.body };
package/routes/files.js CHANGED
@@ -130,7 +130,7 @@ router.get(
130
130
  router.get(
131
131
  "/download/*",
132
132
  error_catcher(async (req, res) => {
133
- const role = req.user && req.user.id ? req.user.role_id : 10;
133
+ const role = req.user && req.user.id ? req.user.role_id : 100;
134
134
  const user_id = req.user && req.user.id;
135
135
  const serve_path = req.params[0];
136
136
  const file = await File.findOne(serve_path);
@@ -155,7 +155,7 @@ router.post(
155
155
  isAdmin,
156
156
 
157
157
  error_catcher(async (req, res) => {
158
- const role = req.user && req.user.id ? req.user.role_id : 10;
158
+ const role = req.user && req.user.id ? req.user.role_id : 100;
159
159
  const user_id = req.user && req.user.id;
160
160
  const files = req.body.files;
161
161
  const location = req.body.location;
@@ -189,7 +189,7 @@ router.post(
189
189
  router.get(
190
190
  "/serve/*",
191
191
  error_catcher(async (req, res) => {
192
- const role = req.user && req.user.id ? req.user.role_id : 10;
192
+ const role = req.user && req.user.id ? req.user.role_id : 100;
193
193
  const user_id = req.user && req.user.id;
194
194
  const serve_path = req.params[0];
195
195
  //let file;
@@ -201,7 +201,7 @@ router.get(
201
201
  (role <= file.min_role_read || (user_id && user_id === file.user_id))
202
202
  ) {
203
203
  res.type(file.mimetype);
204
- const cacheability = file.min_role_read === 10 ? "public" : "private";
204
+ const cacheability = file.min_role_read === 100 ? "public" : "private";
205
205
  res.set("Cache-Control", `${cacheability}, max-age=86400`);
206
206
  if (file.s3_store) s3storage.serveObject(file, res, false);
207
207
  else res.sendFile(file.location);
@@ -228,7 +228,7 @@ router.get(
228
228
  router.get(
229
229
  "/resize/:width_str/:height_str/*",
230
230
  error_catcher(async (req, res) => {
231
- const role = req.user && req.user.id ? req.user.role_id : 10;
231
+ const role = req.user && req.user.id ? req.user.role_id : 100;
232
232
  const user_id = req.user && req.user.id;
233
233
  const { width_str, height_str } = req.params;
234
234
  const serve_path = req.params[0];
@@ -240,7 +240,7 @@ router.get(
240
240
  (role <= file.min_role_read || (user_id && user_id === file.user_id))
241
241
  ) {
242
242
  res.type(file.mimetype);
243
- const cacheability = file.min_role_read === 10 ? "public" : "private";
243
+ const cacheability = file.min_role_read === 100 ? "public" : "private";
244
244
  res.set("Cache-Control", `${cacheability}, max-age=86400`);
245
245
  //TODO s3
246
246
  if (file.s3_store) s3storage.serveObject(file, res, false);
@@ -386,7 +386,7 @@ router.post(
386
386
  let { folder } = req.body;
387
387
  let jsonResp = {};
388
388
  const min_role_upload = getState().getConfig("min_role_upload", 1);
389
- const role = req.user && req.user.id ? req.user.role_id : 10;
389
+ const role = req.user && req.user.id ? req.user.role_id : 100;
390
390
  let file_for_redirect;
391
391
  if (role > +min_role_upload) {
392
392
  if (!req.xhr) req.flash("warning", req.__("Not authorized"));
@@ -426,7 +426,7 @@ const welcome_page = async (req) => {
426
426
  * @returns {Promise<void>}
427
427
  */
428
428
  const no_views_logged_in = async (req, res) => {
429
- const role = req.user && req.user.id ? req.user.role_id : 10;
429
+ const role = req.user && req.user.id ? req.user.role_id : 100;
430
430
  if (role > 1 || req.user.tenant !== db.getTenantSchema())
431
431
  res.sendWrap(req.__("Hello"), req.__("Welcome to Saltcorn!"));
432
432
  else {
@@ -463,7 +463,7 @@ const no_views_logged_in = async (req, res) => {
463
463
  const get_config_response = async (role_id, res, req) => {
464
464
  const modernCfg = getState().getConfig("home_page_by_role", false);
465
465
  // predefined roles
466
- const legacy_role = { 10: "public", 8: "user", 4: "staff", 1: "admin" }[
466
+ const legacy_role = { 100: "public", 80: "user", 40: "staff", 1: "admin" }[
467
467
  role_id
468
468
  ];
469
469
  let homeCfg = modernCfg && modernCfg[role_id];
@@ -497,7 +497,7 @@ module.exports =
497
497
  */
498
498
  async (req, res) => {
499
499
  const isAuth = req.user && req.user.id;
500
- const role_id = req.user ? req.user.role_id : 10;
500
+ const role_id = req.user ? req.user.role_id : 100;
501
501
  const cfgResp = await get_config_response(role_id, res, req);
502
502
  if (cfgResp) return;
503
503
 
package/routes/index.js CHANGED
@@ -1,44 +1,5 @@
1
1
  /**
2
2
  * Index is Main Router of App
3
- * @category server
4
- * @module routes/index
5
- * @subcategory routes
6
- */
7
-
8
- /**
9
- * All files in the routes module.
10
- * @namespace routes_overview
11
- * @property {module:routes/actions} actions
12
- * @property {module:routes/admin} admin
13
- * @property {module:routes/api} api
14
- * @property {module:routes/config} config
15
- * @property {module:routes/crashlog} crashlog
16
- * @property {module:routes/delete} delete
17
- * @property {module:routes/edit} edit
18
- * @property {module:routes/eventlog} eventlog
19
- * @property {module:routes/events} events
20
- * @property {module:routes/fields} fields
21
- * @property {module:routes/files} files
22
- * @property {module:routes/homepage} homepage
23
- * @property {module:routes/infoarch} infoarch
24
- * @property {module:routes/library} library
25
- * @property {module:routes/list} list
26
- * @property {module:routes/menu} menu
27
- * @property {module:routes/packs} packs
28
- * @property {module:routes/page} page
29
- * @property {module:routes/pageedit} pageedit
30
- * @property {module:routes/plugins} plugins
31
- * @property {module:routes/scapi} scapi
32
- * @property {module:routes/search} search
33
- * @property {module:routes/settings} settings
34
- * @property {module:routes/tables} tables
35
- * @property {module:routes/tenant} tenant
36
- * @property {module:routes/utils} utils
37
- * @property {module:routes/view} view
38
- * @property {module:routes/viewedit} viewedit
39
- *
40
- * @category server
41
- * @subcategory routes
42
3
  */
43
4
 
44
5
  const table = require("./tables");
@@ -71,7 +32,8 @@ const useradmin = require("../auth/admin");
71
32
  const roleadmin = require("../auth/roleadmin");
72
33
  const tags = require("./tags");
73
34
  const tagentries = require("./tag_entries");
74
- const dataDiagram = require("./diagram");
35
+ const diagram = require("./diagram");
36
+ const sync = require("./sync");
75
37
 
76
38
  module.exports =
77
39
  /**
@@ -109,5 +71,6 @@ module.exports =
109
71
  app.use("/roleadmin", roleadmin);
110
72
  app.use("/tag", tags);
111
73
  app.use("/tag-entries", tagentries);
112
- app.use("/diagram", dataDiagram);
74
+ app.use("/diagram", diagram);
75
+ app.use("/sync", sync);
113
76
  };
package/routes/menu.js CHANGED
@@ -454,7 +454,7 @@ router.post(
454
454
  "/runaction/:name",
455
455
  error_catcher(async (req, res) => {
456
456
  const { name } = req.params;
457
- const role = (req.user || {}).role_id || 10;
457
+ const role = (req.user || {}).role_id || 100;
458
458
  const state = getState();
459
459
  const menu_items = state.getConfig("menu_items");
460
460
  let menu_item;
package/routes/page.js CHANGED
@@ -42,7 +42,7 @@ router.get(
42
42
  state.log(3, `Route /page/${pagename} user=${req.user?.id}`);
43
43
  const tic = state.logLevel >= 5 ? new Date() : null;
44
44
 
45
- const role = req.user && req.user.id ? req.user.role_id : 10;
45
+ const role = req.user && req.user.id ? req.user.role_id : 100;
46
46
  const db_page = await Page.findOne({ name: pagename });
47
47
  if (db_page && role <= db_page.min_role) {
48
48
  const contents = await db_page.run(req.query, { res, req });
@@ -104,7 +104,7 @@ router.post(
104
104
  "/:pagename/action/:rndid",
105
105
  error_catcher(async (req, res) => {
106
106
  const { pagename, rndid } = req.params;
107
- const role = req.user && req.user.id ? req.user.role_id : 10;
107
+ const role = req.user && req.user.id ? req.user.role_id : 100;
108
108
  const db_page = await Page.findOne({ name: pagename });
109
109
  if (db_page && role <= db_page.min_role) {
110
110
  let col;
@@ -387,7 +387,7 @@ router.get(
387
387
  isAdmin,
388
388
  error_catcher(async (req, res) => {
389
389
  const { pagename } = req.params;
390
- const page = await Page.findOne({ name: pagename });
390
+ const [page] = await Page.find({ name: pagename });
391
391
  if (!page) {
392
392
  req.flash("error", req.__(`Page %s not found`, pagename));
393
393
  res.redirect(`/pageedit`);
@@ -506,7 +506,7 @@ router.post(
506
506
  const valres = form.validate(req.body);
507
507
  if (valres.success) {
508
508
  const home_page_by_role =
509
- getState().getConfigCopy("home_page_by_role", []) || [];
509
+ getState().getConfigCopy("home_page_by_role", {}) || {};
510
510
  for (const role of roles) {
511
511
  home_page_by_role[role.id] = valres.success[role.role];
512
512
  }
package/routes/scapi.js CHANGED
@@ -49,7 +49,7 @@ function accessAllowedRead(req, user) {
49
49
  ? req.user.role_id
50
50
  : user && user.role_id
51
51
  ? user.role_id
52
- : 10;
52
+ : 100;
53
53
 
54
54
  if (role === 1) return true;
55
55
  return false;
package/routes/search.js CHANGED
@@ -165,7 +165,7 @@ const searchForm = () =>
165
165
  * @returns {Promise<void>}
166
166
  */
167
167
  const runSearch = async ({ q, _page, table }, req, res) => {
168
- const role = (req.user || {}).role_id || 10;
168
+ const role = (req.user || {}).role_id || 100;
169
169
  // globalSearch contains list of pairs: table, view
170
170
  const cfg = getState().getConfig("globalSearch");
171
171
  const page_size = getState().getConfig("search_page_size");
@@ -269,10 +269,9 @@ const runSearch = async ({ q, _page, table }, req, res) => {
269
269
  router.get(
270
270
  "/",
271
271
  error_catcher(async (req, res) => {
272
-
273
272
  const min_role = getState().getConfig("min_role_search");
274
- const role = (req.user || {}).role_id || 10;
275
- if(role>min_role){
273
+ const role = (req.user || {}).role_id || 100;
274
+ if (role > min_role) {
276
275
  res.redirect("/"); // silent redirect to home page
277
276
  return;
278
277
  }
@@ -283,7 +282,7 @@ router.get(
283
282
  const cfg = getState().getConfig("globalSearch");
284
283
 
285
284
  if (!cfg) {
286
- const role = (req.user || {}).role_id || 10;
285
+ const role = (req.user || {}).role_id || 100;
287
286
 
288
287
  req.flash("warning", req.__("Search not configured"));
289
288
  res.redirect(role === 1 ? "/search/config" : "/");
package/routes/sync.js ADDED
@@ -0,0 +1,84 @@
1
+ const { error_catcher } = require("./utils.js");
2
+ const Router = require("express-promise-router");
3
+ const db = require("@saltcorn/data/db");
4
+ const { getState } = require("@saltcorn/data/db/state");
5
+ const Table = require("@saltcorn/data/models/table");
6
+
7
+ const router = new Router();
8
+ module.exports = router;
9
+
10
+ const pickFields = (table, row) => {
11
+ const result = {};
12
+ for (const { name, type } of table.getFields()) {
13
+ if (name === "id") continue;
14
+ if (type?.name === "Date") {
15
+ result[name] = row[name] ? new Date(row[name]) : undefined;
16
+ } else {
17
+ result[name] = row[name];
18
+ }
19
+ }
20
+ return result;
21
+ };
22
+
23
+ const allowInsert = (table, user) => {
24
+ const role = user?.role_id || 100;
25
+ return table.min_role_write >= role;
26
+ };
27
+
28
+ const throwWithCode = (message, code) => {
29
+ const err = new Error(message);
30
+ err.statusCode = code;
31
+ throw err;
32
+ };
33
+
34
+ /**
35
+ * insert the offline data uploaded by the mobile-app
36
+ */
37
+ router.post(
38
+ "/table_data",
39
+ error_catcher(async (req, res) => {
40
+ // TODO sqlite
41
+ getState().log(
42
+ 4,
43
+ `POST /sync/table_data user: '${req.user ? req.user.id : "public"}'`
44
+ );
45
+ let aborted = false;
46
+ req.socket.on("close", () => {
47
+ aborted = true;
48
+ });
49
+ req.socket.on("timeout", () => {
50
+ aborted = true;
51
+ });
52
+ const client = db.isSQLite ? db : await db.getClient();
53
+ try {
54
+ await client.query("BEGIN");
55
+ await client.query("SET CONSTRAINTS ALL DEFERRED");
56
+ for (const [tblName, offlineRows] of Object.entries(req.body.data) ||
57
+ []) {
58
+ const table = Table.findOne({ name: tblName });
59
+ if (!table) throw new Error(`The table '${tblName}' does not exist.`);
60
+ if (!allowInsert(table, req.user))
61
+ throwWithCode(req.__("Not authorized"), 401);
62
+ if (tblName !== "users") {
63
+ for (const newRow of offlineRows.map((row) =>
64
+ pickFields(table, row)
65
+ )) {
66
+ if (aborted) throw new Error("connection closed by client");
67
+ await db.insert(table.name, newRow, { client: client });
68
+ }
69
+ }
70
+ }
71
+ if (aborted) throw new Error("connection closed by client");
72
+ await client.query("COMMIT");
73
+ res.json({ success: true });
74
+ } catch (error) {
75
+ await client.query("ROLLBACK");
76
+ getState().log(2, `POST /sync/table_data error: '${error.message}'`);
77
+ res
78
+ .status(error.statusCode || 400)
79
+ .json({ error: error.message || error });
80
+ } finally {
81
+ if (!db.isSQLite) await client.release(true);
82
+ }
83
+ })
84
+ );
package/routes/tables.js CHANGED
@@ -53,6 +53,8 @@ const { getState } = require("@saltcorn/data/db/state");
53
53
  const { cardHeaderTabs } = require("@saltcorn/markup/layout_utils");
54
54
  const { tablesList } = require("./common_lists");
55
55
  const { InvalidConfiguration } = require("@saltcorn/data/utils");
56
+ const { sleep } = require("@saltcorn/data/utils");
57
+
56
58
  const path = require("path");
57
59
  /**
58
60
  * @type {object}
@@ -539,11 +541,7 @@ const attribBadges = (f) => {
539
541
  let s = "";
540
542
  if (f.attributes) {
541
543
  Object.entries(f.attributes).forEach(([k, v]) => {
542
- if (
543
- ["summary_field", "default", "on_delete_cascade", "on_delete"].includes(
544
- k
545
- )
546
- )
544
+ if (["summary_field", "on_delete_cascade", "on_delete"].includes(k))
547
545
  return;
548
546
  if (v || v === 0) s += badge("secondary", k);
549
547
  });
@@ -913,10 +911,12 @@ router.post(
913
911
  rest.provider_name !== "Database table"
914
912
  ) {
915
913
  const table = await Table.create(name, rest);
914
+ await sleep(500); // Allow other workers to load this view
916
915
  res.redirect(`/table/provider-cfg/${table.id}`);
917
916
  } else {
918
917
  delete rest.provider_name;
919
918
  const table = await Table.create(name, rest);
919
+ await sleep(500); // Allow other workers to load this view
920
920
  req.flash("success", req.__(`Table %s created`, name));
921
921
  res.redirect(`/table/${table.id}`);
922
922
  }
package/routes/tenant.js CHANGED
@@ -42,7 +42,7 @@ const {
42
42
  code,
43
43
  } = require("@saltcorn/markup/tags");
44
44
  const db = require("@saltcorn/data/db");
45
- //const url = require("url");
45
+
46
46
  const { loadAllPlugins, loadAndSaveNewPlugin } = require("../load_plugins");
47
47
  const { isAdmin, error_catcher } = require("./utils.js");
48
48
  const User = require("@saltcorn/data/models/user");
@@ -53,6 +53,7 @@ const {
53
53
  save_config_from_form,
54
54
  } = require("../markup/admin.js");
55
55
  const { getConfig } = require("@saltcorn/data/models/config");
56
+ //const {quote} = require("@saltcorn/db-common");
56
57
  // todo add button backup / restore for particular tenant (available in admin tenants screens)
57
58
  //const {
58
59
  // create_backup,
@@ -75,6 +76,7 @@ const remove_leading_chars = (cs, s) =>
75
76
  /**
76
77
  * Declare Form to create Tenant
77
78
  * @param {object} req - Request
79
+ * @param base_url - Base URL
78
80
  * @returns {Form} - Saltcorn Form Declaration
79
81
  * @category server
80
82
  */
@@ -109,8 +111,8 @@ const tenant_form = (req, base_url) =>
109
111
  // TBD To allow few roles to create tenants - currently only one role has such rights simultaneously
110
112
  const create_tenant_allowed = (req) => {
111
113
  const required_role =
112
- +getRootState().getConfig("role_to_create_tenant") || 10;
113
- const user_role = req.user ? req.user.role_id : 10;
114
+ +getRootState().getConfig("role_to_create_tenant") || 100;
115
+ const user_role = req.user ? req.user.role_id : 100;
114
116
  return user_role <= required_role;
115
117
  };
116
118
 
@@ -227,6 +229,7 @@ router.get(
227
229
  * Return URL of new Tenant
228
230
  * @param {object} req - Request
229
231
  * @param {string} subdomain - Tenant Subdomain name string
232
+ * @param base_url - Base URL
230
233
  * @returns {string}
231
234
  */
232
235
  const getNewURL = (req, subdomain, base_url) => {
@@ -280,7 +283,7 @@ router.post(
280
283
  const description = valres.success.description;
281
284
  // get list of tenants
282
285
  const allTens = await getAllTenants();
283
- if (allTens.includes(subdomain) || !subdomain) {
286
+ if (allTens.includes(subdomain) || !subdomain || subdomain === "public") {
284
287
  form.errors.subdomain = req.__(
285
288
  "A site with this subdomain already exists"
286
289
  );
@@ -446,7 +449,7 @@ const tenant_settings_form = (req) =>
446
449
  "tenant_template",
447
450
  "tenant_baseurl",
448
451
  "tenant_create_unauth_redirect",
449
- { section_header: "Tenant application capabilities" },
452
+ { section_header: req.__("Tenant application capabilities") },
450
453
  "tenants_install_git",
451
454
  "tenants_set_npm_modules",
452
455
  "tenants_unsafe_plugins",
@@ -536,8 +539,11 @@ const get_tenant_info = async (subdomain) => {
536
539
  // get tenant row
537
540
  const ten = await Tenant.findOne({ subdomain: saneDomain });
538
541
  if (ten) {
542
+ //info.ten = ten;
539
543
  info.description = ten.description;
540
544
  info.created = ten.created;
545
+ info.template = ten.template;
546
+ info.email = ten.email;
541
547
  }
542
548
 
543
549
  // get data from tenant schema
@@ -547,10 +553,19 @@ const get_tenant_info = async (subdomain) => {
547
553
  if (firstUser && firstUser.length > 0) {
548
554
  info.first_user_email = firstUser[0].email;
549
555
  }
556
+ // todo sort in alphabet order
557
+ // config items count
558
+ info.nconfigs = await db.count("_sc_config");
559
+ // error messages count
560
+ info.nerrors = await db.count("_sc_errors");
561
+ // event log
562
+ info.nevent_log = await db.count("_sc_event_log");
550
563
  // users count
551
564
  info.nusers = await db.count("users");
552
565
  // roles count
553
566
  info.nroles = await db.count("_sc_roles");
567
+ // table_constraints count
568
+ info.ntable_constraints = await db.count("_sc_table_constraints");
554
569
  // tables count
555
570
  info.ntables = await db.count("_sc_tables");
556
571
  // table fields count
@@ -561,19 +576,25 @@ const get_tenant_info = async (subdomain) => {
561
576
  info.nfiles = await db.count("_sc_files");
562
577
  // pages count
563
578
  info.npages = await db.count("_sc_pages");
564
- // triggers (actions) ccount
579
+ // triggers (actions) count
565
580
  info.nactions = await db.count("_sc_triggers");
566
- // error messages count
567
- info.nerrors = await db.count("_sc_errors");
568
- // config items count
569
- info.nconfigs = await db.count("_sc_config");
570
581
  // plugins count
571
582
  info.nplugins = await db.count("_sc_plugins");
572
583
  // migration count
573
584
  info.nmigrations = await db.count("_sc_migrations");
574
585
  // library count
575
586
  info.nlibrary = await db.count("_sc_library");
576
- // TBD decide Do we need count tenants, table constraints
587
+ // notifications
588
+ info.nnotifications = await db.count("_sc_notifications");
589
+ // tags
590
+ info.ntags = await db.count("_sc_tags");
591
+ // tag_entries
592
+ info.ntag_entries = await db.count("_sc_tag_entries");
593
+ // snapshots
594
+ info.nsnapshots = await db.count("_sc_snapshots");
595
+ // session - Only for main app?
596
+ //info.nsession = await db.count("_sc_session");
597
+
577
598
  // base url
578
599
  info.base_url = await getConfig("base_url");
579
600
  return info;
@@ -628,51 +649,76 @@ router.get(
628
649
  { href: "mailto:" + info.first_user_email },
629
650
  info.first_user_email
630
651
  )
631
- )
652
+ ),
653
+ th(req.__("Template")),
654
+ td(a({ href: info.base_url }, info.template))
632
655
  ),
633
656
  tr(
634
657
  th(req.__("Users")),
635
- td(a({ href: info.base_url + "useradmin" }, info.nusers))
636
- ),
637
- tr(
658
+ td(a({ href: info.base_url + "useradmin" }, info.nusers)),
638
659
  th(req.__("Roles")),
639
660
  td(a({ href: info.base_url + "roleadmin" }, info.nroles))
640
661
  ),
641
662
  tr(
642
663
  th(req.__("Tables")),
643
- td(a({ href: info.base_url + "table" }, info.ntables))
644
- ),
645
- tr(
664
+ td(a({ href: info.base_url + "table" }, info.ntables)),
646
665
  th(req.__("Table columns")),
647
666
  td(a({ href: info.base_url + "table" }, info.nfields))
648
667
  ),
649
668
  tr(
650
- th(req.__("Views")),
651
- td(a({ href: info.base_url + "viewedit" }, info.nviews))
669
+ th(req.__("Table constraints")),
670
+ td(
671
+ a(
672
+ { href: info.base_url + "table" },
673
+ info.ntable_constraints
674
+ )
675
+ ),
676
+ th(req.__("Library")),
677
+ td(a({ href: info.base_url + "library/list" }, info.nlibrary))
652
678
  ),
653
679
  tr(
680
+ th(req.__("Views")),
681
+ td(a({ href: info.base_url + "viewedit" }, info.nviews)),
654
682
  th(req.__("Pages")),
655
683
  td(a({ href: info.base_url + "pageedit" }, info.npages))
656
684
  ),
657
685
  tr(
658
686
  th(req.__("Files")),
659
- td(a({ href: info.base_url + "files" }, info.nfiles))
660
- ),
661
- tr(
687
+ td(a({ href: info.base_url + "files" }, info.nfiles)),
662
688
  th(req.__("Actions")),
663
689
  td(a({ href: info.base_url + "actions" }, info.nactions))
664
690
  ),
665
691
  tr(
666
692
  th(req.__("Modules")),
667
- td(a({ href: info.base_url + "plugins" }, info.nplugins))
668
- ),
669
- tr(
693
+ td(a({ href: info.base_url + "plugins" }, info.nplugins)),
670
694
  th(req.__("Configuration items")),
671
695
  td(a({ href: info.base_url + "admin" }, info.nconfigs))
672
696
  ),
673
697
  tr(
698
+ // Crashlogs only for main site?
674
699
  th(req.__("Crashlogs")),
675
- td(a({ href: info.base_url + "crashlog" }, info.nerrors))
700
+ td(a({ href: info.base_url + "crashlog" }, info.nerrors)),
701
+ //th(req.__("Sessions")),
702
+ //td(a({ href: info.base_url + "crashlog" }, info.nsessions)),
703
+ th(req.__("Event logs")),
704
+ td(a({ href: info.base_url + "eventlog" }, info.nevent_log))
705
+ // Notifications only for main site?
706
+ //th(req.__("Notifications")),
707
+ //td(a({ href: info.base_url + "???" }, info.nnotifications)),
708
+ ),
709
+ tr(
710
+ th(req.__("Snapshots")),
711
+ td(
712
+ a({ href: info.base_url + "admin/backup" }, info.nsnapshots)
713
+ ),
714
+ th(req.__("Migrations")),
715
+ td(a({ href: info.base_url + "admin" }, info.nmigrations))
716
+ ),
717
+ tr(
718
+ th(req.__("Tags")),
719
+ td(a({ href: info.base_url + "tag" }, info.ntags)),
720
+ th(req.__("Tag Entries")),
721
+ td(a({ href: info.base_url + "tag" }, info.ntag_entries))
676
722
  )
677
723
  ),
678
724
  ],
@@ -697,6 +743,7 @@ router.get(
697
743
  name: "description",
698
744
  label: req.__("Description"),
699
745
  type: "String",
746
+ fieldview: "textarea",
700
747
  },
701
748
  ],
702
749
  values: {