Add advantech package

Former-commit-id: 8955213aef
This commit is contained in:
Owen
2026-05-25 17:56:01 -07:00
parent d2bd8cb97d
commit 7c22fe274a
13 changed files with 554 additions and 0 deletions

3
packages/advantech/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
.build
*.tgz
bin

View File

@@ -0,0 +1,53 @@
MODNAME := newt
VERSION := 1.12.5
RELEASE_URL := https://github.com/fosrl/newt/releases/download/$(VERSION)
PLATFORM ?= v4
# Map Advantech platform to newt release binary name
arch_v4 := arm64
arch_v4i := arm64
arch_v3 := arm32
arch_v2i := arm32
NEWT_ARCH := $(arch_$(PLATFORM))
ifeq ($(NEWT_ARCH),)
$(error Unknown platform '$(PLATFORM)'. Supported: v4, v4i, v3, v2i)
endif
BINARY := newt_linux_$(NEWT_ARCH)
BINDIR := bin
OUTFILE := $(MODNAME).$(PLATFORM).tgz
STAGEDIR := .build/$(MODNAME)
.PHONY: all clean
all: $(OUTFILE)
# Cache the downloaded binary in bin/ (re-download only if missing)
$(BINDIR)/$(BINARY):
mkdir -p $(BINDIR)
@echo "Downloading $(BINARY) $(VERSION) for platform $(PLATFORM)..."
wget -q -O $@ "$(RELEASE_URL)/$(BINARY)" 2>/dev/null || \
curl -fsSL -o $@ "$(RELEASE_URL)/$(BINARY)"
chmod +x $@
@echo "Binary ready: $@"
# Build the package
$(OUTFILE): $(BINDIR)/$(BINARY) $(shell find merge -type f 2>/dev/null)
@rm -rf $(STAGEDIR)
@mkdir -p $(STAGEDIR)/bin
@cp $(BINDIR)/$(BINARY) $(STAGEDIR)/bin/newt
@chmod +x $(STAGEDIR)/bin/newt
@cp -r merge/. $(STAGEDIR)/
@chmod +x \
$(STAGEDIR)/etc/init \
$(STAGEDIR)/etc/install \
$(STAGEDIR)/etc/uninstall \
$(STAGEDIR)/www/index.cgi
tar -c --owner=0 --group=0 --mtime="2001-01-01 UTC" \
-C .build $(MODNAME) | gzip -n > $@
@echo "Created: $@"
clean:
rm -rf .build $(MODNAME).*.tgz
@echo "Cleaned."

View File

@@ -0,0 +1,61 @@
MOD_PANGOLIN_SITE_ENABLED=0
MOD_PANGOLIN_SITE_ENDPOINT=
MOD_PANGOLIN_SITE_ID=
MOD_PANGOLIN_SITE_SECRET=
# Core networking
MOD_PANGOLIN_SITE_MTU=
MOD_PANGOLIN_SITE_DNS=
MOD_PANGOLIN_SITE_INTERFACE=
MOD_PANGOLIN_SITE_PORT=
MOD_PANGOLIN_SITE_UPDOWN_SCRIPT=
MOD_PANGOLIN_SITE_PREFER_ENDPOINT=
# Logging
MOD_PANGOLIN_SITE_LOG_LEVEL=
# Behavior toggles (true/false)
MOD_PANGOLIN_SITE_DISABLE_CLIENTS=
MOD_PANGOLIN_SITE_USE_NATIVE_INTERFACE=
MOD_PANGOLIN_SITE_ENFORCE_HC_CERT=
MOD_PANGOLIN_SITE_NO_CLOUD=
# Timers
MOD_PANGOLIN_SITE_PING_INTERVAL=
MOD_PANGOLIN_SITE_PING_TIMEOUT=
MOD_PANGOLIN_SITE_UDP_PROXY_IDLE_TIMEOUT=
# Docker integration
MOD_PANGOLIN_SITE_DOCKER_SOCKET=
MOD_PANGOLIN_SITE_DOCKER_ENFORCE_NETWORK_VALIDATION=
# Health / files
MOD_PANGOLIN_SITE_HEALTH_FILE=
MOD_PANGOLIN_SITE_BLUEPRINT_FILE=
MOD_PANGOLIN_SITE_PROVISIONING_BLUEPRINT_FILE=
MOD_PANGOLIN_SITE_CONFIG_FILE=
# Provisioning
MOD_PANGOLIN_SITE_PROVISIONING_KEY=
MOD_PANGOLIN_SITE_NAME=
# Auth daemon
MOD_PANGOLIN_SITE_AUTH_DAEMON_ENABLED=
MOD_PANGOLIN_SITE_AD_GENERATE_RANDOM_PASSWORD=
MOD_PANGOLIN_SITE_AD_KEY=
MOD_PANGOLIN_SITE_AD_PRINCIPALS_FILE=
MOD_PANGOLIN_SITE_AD_CA_CERT_PATH=
# Metrics / observability
MOD_PANGOLIN_SITE_METRICS_PROMETHEUS_ENABLED=
MOD_PANGOLIN_SITE_METRICS_OTLP_ENABLED=
MOD_PANGOLIN_SITE_ADMIN_ADDR=
MOD_PANGOLIN_SITE_REGION=
MOD_PANGOLIN_SITE_METRICS_ASYNC_BYTES=
MOD_PANGOLIN_SITE_PPROF_ENABLED=
# mTLS
MOD_PANGOLIN_SITE_TLS_CLIENT_CERT=
MOD_PANGOLIN_SITE_TLS_CLIENT_KEY=
MOD_PANGOLIN_SITE_TLS_CLIENT_CAS=
MOD_PANGOLIN_SITE_TLS_CLIENT_CERT_PKCS12=

