diff --git a/containers/libreportal/frontend/components/apps/core/css/apps.css b/containers/libreportal/frontend/components/apps/core/css/apps.css
index 3bb1600..1b67208 100644
--- a/containers/libreportal/frontend/components/apps/core/css/apps.css
+++ b/containers/libreportal/frontend/components/apps/core/css/apps.css
@@ -634,9 +634,21 @@
}
.lp-instance-head h3 {
margin: 0;
+ /* Takes the slack so the close button stays pinned right once the icon is
+ added ahead of the title — space-between alone would centre the heading. */
+ flex: 1;
font-size: 18px;
font-weight: 700;
}
+/* Reuses .app-card-icon (the grid card's holder) for identical framing, scaled
+ down to sit on a heading line rather than a card. */
+.lp-instance-icon {
+ width: 44px;
+ height: 44px;
+ min-width: 44px;
+ padding: 8px;
+ border-radius: 10px;
+}
.lp-instance-x {
background: transparent;
border: none;
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 e29ba2e..6410cc7 100644
--- a/containers/libreportal/frontend/components/apps/core/js/instance-manager.js
+++ b/containers/libreportal/frontend/components/apps/core/js/instance-manager.js
@@ -33,12 +33,24 @@ class InstanceManager {
}
}
+ // The type's entry in the apps list — source of both the heading and the icon.
+ _typeApp(typeSlug) {
+ return (window.apps || []).find(x => (x.command || '').split(' ').pop() === typeSlug);
+ }
+
// The type's display title, for the modal heading.
_typeTitle(typeSlug) {
- const a = (window.apps || []).find(x => (x.command || '').split(' ').pop() === typeSlug);
+ const a = this._typeApp(typeSlug);
return a ? (a.name || typeSlug).split(' - ')[0].trim() : typeSlug;
}
+ // The type's icon. Same source and same default the grid cards use, so the
+ // modal shows the exact artwork the user just clicked on.
+ _typeIcon(typeSlug) {
+ const a = this._typeApp(typeSlug);
+ return (a && a.icon) || '/core/icons/apps/default.svg';
+ }
+
// user text -> [a-z0-9] id (mirrors the backend's instanceIdPart)
_idPart(raw) {
return String(raw || '').toLowerCase().replace(/[^a-z0-9]/g, '');
@@ -59,6 +71,7 @@ class InstanceManager {
async openCreateModal(typeSlug) {
if (document.getElementById('lp-instance-modal')) return;
const title = this._typeTitle(typeSlug);
+ const icon = this._typeIcon(typeSlug);
this.domains = await this._loadDomains();
// With no CFG_DOMAIN_n set, a subdomain has nothing to attach to — the backend
@@ -76,6 +89,9 @@ class InstanceManager {
overlay.innerHTML = `
+
+
+
New ${this._esc(title)} instance
diff --git a/scripts/webui/data/generators/apps/webui_services.sh b/scripts/webui/data/generators/apps/webui_services.sh
index 0289cd1..34f0499 100755
--- a/scripts/webui/data/generators/apps/webui_services.sh
+++ b/scripts/webui/data/generators/apps/webui_services.sh
@@ -47,6 +47,17 @@ EOF
local install_date=$(sqlite3 "$docker_dir/$db_file" "SELECT install_date, install_time FROM apps WHERE name = '$app_name';" 2>/dev/null)
local installed_date="${install_date}|"
+ # This app's domain, resolved the same way initializeAppVariables does
+ # (CFG__DOMAIN picks a CFG_DOMAIN_ slot). Resolved here rather than
+ # read from $domain_full — this generator never calls initializeAppVariables,
+ # so that global is either unset or left over from an unrelated app. Empty
+ # means no Traefik router exists for any of this app's ports, whatever their
+ # traefik column says.
+ local _dom_idx_var="CFG_${app_name^^}_DOMAIN"
+ local _dom_idx="${!_dom_idx_var:-1}"
+ local _dom_var="CFG_DOMAIN_${_dom_idx}"
+ local app_domain_full="${!_dom_var:-}"
+
# Get all Docker services for this app
local docker_services=$(sqlite3 "$docker_dir/$db_file" "SELECT service_name, resource_value FROM network_resources WHERE app_name = '$app_name' AND resource_type = 'ip' AND status = 'active' ORDER BY service_name;" 2>/dev/null)
@@ -63,18 +74,28 @@ EOF
local access_type=$(echo "$port_mapping" | cut -d':' -f3)
local protocol=$(echo "$port_mapping" | cut -d':' -f4)
- # Get server IP (from system config or default)
+ # Host for the "Open" links. CFG_SERVER_IP stays an explicit
+ # override, but no config file actually defines it today, so
+ # this always fell through to "localhost" — a URL that only
+ # works for someone browsing ON the server. $local_ip_v4 (the
+ # source IP of the default route) is what LAN/VPN clients dial,
+ # and matches the host APP_URL is stamped with.
local server_ip=""
if [[ -f "${containers_dir}libreportal/config/generated/configs.json" ]]; then
server_ip=$(grep -o '"CFG_SERVER_IP":[[:space:]]*"[^"]*"' "${containers_dir}libreportal/config/generated/configs.json" | cut -d'"' -f4)
fi
+ [[ -z "$server_ip" ]] && server_ip="${local_ip_v4:-localhost}"
[[ -z "$server_ip" ]] && server_ip="localhost"
-
- # Determine if Traefik managed (check if service has Traefik labels)
+
+ # Whether this port really has a Traefik router. Read from the
+ # port's own traefik column below (field 7), not inferred from
+ # the access type — "public" only means the port is published
+ # on the host, and plenty of public ports carry no router. The
+ # old assumption reported traefikManaged:true for apps whose
+ # compose had traefik.enable:false, including every app on a
+ # box with no domain configured.
local traefik_managed="false"
- # For now, assume public access means Traefik managed
- [[ "$access_type" == "public" ]] && traefik_managed="true"
-
+
# Per-port URL path (10th col of CFG__PORT_N).
# Set when the matching port row is found below; left
# empty for ports without a web UI (DNS, etc.).
@@ -106,6 +127,14 @@ EOF
local config_button_enabled
local config_button_text
local config_url_path=""
+ # Field 7 = the traefik column. Absent on the 8-col
+ # legacy layout, where field 6 held it instead.
+ local config_traefik="false"
+ if [[ "$field_count" -ge 9 ]]; then
+ config_traefik=$(echo "$var_value" | cut -d'|' -f7)
+ else
+ config_traefik=$(echo "$var_value" | cut -d'|' -f6)
+ fi
if [[ "$field_count" -ge 10 ]]; then
config_login_required=$(echo "$var_value" | cut -d'|' -f6)
config_button_enabled=$(echo "$var_value" | cut -d'|' -f8)
@@ -129,6 +158,12 @@ EOF
button_text="$config_button_text"
login_required="$config_login_required"
port_url_path="$config_url_path"
+ # A router also needs a domain to attach to —
+ # mirrors the guard in initializeAppVariables, so
+ # the WebUI and the generated compose agree.
+ if [[ "$config_traefik" == "true" && -n "$app_domain_full" ]]; then
+ traefik_managed="true"
+ fi
fi
done < "$app_config_file"
fi