diff --git a/containers/libreportal/backend/routes/setup-routes.js b/containers/libreportal/backend/routes/setup-routes.js
index d245e98..4ab2002 100644
--- a/containers/libreportal/backend/routes/setup-routes.js
+++ b/containers/libreportal/backend/routes/setup-routes.js
@@ -144,6 +144,32 @@ async function enqueueTask(spec) {
return id;
}
+// Check a path full of .lpapp exports, without importing anything.
+//
+// Enqueues the host-side check and returns immediately; the result lands in
+// frontend/data/system/import_check.json, which the wizard polls. Read-only,
+// and a .lpapp is not encrypted, so no secret crosses this boundary β unlike a
+// backup repository, which is why that one stays in the terminal installer.
+router.post('/import-check', requireAuth, async (req, res) => {
+ const p = String((req.body && req.body.path) || '').trim();
+ if (!p || !p.startsWith('/')) {
+ return res.status(400).json({ error: 'An absolute path is required' });
+ }
+ // Shell-quote: this reaches a command line, and a path is user input.
+ const quoted = `'${p.replace(/'/g, "'\\''")}'`;
+ try {
+ const id = await enqueueTask({
+ command: `libreportal app import-check ${quoted} --publish`,
+ type: 'import',
+ app: 'libreportal',
+ setupRole: 'config'
+ });
+ res.json({ ok: true, taskId: id });
+ } catch (e) {
+ res.status(500).json({ error: e.message || String(e) });
+ }
+});
+
router.post('/save', requireAuth, async (req, res) => {
const payload = req.body || {};
diff --git a/containers/libreportal/frontend/core/setup/js/setup-wizard.js b/containers/libreportal/frontend/core/setup/js/setup-wizard.js
index e1e093d..b2b9bc8 100755
--- a/containers/libreportal/frontend/core/setup/js/setup-wizard.js
+++ b/containers/libreportal/frontend/core/setup/js/setup-wizard.js
@@ -21,8 +21,8 @@ class SetupWizard {
// Storage sits BEFORE Recommended on purpose: a location has to exist
// before an app can be placed on it, and the Recommended step can then
// offer the big apps a home other than the system disk.
- this.stepNames = ['Experience', 'Identity', 'Domains', 'Storage', 'Backups', 'Recommended', 'Metrics'];
- this.stepIcons = ['π±', 'πͺ', 'π°οΈ', 'πΎ', 'π', 'π‘οΈ', 'π'];
+ this.stepNames = ['Experience', 'Identity', 'Domains', 'Storage', 'Backups', 'Import', 'Recommended', 'Metrics'];
+ this.stepIcons = ['π±', 'πͺ', 'π°οΈ', 'πΎ', 'π', 'π¦', 'π‘οΈ', 'π'];
// Storage is skipped entirely when this box has nowhere else to put things
// β one disk means one answer, and a step with nothing in it is noise.
// Set by loadStorage() once the candidate scan comes back.
@@ -38,6 +38,9 @@ class SetupWizard {
this.storageSystemChoice = 'primary';
// Backup destination: '' = none, 'primary' = system disk, else a drive path.
this.backupDest = '';
+ // .lpapp exports found at the path the user gave, and which to import.
+ this.importResults = [];
+ this.importSelected = [];
this.installLevel = 'beginner';
this.totalSteps = this._effectiveTotalSteps();
this.domainCount = 0; // tracked dynamically as the user adds rows
@@ -262,8 +265,29 @@ class SetupWizard {
-
+
+
+
Import
+ ?
+
+
+ Folder or file
+
+
+
+
+
+ Optional. Leave blank to skip.
+
+
+
+
+
+
Recommended Apps
Pre-selected to give you a working install out of the box.
@@ -287,7 +311,7 @@ class SetupWizard {
default β they're only useful if the user wants the MONITORING
toggle on apps to do anything. Advanced-only: this whole step
is skipped when the user chose Beginner on step 1. -->
-
+
Metrics Apps
Optional. Install these to enable per-app "Export metrics to Grafana" later.
@@ -336,6 +360,7 @@ class SetupWizard {
this.attachLiveValidation();
+ $('#sw-import-check').addEventListener('click', () => this.checkImportPath());
$('#sw-back').addEventListener('click', () => this.prev());
$('#sw-next').addEventListener('click', () => this.next());
@@ -582,6 +607,9 @@ class SetupWizard {
this.storageSystemChoice = 'primary';
// Backup destination: '' = none, 'primary' = system disk, else a drive path.
this.backupDest = '';
+ // .lpapp exports found at the path the user gave, and which to import.
+ this.importResults = [];
+ this.importSelected = [];
this.storageSystemChoice = 'primary';
return;
}
@@ -699,6 +727,94 @@ class SetupWizard {
+ 'It is shown on the Backup page once setup finishes.';
}
+ // Ask the host to inspect a path, then poll for the answer.
+ //
+ // The check runs on the host (it needs tar and the app templates), so this
+ // enqueues it and watches the file it publishes. Polling rather than a
+ // synchronous route because the task daemon owns the FIFO β the wizard has no
+ // way to run a command itself, by design.
+ async checkImportPath() {
+ const input = this.container.querySelector('#sw-import-path');
+ const box = this.container.querySelector('#sw-import-results');
+ const btn = this.container.querySelector('#sw-import-check');
+ if (!input || !box) return;
+
+ const path = input.value.trim();
+ this.importSelected = [];
+ if (!path) { box.innerHTML = ''; return; }
+ if (!path.startsWith('/')) {
+ box.innerHTML = '