View File

@@ -0,0 +1,4 @@
Pangolin Site (newt) is a WireGuard-based tunnel client that connects this router to a
Pangolin server, enabling secure remote access to local resources without opening
firewall ports. Configure the server endpoint, ID, and secret via the web
interface to establish the tunnel automatically on startup.

View File

@@ -0,0 +1,124 @@
#!/bin/sh
MODNAME=newt
PIDFILE=/tmp/newt.pid
LOGFILE=/tmp/newt.log
set_setting_raw() {
key="$1"
value="$2"
[ -f "/opt/$MODNAME/etc/settings" ] || cp "/opt/$MODNAME/etc/defaults" "/opt/$MODNAME/etc/settings" 2>/dev/null
awk -v k="$key" -v v="$value" '
BEGIN { done=0 }
index($0, k "=") == 1 { print k "=" v; done=1; next }
{ print }
END { if (!done) print k "=" v }
' "/opt/$MODNAME/etc/settings" > "/tmp/${MODNAME}.settings.tmp" && mv "/tmp/${MODNAME}.settings.tmp" "/opt/$MODNAME/etc/settings"
}
/usr/bin/logger -t $MODNAME "DEBUG: $0 $@"
case "$1" in
start)
. /opt/$MODNAME/etc/settings
if [ "$MOD_PANGOLIN_SITE_ENABLED" != "1" ]; then
echo "$MODNAME is disabled in settings, skipping start"
exit 0
fi
if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE" 2>/dev/null)" 2>/dev/null; then
echo "$MODNAME is already running (PID: $(cat "$PIDFILE"))"
exit 0
fi
# Starting newt implies enable at boot.
set_setting_raw "MOD_PANGOLIN_SITE_ENABLED" "1"
# Map module settings to Newt environment variables.
[ -n "$MOD_PANGOLIN_SITE_ENDPOINT" ] && export PANGOLIN_ENDPOINT="$MOD_PANGOLIN_SITE_ENDPOINT"
[ -n "$MOD_PANGOLIN_SITE_ID" ] && export NEWT_ID="$MOD_PANGOLIN_SITE_ID"
[ -n "$MOD_PANGOLIN_SITE_SECRET" ] && export NEWT_SECRET="$MOD_PANGOLIN_SITE_SECRET"
[ -n "$MOD_PANGOLIN_SITE_MTU" ] && export MTU="$MOD_PANGOLIN_SITE_MTU"
[ -n "$MOD_PANGOLIN_SITE_DNS" ] && export DNS="$MOD_PANGOLIN_SITE_DNS"
[ -n "$MOD_PANGOLIN_SITE_LOG_LEVEL" ] && export LOG_LEVEL="$MOD_PANGOLIN_SITE_LOG_LEVEL"
[ -n "$MOD_PANGOLIN_SITE_UPDOWN_SCRIPT" ] && export UPDOWN_SCRIPT="$MOD_PANGOLIN_SITE_UPDOWN_SCRIPT"
[ -n "$MOD_PANGOLIN_SITE_INTERFACE" ] && export INTERFACE="$MOD_PANGOLIN_SITE_INTERFACE"
[ -n "$MOD_PANGOLIN_SITE_PORT" ] && export PORT="$MOD_PANGOLIN_SITE_PORT"
[ -n "$MOD_PANGOLIN_SITE_DISABLE_CLIENTS" ] && export DISABLE_CLIENTS="$MOD_PANGOLIN_SITE_DISABLE_CLIENTS"
[ -n "$MOD_PANGOLIN_SITE_USE_NATIVE_INTERFACE" ] && export USE_NATIVE_INTERFACE="$MOD_PANGOLIN_SITE_USE_NATIVE_INTERFACE"
[ -n "$MOD_PANGOLIN_SITE_ENFORCE_HC_CERT" ] && export ENFORCE_HC_CERT="$MOD_PANGOLIN_SITE_ENFORCE_HC_CERT"
[ -n "$MOD_PANGOLIN_SITE_DOCKER_SOCKET" ] && export DOCKER_SOCKET="$MOD_PANGOLIN_SITE_DOCKER_SOCKET"
[ -n "$MOD_PANGOLIN_SITE_PING_INTERVAL" ] && export PING_INTERVAL="$MOD_PANGOLIN_SITE_PING_INTERVAL"
[ -n "$MOD_PANGOLIN_SITE_PING_TIMEOUT" ] && export PING_TIMEOUT="$MOD_PANGOLIN_SITE_PING_TIMEOUT"
[ -n "$MOD_PANGOLIN_SITE_UDP_PROXY_IDLE_TIMEOUT" ] && export NEWT_UDP_PROXY_IDLE_TIMEOUT="$MOD_PANGOLIN_SITE_UDP_PROXY_IDLE_TIMEOUT"
[ -n "$MOD_PANGOLIN_SITE_DOCKER_ENFORCE_NETWORK_VALIDATION" ] && export DOCKER_ENFORCE_NETWORK_VALIDATION="$MOD_PANGOLIN_SITE_DOCKER_ENFORCE_NETWORK_VALIDATION"
[ -n "$MOD_PANGOLIN_SITE_HEALTH_FILE" ] && export HEALTH_FILE="$MOD_PANGOLIN_SITE_HEALTH_FILE"
[ -n "$MOD_PANGOLIN_SITE_AUTH_DAEMON_ENABLED" ] && export AUTH_DAEMON_ENABLED="$MOD_PANGOLIN_SITE_AUTH_DAEMON_ENABLED"
[ -n "$MOD_PANGOLIN_SITE_AD_GENERATE_RANDOM_PASSWORD" ] && export AD_GENERATE_RANDOM_PASSWORD="$MOD_PANGOLIN_SITE_AD_GENERATE_RANDOM_PASSWORD"
[ -n "$MOD_PANGOLIN_SITE_AD_KEY" ] && export AD_KEY="$MOD_PANGOLIN_SITE_AD_KEY"
[ -n "$MOD_PANGOLIN_SITE_AD_PRINCIPALS_FILE" ] && export AD_PRINCIPALS_FILE="$MOD_PANGOLIN_SITE_AD_PRINCIPALS_FILE"
[ -n "$MOD_PANGOLIN_SITE_AD_CA_CERT_PATH" ] && export AD_CA_CERT_PATH="$MOD_PANGOLIN_SITE_AD_CA_CERT_PATH"
[ -n "$MOD_PANGOLIN_SITE_METRICS_PROMETHEUS_ENABLED" ] && export NEWT_METRICS_PROMETHEUS_ENABLED="$MOD_PANGOLIN_SITE_METRICS_PROMETHEUS_ENABLED"
[ -n "$MOD_PANGOLIN_SITE_METRICS_OTLP_ENABLED" ] && export NEWT_METRICS_OTLP_ENABLED="$MOD_PANGOLIN_SITE_METRICS_OTLP_ENABLED"
[ -n "$MOD_PANGOLIN_SITE_ADMIN_ADDR" ] && export NEWT_ADMIN_ADDR="$MOD_PANGOLIN_SITE_ADMIN_ADDR"
[ -n "$MOD_PANGOLIN_SITE_REGION" ] && export NEWT_REGION="$MOD_PANGOLIN_SITE_REGION"
[ -n "$MOD_PANGOLIN_SITE_METRICS_ASYNC_BYTES" ] && export NEWT_METRICS_ASYNC_BYTES="$MOD_PANGOLIN_SITE_METRICS_ASYNC_BYTES"
[ -n "$MOD_PANGOLIN_SITE_PPROF_ENABLED" ] && export NEWT_PPROF_ENABLED="$MOD_PANGOLIN_SITE_PPROF_ENABLED"
[ -n "$MOD_PANGOLIN_SITE_TLS_CLIENT_CERT" ] && export TLS_CLIENT_CERT="$MOD_PANGOLIN_SITE_TLS_CLIENT_CERT"
[ -n "$MOD_PANGOLIN_SITE_TLS_CLIENT_KEY" ] && export TLS_CLIENT_KEY="$MOD_PANGOLIN_SITE_TLS_CLIENT_KEY"
[ -n "$MOD_PANGOLIN_SITE_TLS_CLIENT_CAS" ] && export TLS_CLIENT_CAS="$MOD_PANGOLIN_SITE_TLS_CLIENT_CAS"
[ -n "$MOD_PANGOLIN_SITE_TLS_CLIENT_CERT_PKCS12" ] && export TLS_CLIENT_CERT_PKCS12="$MOD_PANGOLIN_SITE_TLS_CLIENT_CERT_PKCS12"
[ -n "$MOD_PANGOLIN_SITE_BLUEPRINT_FILE" ] && export BLUEPRINT_FILE="$MOD_PANGOLIN_SITE_BLUEPRINT_FILE"
[ -n "$MOD_PANGOLIN_SITE_PROVISIONING_BLUEPRINT_FILE" ] && export PROVISIONING_BLUEPRINT_FILE="$MOD_PANGOLIN_SITE_PROVISIONING_BLUEPRINT_FILE"
[ -n "$MOD_PANGOLIN_SITE_NO_CLOUD" ] && export NO_CLOUD="$MOD_PANGOLIN_SITE_NO_CLOUD"
[ -n "$MOD_PANGOLIN_SITE_PROVISIONING_KEY" ] && export NEWT_PROVISIONING_KEY="$MOD_PANGOLIN_SITE_PROVISIONING_KEY"
[ -n "$MOD_PANGOLIN_SITE_NAME" ] && export NEWT_NAME="$MOD_PANGOLIN_SITE_NAME"
[ -n "$MOD_PANGOLIN_SITE_CONFIG_FILE" ] && export CONFIG_FILE="$MOD_PANGOLIN_SITE_CONFIG_FILE"
export NEWT_SYSTEM_SUBSTRATE="ADVANTECH_ROUTER_APP"
export NEWT_PID_FILE="$PIDFILE"
echo "Starting $MODNAME..."
if [ -n "$MOD_PANGOLIN_SITE_PREFER_ENDPOINT" ]; then
/opt/$MODNAME/bin/newt --prefer-endpoint "$MOD_PANGOLIN_SITE_PREFER_ENDPOINT" >> "$LOGFILE" 2>&1 &
else
/opt/$MODNAME/bin/newt >> "$LOGFILE" 2>&1 &
fi
echo $! > "$PIDFILE"
echo "Started $MODNAME (PID: $(cat "$PIDFILE"))"
exit 0
;;
stop)
echo "Stopping $MODNAME..."
if [ -f "$PIDFILE" ]; then
kill "$(cat "$PIDFILE" 2>/dev/null)" 2>/dev/null
rm -f "$PIDFILE"
fi
killall newt 2>/dev/null
# Stopping newt implies disable at boot.
set_setting_raw "MOD_PANGOLIN_SITE_ENABLED" "0"
echo "Stopped $MODNAME"
exit 0
;;
restart)
$0 stop
sleep 1
# Restart should remain enabled after reboot.
set_setting_raw "MOD_PANGOLIN_SITE_ENABLED" "1"
$0 start
;;
status)
if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE" 2>/dev/null)" 2>/dev/null; then
echo "$MODNAME is running (PID: $(cat "$PIDFILE"))"
exit 0
else
echo "$MODNAME is not running"
exit 1
fi
;;
defaults)
cp /opt/$MODNAME/etc/defaults /opt/$MODNAME/etc/settings 2>/dev/null
;;
*)
echo "Usage: $0 {start|stop|restart|status|defaults}"
exit 1
esac

