diff --git a/containers/libreportal/frontend/components/apps/core/css/apps.css b/containers/libreportal/frontend/components/apps/core/css/apps.css
index f8f1dd7..3bb1600 100644
--- a/containers/libreportal/frontend/components/apps/core/css/apps.css
+++ b/containers/libreportal/frontend/components/apps/core/css/apps.css
@@ -686,6 +686,41 @@
color: var(--text-secondary);
min-height: 14px;
}
+/* LAN-only toggle. Row-oriented, unlike .lp-instance-field, so the label sits
+ beside the box; the checkbox keeps its intrinsic size rather than inheriting
+ the full-width input rule above. */
+.lp-instance-check {
+ display: flex;
+ align-items: center;
+ gap: 9px;
+ margin-bottom: 14px;
+ font-size: 13px;
+ color: var(--text-primary);
+ cursor: pointer;
+}
+.lp-instance-check input {
+ width: 15px;
+ height: 15px;
+ flex: 0 0 auto;
+ accent-color: var(--accent);
+ cursor: pointer;
+}
+.lp-instance-check input:disabled {
+ cursor: default;
+}
+.lp-instance-check:has(input:disabled) {
+ cursor: default;
+ opacity: 0.75;
+}
+/* The "no domain configured" note sits between the toggle and the preview line.
+ .lp-instance-hint carries no bottom margin (it normally hangs under a field
+ that supplies its own), so without this it butts straight into "Will be served
+ at" and the two read as one run-on paragraph. */
+.lp-instance-check + .lp-instance-hint {
+ display: block;
+ margin: -6px 0 14px;
+ line-height: 1.45;
+}
.lp-instance-row {
display: flex;
gap: 12px;
diff --git a/containers/libreportal/frontend/components/apps/core/js/instance-manager.js b/containers/libreportal/frontend/components/apps/core/js/instance-manager.js
index 8fa6b51..e29ba2e 100644
--- a/containers/libreportal/frontend/components/apps/core/js/instance-manager.js
+++ b/containers/libreportal/frontend/components/apps/core/js/instance-manager.js
@@ -61,9 +61,14 @@ class InstanceManager {
const title = this._typeTitle(typeSlug);
this.domains = await this._loadDomains();
- const domainOptions = this.domains.length
+ // With no CFG_DOMAIN_n set, a subdomain has nothing to attach to — the backend
+ // would stamp Host(`sub.`) and an unreachable APP_URL. So the modal drops to
+ // LAN-only and locks the choice, matching what initializeAppVariables enforces
+ // server-side. Presenting a domain picker here would have been a lie.
+ const hasDomains = this.domains.length > 0;
+ const domainOptions = hasDomains
? this.domains.map(d => ``).join('')
- : '';
+ : '';
const overlay = document.createElement('div');
overlay.id = 'lp-instance-modal';
@@ -74,7 +79,7 @@ class InstanceManager {
New ${this._esc(title)} instance
- A fully isolated second copy — its own data, database, subdomain, backups and update cadence.
+ A fully isolated second copy — its own data, database, ports, backups and update cadence.
-
+
+ ${hasDomains ? '' : '
No domain is configured, so instances are LAN-only. Add one in Admin → Config to route them on a subdomain.'}
+
+