Fix WebUI service URLs and Traefik flag; add app icon to instance modal

Found while installing two LAN-only Bookstack instances — both in the same
no-domain path as the previous commit:

- apps-services.json advertised every app at http://localhost:<port>. The
  CFG_SERVER_IP override it reads is defined in no config file, so the lookup
  always fell through to the "localhost" default — a URL that only resolves for
  someone browsing on the server itself. Now falls back to $local_ip_v4, the
  same host APP_URL is stamped with.

- traefikManaged was inferred from `access == public`, a stated placeholder.
  Public only means the port is published on the host; it says nothing about a
  router. It reported true for both new instances despite their compose having
  traefik.enable:false. Now read from the port's own traefik column, gated on
  the app's domain actually being set — resolved per-app here rather than from
  $domain_full, which this generator never populates.

- The "New instance" modal led with bare text. It now shows the type's icon in
  the same .app-card-icon holder the grid cards use, so it's visually tied to
  the app the user clicked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
librelad 2026-08-19 03:08:22 +01:00
parent 48c024f69b
commit 6aa6eb81a1
3 changed files with 70 additions and 7 deletions

View File

@ -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;

View File

@ -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 = `
<div class="lp-instance-modal" role="dialog" aria-modal="true" aria-label="New ${this._esc(title)} instance">
<div class="lp-instance-head">
<div class="app-card-icon lp-instance-icon">
<img src="${this._esc(icon)}" alt="" onerror="this.onerror=null; this.src='/core/icons/apps/default.svg'"/>
</div>
<h3>New ${this._esc(title)} instance</h3>
<button type="button" class="lp-instance-x" aria-label="Close">&times;</button>
</div>

View File

@ -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_<APP>_DOMAIN picks a CFG_DOMAIN_<n> 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_<APP>_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