View File

@@ -0,0 +1,15 @@
#!/bin/sh
MODNAME=newt
# Ensure scripts are executable
chmod +x /opt/$MODNAME/etc/init
chmod +x /opt/$MODNAME/etc/install
chmod +x /opt/$MODNAME/etc/uninstall
chmod +x /opt/$MODNAME/bin/newt
chmod +x /opt/$MODNAME/www/index.cgi
# Secure the web interface with router authentication
ln -sf /etc/htpasswd /opt/$MODNAME/www/.htpasswd
exit 0

View File

@@ -0,0 +1 @@
Pangolin Site

View File

@@ -0,0 +1 @@
Tunnel client for Pangolin secure remote access.

View File

@@ -0,0 +1,9 @@
#!/bin/sh
MODNAME=newt
rm -f /opt/$MODNAME/www/.htpasswd
rm -f /tmp/newt.pid
rm -f /tmp/newt.log
exit 0

View File

@@ -0,0 +1 @@
1.12.5

View File

@@ -0,0 +1,282 @@
#!/bin/sh
MODNAME=newt
SETTINGS=/opt/$MODNAME/etc/settings
LOGFILE=/tmp/newt.log
PIDFILE=/tmp/newt.pid
# Escape special HTML characters
htmlesc() {
printf '%s' "$1" | sed \
-e 's/&/\&/g' \
-e 's/</\&lt;/g' \
-e 's/>/\&gt;/g' \
-e 's/"/\&quot;/g'
}
# Decode URL-encoded form values (handles common chars in endpoints/ids/secrets)
urldecode() {
printf '%s' "$1" | sed \
-e 's/+/ /g' \
-e 's/%3[Aa]/:/g' -e 's/%3[aa]/:/g' \
-e 's/%2[Ff]/\//g' -e 's/%2[ff]/\//g' \
-e 's/%40/@/g' \
-e 's/%2[Ee]/./g' \
-e 's/%2[Dd]/-/g' \
-e 's/%5[Ff]/_/g' \
-e 's/%3[Dd]/=/g' \
-e 's/%3[Ff]/?/g' \
-e 's/%23/#/g' \
-e 's/%25/%/g'
}
# Extract a named field from URL-encoded POST data (awk splits on & without tr)
get_field() {
printf '%s' "$2" | awk -v f="${1}=" 'BEGIN{RS="&"} index($0,f)==1 {print substr($0,length(f)+1); exit}'
}
# Check whether newt process is alive
is_running() {
[ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE" 2>/dev/null)" 2>/dev/null
}
ensure_settings_file() {
[ -f "$SETTINGS" ] && return
if [ -f "/opt/$MODNAME/etc/defaults" ]; then
cp "/opt/$MODNAME/etc/defaults" "$SETTINGS" 2>/dev/null
else
printf 'MOD_PANGOLIN_SITE_ENABLED=0\n' > "$SETTINGS"
fi
}
set_setting_raw() {
key="$1"
value="$2"
ensure_settings_file
awk -v k="$key" -v v="$value" '
BEGIN { done=0 }
index($0, k "=") == 1 { print k "=" v; done=1; next }
{ print }
END { if (!done) print k "=" v }
' "$SETTINGS" > "$SETTINGS.tmp" && mv "$SETTINGS.tmp" "$SETTINGS"
}
quote_sh_value() {
printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'
}
# ── Read POST body ──────────────────────────────────────────────────────────
POST_DATA=""
if [ "$REQUEST_METHOD" = "POST" ] && [ -n "$CONTENT_LENGTH" ] && [ "$CONTENT_LENGTH" -gt 0 ] 2>/dev/null; then
POST_DATA=$(dd bs="$CONTENT_LENGTH" count=1 2>/dev/null)
fi
# ── Load current settings ───────────────────────────────────────────────────
MOD_PANGOLIN_SITE_ENABLED=0
MOD_PANGOLIN_SITE_ENDPOINT=""
MOD_PANGOLIN_SITE_ID=""
MOD_PANGOLIN_SITE_SECRET=""
[ -f "$SETTINGS" ] && . "$SETTINGS"
# ── Handle actions ──────────────────────────────────────────────────────────
if [ -n "$POST_DATA" ]; then
ACTION=$(get_field "action" "$POST_DATA")
case "$ACTION" in
save)
EP=$(urldecode "$(get_field "endpoint" "$POST_DATA")")
ID=$(urldecode "$(get_field "id" "$POST_DATA")")
SEC=$(urldecode "$(get_field "secret" "$POST_DATA")")
set_setting_raw "MOD_PANGOLIN_SITE_ENDPOINT" "\"$(quote_sh_value "$EP")\""
set_setting_raw "MOD_PANGOLIN_SITE_ID" "\"$(quote_sh_value "$ID")\""
set_setting_raw "MOD_PANGOLIN_SITE_SECRET" "\"$(quote_sh_value "$SEC")\""
;;
start)
set_setting_raw "MOD_PANGOLIN_SITE_ENABLED" "1"
/opt/$MODNAME/etc/init start >/dev/null 2>&1
;;
stop)
/opt/$MODNAME/etc/init stop >/dev/null 2>&1
set_setting_raw "MOD_PANGOLIN_SITE_ENABLED" "0"
;;
restart)
set_setting_raw "MOD_PANGOLIN_SITE_ENABLED" "1"
/opt/$MODNAME/etc/init restart >/dev/null 2>&1
;;
clearlog)
printf '' > "$LOGFILE"
;;
esac
fi
# Reload settings after actions.
MOD_PANGOLIN_SITE_ENABLED=0
MOD_PANGOLIN_SITE_ENDPOINT=""
MOD_PANGOLIN_SITE_ID=""
MOD_PANGOLIN_SITE_SECRET=""
[ -f "$SETTINGS" ] && . "$SETTINGS"
# ── Status ──────────────────────────────────────────────────────────────────
if is_running; then
STATUS_TEXT="Running"
STATUS_CLASS="running"
DISABLED="disabled"
SETTINGS_HINT='<div class="notice">Stop Newt first to edit settings.</div>'
else
STATUS_TEXT="Stopped"
STATUS_CLASS="stopped"
DISABLED=""
SETTINGS_HINT=""
fi
EP_ESC=$(htmlesc "$MOD_PANGOLIN_SITE_ENDPOINT")
ID_ESC=$(htmlesc "$MOD_PANGOLIN_SITE_ID")
SEC_ESC=$(htmlesc "$MOD_PANGOLIN_SITE_SECRET")
EP_INPUT_ESC="$EP_ESC"
[ -z "$MOD_PANGOLIN_SITE_ENDPOINT" ] && EP_INPUT_ESC="https://app.pangolin.net"
# ── Log (escape HTML and dollar signs to prevent shell expansion in heredoc) ─
LOG_HTML=""
if [ -f "$LOGFILE" ]; then
LOG_HTML=$(tail -100 "$LOGFILE" 2>/dev/null | sed \
-e 's/&/\&amp;/g' \
-e 's/</\&lt;/g' \
-e 's/>/\&gt;/g' \
-e 's/\$/\&#36;/g')
fi
# ── Output ──────────────────────────────────────────────────────────────────
printf 'Content-type: text/html\r\n\r\n'
# Static head — split around the conditional refresh meta tag
cat << 'HEAD_START'
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
HEAD_START
# Only auto-refresh while newt is running (avoids wiping settings form mid-edit)
[ "$STATUS_CLASS" = "running" ] && printf '<meta http-equiv="refresh" content="10">\n'
cat << 'STATIC_HEAD'
<title>Pangolin Site</title>
<style>
*{box-sizing:border-box}
body{margin:0;padding:8px;border-top:3px solid #79BD28;background:#fff;color:#000}
body,table,tr,td,a,input,select,textarea,button{font-family:Verdana,Arial,Helvetica,sans-serif;font-size:12px}
a{color:#004280;text-decoration:none}
a:hover{text-decoration:underline}
.topline{display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;margin-bottom:6px}
.app-title{background:#004280;color:#fff;font-weight:bold;padding:6px 8px;border:1px solid #00315f;border-bottom:none}
.window{background:#f4f4f4;border:2px solid #004280;padding:0}
.section{border-top:1px solid #9fb7d5}
.section:first-child{border-top:none}
.section-head{background:#c0e0ff;color:#000;font-weight:bold;padding:5px 8px;border-bottom:1px solid #9fb7d5;display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap}
.section-body{padding:10px 8px}
.badge{display:inline-block;padding:2px 10px;border-radius:2px;color:#fff;font-weight:bold;line-height:1.4}
.running{background:#008000}
.stopped{background:#800000}
.row{display:flex;align-items:center;gap:8px;margin:6px 0}
.row label{width:90px;font-weight:bold}
.row input{width:360px;max-width:100%;padding:2px 4px;border:1px solid #7f9db9;background:#fff;height:23px}
.btn{height:24px;padding:0 10px;border:1px solid #7f9db9;background:#efefef;color:#000;cursor:pointer}
.btn:disabled{color:#777;background:#e5e5e5}
.btn + .btn{margin-left:6px}
.btn-start{border-color:#2d7a2d;background:#dff0df}
.btn-stop{border-color:#8a2c2c;background:#f7dddd}
.btn-restart{border-color:#9a6a1f;background:#f7ecd8}
.btn-save{border-color:#2e5f92;background:#dce9f8}
.hint{color:#808080;font-weight:normal}
.notice{margin:0 0 10px 0;color:#ff0000;font-style:italic}
.log{background:#fff;border:1px solid #7f9db9;color:#000;font-family:monospace;font-size:12px;line-height:1.35;padding:6px;height:300px;overflow-y:auto;white-space:pre-wrap;word-break:break-word}
</style>
</head>
<body>
<div class="topline">
<a href="/">&laquo; Back to Router</a>
</div>
<div class="app-title">Router Apps - Pangolin Site</div>
<div class="window">
STATIC_HEAD
# Status card (double-quoted heredoc — variables expand)
cat << STATUS_CARD
<div class="section">
<div class="section-head">Status</div>
<div class="section-body">
<div><span class="badge ${STATUS_CLASS}">${STATUS_TEXT}</span></div>
<div style="margin-top:8px">
<form method="post" style="display:inline">
<input type="hidden" name="action" value="start">
<button class="btn btn-start" type="submit">Start</button>
</form>
<form method="post" style="display:inline">
<input type="hidden" name="action" value="stop">
<button class="btn btn-stop" type="submit">Stop</button>
</form>
<form method="post" style="display:inline">
<input type="hidden" name="action" value="restart">
<button class="btn btn-restart" type="submit">Restart</button>
</form>
</div>
</div>
</div>
STATUS_CARD
# Settings card
cat << SETTINGS_CARD
<div class="section">
<div class="section-head">Settings</div>
<div class="section-body">
${SETTINGS_HINT}<form method="post">
<input type="hidden" name="action" value="save">
<div class="row">
<label>Endpoint</label>
<input type="text" name="endpoint" value="${EP_INPUT_ESC}" ${DISABLED}>
</div>
<div class="row">
<label>ID</label>
<input type="text" name="id" value="${ID_ESC}" ${DISABLED}>
</div>
<div class="row">
<label>Secret</label>
<input type="password" name="secret" value="${SEC_ESC}" ${DISABLED}>
</div>
<div style="margin-top:12px">
<button class="btn btn-save" type="submit" ${DISABLED}>Save</button>
</div>
</form>
</div>
</div>
SETTINGS_CARD
# Log card header
cat << 'LOG_HEADER'
<div class="section">
<div class="section-head">
<span>Log <span class="hint">(last 100 lines, auto-refreshes every 10s while running)</span></span>
<span style="display:inline-flex;gap:6px">
<form method="post" style="margin:0">
<input type="hidden" name="action" value="clearlog">
<button class="btn" type="submit">Clear Log</button>
</form>
<form method="get" style="margin:0">
<button class="btn" type="submit">Refresh</button>
</form>
</span>
</div>
<div class="section-body">
<div class="log">
LOG_HEADER
# Log content — printed with printf to prevent any shell expansion
printf '%s' "$LOG_HTML"
# Close tags (static)
cat << 'STATIC_FOOTER'
</div>
</div>
</div>
</body>
</html>
STATIC_FOOTER

View File

View File