@saltcorn/server 0.7.3 → 0.7.4-beta.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.
package/routes/admin.js CHANGED
@@ -18,7 +18,13 @@ const { spawn } = require("child_process");
18
18
  const User = require("@saltcorn/data/models/user");
19
19
  const path = require("path");
20
20
  const { getAllTenants } = require("@saltcorn/admin-models/models/tenant");
21
- const { post_btn, renderForm, mkTable, link } = require("@saltcorn/markup");
21
+ const {
22
+ post_btn,
23
+ renderForm,
24
+ mkTable,
25
+ link,
26
+ localeDateTime,
27
+ } = require("@saltcorn/markup");
22
28
  const {
23
29
  div,
24
30
  a,
@@ -42,7 +48,6 @@ const {
42
48
  select,
43
49
  option,
44
50
  fieldset,
45
- legend,
46
51
  ul,
47
52
  li,
48
53
  ol,
@@ -63,6 +68,7 @@ const {
63
68
  restore,
64
69
  auto_backup_now,
65
70
  } = require("@saltcorn/admin-models/models/backup");
71
+ const Snapshot = require("@saltcorn/admin-models/models/snapshot");
66
72
  const {
67
73
  runConfigurationCheck,
68
74
  } = require("@saltcorn/admin-models/models/config-check");
@@ -89,6 +95,7 @@ const moment = require("moment");
89
95
  const View = require("@saltcorn/data/models/view");
90
96
  const { getConfigFile } = require("@saltcorn/data/db/connect");
91
97
  const os = require("os");
98
+ const Page = require("@saltcorn/data/models/page");
92
99
 
93
100
  /**
94
101
  * @type {object}
@@ -118,6 +125,7 @@ const site_id_form = (req) =>
118
125
  "page_custom_html",
119
126
  "development_mode",
120
127
  "log_sql",
128
+ "log_level",
121
129
  "plugins_store_endpoint",
122
130
  "packs_store_endpoint",
123
131
  ...(getConfigFile() ? ["multitenancy_enabled"] : []),
@@ -146,7 +154,7 @@ const email_form = async (req) => {
146
154
  return form;
147
155
  };
148
156
 
149
- const app_files_table = (file, req) =>
157
+ const app_files_table = (files, req) =>
150
158
  mkTable(
151
159
  [
152
160
  {
@@ -160,7 +168,7 @@ const app_files_table = (file, req) =>
160
168
  key: (r) => link(`/files/download/${r.id}`, req.__("Download")),
161
169
  },
162
170
  ],
163
- [file]
171
+ files
164
172
  );
165
173
 
166
174
  /**
@@ -337,6 +345,9 @@ router.get(
337
345
  backupForm.values.auto_backup_expire_days = getState().getConfig(
338
346
  "auto_backup_expire_days"
339
347
  );
348
+ const aSnapshotForm = snapshotForm(req);
349
+ aSnapshotForm.values.snapshots_enabled =
350
+ getState().getConfig("snapshots_enabled");
340
351
  const isRoot = db.getTenantSchema() === db.connectObj.default_schema;
341
352
 
342
353
  send_admin_page({
@@ -354,7 +365,7 @@ router.get(
354
365
  post_btn(
355
366
  "/admin/backup",
356
367
  i({ class: "fas fa-download me-2" }) +
357
- req.__("Download a backup"),
368
+ req.__("Download a backup"),
358
369
  req.csrfToken(),
359
370
  {
360
371
  btnClass: "btn-outline-primary",
@@ -373,22 +384,38 @@ router.get(
373
384
  },
374
385
  isRoot
375
386
  ? {
376
- type: "card",
377
- title: req.__("Automated backup"),
378
- contents: div(
379
- renderForm(backupForm, req.csrfToken()),
380
- a(
381
- { href: "/admin/auto-backup-list" },
382
- "Restore/download automated backups »"
383
- ),
384
- script(
385
- domReady(
386
- `$('#btnBackupNow').prop('disabled', $('#inputauto_backup_frequency').val()==='Never');`
387
- )
388
- )
387
+ type: "card",
388
+ title: req.__("Automated backup"),
389
+ contents: div(
390
+ renderForm(backupForm, req.csrfToken()),
391
+ a(
392
+ { href: "/admin/auto-backup-list" },
393
+ "Restore/download automated backups »"
389
394
  ),
390
- }
395
+ script(
396
+ domReady(
397
+ `$('#btnBackupNow').prop('disabled', $('#inputauto_backup_frequency').val()==='Never');`
398
+ )
399
+ )
400
+ ),
401
+ }
391
402
  : { type: "blank", contents: "" },
403
+ {
404
+ type: "card",
405
+ title: req.__("Snapshots"),
406
+ contents: div(
407
+ p(
408
+ i(
409
+ "Snapshots store your application structure and definition, without the table data. Individual views and pages can be restored from snapshots from the <a href='/viewedit'>view</a> or <a href='/pageedit'>pages</a> overviews (\"Restore\" from individual page or view dropdowns)."
410
+ )
411
+ ),
412
+ renderForm(aSnapshotForm, req.csrfToken()),
413
+ a(
414
+ { href: "/admin/snapshot-list" },
415
+ "List/download snapshots &raquo;"
416
+ )
417
+ ),
418
+ },
392
419
  ],
393
420
  },
394
421
  });
@@ -464,6 +491,103 @@ router.get(
464
491
  })
465
492
  );
466
493
 
494
+ router.get(
495
+ "/snapshot-list",
496
+ isAdmin,
497
+ error_catcher(async (req, res) => {
498
+ const snaps = await Snapshot.find();
499
+ send_admin_page({
500
+ res,
501
+ req,
502
+ active_sub: "Backup",
503
+ contents: {
504
+ above: [
505
+ {
506
+ type: "card",
507
+ title: req.__("Download snapshots"),
508
+ contents: div(
509
+ ul(
510
+ snaps.map((snap) =>
511
+ li(
512
+ a(
513
+ {
514
+ href: `/admin/snapshot-download/${encodeURIComponent(
515
+ snap.id
516
+ )}`,
517
+ target: "_blank",
518
+ },
519
+ `${localeDateTime(snap.created)} (${moment(
520
+ snap.created
521
+ ).fromNow()})`
522
+ )
523
+ )
524
+ )
525
+ )
526
+ ),
527
+ },
528
+ ],
529
+ },
530
+ });
531
+ })
532
+ );
533
+
534
+ router.get(
535
+ "/snapshot-download/:id",
536
+ isAdmin,
537
+ error_catcher(async (req, res) => {
538
+ const { id } = req.params;
539
+ const snap = await Snapshot.findOne({ id });
540
+ res.send(snap.pack);
541
+ })
542
+ );
543
+
544
+ router.get(
545
+ "/snapshot-restore/:type/:name",
546
+ isAdmin,
547
+ error_catcher(async (req, res) => {
548
+ const { type, name } = req.params;
549
+ const snaps = await Snapshot.entity_history(type, name);
550
+ res.send(
551
+ mkTable(
552
+ [
553
+ {
554
+ label: "When",
555
+ key: (r) =>
556
+ `${localeDateTime(r.created)} (${moment(r.created).fromNow()})`,
557
+ },
558
+
559
+ {
560
+ label: req.__("Restore"),
561
+ key: (r) =>
562
+ post_btn(
563
+ `/admin/snapshot-restore/${type}/${name}/${r.id}`,
564
+ req.__("Restore"),
565
+ req.csrfToken()
566
+ ),
567
+ },
568
+ ],
569
+ snaps
570
+ )
571
+ );
572
+ })
573
+ );
574
+
575
+ router.post(
576
+ "/snapshot-restore/:type/:name/:id",
577
+ isAdmin,
578
+ error_catcher(async (req, res) => {
579
+ const { type, name, id } = req.params;
580
+ const snap = await Snapshot.findOne({ id });
581
+ await snap.restore_entity(type, name);
582
+ req.flash(
583
+ "success",
584
+ `${type} ${name} restored to snapshot saved ${moment(
585
+ snap.created
586
+ ).fromNow()}`
587
+ );
588
+ res.redirect(`/${type}edit`);
589
+ })
590
+ );
467
591
  router.get(
468
592
  "/auto-backup-download/:filename",
469
593
  isAdmin,
@@ -540,6 +664,43 @@ const autoBackupForm = (req) =>
540
664
  ],
541
665
  });
542
666
 
667
+ const snapshotForm = (req) =>
668
+ new Form({
669
+ action: "/admin/set-snapshot",
670
+ onChange: `saveAndContinue(this);`,
671
+ noSubmitButton: true,
672
+ additionalButtons: [
673
+ {
674
+ label: "Snapshot now",
675
+ id: "btnSnapNow",
676
+ class: "btn btn-outline-secondary",
677
+ onclick: "ajax_post('/admin/snapshot-now')",
678
+ },
679
+ ],
680
+ fields: [
681
+ {
682
+ type: "Bool",
683
+ label: req.__("Periodic snapshots enabled"),
684
+ name: "snapshots_enabled",
685
+ sublabel: req.__(
686
+ "Snapshot will be made every hour if there are changes"
687
+ ),
688
+ },
689
+ ],
690
+ });
691
+ router.post(
692
+ "/set-snapshot",
693
+ isAdmin,
694
+ error_catcher(async (req, res) => {
695
+ const form = await snapshotForm(req);
696
+ form.validate(req.body);
697
+
698
+ await save_config_from_form(form);
699
+ req.flash("success", req.__("Snapshot settings updated"));
700
+ if (!req.xhr) res.redirect("/admin/backup");
701
+ else res.json({ success: "ok" });
702
+ })
703
+ );
543
704
  router.post(
544
705
  "/set-auto-backup",
545
706
  isAdmin,
@@ -579,6 +740,22 @@ router.post(
579
740
  })
580
741
  );
581
742
 
743
+ router.post(
744
+ "/snapshot-now",
745
+ isAdmin,
746
+ error_catcher(async (req, res) => {
747
+ try {
748
+ const taken = await Snapshot.take_if_changed();
749
+ if (taken) req.flash("success", req.__("Snapshot successful"));
750
+ else
751
+ req.flash("success", req.__("No changes detected, snapshot skipped"));
752
+ } catch (e) {
753
+ req.flash("error", e.message);
754
+ }
755
+ res.json({ reload_page: true });
756
+ })
757
+ );
758
+
582
759
  /**
583
760
  * @name get/system
584
761
  * @function
@@ -654,47 +831,47 @@ router.get(
654
831
  th(req.__("Saltcorn version")),
655
832
  td(
656
833
  packagejson.version +
657
- (isRoot && can_update
658
- ? post_btn(
659
- "/admin/upgrade",
660
- req.__("Upgrade"),
661
- req.csrfToken(),
662
- {
663
- btnClass: "btn-primary btn-sm",
664
- formClass: "d-inline",
665
- }
666
- )
667
- : isRoot && is_latest
834
+ (isRoot && can_update
835
+ ? post_btn(
836
+ "/admin/upgrade",
837
+ req.__("Upgrade"),
838
+ req.csrfToken(),
839
+ {
840
+ btnClass: "btn-primary btn-sm",
841
+ formClass: "d-inline",
842
+ }
843
+ )
844
+ : isRoot && is_latest
668
845
  ? span(
669
- { class: "badge bg-primary ms-2" },
670
- req.__("Latest")
671
- ) +
672
- post_btn(
673
- "/admin/check-for-upgrade",
674
- req.__("Check for updates"),
675
- req.csrfToken(),
676
- {
677
- btnClass: "btn-primary btn-sm px-1 py-0",
678
- formClass: "d-inline",
679
- }
680
- )
846
+ { class: "badge bg-primary ms-2" },
847
+ req.__("Latest")
848
+ ) +
849
+ post_btn(
850
+ "/admin/check-for-upgrade",
851
+ req.__("Check for updates"),
852
+ req.csrfToken(),
853
+ {
854
+ btnClass: "btn-primary btn-sm px-1 py-0",
855
+ formClass: "d-inline",
856
+ }
857
+ )
681
858
  : "")
682
859
  )
683
860
  ),
684
861
  git_commit &&
685
- tr(
686
- th(req.__("git commit")),
687
- td(
688
- a(
689
- {
690
- href:
691
- "https://github.com/saltcorn/saltcorn/commit/" +
692
- git_commit,
693
- },
694
- git_commit.substring(0, 6)
695
- )
862
+ tr(
863
+ th(req.__("git commit")),
864
+ td(
865
+ a(
866
+ {
867
+ href:
868
+ "https://github.com/saltcorn/saltcorn/commit/" +
869
+ git_commit,
870
+ },
871
+ git_commit.substring(0, 6)
696
872
  )
697
- ),
873
+ )
874
+ ),
698
875
  tr(th(req.__("Node.js version")), td(process.version)),
699
876
  tr(
700
877
  th(req.__("Database")),
@@ -804,7 +981,7 @@ router.post(
804
981
  res.attachment(fileName);
805
982
  const file = fs.createReadStream(fileName);
806
983
  file.on("end", function () {
807
- fs.unlink(fileName, function () {});
984
+ fs.unlink(fileName, function () { });
808
985
  });
809
986
  file.pipe(res);
810
987
  })
@@ -827,7 +1004,7 @@ router.post(
827
1004
  );
828
1005
  if (err) req.flash("error", err);
829
1006
  else req.flash("success", req.__("Successfully restored backup"));
830
- fs.unlink(newPath, function () {});
1007
+ fs.unlink(newPath, function () { });
831
1008
  res.redirect(`/admin`);
832
1009
  })
833
1010
  );
@@ -902,7 +1079,7 @@ const clearAllForm = (req) =>
902
1079
  {
903
1080
  type: "Bool",
904
1081
  name: "plugins",
905
- label: req.__("Plugins"),
1082
+ label: req.__("Modules"),
906
1083
  default: true,
907
1084
  },
908
1085
  ],
@@ -972,8 +1149,8 @@ router.post(
972
1149
  req.__(
973
1150
  "LetsEncrypt SSL enabled. Restart for changes to take effect."
974
1151
  ) +
975
- " " +
976
- a({ href: "/admin/system" }, req.__("Restart here"))
1152
+ " " +
1153
+ a({ href: "/admin/system" }, req.__("Restart here"))
977
1154
  );
978
1155
  res.redirect("/useradmin/ssl");
979
1156
  } catch (e) {
@@ -1044,13 +1221,13 @@ router.get(
1044
1221
  contents: div(
1045
1222
  pass
1046
1223
  ? div(
1047
- { class: "alert alert-success", role: "alert" },
1048
- i({ class: "fas fa-check-circle fa-lg me-2" }),
1049
- h5(
1050
- { class: "d-inline" },
1051
- req.__("No errors detected during configuration check")
1052
- )
1224
+ { class: "alert alert-success", role: "alert" },
1225
+ i({ class: "fas fa-check-circle fa-lg me-2" }),
1226
+ h5(
1227
+ { class: "d-inline" },
1228
+ req.__("No errors detected during configuration check")
1053
1229
  )
1230
+ )
1054
1231
  : errors.map(mkError)
1055
1232
  ),
1056
1233
  },
@@ -1064,19 +1241,66 @@ router.get(
1064
1241
  })
1065
1242
  );
1066
1243
 
1244
+ const buildDialogScript = () => {
1245
+ return `<script>
1246
+ function swapEntryInputs(activeTab, activeInput, disabledTab, disabledInput) {
1247
+ activeTab.addClass("active");
1248
+ activeInput.removeClass("d-none");
1249
+ activeInput.addClass("d-block");
1250
+ activeInput.attr("name", "entryPoint");
1251
+ disabledTab.removeClass("active");
1252
+ disabledInput.removeClass("d-block");
1253
+ disabledInput.addClass("d-none");
1254
+ disabledInput.removeAttr("name");
1255
+ }
1256
+
1257
+ function showEntrySelect(type) {
1258
+ const viewNavLin = $("#viewNavLinkID");
1259
+ const pageNavLink = $("#pageNavLinkID");
1260
+ const viewInp = $("#viewInputID");
1261
+ const pageInp = $("#pageInputID");
1262
+ if (type === "page") {
1263
+ swapEntryInputs(pageNavLink, pageInp, viewNavLin, viewInp);
1264
+ }
1265
+ else if (type === "view") {
1266
+ swapEntryInputs(viewNavLin, viewInp, pageNavLink, pageInp);
1267
+ }
1268
+ $("#entryPointTypeID").attr("value", type);
1269
+ }
1270
+
1271
+ function handleMessages() {
1272
+ notifyAlert("This is still under development and might run longer.")
1273
+ ${
1274
+ getState().getConfig("apple_team_id") &&
1275
+ getState().getConfig("apple_team_id") !== "null"
1276
+ ? ""
1277
+ : `
1278
+ if ($("#iOSCheckboxId")[0].checked) {
1279
+ notifyAlert(
1280
+ "No 'Apple Team ID' is configured, I will try to build a project for the iOS simulator."
1281
+ );
1282
+ }`
1283
+ }
1284
+ }
1285
+ </script>`;
1286
+ };
1287
+
1067
1288
  router.get(
1068
1289
  "/build-mobile-app",
1069
1290
  isAdmin,
1070
1291
  error_catcher(async (req, res) => {
1071
- const isRoot = db.getTenantSchema() === db.connectObj.default_schema;
1072
1292
  const views = await View.find();
1073
- const execBuildMsg =
1074
- "This is still under development and might run longer.";
1293
+ const pages = await Page.find();
1075
1294
 
1076
1295
  send_admin_page({
1077
1296
  res,
1078
1297
  req,
1079
1298
  active_sub: "Mobile app",
1299
+ headers: [
1300
+ {
1301
+ headerTag: buildDialogScript(),
1302
+ },
1303
+ ],
1080
1304
  contents: {
1081
1305
  above: [
1082
1306
  {
@@ -1094,34 +1318,77 @@ router.get(
1094
1318
  name: "_csrf",
1095
1319
  value: req.csrfToken(),
1096
1320
  }),
1321
+ input({
1322
+ type: "hidden",
1323
+ name: "entryPointType",
1324
+ value: "view",
1325
+ id: "entryPointTypeID",
1326
+ }),
1097
1327
  div(
1098
1328
  { class: "container ps-2" },
1099
1329
  div(
1100
1330
  { class: "row pb-2" },
1101
- div({ class: "col-sm-4 fw-bold" }, "Entry view"),
1102
- div({ class: "col-sm-4 fw-bold" }, "Platform"),
1331
+ div({ class: "col-sm-4 fw-bold" }, req.__("Entry point")),
1332
+ div({ class: "col-sm-4 fw-bold" }, req.__("Platform")),
1103
1333
  div(
1104
1334
  {
1105
1335
  class: "col-sm-1 fw-bold d-flex justify-content-center",
1106
1336
  },
1107
- "docker"
1337
+ req.__("docker")
1108
1338
  )
1109
1339
  ),
1110
1340
  div(
1111
1341
  { class: "row" },
1112
1342
  div(
1113
1343
  { class: "col-sm-4" },
1344
+ // 'view/page' tabs
1345
+ ul(
1346
+ { class: "nav nav-pills" },
1347
+ li(
1348
+ {
1349
+ class: "nav-item",
1350
+ onClick: "showEntrySelect('view')",
1351
+ },
1352
+ div(
1353
+ { class: "nav-link active", id: "viewNavLinkID" },
1354
+ req.__("View")
1355
+ )
1356
+ ),
1357
+ li(
1358
+ {
1359
+ class: "nav-item",
1360
+ onClick: "showEntrySelect('page')",
1361
+ },
1362
+ div(
1363
+ { class: "nav-link", id: "pageNavLinkID" },
1364
+ req.__("Page")
1365
+ )
1366
+ )
1367
+ ),
1368
+ // select entry-view
1114
1369
  select(
1115
1370
  {
1116
1371
  class: "form-control",
1117
- name: "entryView",
1118
- id: "entryViewInput",
1372
+ name: "entryPoint",
1373
+ id: "viewInputID",
1119
1374
  },
1120
1375
  views
1121
1376
  .map((view) =>
1122
1377
  option({ value: view.name }, view.name)
1123
1378
  )
1124
1379
  .join(",")
1380
+ ),
1381
+ // select entry-page
1382
+ select(
1383
+ {
1384
+ class: "form-control d-none",
1385
+ id: "pageInputID",
1386
+ },
1387
+ pages
1388
+ .map((page) =>
1389
+ option({ value: page.name }, page.name)
1390
+ )
1391
+ .join(",")
1125
1392
  )
1126
1393
  ),
1127
1394
  div(
@@ -1131,7 +1398,7 @@ router.get(
1131
1398
  { class: "container ps-0" },
1132
1399
  div(
1133
1400
  { class: "row" },
1134
- div({ class: "col-sm-8" }, "android"),
1401
+ div({ class: "col-sm-8" }, req.__("android")),
1135
1402
  div(
1136
1403
  { class: "col-sm" },
1137
1404
  input({
@@ -1144,7 +1411,7 @@ router.get(
1144
1411
  ),
1145
1412
  div(
1146
1413
  { class: "row" },
1147
- div({ class: "col-sm-8" }, "iOS"),
1414
+ div({ class: "col-sm-8" }, req.__("iOS")),
1148
1415
  div(
1149
1416
  { class: "col-sm" },
1150
1417
  input({
@@ -1176,7 +1443,7 @@ router.get(
1176
1443
  for: "appNameInputId",
1177
1444
  class: "form-label fw-bold",
1178
1445
  },
1179
- "App file"
1446
+ req.__("App file")
1180
1447
  ),
1181
1448
  input({
1182
1449
  type: "text",
@@ -1196,14 +1463,14 @@ router.get(
1196
1463
  for: "serverURLInputId",
1197
1464
  class: "form-label fw-bold",
1198
1465
  },
1199
- "Server URL"
1466
+ req.__("Server URL")
1200
1467
  ),
1201
1468
  input({
1202
1469
  type: "text",
1203
1470
  class: "form-control",
1204
1471
  name: "serverURL",
1205
1472
  id: "serverURLInputId",
1206
- placeholder: "http://10.0.2.2:3000",
1473
+ placeholder: getState().getConfig("base_url") || "",
1207
1474
  })
1208
1475
  )
1209
1476
  )
@@ -1211,12 +1478,12 @@ router.get(
1211
1478
  button(
1212
1479
  {
1213
1480
  type: "submit",
1214
- onClick: `notifyAlert('${execBuildMsg}'); press_store_button(this);`,
1481
+ onClick: `handleMessages(); press_store_button(this);`,
1215
1482
  class: "btn btn-warning",
1216
1483
  },
1217
1484
  i({ class: "fas fa-hammer pe-2" }),
1218
1485
 
1219
- "Build mobile app"
1486
+ req.__("Build mobile app")
1220
1487
  )
1221
1488
  )
1222
1489
  ),
@@ -1232,7 +1499,8 @@ router.post(
1232
1499
  isAdmin,
1233
1500
  error_catcher(async (req, res) => {
1234
1501
  let {
1235
- entryView,
1502
+ entryPoint,
1503
+ entryPointType,
1236
1504
  androidPlatform,
1237
1505
  iOSPlatform,
1238
1506
  useDocker,
@@ -1250,12 +1518,20 @@ router.post(
1250
1518
  req.flash("error", req.__("Only the android build supports docker."));
1251
1519
  return res.redirect("/admin/build-mobile-app");
1252
1520
  }
1253
- if (appFile && !appFile.endsWith(".apk")) appFile = `${appFile}.apk`;
1521
+ if (!serverURL || serverURL.length == 0) {
1522
+ serverURL = getState().getConfig("base_url") || "";
1523
+ }
1524
+ if (!serverURL.startsWith("http")) {
1525
+ req.flash("error", req.__("Please enter a valid server URL."));
1526
+ return res.redirect("/admin/build-mobile-app");
1527
+ }
1254
1528
  const appOut = path.join(__dirname, "..", "mobile-app-out");
1255
1529
  const spawnParams = [
1256
1530
  "build-app",
1257
- "-v",
1258
- entryView,
1531
+ "-e",
1532
+ entryPoint,
1533
+ "-t",
1534
+ entryPointType,
1259
1535
  "-c",
1260
1536
  appOut,
1261
1537
  "-b",
@@ -1263,7 +1539,13 @@ router.post(
1263
1539
  ];
1264
1540
  if (useDocker) spawnParams.push("-d");
1265
1541
  if (androidPlatform) spawnParams.push("-p", "android");
1266
- if (iOSPlatform) spawnParams.push("-p", "ios");
1542
+ if (iOSPlatform) {
1543
+ spawnParams.push("-p", "ios");
1544
+ const teamId = getState().getConfig("apple_team_id");
1545
+ if (!teamId || teamId === "null") {
1546
+ spawnParams.push("--buildForEmulator");
1547
+ }
1548
+ }
1267
1549
  if (appFile) spawnParams.push("-a", appFile);
1268
1550
  if (serverURL) spawnParams.push("-s", serverURL);
1269
1551
  const child = spawn("saltcorn", spawnParams, {
@@ -1281,10 +1563,13 @@ router.post(
1281
1563
  });
1282
1564
  child.on("exit", async function (exitCode, signal) {
1283
1565
  if (exitCode === 0) {
1284
- const file = await File.from_existing_file(
1285
- appOut,
1286
- appFile ? appFile : "app-debug.apk",
1287
- req.user.id
1566
+ const files = await Promise.all(
1567
+ fs
1568
+ .readdirSync(appOut)
1569
+ .map(
1570
+ async (outFile) =>
1571
+ await File.from_existing_file(appOut, outFile, req.user.id)
1572
+ )
1288
1573
  );
1289
1574
  res.sendWrap(req.__(`Admin`), {
1290
1575
  above: [
@@ -1293,7 +1578,7 @@ router.post(
1293
1578
  title: req.__("Build Result"),
1294
1579
  contents: div("The build was successfully"),
1295
1580
  },
1296
- app_files_table(file, req),
1581
+ files.length > 0 ? app_files_table(files, req) : "",
1297
1582
  ],
1298
1583
  });
1299
1584
  } else