fix(multi): Exposure fixes, MagicMirror2 port, Tor Shield health card

Exposure Manager:
- Fix RPCD subshell issues in status and ssl_list methods
- Fix JS views to handle both array and object API responses

MagicMirror2:
- Change default port from 8082 to 8085 (avoid CyberFeed conflict)
- Update mm2ctl, RPCD, settings.js, dashboard.js, config

Tor Shield:
- Add restart method to RPCD and API
- Add health status minicard (Service, Bootstrap, DNS, Kill Switch)

Portal:
- Add 'active-ports' section for detected services
- Separate portal apps (Services) from detected ports (Active Ports)

Service Detection:
- Prioritize port-based identification over process name

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-01-25 17:22:52 +01:00
parent 7566014096
commit a1bad31807
17 changed files with 212 additions and 78 deletions

View File

@ -14,8 +14,10 @@ return view.extend({
render: function(data) {
var status = data[0] || {};
var conflicts = data[1] || [];
var conflictsResult = data[1] || {};
// Handle both direct array and wrapped object responses
var conflicts = Array.isArray(conflictsResult) ? conflictsResult : (conflictsResult.conflicts || []);
var services = status.services || {};
var tor = status.tor || {};
var ssl = status.ssl || {};

View File

@ -13,8 +13,10 @@ return view.extend({
},
render: function(data) {
var services = data[0] || [];
var config = data[1] || [];
var scanResult = data[0] || {};
var configResult = data[1] || {};
var services = Array.isArray(scanResult) ? scanResult : (scanResult.services || []);
var config = Array.isArray(configResult) ? configResult : (configResult.known_services || []);
var self = this;
// Inject CSS

View File

@ -13,8 +13,10 @@ return view.extend({
},
render: function(data) {
var sslBackends = data[0] || [];
var allServices = data[1] || [];
var sslResult = data[0] || {};
var scanResult = data[1] || {};
var sslBackends = Array.isArray(sslResult) ? sslResult : (sslResult.backends || []);
var allServices = Array.isArray(scanResult) ? scanResult : (scanResult.services || []);
var self = this;
// Inject CSS

View File

@ -13,8 +13,10 @@ return view.extend({
},
render: function(data) {
var torServices = data[0] || [];
var allServices = data[1] || [];
var torResult = data[0] || {};
var scanResult = data[1] || {};
var torServices = Array.isArray(torResult) ? torResult : (torResult.services || []);
var allServices = Array.isArray(scanResult) ? scanResult : (scanResult.services || []);
var self = this;
// Inject CSS

View File

@ -137,23 +137,32 @@ case "$1" in
json_close_array
json_close_object
# HAProxy SSL backends
# HAProxy SSL backends - use temp file to avoid subshell
HAPROXY_CONFIG="/srv/lxc/haproxy/rootfs/etc/haproxy/haproxy.cfg"
ssl_count=0
[ -f "$HAPROXY_CONFIG" ] && ssl_count=$(grep -c "^backend.*_backend$" "$HAPROXY_CONFIG" 2>/dev/null || echo 0)
TMP_SSL="/tmp/exposure_ssl_$$"
if [ -f "$HAPROXY_CONFIG" ]; then
grep -E "^backend .+_backend$" "$HAPROXY_CONFIG" 2>/dev/null | while read line; do
backend=$(echo "$line" | awk '{print $2}' | sed 's/_backend$//')
domain=$(grep "acl host_${backend} " "$HAPROXY_CONFIG" 2>/dev/null | awk '{print $NF}')
echo "$backend ${domain:-N/A}"
done > "$TMP_SSL"
fi
json_add_object "ssl"
json_add_int "count" "$ssl_count"
json_add_array "backends"
if [ -f "$HAPROXY_CONFIG" ]; then
grep -E "^backend .+_backend$" "$HAPROXY_CONFIG" 2>/dev/null | while read line; do
backend=$(echo "$line" | awk '{print $2}' | sed 's/_backend$//')
domain=$(grep "acl host_${backend} " "$HAPROXY_CONFIG" 2>/dev/null | awk '{print $NF}')
if [ -f "$TMP_SSL" ]; then
while read backend domain; do
[ -z "$backend" ] && continue
json_add_object ""
json_add_string "service" "$backend"
json_add_string "domain" "${domain:-N/A}"
json_add_string "domain" "$domain"
json_close_object
done
done < "$TMP_SSL"
rm -f "$TMP_SSL"
fi
json_close_array
json_close_object
@ -195,23 +204,32 @@ case "$1" in
ssl_list)
HAPROXY_CONFIG="/srv/lxc/haproxy/rootfs/etc/haproxy/haproxy.cfg"
TMP_SSLLIST="/tmp/exposure_ssllist_$$"
json_init
json_add_array "backends"
# Extract backend info to temp file to avoid subshell issues
if [ -f "$HAPROXY_CONFIG" ]; then
grep -E "^backend .+_backend$" "$HAPROXY_CONFIG" 2>/dev/null | while read line; do
backend=$(echo "$line" | awk '{print $2}')
service=$(echo "$backend" | sed 's/_backend$//')
domain=$(grep "acl host_${service} " "$HAPROXY_CONFIG" 2>/dev/null | awk '{print $NF}')
server=$(grep -A5 "backend $backend" "$HAPROXY_CONFIG" 2>/dev/null | grep "server " | awk '{print $3}')
echo "$service|${domain:-N/A}|${server:-N/A}"
done > "$TMP_SSLLIST"
fi
json_init
json_add_array "backends"
if [ -f "$TMP_SSLLIST" ]; then
while IFS='|' read service domain server; do
[ -z "$service" ] && continue
json_add_object ""
json_add_string "service" "$service"
json_add_string "domain" "${domain:-N/A}"
json_add_string "backend" "${server:-N/A}"
json_add_string "domain" "$domain"
json_add_string "backend" "$server"
json_close_object
done
done < "$TMP_SSLLIST"
rm -f "$TMP_SSLLIST"
fi
json_close_array

View File

@ -126,7 +126,7 @@ return view.extend({
]),
E('div', { 'class': 'mm2-card' }, [
E('div', { 'class': 'mm2-stat' }, [
E('div', { 'class': 'mm2-stat-value' }, ':' + (config.port || 8082)),
E('div', { 'class': 'mm2-stat-value' }, ':' + (config.port || 8085)),
E('div', { 'class': 'mm2-stat-label' }, _('Web Port'))
])
]),

View File

@ -67,7 +67,7 @@ return view.extend({
o = s.option(form.Value, 'port', _('Web Port'));
o.datatype = 'port';
o.default = '8082';
o.default = '8085';
o.rmempty = false;
o = s.option(form.Value, 'address', _('Listen Address'));

View File

@ -26,7 +26,7 @@ get_status() {
fi
local enabled=$(uci -q get magicmirror2.main.enabled || echo "0")
local port=$(uci -q get magicmirror2.main.port || echo "8082")
local port=$(uci -q get magicmirror2.main.port || echo "8085")
local router_ip=$(uci -q get network.lan.ipaddr || echo "192.168.1.1")
[ "$running" = "1" ] && web_url="http://${router_ip}:${port}"
@ -54,7 +54,7 @@ EOF
# Get main configuration
get_config() {
local enabled=$(uci -q get magicmirror2.main.enabled || echo "0")
local port=$(uci -q get magicmirror2.main.port || echo "8082")
local port=$(uci -q get magicmirror2.main.port || echo "8085")
local address=$(uci -q get magicmirror2.main.address || echo "0.0.0.0")
local data_path=$(uci -q get magicmirror2.main.data_path || echo "/srv/magicmirror2")
local memory_limit=$(uci -q get magicmirror2.main.memory_limit || echo "512M")
@ -327,7 +327,7 @@ set_config() {
# Get web URL for iframe
get_web_url() {
local router_ip=$(uci -q get network.lan.ipaddr || echo "192.168.1.1")
local port=$(uci -q get magicmirror2.main.port || echo "8082")
local port=$(uci -q get magicmirror2.main.port || echo "8085")
cat <<EOF
{

View File

@ -440,6 +440,13 @@ return baseclass.extend({
icon: '\ud83d\udce6',
path: 'admin/secubox/services',
order: 8
},
'active-ports': {
id: 'active-ports',
name: 'Active Ports',
icon: '\ud83d\udd0c',
path: 'admin/secubox/services',
order: 9
}
},

View File

@ -156,7 +156,8 @@ return view.extend({
this.renderNetworkSection(),
this.renderMonitoringSection(),
this.renderSystemSection(),
this.renderServicesSection()
this.renderServicesAppsSection(),
this.renderActivePortsSection()
])
]);
@ -171,7 +172,7 @@ return view.extend({
var sections = portal.getSections();
// Sections that link to other pages vs tabs within portal
var linkSections = ['portal', 'hub', 'admin'];
var tabSections = ['security', 'network', 'monitoring', 'system', 'services'];
var tabSections = ['security', 'network', 'monitoring', 'system', 'services', 'active-ports'];
return E('div', { 'class': 'sb-portal-header' }, [
// Brand
@ -443,7 +444,13 @@ return view.extend({
'System administration and configuration tools', apps);
},
renderServicesSection: function() {
renderServicesAppsSection: function() {
var apps = portal.getInstalledAppsBySection('services', this.installedApps);
return this.renderAppSection('services', 'Services',
'Application services running on your network', apps);
},
renderActivePortsSection: function() {
var self = this;
var services = this.detectedServices || [];
@ -514,9 +521,9 @@ return view.extend({
});
if (serviceCards.length === 0) {
return E('div', { 'class': 'sb-portal-section', 'data-section': 'services' }, [
return E('div', { 'class': 'sb-portal-section', 'data-section': 'active-ports' }, [
E('div', { 'class': 'sb-section-header' }, [
E('h2', { 'class': 'sb-section-title' }, '🔌 Active Services'),
E('h2', { 'class': 'sb-section-title' }, '🔌 Active Ports'),
E('p', { 'class': 'sb-section-subtitle' }, 'Detected services listening on network ports')
]),
E('div', { 'class': 'sb-section-empty' }, [
@ -527,9 +534,9 @@ return view.extend({
]);
}
return E('div', { 'class': 'sb-portal-section', 'data-section': 'services' }, [
return E('div', { 'class': 'sb-portal-section', 'data-section': 'active-ports' }, [
E('div', { 'class': 'sb-section-header' }, [
E('h2', { 'class': 'sb-section-title' }, '🔌 Active Services'),
E('h2', { 'class': 'sb-section-title' }, '🔌 Active Ports'),
E('p', { 'class': 'sb-section-subtitle' }, 'Detected services listening on network ports')
]),
E('div', { 'class': 'sb-app-grid' }, serviceCards)

View File

@ -27,6 +27,12 @@ var callDisable = rpc.declare({
expect: { success: false }
});
var callRestart = rpc.declare({
object: 'luci.tor-shield',
method: 'restart',
expect: { success: false }
});
var callCircuits = rpc.declare({
object: 'luci.tor-shield',
method: 'circuits',
@ -161,6 +167,7 @@ return baseclass.extend({
getStatus: callStatus,
enable: callEnable,
disable: callDisable,
restart: callRestart,
getCircuits: callCircuits,
newIdentity: callNewIdentity,
checkLeaks: callCheckLeaks,

View File

@ -87,6 +87,27 @@ return view.extend({
});
},
// Handle restart
handleRestart: function() {
var self = this;
ui.showModal(_('Restart Tor Shield'), [
E('p', { 'class': 'spinning' }, _('Restarting Tor Shield service...'))
]);
api.restart().then(function(result) {
ui.hideModal();
if (result.success) {
ui.addNotification(null, E('p', _('Tor Shield is restarting. Please wait for bootstrap to complete.')), 'info');
} else {
ui.addNotification(null, E('p', result.error || _('Failed to restart')), 'error');
}
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, E('p', _('Error: %s').format(err.message || err)), 'error');
});
},
// Handle leak test
handleLeakTest: function() {
var self = this;
@ -373,6 +394,54 @@ return view.extend({
])
]),
// Health Status Minicard
E('div', { 'class': 'tor-health-card', 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; margin-bottom: 20px;' }, [
E('div', { 'class': 'tor-health-item', 'style': 'display: flex; align-items: center; gap: 12px; padding: 16px; background: var(--tor-bg-card, #1a1a24); border-radius: 12px; border: 1px solid rgba(255,255,255,0.05);' }, [
E('div', {
'class': 'tor-health-indicator',
'style': 'width: 12px; height: 12px; border-radius: 50%; background: ' + (isProtected ? '#10b981' : isConnecting ? '#f59e0b' : '#6b7280') + '; box-shadow: 0 0 8px ' + (isProtected ? '#10b981' : isConnecting ? '#f59e0b' : 'transparent') + ';'
}),
E('div', {}, [
E('div', { 'style': 'font-size: 14px; font-weight: 600; color: var(--tor-text, #fff);' }, _('Service')),
E('div', { 'style': 'font-size: 12px; color: var(--tor-text-muted, #a0a0b0);' },
status.running ? _('Running') : _('Stopped'))
])
]),
E('div', { 'class': 'tor-health-item', 'style': 'display: flex; align-items: center; gap: 12px; padding: 16px; background: var(--tor-bg-card, #1a1a24); border-radius: 12px; border: 1px solid rgba(255,255,255,0.05);' }, [
E('div', {
'class': 'tor-health-indicator',
'style': 'width: 12px; height: 12px; border-radius: 50%; background: ' + (status.bootstrap >= 100 ? '#10b981' : status.bootstrap > 0 ? '#f59e0b' : '#6b7280') + '; box-shadow: 0 0 8px ' + (status.bootstrap >= 100 ? '#10b981' : status.bootstrap > 0 ? '#f59e0b' : 'transparent') + ';'
}),
E('div', {}, [
E('div', { 'style': 'font-size: 14px; font-weight: 600; color: var(--tor-text, #fff);' }, _('Bootstrap')),
E('div', { 'style': 'font-size: 12px; color: var(--tor-text-muted, #a0a0b0);' },
status.bootstrap >= 100 ? _('Complete') : status.bootstrap + '%')
])
]),
E('div', { 'class': 'tor-health-item', 'style': 'display: flex; align-items: center; gap: 12px; padding: 16px; background: var(--tor-bg-card, #1a1a24); border-radius: 12px; border: 1px solid rgba(255,255,255,0.05);' }, [
E('div', {
'class': 'tor-health-indicator',
'style': 'width: 12px; height: 12px; border-radius: 50%; background: ' + (status.dns_over_tor ? '#10b981' : '#f59e0b') + '; box-shadow: 0 0 8px ' + (status.dns_over_tor ? '#10b981' : '#f59e0b') + ';'
}),
E('div', {}, [
E('div', { 'style': 'font-size: 14px; font-weight: 600; color: var(--tor-text, #fff);' }, _('DNS')),
E('div', { 'style': 'font-size: 12px; color: var(--tor-text-muted, #a0a0b0);' },
status.dns_over_tor ? _('Protected') : _('Exposed'))
])
]),
E('div', { 'class': 'tor-health-item', 'style': 'display: flex; align-items: center; gap: 12px; padding: 16px; background: var(--tor-bg-card, #1a1a24); border-radius: 12px; border: 1px solid rgba(255,255,255,0.05);' }, [
E('div', {
'class': 'tor-health-indicator',
'style': 'width: 12px; height: 12px; border-radius: 50%; background: ' + (status.kill_switch ? '#10b981' : '#6b7280') + '; box-shadow: 0 0 8px ' + (status.kill_switch ? '#10b981' : 'transparent') + ';'
}),
E('div', {}, [
E('div', { 'style': 'font-size: 14px; font-weight: 600; color: var(--tor-text, #fff);' }, _('Kill Switch')),
E('div', { 'style': 'font-size: 12px; color: var(--tor-text-muted, #a0a0b0);' },
status.kill_switch ? _('Active') : _('Disabled'))
])
])
]),
// Actions Card
E('div', { 'class': 'tor-card' }, [
E('div', { 'class': 'tor-card-header' }, [
@ -393,6 +462,11 @@ return view.extend({
'click': L.bind(this.handleLeakTest, this),
'disabled': !isActive
}, ['\uD83D\uDD0D ', _('Leak Test')]),
E('button', {
'class': 'tor-btn tor-btn-warning',
'click': L.bind(this.handleRestart, this),
'disabled': !status.enabled
}, ['\u21BB ', _('Restart')]),
E('a', {
'class': 'tor-btn',
'href': L.url('admin', 'services', 'tor-shield', 'circuits')

View File

@ -707,9 +707,21 @@ save_settings() {
}
# Main dispatcher
# Restart Tor Shield service
do_restart() {
json_init
/etc/init.d/tor-shield restart >/dev/null 2>&1 &
json_add_boolean "success" 1
json_add_string "message" "Tor Shield restarting"
json_dump
}
case "$1" in
list)
echo '{"status":{},"enable":{"preset":"str"},"disable":{},"circuits":{},"new_identity":{},"check_leaks":{},"hidden_services":{},"add_hidden_service":{"name":"str","local_port":"int","virtual_port":"int"},"remove_hidden_service":{"name":"str"},"exit_ip":{},"bandwidth":{},"presets":{},"bridges":{},"set_bridges":{"enabled":"bool","type":"str"},"settings":{},"save_settings":{"mode":"str","dns_over_tor":"bool","kill_switch":"bool","socks_port":"int","trans_port":"int","dns_port":"int","exit_nodes":"str","exclude_exit_nodes":"str","strict_nodes":"bool"}}'
echo '{"status":{},"enable":{"preset":"str"},"disable":{},"restart":{},"circuits":{},"new_identity":{},"check_leaks":{},"hidden_services":{},"add_hidden_service":{"name":"str","local_port":"int","virtual_port":"int"},"remove_hidden_service":{"name":"str"},"exit_ip":{},"bandwidth":{},"presets":{},"bridges":{},"set_bridges":{"enabled":"bool","type":"str"},"settings":{},"save_settings":{"mode":"str","dns_over_tor":"bool","kill_switch":"bool","socks_port":"int","trans_port":"int","dns_port":"int","exit_nodes":"str","exclude_exit_nodes":"str","strict_nodes":"bool"}}'
;;
call)
case "$2" in
@ -722,6 +734,9 @@ case "$1" in
disable)
do_disable
;;
restart)
do_restart
;;
circuits)
get_circuits
;;

View File

@ -60,7 +60,7 @@ define Package/secubox-app-magicmirror2/postinst
echo " mm2ctl install"
echo " /etc/init.d/magicmirror2 start"
echo ""
echo "Web interface: http://<router-ip>:8082"
echo "Web interface: http://<router-ip>:8085"
echo ""
echo "To manage modules:"
echo " mm2ctl module list"

View File

@ -2,7 +2,7 @@
config magicmirror2 'main'
option enabled '0'
option port '8082'
option port '8085'
option address '0.0.0.0'
option data_path '/srv/magicmirror2'
option memory_limit '512M'

View File

@ -51,7 +51,7 @@ Examples:
mm2ctl module list
mm2ctl config
Web Interface: http://<router-ip>:8082
Web Interface: http://<router-ip>:8085
EOF
}
@ -66,7 +66,7 @@ uci_set() { uci set ${CONFIG}.$1="$2" && uci commit ${CONFIG}; }
# Load configuration with defaults
load_config() {
port="$(uci_get main.port || echo 8082)"
port="$(uci_get main.port || echo 8085)"
address="$(uci_get main.address || echo 0.0.0.0)"
data_path="$(uci_get main.data_path || echo /srv/magicmirror2)"
memory_limit="$(uci_get main.memory_limit || echo 512M)"
@ -255,7 +255,7 @@ lxc_create_docker_rootfs() {
cat >> "$rootfs/opt/start-mm2.sh" << 'START'
export PATH="/usr/local/bin:/usr/bin:/bin:$PATH"
export NODE_ENV=production
export MM_PORT="${MM2_PORT:-8082}"
export MM_PORT="${MM2_PORT:-8085}"
export MM_ADDRESS="${MM2_ADDRESS:-0.0.0.0}"
MM_DIR="/opt/magic_mirror"

View File

@ -1200,49 +1200,47 @@ case "$1" in
addr=$(echo "$local" | sed 's/:[^:]*$//')
name=""; icon=""; category="other"; path=""
# First: identify by process name (most accurate)
case "$proc" in
sshd|dropbear) name="SSH"; icon="lock"; category="system" ;;
dnsmasq|named|unbound) name="DNS"; icon="globe"; category="system" ;;
haproxy) name="HAProxy"; icon="arrow"; category="proxy" ;;
nginx|uhttpd) name="Web Server"; icon="settings"; category="system" ;;
gitea) name="Gitea"; icon="git"; path=":$port"; category="app" ;;
hexo|node) [ "$port" = "4000" ] && { name="HexoJS"; icon="blog"; path=":$port"; category="app"; } ;;
crowdsec|lapi) name="CrowdSec"; icon="security"; category="security" ;;
netifyd) name="Netifyd"; icon="chart"; path=":$port"; category="monitoring" ;;
streamlit|python*)
[ "$port" = "8501" ] && { name="Streamlit"; icon="app"; path=":$port"; category="app"; }
;;
slimserver|squeezeboxserver) name="Lyrion"; icon="music"; path=":$port"; category="media" ;;
tor) name="Tor"; icon="onion"; category="privacy" ;;
cyberfeed*) name="CyberFeed"; icon="feed"; path=":$port"; category="app" ;;
metabolizer*) name="Metabolizer"; icon="blog"; path=":$port"; category="app" ;;
magicmirror*|electron) name="MagicMirror"; icon="app"; path=":$port"; category="app" ;;
picobrew*) name="PicoBrew"; icon="app"; path=":$port"; category="app" ;;
# First: identify by well-known port (most reliable for multi-service ports)
case "$port" in
22) name="SSH"; icon="lock"; category="system" ;;
53) name="DNS"; icon="globe"; category="system" ;;
80) name="HTTP"; icon="arrow"; path="/"; category="proxy" ;;
443) name="HTTPS"; icon="shield"; path="/"; category="proxy" ;;
2222) name="Gitea SSH"; icon="git"; category="app" ;;
3000) name="Gitea"; icon="git"; path=":3000"; category="app" ;;
3483) name="Squeezebox"; icon="music"; category="media" ;;
4000) name="HexoJS"; icon="blog"; path=":4000"; category="app" ;;
6060) name="CrowdSec LAPI"; icon="security"; category="security" ;;
8081) name="LuCI"; icon="settings"; path=":8081"; category="system" ;;
8085) name="MagicMirror2"; icon="app"; path=":8085"; category="app" ;;
8086) name="Netifyd"; icon="chart"; path=":8086"; category="monitoring" ;;
8404) name="HAProxy Stats"; icon="stats"; path=":8404/stats"; category="monitoring" ;;
8444) name="LuCI HTTPS"; icon="admin"; path=":8444"; category="system" ;;
8501) name="Streamlit"; icon="app"; path=":8501"; category="app" ;;
9000) name="Lyrion"; icon="music"; path=":9000"; category="media" ;;
9050) name="Tor SOCKS"; icon="onion"; category="privacy" ;;
9090) name="Lyrion CLI"; icon="music"; category="media" ;;
esac
# Fallback: identify by port number if process didn't match
# Fallback: identify by process name if port didn't match
if [ -z "$name" ]; then
case "$port" in
22) name="SSH"; icon="lock"; category="system" ;;
53) name="DNS"; icon="globe"; category="system" ;;
80) name="HTTP"; icon="arrow"; path="/"; category="proxy" ;;
443) name="HTTPS"; icon="shield"; path="/"; category="proxy" ;;
3000) name="Gitea"; icon="git"; path=":3000"; category="app" ;;
4000) name="HexoJS"; icon="blog"; path=":4000"; category="app" ;;
6060) name="CrowdSec LAPI"; icon="security"; category="security" ;;
8080) name="Web App"; icon="app"; path=":8080"; category="app" ;;
8081) name="LuCI"; icon="settings"; path=":8081"; category="system" ;;
8082) name="CyberFeed"; icon="feed"; path=":8082"; category="app" ;;
8086) name="Netifyd"; icon="chart"; path=":8086"; category="monitoring" ;;
8404) name="HAProxy Stats"; icon="stats"; path=":8404/stats"; category="monitoring" ;;
8444) name="LuCI HTTPS"; icon="admin"; path=":8444"; category="system" ;;
8501) name="Streamlit"; icon="app"; path=":8501"; category="app" ;;
9000) name="Lyrion"; icon="music"; path=":9000"; category="media" ;;
9050) name="Tor SOCKS"; icon="onion"; category="privacy" ;;
9090) name="Lyrion CLI"; icon="music"; category="media" ;;
2222) name="Gitea SSH"; icon="git"; category="app" ;;
3483) name="Squeezebox"; icon="music"; category="media" ;;
case "$proc" in
sshd|dropbear) name="SSH"; icon="lock"; category="system" ;;
dnsmasq|named|unbound) name="DNS"; icon="globe"; category="system" ;;
haproxy) name="HAProxy"; icon="arrow"; category="proxy" ;;
nginx|uhttpd) name="Web Server"; icon="settings"; category="system" ;;
gitea) name="Gitea"; icon="git"; path=":$port"; category="app" ;;
hexo|node) name="HexoJS"; icon="blog"; path=":$port"; category="app" ;;
crowdsec|lapi) name="CrowdSec"; icon="security"; category="security" ;;
netifyd) name="Netifyd"; icon="chart"; path=":$port"; category="monitoring" ;;
slimserver|squeezeboxserver) name="Lyrion"; icon="music"; path=":$port"; category="media" ;;
tor) name="Tor"; icon="onion"; category="privacy" ;;
cyberfeed*) name="CyberFeed"; icon="feed"; path=":$port"; category="app" ;;
metabolizer*) name="Metabolizer"; icon="blog"; path=":$port"; category="app" ;;
magicmirror*|electron) name="MagicMirror"; icon="app"; path=":$port"; category="app" ;;
picobrew*) name="PicoBrew"; icon="app"; path=":$port"; category="app" ;;
streamlit) name="Streamlit"; icon="app"; path=":$port"; category="app" ;;
python*) name="Python App"; icon="app"; path=":$port"; category="app" ;;
*) name="$proc"; icon=""; category="other"; path=":$port" ;;
esac
fi