Source — /opt/jfmsrv01/scripts/publish_handoff.sh

/opt/jfmsrv01/scripts/publish_handoff.sh

#!/usr/bin/env bash
# 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/legaltranscribe_credentials.txt ]] && source /root/legaltranscribe_credentials.txt

# -----------------------------------------------------------------------------
# Helpers
# -----------------------------------------------------------------------------

sanitise() {
    # Redact passwords, keys, secrets from text
    sed \
        -e 's/\(PASSWORD\s*=\s*\).*/\1[REDACTED]/Ig' \
        -e 's/\(PASSWORD\s*=>\s*\).*/\1[REDACTED]/Ig' \
        -e 's/\(SECRET\s*=\s*\).*/\1[REDACTED]/Ig' \
        -e 's/\(KEY\s*=\s*\).*/\1[REDACTED]/Ig' \
        -e 's/\(TOKEN\s*=\s*\).*/\1[REDACTED]/Ig' \
        -e 's/\(APP_KEY\s*=\s*\).*/\1[REDACTED]/Ig' \
        -e 's/\(REDIS_PASSWORD\s*=\s*\).*/\1[REDACTED]/Ig' \
        -e 's/\(DB_PASSWORD\s*=\s*\).*/\1[REDACTED]/Ig' \
        -e 's/\(MAIL_PASSWORD\s*=\s*\).*/\1[REDACTED]/Ig' \
        -e 's/\(requirepass\s\+\)\S*/\1[REDACTED]/Ig'
}

esc() { sed 's/</\&lt;/g; s/>/\&gt;/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="warn-bar">⚠ Safe handoff files only. Passwords and secrets are redacted.</div>
$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
    html_page "Mainsite .env (sanitised)" "<main>$(file_block '.env (passwords redacted)' "$(cat "$MAINSITE/.env" 2>/dev/null | sanitise | esc)")</main>" > "$PUB/mainsite/env-sanitised.html"

    # 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
    html_page "Admin .env (sanitised)" "<main>$(file_block '.env (passwords redacted)' "$(cat "$ADMINSITE/.env" 2>/dev/null | sanitise | esc)")</main>" > "$PUB/admin/env-sanitised.html"

    # 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=$(PGPASSWORD=[REDACTED]" psql -U "${DB_USER:-jfmsrv01}" -h 127.0.0.1 admin \
        -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=$(PGPASSWORD=[REDACTED]" psql -U "${DB_USER:-jfmsrv01}" -h 127.0.0.1 admin \
            -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]" pg_dump \
        -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 [[ -f "$TRANS/artisan" ]]; then

    # .env sanitised
    html_page "Transcription .env (sanitised)" "<main>$(file_block '.env (passwords redacted)' "$(cat "$TRANS/.env" 2>/dev/null | sanitise | esc)")</main>" > "$PUB/transcription/env-sanitised.html"

    # 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=$(PGPASSWORD=[REDACTED]" psql -U "${DB_USER:-jfmsrv01}" -h 127.0.0.1 jfmsrv01 \
        -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=$(PGPASSWORD=[REDACTED]" psql -U "${DB_USER:-jfmsrv01}" -h 127.0.0.1 jfmsrv01 \
            -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]" pg_dump \
        -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]" info 2>/dev/null | sanitise | esc || echo 'could not connect')")</main>" > "$PUB/shared/redis-info.html"

# PostgreSQL databases
PG_BODY="<main><h2>PostgreSQL Databases</h2>"
PG_BODY+="$(file_block 'psql \l' "$(PGPASSWORD=[REDACTED]" psql -U "${DB_USER:-jfmsrv01}" -h 127.0.0.1 jfmsrv01 -c '\l' 2>/dev/null | esc || echo 'error')")"
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="warn-bar">⚠ Safe handoff files only — passwords redacted, no full DB dumps, no .env secrets</div>
<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=[REDACTED]