/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() {
[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 [[ -f "$TRANS/artisan" ]]; 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
# SpliceVPN extra handoff pack.
# Keep this at the end so it patches the generated index after publish_handoff.sh rebuilds it.
if [ -x /opt/jfmsrv01/scripts/publish_splicevpn_handoff_pack.sh ]; then
/opt/jfmsrv01/scripts/publish_splicevpn_handoff_pack.sh || true
fi