/root/jfmsrv01-build/05_handoff_portal.sh
#!/usr/bin/env bash
# =============================================================================
# JFM Server 01 - Handoff Portal Setup
# Creates the public developer handoff area on port 9002
#
# Run as root on jfmsrv01
# =============================================================================
set -euo pipefail
HANDOFF_PORT="9002"
HANDOFF_SLUG="vqngx6j8dc9q1lvt7g2a7a646q2t"
HANDOFF_BASE="/var/www/handoff"
HANDOFF_PUBLIC="$HANDOFF_BASE/public/$HANDOFF_SLUG"
SCRIPTS_BASE="/opt/jfmsrv01/scripts"
APP_USER="jfmsrv01"
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m'
info() { echo -e "${BLUE}[INFO]${NC} $*"; }
success() { echo -e "${GREEN}[OK]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
[[ $EUID -ne 0 ]] && { echo "Run as root."; exit 1; }
info "Setting up handoff portal on port $HANDOFF_PORT"
# Create directories
mkdir -p "$HANDOFF_PUBLIC"/{server,mainsite,admin,transcription,shared}
mkdir -p "$SCRIPTS_BASE"
chown -R www-data:www-data "$HANDOFF_BASE"
chmod -R 755 "$HANDOFF_BASE"
success "Directories created"
# Nginx config
cat > /etc/nginx/sites-available/handoff <<EOF
server {
listen $HANDOFF_PORT;
server_name _;
allow 192.168.0.0/16;
allow 127.0.0.1;
deny all;
location /$HANDOFF_SLUG/ {
alias $HANDOFF_PUBLIC/;
autoindex on;
autoindex_exact_size off;
autoindex_localtime on;
add_header Cache-Control "no-store, no-cache, must-revalidate";
expires -1;
try_files \$uri \$uri/ =404;
}
location = / { return 301 http://\$host:$HANDOFF_PORT/$HANDOFF_SLUG/; }
location / { return 301 http://\$host:$HANDOFF_PORT/$HANDOFF_SLUG/; }
access_log /var/log/nginx/handoff_access.log;
error_log /var/log/nginx/handoff_error.log;
}
EOF
ln -sf /etc/nginx/sites-available/handoff /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
ufw allow from 192.168.0.0/16 to any port "$HANDOFF_PORT" comment "Handoff portal internal" 2>/dev/null || true
success "Nginx configured"
# Write the publish script
cat > "$SCRIPTS_BASE/publish_handoff.sh" << 'PUBLISHEOF'
#!/usr/bin/env bash
# =============================================================================
# Handoff Portal Publisher
# Regenerates all handoff HTML files for every project.
# Run automatically after every patch. Pass "db" to also backup database.
# =============================================================================
set -euo pipefail
HANDOFF_SLUG="vqngx6j8dc9q1lvt7g2a7a646q2t"
PUB="/var/www/handoff/public/$HANDOFF_SLUG"
SCRIPTS="/opt/jfmsrv01/scripts"
TS=$(date +%Y%m%d_%H%M%S)
RND=$RANDOM
DB_CHANGED="${1:-}"
log() { echo "$(date '+%H:%M:%S') [HANDOFF] $*"; }
# Load DB creds
[[ -f /root/jfmsrv01_credentials.txt ]] && source /root/jfmsrv01_credentials.txt
# -----------------------------------------------------------------------------
# Helpers
# -----------------------------------------------------------------------------
sanitise() {
[REDACTED SECRET-LIKE LINE]
sed \
-e 's/\(PASSWORD\s*=[REDACTED]
-e 's/\(PASSWORD\s*=[REDACTED]
-e 's/\(SECRET\s*=[REDACTED]
-e 's/\(KEY\s*=\s*\).*/\1[REDACTED]/Ig' \
-e 's/\(TOKEN\s*=[REDACTED]
-e 's/\(APP_KEY\s*=[REDACTED]
-e 's/\(REDIS_PASSWORD\s*=[REDACTED]
-e 's/\(DB_PASSWORD\s*=[REDACTED]
-e 's/\(MAIL_PASSWORD\s*=[REDACTED]
-e 's/\(requirepass\s\+\)\S*/\1[REDACTED]/Ig'
}
esc() { sed 's/</\</g; s/>/\>/g'; }
html_page() {
local TITLE="$1"
local BODY="$2"
cat <<HTMLEOF
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>$TITLE</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Courier New',monospace;background:#060910;color:#cdd9e5;padding:0;font-size:13px;line-height:1.6}
header{background:#0d1117;border-bottom:1px solid #1c2333;padding:16px 32px;display:flex;align-items:center;gap:16px;position:sticky;top:0;z-index:100}
.logo{font-family:Georgia,serif;font-size:20px;font-weight:700;color:#2f81f7}
.title{color:#cdd9e5;font-size:14px}
.meta{margin-left:auto;font-size:11px;color:#768390}
nav{background:#0d1117;border-bottom:1px solid #1c2333;padding:0 32px;display:flex;gap:0;overflow-x:auto}
nav a{color:#768390;text-decoration:none;padding:10px 16px;font-size:12px;border-bottom:2px solid transparent;white-space:nowrap}
nav a:hover{color:#cdd9e5;border-bottom-color:#2f81f7}
main{padding:32px;max-width:1600px}
h2{font-family:Georgia,serif;color:#2f81f7;font-size:16px;margin:32px 0 12px;border-bottom:1px solid #1c2333;padding-bottom:8px}
h3{color:#79c0ff;font-size:13px;margin:20px 0 8px;text-transform:uppercase;letter-spacing:1px}
pre{background:#0d1117;border:1px solid #1c2333;padding:16px;overflow-x:auto;border-radius:4px;white-space:pre-wrap;word-break:break-all;font-size:12px;max-height:600px;overflow-y:auto}
.file-block{background:#0d1117;border:1px solid #1c2333;border-radius:4px;margin-bottom:16px;overflow:hidden}
.file-header{background:#111820;padding:8px 16px;font-size:11px;color:#79c0ff;border-bottom:1px solid #1c2333;display:flex;justify-content:space-between}
.file-content{padding:16px;overflow-x:auto}
a{color:#2f81f7;text-decoration:none}
a:hover{text-decoration:underline}
.ok{color:#3fb950}.warn{color:#d29922}.err{color:#f85149}
.grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}
table{border-collapse:collapse;width:100%;font-size:12px}
td,th{border:1px solid #1c2333;padding:6px 12px;text-align:left}
th{background:#111820;color:#79c0ff}
tr:hover{background:#0d1117}
.timestamp{padding:16px 32px;background:#080c12;border-top:1px solid #1c2333;font-size:11px;color:#768390}
.warn-bar{background:#2d1f00;border-bottom:1px solid #6e4c00;padding:8px 32px;font-size:12px;color:#d29922}
@media(max-width:768px){.grid{grid-template-columns:1fr}}
</style>
</head>
<body>
<header>
<div class="logo">jfmsrv01</div>
<div class="title">/ $TITLE</div>
<div class="meta">$(date '+%Y-%m-%d %H:%M:%S %Z')</div>
</header>
<div class=[REDACTED]
$BODY
<div class="timestamp">Auto-generated by publish_handoff.sh | $TS</div>
</body></html>
HTMLEOF
}
file_block() {
local LABEL="$1"
local CONTENT="$2"
echo "<div class=\"file-block\"><div class=\"file-header\"><span>$LABEL</span></div><div class=\"file-content\"><pre>$CONTENT</pre></div></div>"
}
# -----------------------------------------------------------------------------
# SERVER PROJECT
# -----------------------------------------------------------------------------
log "Generating: server"
mkdir -p "$PUB/server"
# --- Service status
SVC_BODY="<main>"
SVC_BODY+="<h2>Service Status</h2>"
SVC_BODY+="$(file_block 'systemctl status' "$(systemctl status nginx postgresql redis-server clamav-daemon supervisor php8.3-fpm 2>&1 | esc)")"
SVC_BODY+="<h2>Firewall Rules</h2>"
SVC_BODY+="$(file_block 'ufw status verbose' "$(ufw status verbose 2>&1 | esc)")"
SVC_BODY+="<h2>Cron Jobs</h2>"
SVC_BODY+="$(file_block '/etc/cron.d/' "$(ls -la /etc/cron.d/ 2>&1 | esc)")"
SVC_BODY+="$(file_block 'cron.d contents' "$(cat /etc/cron.d/jfmsrv01* 2>/dev/null | esc || echo 'none yet')")"
SVC_BODY+="<h2>Open Ports</h2>"
SVC_BODY+="$(file_block 'ss -tlnp' "$(ss -tlnp 2>&1 | esc)")"
SVC_BODY+="</main>"
html_page "Server Status" "$SVC_BODY" > "$PUB/server/status.html"
# --- Nginx configs
NGX_BODY="<main>"
NGX_BODY+="<h2>Nginx Sites</h2>"
for F in /etc/nginx/sites-enabled/*; do
NGX_BODY+="$(file_block "$F" "$(cat "$F" 2>/dev/null | esc)")"
done
NGX_BODY+="</main>"
html_page "Nginx Configuration" "$NGX_BODY" > "$PUB/server/nginx-config.html"
# --- PHP info
PHP_BODY="<main>"
PHP_BODY+="<h2>PHP Version and Extensions</h2>"
PHP_BODY+="$(file_block 'php -v' "$(php -v 2>&1 | esc)")"
PHP_BODY+="$(file_block 'php -m (modules)' "$(php -m 2>&1 | esc)")"
PHP_BODY+="$(file_block 'php -i (key settings)' "$(php -i 2>&1 | grep -E 'memory_limit|upload_max|post_max|max_execution|error_reporting|display_errors|opcache' | esc)")"
PHP_BODY+="</main>"
html_page "PHP Info" "$PHP_BODY" > "$PUB/server/php-info.html"
# --- System info
SYS_BODY="<main>"
SYS_BODY+="<h2>System</h2>"
SYS_BODY+="$(file_block 'uname / lsb_release' "$(uname -a 2>&1 | esc)
$(lsb_release -a 2>&1 | esc)")"
SYS_BODY+="$(file_block 'uptime / memory' "$(uptime 2>&1 | esc)
$(free -h 2>&1 | esc)")"
SYS_BODY+="$(file_block 'disk usage (df -h)' "$(df -h 2>&1 | esc)")"
SYS_BODY+="$(file_block 'top processes (ps aux --sort=-%mem)' "$(ps aux --sort=-%mem 2>&1 | head -20 | esc)")"
SYS_BODY+="<h2>Tool Versions</h2>"
SYS_BODY+="$(file_block 'versions' "PHP: $(php --version 2>/dev/null | head -1 | esc)
Node: $(node --version 2>/dev/null | esc)
npm: $(npm --version 2>/dev/null | esc)
Python: $(python3 --version 2>/dev/null | esc)
Composer: $(composer --version 2>/dev/null | head -1 | esc)
PostgreSQL: $(psql --version 2>/dev/null | esc)
Redis: $(redis-server --version 2>/dev/null | esc)
Git: $(git --version 2>/dev/null | esc)
ffmpeg: $(ffmpeg -version 2>/dev/null | head -1 | esc)
ClamAV: $(clamscan --version 2>/dev/null | esc)")"
SYS_BODY+="</main>"
html_page "System Info" "$SYS_BODY" > "$PUB/server/system-info.html"
# --- Changelog + Handover
if [[ -f "$SCRIPTS/CHANGELOG.md" ]]; then
html_page "Server Changelog" "<main>$(file_block 'CHANGELOG.md' "$(cat "$SCRIPTS/CHANGELOG.md" | esc)")</main>" > "$PUB/server/CHANGELOG.html"
cp "$SCRIPTS/CHANGELOG.md" "$PUB/server/CHANGELOG.md"
fi
if [[ -f "$SCRIPTS/HANDOVER.md" ]]; then
html_page "Server Handover" "<main>$(file_block 'HANDOVER.md' "$(cat "$SCRIPTS/HANDOVER.md" | esc)")</main>" > "$PUB/server/HANDOVER.html"
cp "$SCRIPTS/HANDOVER.md" "$PUB/server/HANDOVER.md"
fi
if [[ -f "$SCRIPTS/RULES.md" ]]; then
html_page "Project Rules and Standards" "<main>$(file_block 'RULES.md' "$(cat "$SCRIPTS/RULES.md" | esc)")</main>" > "$PUB/server/RULES.html"
cp "$SCRIPTS/RULES.md" "$PUB/server/RULES.md"
fi
# --- Server error logs (unique name each run)
rm -f "$PUB/server/error-log-"*.html "$PUB/server/error-log-"*.txt 2>/dev/null || true
ELOG="error-log-$(date +%Y%m%d-%H%M%S)-$RND"
ELOG_BODY="<main>"
ELOG_BODY+="<h2>Nginx Error Log</h2>$(file_block '/var/log/nginx/error.log (last 100)' "$(tail -100 /var/log/nginx/error.log 2>/dev/null | esc || echo 'empty')")"
ELOG_BODY+="<h2>Nginx Handoff Log</h2>$(file_block '/var/log/nginx/handoff_error.log' "$(tail -50 /var/log/nginx/handoff_error.log 2>/dev/null | esc || echo 'empty')")"
ELOG_BODY+="<h2>Cleanup Log</h2>$(file_block '/var/log/jfmsrv01_cleanup.log' "$(tail -50 /var/log/jfmsrv01_cleanup.log 2>/dev/null | esc || echo 'empty')")"
ELOG_BODY+="<h2>ClamAV Scan Log</h2>$(file_block '/var/log/jfmsrv01_scan.log' "$(tail -50 /var/log/jfmsrv01_scan.log 2>/dev/null | esc || echo 'empty')")"
ELOG_BODY+="<h2>Supervisor Log</h2>$(file_block '/var/log/supervisor/supervisord.log' "$(tail -50 /var/log/supervisor/supervisord.log 2>/dev/null | esc || echo 'empty')")"
ELOG_BODY+="</main>"
html_page "Server Error Logs" "$ELOG_BODY" > "$PUB/server/${ELOG}.html"
{
echo "=== Nginx Error ===" && tail -100 /var/log/nginx/error.log 2>/dev/null || true
echo "=== Cleanup ===" && tail -50 /var/log/jfmsrv01_cleanup.log 2>/dev/null || true
echo "=== Scan ===" && tail -50 /var/log/jfmsrv01_scan.log 2>/dev/null || true
} > "$PUB/server/${ELOG}.txt"
# --- Diagnostics
if [[ -f "$SCRIPTS/diagnostics.sh" ]]; then
DIAG=$(bash "$SCRIPTS/diagnostics.sh" 2>/dev/null | python3 -m json.tool 2>/dev/null || echo '{}')
html_page "Diagnostics" "<main>$(file_block 'diagnostics.sh output' "$(echo "$DIAG" | esc)")</main>" > "$PUB/server/diagnostics.html"
fi
# --- Smoke test
if [[ -f "$SCRIPTS/smoke_test.sh" ]]; then
SMOKE=$(bash "$SCRIPTS/smoke_test.sh" 2>&1 || true)
html_page "Smoke Test" "<main>$(file_block 'smoke_test.sh output' "$(echo "$SMOKE" | esc)")</main>" > "$PUB/server/smoke-test.html"
fi
log "Server done"
# -----------------------------------------------------------------------------
# MAINSITE PROJECT (Statamic)
# -----------------------------------------------------------------------------
log "Generating: mainsite"
mkdir -p "$PUB/mainsite"
MAINSITE="/var/www/mainsite"
if [[ -d "$MAINSITE" ]]; then
# .env sanitised
[REDACTED SECRET-LIKE LINE]
# composer.json
html_page "Mainsite composer.json" "<main>$(file_block 'composer.json' "$(cat "$MAINSITE/composer.json" 2>/dev/null | esc)")</main>" > "$PUB/mainsite/composer-json.html"
# Route list
ROUTES=$(cd "$MAINSITE" && php artisan route:list 2>&1 | esc)
html_page "Mainsite Routes" "<main>$(file_block 'php artisan route:list' "$ROUTES")</main>" > "$PUB/mainsite/route-list.html"
cd "$MAINSITE" && php artisan route:list 2>/dev/null > "$PUB/mainsite/route-list.txt" || true
# Directory tree
html_page "Mainsite File Tree" "<main>$(file_block 'directory tree (vendor excluded)' "$(find "$MAINSITE" -not -path '*/vendor/*' -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/storage/framework/*' 2>/dev/null | sort | esc)")</main>" > "$PUB/mainsite/file-tree.html"
# Statamic config files
CFG_BODY="<main><h2>Statamic Config Files</h2>"
for F in "$MAINSITE/config/statamic/"*.php; do
[[ -f "$F" ]] && CFG_BODY+="$(file_block "$(basename $F)" "$(cat "$F" | esc)")"
done
CFG_BODY+="</main>"
html_page "Mainsite Statamic Config" "$CFG_BODY" > "$PUB/mainsite/statamic-config.html"
# Error log - unique name
rm -f "$PUB/mainsite/error-log-"*.html "$PUB/mainsite/error-log-"*.txt 2>/dev/null || true
ELOG_M="error-log-$(date +%Y%m%d-%H%M%S)-$RND"
LOG_CONTENT=$(tail -300 "$MAINSITE/storage/logs/laravel.log" 2>/dev/null | esc || echo "No log file")
html_page "Mainsite Error Log" "<main>$(file_block 'storage/logs/laravel.log (last 300 lines)' "$LOG_CONTENT")</main>" > "$PUB/mainsite/${ELOG_M}.html"
tail -300 "$MAINSITE/storage/logs/laravel.log" 2>/dev/null > "$PUB/mainsite/${ELOG_M}.txt" || echo "no log" > "$PUB/mainsite/${ELOG_M}.txt"
# Nginx access log tail
html_page "Mainsite Nginx Access Log" "<main>$(file_block '/var/log/nginx/mainsite_access.log (last 100)' "$(tail -100 /var/log/nginx/mainsite_access.log 2>/dev/null | esc || echo 'empty')")</main>" > "$PUB/mainsite/nginx-access.html"
# Sanitised source ZIP
log "Building mainsite source ZIP"
cd "$MAINSITE"
rm -f "$PUB/mainsite/mainsite-sanitised-"*.zip 2>/dev/null || true
zip -r "/tmp/mainsite-src-$TS.zip" . \
--exclude "*.env" --exclude "vendor/*" --exclude "node_modules/*" \
--exclude "storage/logs/*" --exclude "storage/framework/cache/*" \
--exclude "storage/framework/sessions/*" --exclude ".git/*" \
2>/dev/null || true
mv "/tmp/mainsite-src-$TS.zip" "$PUB/mainsite/mainsite-sanitised-latest.zip"
fi
log "Mainsite done"
# -----------------------------------------------------------------------------
# ADMIN PROJECT (Filament)
# -----------------------------------------------------------------------------
log "Generating: admin"
mkdir -p "$PUB/admin"
ADMINSITE="/var/www/admin"
if [[ -d "$ADMINSITE" ]]; then
# .env sanitised
[REDACTED SECRET-LIKE LINE]
# composer.json
html_page "Admin composer.json" "<main>$(file_block 'composer.json' "$(cat "$ADMINSITE/composer.json" 2>/dev/null | esc)")</main>" > "$PUB/admin/composer-json.html"
# Route list
ROUTES=$(cd "$ADMINSITE" && php artisan route:list 2>&1 | esc)
html_page "Admin Routes" "<main>$(file_block 'php artisan route:list' "$ROUTES")</main>" > "$PUB/admin/route-list.html"
cd "$ADMINSITE" && php artisan route:list 2>/dev/null > "$PUB/admin/route-list.txt" || true
# Directory tree
html_page "Admin File Tree" "<main>$(file_block 'directory tree' "$(find "$ADMINSITE" -not -path '*/vendor/*' -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/storage/framework/*' 2>/dev/null | sort | esc)")</main>" > "$PUB/admin/file-tree.html"
# Migrations list
MIGS=$(cd "$ADMINSITE" && php artisan migrate:status 2>&1 | esc)
html_page "Admin Migration Status" "<main>$(file_block 'php artisan migrate:status' "$MIGS")</main>" > "$PUB/admin/migration-status.html"
# Migration files content
MIG_BODY="<main><h2>Migration Files</h2>"
for F in "$ADMINSITE/database/migrations/"*.php; do
[[ -f "$F" ]] && MIG_BODY+="$(file_block "$(basename $F)" "$(cat "$F" | esc)")"
done
MIG_BODY+="</main>"
html_page "Admin Migrations" "$MIG_BODY" > "$PUB/admin/migrations.html"
# Models
MDL_BODY="<main><h2>Models</h2>"
for F in "$ADMINSITE/app/Models/"*.php; do
[[ -f "$F" ]] && MDL_BODY+="$(file_block "$(basename $F)" "$(cat "$F" | esc)")"
done
MDL_BODY+="</main>"
html_page "Admin Models" "$MDL_BODY" > "$PUB/admin/models.html"
# Filament resources/pages/widgets
FIL_BODY="<main><h2>Filament Resources</h2>"
for F in $(find "$ADMINSITE/app/Filament" -name "*.php" 2>/dev/null | sort); do
FIL_BODY+="$(file_block "${F#$ADMINSITE/}" "$(cat "$F" | esc)")"
done
FIL_BODY+="<h2>Providers</h2>"
for F in "$ADMINSITE/app/Providers/"*.php; do
[[ -f "$F" ]] && FIL_BODY+="$(file_block "$(basename $F)" "$(cat "$F" | esc)")"
done
FIL_BODY+="</main>"
html_page "Admin Filament Structure" "$FIL_BODY" > "$PUB/admin/filament-structure.html"
# Database schema (column details)
DB_BODY="<main><h2>Database Tables</h2>"
TABLES=[REDACTED]
-t -c "SELECT tablename FROM pg_tables WHERE schemaname='public' ORDER BY tablename" 2>/dev/null || echo "")
for TABLE in $TABLES; do
TABLE=$(echo "$TABLE" | tr -d ' ')
[[ -z "$TABLE" ]] && continue
COL_INFO=[REDACTED]
-c "\d $TABLE" 2>/dev/null | esc || echo "error")
DB_BODY+="$(file_block "Table: $TABLE" "$COL_INFO")"
done
DB_BODY+="</main>"
html_page "Admin Database Schema" "$DB_BODY" > "$PUB/admin/database-schema.html"
# Schema only SQL
rm -f "$PUB/admin/admin-schema-only-"*.sql.gz 2>/dev/null || true
PGPASSWORD=[REDACTED]
-U "${DB_USER:-jfmsrv01}" -h 127.0.0.1 --schema-only admin 2>/dev/null | \
gzip > "$PUB/admin/admin-schema-only-latest.sql.gz" || true
# Full DB backup only if requested
if [[ "$DB_CHANGED" == "db" ]]; then
log "DB backup requested"
bash "/opt/jfmsrv01/scripts/backup.sh" database 2>/dev/null || true
fi
# Error log - unique name
rm -f "$PUB/admin/error-log-"*.html "$PUB/admin/error-log-"*.txt 2>/dev/null || true
ELOG_A="error-log-$(date +%Y%m%d-%H%M%S)-$RND"
LOG_CONTENT=$(tail -300 "$ADMINSITE/storage/logs/laravel.log" 2>/dev/null | esc || echo "No log file")
html_page "Admin Error Log" "<main>$(file_block 'storage/logs/laravel.log (last 300 lines)' "$LOG_CONTENT")</main>" > "$PUB/admin/${ELOG_A}.html"
tail -300 "$ADMINSITE/storage/logs/laravel.log" 2>/dev/null > "$PUB/admin/${ELOG_A}.txt" || echo "no log" > "$PUB/admin/${ELOG_A}.txt"
# auth.php config
html_page "Admin Auth Config" "<main>$(file_block 'config/auth.php' "$(cat "$ADMINSITE/config/auth.php" 2>/dev/null | esc)")</main>" > "$PUB/admin/auth-config.html"
# Sanitised source ZIP
log "Building admin source ZIP"
cd "$ADMINSITE"
rm -f "$PUB/admin/admin-sanitised-"*.zip 2>/dev/null || true
zip -r "/tmp/admin-src-$TS.zip" . \
--exclude "*.env" --exclude "vendor/*" --exclude "node_modules/*" \
--exclude "storage/logs/*" --exclude "storage/framework/cache/*" \
--exclude "storage/framework/sessions/*" --exclude ".git/*" \
2>/dev/null || true
mv "/tmp/admin-src-$TS.zip" "$PUB/admin/admin-sanitised-latest.zip"
fi
log "Admin done"
# -----------------------------------------------------------------------------
# TRANSCRIPTION PROJECT
# -----------------------------------------------------------------------------
log "Generating: transcription"
mkdir -p "$PUB/transcription"
TRANS="/opt/jfmsrv01/app"
if [[ -d "$TRANS" ]]; then
# .env sanitised
[REDACTED SECRET-LIKE LINE]
# Route list
ROUTES=$(cd "$TRANS" && php artisan route:list 2>&1 | esc)
html_page "Transcription Routes" "<main>$(file_block 'php artisan route:list' "$ROUTES")</main>" > "$PUB/transcription/route-list.html"
cd "$TRANS" && php artisan route:list 2>/dev/null > "$PUB/transcription/route-list.txt" || true
# Migrations
MIG_BODY="<main><h2>Migration Files</h2>"
for F in "$TRANS/database/migrations/"*.php; do
[[ -f "$F" ]] && MIG_BODY+="$(file_block "$(basename $F)" "$(cat "$F" | esc)")"
done
MIG_BODY+="</main>"
html_page "Transcription Migrations" "$MIG_BODY" > "$PUB/transcription/migrations.html"
# Models
MDL_BODY="<main><h2>Models</h2>"
for F in "$TRANS/app/Models/"*.php; do
[[ -f "$F" ]] && MDL_BODY+="$(file_block "$(basename $F)" "$(cat "$F" | esc)")"
done
MDL_BODY+="</main>"
html_page "Transcription Models" "$MDL_BODY" > "$PUB/transcription/models.html"
# Services
SVC_BODY="<main><h2>Services</h2>"
for F in "$TRANS/app/Services/"*.php; do
[[ -f "$F" ]] && SVC_BODY+="$(file_block "$(basename $F)" "$(cat "$F" | esc)")"
done
SVC_BODY+="</main>"
html_page "Transcription Services" "$SVC_BODY" > "$PUB/transcription/services.html"
# Events
EVT_BODY="<main><h2>Events</h2>"
for F in "$TRANS/app/Events/"*.php; do
[[ -f "$F" ]] && EVT_BODY+="$(file_block "$(basename $F)" "$(cat "$F" | esc)")"
done
EVT_BODY+="</main>"
html_page "Transcription Events" "$EVT_BODY" > "$PUB/transcription/events.html"
# DB schema
DB_BODY="<main><h2>Database Tables</h2>"
TABLES=[REDACTED]
-t -c "SELECT tablename FROM pg_tables WHERE schemaname='public' ORDER BY tablename" 2>/dev/null || echo "")
for TABLE in $TABLES; do
TABLE=$(echo "$TABLE" | tr -d ' ')
[[ -z "$TABLE" ]] && continue
COL_INFO=[REDACTED]
-c "\d $TABLE" 2>/dev/null | esc || echo "error")
DB_BODY+="$(file_block "Table: $TABLE" "$COL_INFO")"
done
DB_BODY+="</main>"
html_page "Transcription Database Schema" "$DB_BODY" > "$PUB/transcription/database-schema.html"
# Schema SQL
rm -f "$PUB/transcription/transcription-schema-"*.sql.gz 2>/dev/null || true
PGPASSWORD=[REDACTED]
-U "${DB_USER:-jfmsrv01}" -h 127.0.0.1 --schema-only jfmsrv01 2>/dev/null | \
gzip > "$PUB/transcription/transcription-schema-latest.sql.gz" || true
# Error log
rm -f "$PUB/transcription/error-log-"*.html "$PUB/transcription/error-log-"*.txt 2>/dev/null || true
ELOG_T="error-log-$(date +%Y%m%d-%H%M%S)-$RND"
LOG_CONTENT=$(tail -300 "$TRANS/storage/logs/laravel.log" 2>/dev/null | esc || echo "Platform not yet started")
html_page "Transcription Error Log" "<main>$(file_block 'laravel.log (last 300)' "$LOG_CONTENT")</main>" > "$PUB/transcription/${ELOG_T}.html"
# Changelog + Handover
if [[ -f "$SCRIPTS/CHANGELOG.md" ]]; then
html_page "Transcription Changelog" "<main>$(file_block 'CHANGELOG.md' "$(cat "$SCRIPTS/CHANGELOG.md" | esc)")</main>" > "$PUB/transcription/CHANGELOG.html"
cp "$SCRIPTS/CHANGELOG.md" "$PUB/transcription/CHANGELOG.md"
fi
if [[ -f "$SCRIPTS/HANDOVER.md" ]]; then
html_page "Transcription Handover" "<main>$(file_block 'HANDOVER.md' "$(cat "$SCRIPTS/HANDOVER.md" | esc)")</main>" > "$PUB/transcription/HANDOVER.html"
cp "$SCRIPTS/HANDOVER.md" "$PUB/transcription/HANDOVER.md"
fi
# Supervisor config
html_page "Transcription Worker Config" "<main>$(file_block 'supervisor config' "$(cat /etc/supervisor/conf.d/jfmsrv01-worker.conf 2>/dev/null | esc || echo 'not yet configured')")</main>" > "$PUB/transcription/worker-config.html"
else
# Platform not started yet - generate all planning documents
log "Transcription platform not yet started - generating planning docs"
# Project handover (comprehensive)
if [[ -f "$SCRIPTS/TRANSCRIPTION_HANDOVER.md" ]]; then
html_page "Transcription Platform — Full Handover" "<main>$(file_block 'TRANSCRIPTION_HANDOVER.md' "$(cat "$SCRIPTS/TRANSCRIPTION_HANDOVER.md" | esc)")</main>" > "$PUB/transcription/TRANSCRIPTION_HANDOVER.html"
cp "$SCRIPTS/TRANSCRIPTION_HANDOVER.md" "$PUB/transcription/TRANSCRIPTION_HANDOVER.md"
fi
# Master project plan
if [[ -f "$SCRIPTS/legal_transcription_platform_master_plan.txt" ]]; then
html_page "Master Project Plan" "<main>$(file_block 'legal_transcription_platform_master_plan.txt' "$(cat "$SCRIPTS/legal_transcription_platform_master_plan.txt" | esc)")</main>" > "$PUB/transcription/master-plan.html"
cp "$SCRIPTS/legal_transcription_platform_master_plan.txt" "$PUB/transcription/master-plan.txt"
fi
# Storage design plan
if [[ -f "$SCRIPTS/legal_transcription_server_storage_design_plan.txt" ]]; then
html_page "Server and Storage Design Plan" "<main>$(file_block 'storage_design_plan.txt' "$(cat "$SCRIPTS/legal_transcription_server_storage_design_plan.txt" | esc)")</main>" > "$PUB/transcription/storage-design.html"
cp "$SCRIPTS/legal_transcription_server_storage_design_plan.txt" "$PUB/transcription/storage-design.txt"
fi
# Build scripts overview
BUILD_BODY="<main><h2>Build Scripts (ready to run)</h2>"
BUILD_BODY+="<p style='padding:12px 0;color:#768390'>All scripts are in /jfmsrv01-build/ on the server. Run in order.</p>"
for BSCRIPT in 01_storage_layout.sh 02_app_skeleton.sh 03_admin_safety_tools.sh 04_first_firm.sh; do
if [[ -f "/jfmsrv01-build/$BSCRIPT" ]]; then
BUILD_BODY+="$(file_block "/jfmsrv01-build/$BSCRIPT" "$(cat "/jfmsrv01-build/$BSCRIPT" | esc)")"
fi
done
BUILD_BODY+="</main>"
html_page "Transcription Build Scripts" "$BUILD_BODY" > "$PUB/transcription/build-scripts.html"
# Status page
STATUS_BODY="<main>
<h2>Project Status: ON HOLD — Ready to Build</h2>
<p style='padding:16px 0;color:#768390'>All planning is complete. The server infrastructure is ready. Run the build scripts to start.</p>
<h2>What Is Ready</h2>
$(file_block 'infrastructure ready' "PostgreSQL installed and running
Redis installed and running (AOF persistence)
PHP 8.3 installed with all required extensions
Python 3.12 installed with document libraries
ffmpeg installed
LibreOffice headless installed
ClamAV installed and running
Supervisor installed
Nginx installed
Node.js 20 installed
All build scripts tested and ready in /jfmsrv01-build/")
<h2>To Resume — Run In Order</h2>
$(file_block 'build sequence' "sudo bash /jfmsrv01-build/01_storage_layout.sh
sudo bash /jfmsrv01-build/02_app_skeleton.sh
sudo bash /jfmsrv01-build/03_admin_safety_tools.sh
sudo bash /jfmsrv01-build/04_first_firm.sh
# Then configure Nginx vhost and start building Phase 1")
<h2>Planning Documents</h2>
$(file_block 'available documents' "TRANSCRIPTION_HANDOVER.md — comprehensive project handover (start here)
master-plan.txt — full master project plan (1494 lines)
storage-design.txt — server and storage design plan (1200 lines)
build-scripts.html — all four build scripts with full content
RULES.md — project rules and standards")
</main>"
html_page "Transcription Platform Status" "$STATUS_BODY" > "$PUB/transcription/status.html"
# Server changelog and handover (shared with server project)
if [[ -f "$SCRIPTS/CHANGELOG.md" ]]; then
html_page "Server Changelog" "<main>$(file_block 'CHANGELOG.md' "$(cat "$SCRIPTS/CHANGELOG.md" | esc)")</main>" > "$PUB/transcription/CHANGELOG.html"
cp "$SCRIPTS/CHANGELOG.md" "$PUB/transcription/CHANGELOG.md"
fi
if [[ -f "$SCRIPTS/RULES.md" ]]; then
html_page "Project Rules" "<main>$(file_block 'RULES.md' "$(cat "$SCRIPTS/RULES.md" | esc)")</main>" > "$PUB/transcription/RULES.html"
cp "$SCRIPTS/RULES.md" "$PUB/transcription/RULES.md"
fi
fi
log "Transcription done"
# -----------------------------------------------------------------------------
# SHARED
# -----------------------------------------------------------------------------
log "Generating: shared"
mkdir -p "$PUB/shared"
# Redis info
html_page "Redis Info" "<main>$(file_block 'redis-cli info' "$(redis-cli -a "${REDIS_PASSWORD: [REDACTED]
# PostgreSQL databases
PG_BODY="<main><h2>PostgreSQL Databases</h2>"
PG_BODY+=[REDACTED]
PG_BODY+="</main>"
html_page "PostgreSQL Info" "$PG_BODY" > "$PUB/shared/postgresql-info.html"
# ClamAV
html_page "ClamAV Status" "<main>$(file_block 'ClamAV status' "Daemon: $(systemctl is-active clamav-daemon 2>/dev/null)
freshclam: $(systemctl is-active clamav-freshclam 2>/dev/null)
Definition files:
$(find /var/lib/clamav -name '*.cvd' -o -name '*.cld' 2>/dev/null | xargs ls -lh 2>/dev/null | esc || echo 'not found')")</main>" > "$PUB/shared/clamav-status.html"
# Master plan documents
if [[ -f "/mnt/user-data/outputs/jfmsrv01_platform_master_plan.txt" ]] 2>/dev/null; then
cp "/mnt/user-data/outputs/jfmsrv01_platform_master_plan.txt" "$PUB/shared/master-plan.txt" 2>/dev/null || true
fi
log "Shared done"
# -----------------------------------------------------------------------------
# INDEX PAGE
# -----------------------------------------------------------------------------
log "Generating index"
make_section() {
local ID="$1" TITLE="$2" DIR="$PUB/$1"
[[ ! -d "$DIR" ]] && return
echo "<div class='project' id='$ID'><h2>$TITLE</h2><div class='cols'><div class='col'><h3>Browse Online</h3><ul>"
find "$DIR" -maxdepth 1 -name "*.html" | sort | while read F; do
FNAME=$(basename "$F")
echo "<li><a href='$ID/$FNAME' target='_blank'>$FNAME</a></li>"
done
echo "</ul></div><div class='col'><h3>Download</h3><ul>"
find "$DIR" -maxdepth 1 \( -name "*.md" -o -name "*.txt" -o -name "*.zip" -o -name "*.gz" \) | sort | while read F; do
FNAME=$(basename "$F")
echo "<li><a href='$ID/$FNAME' download>$FNAME</a></li>"
done
echo "</ul></div></div></div>"
}
{
cat <<IDXEOF
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>jfmsrv01 — Developer Handoff</title>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&family=Syne:wght@700;800&display=swap" rel="stylesheet">
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'JetBrains Mono',monospace;background:#060910;color:#cdd9e5;font-size:13px;line-height:1.6;min-height:100vh}
header{background:#0d1117;border-bottom:1px solid #1c2333;padding:20px 40px;display:flex;align-items:center;gap:20px;position:sticky;top:0;z-index:100;backdrop-filter:blur(8px)}
.logo{font-family:'Syne',sans-serif;font-size:24px;font-weight:800;color:#2f81f7;letter-spacing:-1px}
.logo span{color:#cdd9e5;font-weight:700}
.meta{margin-left:auto;font-size:11px;color:#768390;text-align:right}
.warn-bar{background:#2d1f00;border-bottom:1px solid #6e4c00;padding:10px 40px;font-size:12px;color:#d29922}
nav{background:#0d1117;border-bottom:1px solid #1c2333;padding:0 40px;display:flex;gap:0;overflow-x:auto;white-space:nowrap}
nav a{color:#768390;text-decoration:none;padding:12px 18px;font-size:12px;border-bottom:2px solid transparent;display:inline-block}
nav a:hover{color:#cdd9e5;border-bottom-color:#2f81f7}
main{padding:40px;max-width:1400px}
.project{background:#0d1117;border:1px solid #1c2333;border-radius:6px;margin-bottom:28px;overflow:hidden}
.project h2{font-family:'Syne',sans-serif;font-size:16px;color:#2f81f7;padding:18px 24px;border-bottom:1px solid #1c2333;background:#080c12}
.cols{display:grid;grid-template-columns:1fr 1fr}
.col{padding:18px 24px}
.col:first-child{border-right:1px solid #1c2333}
.col h3{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;color:#768390;margin-bottom:10px}
ul{list-style:none}
li{margin-bottom:5px}
a{color:#2f81f7;text-decoration:none;font-size:12px}
a:hover{color:#58a6ff;text-decoration:underline}
footer{padding:16px 40px;background:#080c12;border-top:1px solid #1c2333;font-size:11px;color:#768390}
@media(max-width:768px){.cols{grid-template-columns:1fr}.col:first-child{border-right:none;border-bottom:1px solid #1c2333}header,nav,.warn-bar,main,footer{padding-left:16px;padding-right:16px}}
</style>
</head>
<body>
<header>
<div class="logo">jfmsrv01<span>/handoff</span></div>
<div class="meta">Generated: $(date '+%Y-%m-%d %H:%M:%S %Z')<br>Auto-updates on every change</div>
</header>
<div class=[REDACTED]
<nav>
<a href="#server">Server</a>
<a href="#mainsite">Mainsite (Statamic)</a>
<a href="#admin">Admin (Filament)</a>
<a href="#transcription">Transcription Platform</a>
<a href="#shared">Shared</a>
</nav>
<main>
IDXEOF
make_section "server" "🖥 Server Setup & Infrastructure"
make_section "mainsite" "🌐 Mainsite — Statamic"
make_section "admin" "⚙️ Admin Panel — Filament"
make_section "transcription" "🎙 JFM Server 01"
make_section "shared" "🔗 Shared — Redis, PostgreSQL, ClamAV"
cat <<IDXEOF2
</main>
<footer>jfmsrv01 handoff portal | auto-generated by publish_handoff.sh | error logs use unique filenames on every run to prevent caching</footer>
</body></html>
IDXEOF2
} > "$PUB/index.html"
chown -R www-data:www-data /var/www/handoff
chmod -R 755 /var/www/handoff
log "Handoff publish complete: $TS"
echo "URL: http://192.168.100.197:9002/vqngx6j8dc9q1lvt7g2a7a646q2t/"
PUBLISHEOF
chmod 750 "$SCRIPTS_BASE/publish_handoff.sh"
chown "$APP_USER:$APP_USER" "$SCRIPTS_BASE/publish_handoff.sh" 2>/dev/null || true
success "Publish script created"
# Update patch.sh to auto-call publish after every change
PATCH_SCRIPT="$SCRIPTS_BASE/patch.sh"
if [[ -f "$PATCH_SCRIPT" ]] && ! grep -q "publish_handoff" "$PATCH_SCRIPT"; then
sed -i "s|success \"Post-patch complete.|bash \"$SCRIPTS_BASE/publish_handoff.sh\" \"\${DB_CHANGED:-}\"\n success \"Post-patch complete.|" "$PATCH_SCRIPT"
success "patch.sh updated to auto-publish"
fi
# Run initial publish
info "Running initial publish"
bash "$SCRIPTS_BASE/publish_handoff.sh" || warn "Initial publish had warnings"
success "=== HANDOFF PORTAL COMPLETE ==="
echo ""
echo "URL: http://192.168.100.197:9002/$HANDOFF_SLUG/"
echo ""
echo "What's covered per project:"
echo " server: status, nginx config, PHP info, system info, firewall, cron, logs"
echo " mainsite: .env sanitised, composer.json, routes, file tree, statamic config, error log, source ZIP"
echo " admin: .env sanitised, composer.json, routes, file tree, migrations, models, filament structure,"
echo " database schema (full column detail), schema SQL, error log, auth config, source ZIP"
echo " transcription: .env sanitised, routes, migrations, models, services, events, DB schema, error log,"
echo " changelog, handover, worker config, schema SQL"
echo " shared: redis info, postgresql databases, clamav status"
echo ""
echo "Error logs: unique filename on every run — browser cache never serves stale logs"
echo "DB backup: only runs when you pass 'db' argument to publish_handoff.sh"
echo "Auto-publish: runs after every patch.sh post"