Skip to content

Commit d796ee4

Browse files
ADFA-4839: dashboard packaging — compiled JS, loopback bind, supervision, dev push
Phase 2 hardening of the in-server dashboard. - Run compiled JS: tsconfig outDir=dist + 'build'/'start' scripts; dash-node service now runs 'node dist/server.js' (was 'npx ts-node server.ts') with a respawn supervisor (auto-restart on crash, PID-tracked so stop ends the loop cleanly) and a 'health' probe (GET /api/kiwix/jobs). Bootstrap runs 'yarn build' after install. - Resolve views/public via process.cwd() (the service cd's into /library/dashboard) so paths hold once __dirname is .../dist. - Bind the server to 127.0.0.1: nginx already proxies from loopback, so :4000 no longer needs to listen on the device's network interfaces (closes network-reachable :4000). - tools/dev-push-dashboard.sh: push static/dashboard into an installed rootfs + rebuild + restart, for iterating without the ~2h rootfs rebuild (tar excludes node_modules/dist). Compile validated (tsc emits dist cleanly). Runtime under plain node (native better-sqlite3), the supervisor stop/health shell paths, and the provisioning 'yarn build' need on-device verification in the rebuild.
1 parent 0db7bf4 commit d796ee4

6 files changed

Lines changed: 92 additions & 29 deletions

File tree

‎iiab-android‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -442,10 +442,12 @@ install_iiaboa_dashboard() {
442442
# translated by proot (rc=23 under proot on 6.6+ kernels); cp -a is reliable.
443443
cp -a "${dash_src}/." "$dash_dest/"
444444

445-
# Install node dependencies
445+
# Install node dependencies and compile TS->JS (ADFA-4839: dash-node runs dist/server.js).
446446
log "Installing Node dependencies for the dashboard..."
447447
log "Using Yarn for installation..."
448448
(cd "$dash_dest" && yarn install)
449+
log "Building the dashboard (TypeScript -> dist/)..."
450+
(cd "$dash_dest" && yarn build)
449451

450452
# Books dashboard
451453
log "Pre-seeding Gutenberg Catalog Database..."

‎static/dashboard/package.json‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
"main": "index.js",
66
"scripts": {
77
"test": "node --require ts-node/register --test sockets/maps.socket.test.ts test/lang-parity.test.ts",
8-
"typecheck": "tsc --noEmit"
8+
"typecheck": "tsc --noEmit",
9+
"build": "tsc",
10+
"start": "node dist/server.js"
911
},
1012
"keywords": [],
1113
"author": "AppDevForAll",

‎static/dashboard/server.ts‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,12 @@ app.use(helmet({
3131
contentSecurityPolicy: false
3232
}));
3333

34-
// EJS views and static files configuration
34+
// EJS views and static files configuration.
35+
// ADFA-4839: resolve against the working dir (the dash-node service does `cd /library/dashboard`),
36+
// not __dirname — once compiled, __dirname is .../dist, but views/ and public/ live at the root.
3537
app.set('view engine', 'ejs');
36-
app.set('views', path.join(__dirname, 'views'));
37-
app.use(express.static(path.join(__dirname, 'public')));
38+
app.set('views', path.join(process.cwd(), 'views'));
39+
app.use(express.static(path.join(process.cwd(), 'public')));
3840

3941
// ADFA-4838: JSON body parsing + the REST content API.
4042
app.use(express.json());
@@ -61,7 +63,10 @@ io.on('connection', (socket: Socket) => {
6163
});
6264

6365
const PORT = 4000;
64-
server.listen(PORT, () => {
66+
// ADFA-4839: bind to loopback only. nginx (localhost) proxies /dashboard, /api and
67+
// /socket.io to us; there is no reason to expose :4000 on the device's network
68+
// interfaces (Node's default host would). This closes the network-reachable :4000.
69+
server.listen(PORT, '127.0.0.1', () => {
6570
console.log(`===========================================`);
6671
console.log(`K2Go Dashboard active on port ${PORT}`);
6772
console.log(`===========================================`);

‎static/dashboard/tsconfig.json‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
"forceConsistentCasingInFileNames": true,
88
"strict": true,
99
"skipLibCheck": true,
10-
"rootDir": "."
11-
}
10+
"rootDir": ".",
11+
"outDir": "dist"
12+
},
13+
"exclude": ["node_modules", "dist", "**/*.test.ts"]
1214
}

‎static/pdsm/dash-node‎

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,21 @@
11
#!/bin/sh
22
# /usr/local/pdsm/services-available/dash-node
3-
# Simple PDSM service wrapper for IIAB-oA Dashboard (Node.js)
3+
# PDSM service wrapper for the IIAB-oA Dashboard (Node.js).
4+
# ADFA-4839: run the COMPILED JS (dist/server.js) instead of `ts-node server.ts`,
5+
# with a small respawn supervisor and a health probe.
46

57
PDSM_COMMON="/usr/local/pdsm/lib/pdsm-common.sh"
68
[ -r "$PDSM_COMMON" ] && . "$PDSM_COMMON"
79

810
SERVICE_NAME="dash-node"
9-
1011
APP_DIR="/library/dashboard"
11-
# Feature detection: we use npx by default, but if not we rely on
12-
# yarn as engine (currently @32 bits)
13-
if command -v npx >/dev/null 2>&1; then
14-
APP_CMD="npx ts-node server.ts"
15-
else
16-
APP_CMD="yarn run ts-node server.ts"
17-
fi
12+
ENTRY="dist/server.js"
1813
LOGFILE="/var/log/dash-node.log"
14+
SUP_PIDFILE="/var/run/dash-node.sup.pid"
1915

2016
running() {
21-
# Look directly for the actual process name, ignoring the PID file
22-
pgrep -f "ts-node server.ts" >/dev/null 2>&1
17+
# Match the actual server process, not the supervisor shell.
18+
pgrep -f "node $ENTRY" >/dev/null 2>&1
2319
}
2420

2521
start() {
@@ -30,9 +26,16 @@ start() {
3026

3127
echo "[pdsm:$SERVICE_NAME] starting..."
3228
cd "$APP_DIR" || return 1
33-
nohup $APP_CMD > "$LOGFILE" 2>&1 &
3429

35-
for i in $(seq 1 "${PDSM_START_TIMEOUT:-5}"); do
30+
# Supervisor: respawn the server if it exits. Its PID is recorded so stop() can end
31+
# the respawn loop before killing the server. ENTRY/LOGFILE are passed via the env so
32+
# the inner shell expands them at runtime (avoids brittle nested quoting).
33+
ENTRY="$ENTRY" LOGFILE="$LOGFILE" nohup sh -c \
34+
'while :; do node "$ENTRY" >> "$LOGFILE" 2>&1; echo "[dash-node] exited ($?); respawning in 2s" >> "$LOGFILE"; sleep 2; done' \
35+
>/dev/null 2>&1 &
36+
echo $! > "$SUP_PIDFILE"
37+
38+
for i in $(seq 1 "${PDSM_START_TIMEOUT:-8}"); do
3639
if running; then
3740
echo "[pdsm:$SERVICE_NAME] started"
3841
return 0
@@ -45,14 +48,19 @@ start() {
4548
}
4649

4750
stop() {
51+
# Kill the supervisor first so it stops respawning, then the server itself.
52+
if [ -f "$SUP_PIDFILE" ]; then
53+
kill "$(cat "$SUP_PIDFILE")" 2>/dev/null || true
54+
rm -f "$SUP_PIDFILE"
55+
fi
56+
4857
if ! running; then
4958
echo "[pdsm:$SERVICE_NAME] already stopped"
5059
return 0
5160
fi
5261

5362
echo "[pdsm:$SERVICE_NAME] stopping..."
54-
# Terminate by name
55-
pkill -f "ts-node server.ts" 2>/dev/null || true
63+
pkill -f "node $ENTRY" 2>/dev/null || true
5664

5765
for i in $(seq 1 "${PDSM_START_TIMEOUT:-5}"); do
5866
if ! running; then
@@ -62,24 +70,33 @@ stop() {
6270
sleep 1
6371
done
6472

65-
# Force kill if it resists
66-
pkill -9 -f "ts-node server.ts" 2>/dev/null || true
73+
pkill -9 -f "node $ENTRY" 2>/dev/null || true
6774
echo "[pdsm:$SERVICE_NAME] forced stop"
6875
return 1
6976
}
7077

7178
status() {
7279
if running; then
73-
real_pid=$(pgrep -f "ts-node server.ts" | head -n 1)
74-
echo "[pdsm:$SERVICE_NAME] running (PID: $real_pid)"
80+
echo "[pdsm:$SERVICE_NAME] running (PID: $(pgrep -f "node $ENTRY" | head -n 1))"
7581
return 0
7682
fi
7783
echo "[pdsm:$SERVICE_NAME] not running"
7884
return 3
7985
}
8086

87+
health() {
88+
# Liveness: the REST API answers 2xx on loopback (the jobs list is always 200).
89+
if command -v curl >/dev/null 2>&1 && \
90+
curl -fs -o /dev/null "http://127.0.0.1:4000/api/kiwix/jobs"; then
91+
echo "[pdsm:$SERVICE_NAME] healthy"
92+
return 0
93+
fi
94+
echo "[pdsm:$SERVICE_NAME] unhealthy"
95+
return 1
96+
}
97+
8198
case "$1" in
82-
start|stop|status) "$1" ;;
99+
start|stop|status|health) "$1" ;;
83100
restart) stop; start ;;
84-
*) echo "Usage: $0 {start|stop|restart|status}" >&2; exit 1 ;;
101+
*) echo "Usage: $0 {start|stop|restart|status|health}" >&2; exit 1 ;;
85102
esac

‎tools/dev-push-dashboard.sh‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#!/bin/sh
2+
# tools/dev-push-dashboard.sh — ADFA-4839
3+
#
4+
# Dev helper to push an updated dashboard into an INSTALLED rootfs and restart the
5+
# service, WITHOUT the ~2h rootfs rebuild. Run it from INSIDE the proot (where pdsm and
6+
# /library exist), after `git pull` in your clone.
7+
#
8+
# sh tools/dev-push-dashboard.sh [CLONE_DIR]
9+
#
10+
# CLONE_DIR defaults to the repo this script lives in. It copies static/dashboard into
11+
# /library/dashboard (preserving node_modules), rebuilds (yarn build) and restarts
12+
# dash-node. Mirrors what the bootstrap's install_iiaboa_dashboard does.
13+
set -eu
14+
15+
CLONE_DIR="${1:-$(cd "$(dirname "$0")/.." && pwd)}"
16+
SRC="$CLONE_DIR/static/dashboard"
17+
DEST="/library/dashboard"
18+
19+
[ -d "$SRC" ] || { echo "ERROR: $SRC not found (is CLONE_DIR right?)" >&2; exit 1; }
20+
[ -d "$DEST" ] || { echo "ERROR: $DEST not found (is the system installed and are you inside the proot?)" >&2; exit 1; }
21+
22+
echo "[dev-push] copying $SRC -> $DEST (excluding node_modules)..."
23+
# tar is proot-safe (rsync's fchmodat2 isn't translated); exclude node_modules + dist so
24+
# we never clobber the installed native better-sqlite3 build.
25+
( cd "$SRC" && tar --exclude=node_modules --exclude=dist -cf - . ) | ( cd "$DEST" && tar -xf - )
26+
27+
echo "[dev-push] installing deps + building (TS -> dist/)..."
28+
( cd "$DEST" && yarn install && yarn build )
29+
30+
echo "[dev-push] restarting dash-node + nginx..."
31+
/usr/local/bin/pdsm restart dash-node
32+
/usr/local/bin/pdsm restart nginx
33+
34+
echo "[dev-push] done. Health:"
35+
/usr/local/pdsm/services-available/dash-node health || true

0 commit comments

Comments
 (0)