/ Transcription Build Scripts
2026-05-30 16:11:47 AWST
⚠ Safe handoff files only. Passwords and secrets are redacted.

Build Scripts (ready to run)

All scripts are in /jfmsrv01-build/ on the server. Run in order.

/jfmsrv01-build/01_storage_layout.sh
#!/usr/bin/env bash
# =============================================================================
# JFM Server 01 - Phase 2
# Storage Layout Script
#
# Run as root after 00_provision_server.sh has completed.
# Creates the full data folder structure, sets permissions, mounts the data
# disk, and configures the temp/processing cleanup job.
#
# Usage:
#   chmod +x 01_storage_layout.sh
#   sudo bash 01_storage_layout.sh
# =============================================================================

set -euo pipefail

# -----------------------------------------------------------------------------
# Configuration
# -----------------------------------------------------------------------------

APP_USER="jfmsrv01"
APP_GROUP="jfmsrv01"

# Data disk device - set this to your actual data disk.
# Run 'lsblk' to identify the device. Common values:
#   /dev/sdb  (second SCSI disk)
#   /dev/vdb  (KVM/QEMU virtual disk)
#   /dev/nvme1n1 (second NVMe disk)
# Set to "" to skip disk mounting (use if data disk is already mounted
# or if running on a single-disk development VM).
DATA_DISK_DEVICE="/dev/sdb"

# Mount point for the data disk
DATA_MOUNT="/data"

# Base paths
DATA_BASE="/data/jfmsrv01"
BACKUP_BASE="/backups/jfmsrv01"
APP_BASE="/opt/jfmsrv01"

# Temp file cleanup thresholds (hours)
TEMP_MAX_AGE_HOURS=24
PROCESSING_MAX_AGE_HOURS=2
IMPORTS_MAX_AGE_HOURS=1

# Storage quota defaults (GB) - applied to new firms
DEFAULT_QUOTA_GB=50
QUOTA_WARN_PERCENT=80

# -----------------------------------------------------------------------------
# Colour helpers
# -----------------------------------------------------------------------------

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} $*"; }
error()   { echo -e "${RED}[ERROR]${NC} $*"; exit 1; }

[[ $EUID -ne 0 ]] && error "This script must be run as root."
id "$APP_USER" &>/dev/null || error "App user '$APP_USER' not found. Run 00_provision_server.sh first."

info "Starting storage layout setup"
info "Data base: $DATA_BASE | Backup base: $BACKUP_BASE"

# =============================================================================
# Mount data disk
# =============================================================================

if [[ -n "$DATA_DISK_DEVICE" ]]; then
    if ! mountpoint -q "$DATA_MOUNT"; then
        info "Setting up data disk: $DATA_DISK_DEVICE -> $DATA_MOUNT"

        if [[ ! -b "$DATA_DISK_DEVICE" ]]; then
            warn "Device $DATA_DISK_DEVICE not found."
            warn "Skipping disk mount. Using OS disk for data (not recommended for production)."
            warn "To mount later: mkfs.ext4 $DATA_DISK_DEVICE && mount $DATA_DISK_DEVICE $DATA_MOUNT"
        else
            # Check if already formatted
            FS_TYPE=$(blkid -o value -s TYPE "$DATA_DISK_DEVICE" 2>/dev/null || echo "")
            if [[ -z "$FS_TYPE" ]]; then
                info "Formatting $DATA_DISK_DEVICE as ext4"
                mkfs.ext4 -L jfmsrv01-data "$DATA_DISK_DEVICE"
                success "Disk formatted"
            else
                info "Disk already formatted as $FS_TYPE"
            fi

            mkdir -p "$DATA_MOUNT"
            mount "$DATA_DISK_DEVICE" "$DATA_MOUNT"

            # Add to fstab for persistence
            DISK_UUID=$(blkid -o value -s UUID "$DATA_DISK_DEVICE")
            if ! grep -q "$DISK_UUID" /etc/fstab; then
                echo "UUID=$DISK_UUID $DATA_MOUNT ext4 defaults,noatime 0 2" >> /etc/fstab
                info "Added to /etc/fstab (UUID: $DISK_UUID)"
            fi
            success "Data disk mounted at $DATA_MOUNT"
        fi
    else
        success "Data disk already mounted at $DATA_MOUNT"
    fi
else
    info "DATA_DISK_DEVICE not set - using OS disk for data storage"
    warn "For production, attach and configure a separate data disk"
fi

# =============================================================================
# Create directory structure
# =============================================================================

info "Creating application directory structure"

# Application directories
mkdir -p "$APP_BASE/app"
mkdir -p "$APP_BASE/scripts"
mkdir -p "$APP_BASE/workers"

# Main data tree
mkdir -p "$DATA_BASE/firms"
mkdir -p "$DATA_BASE/shared"
mkdir -p "$DATA_BASE/quarantine"
mkdir -p "$DATA_BASE/system"
mkdir -p "$DATA_BASE/imports"
mkdir -p "$DATA_BASE/processing"

# Backup tree
mkdir -p "$BACKUP_BASE/source"
mkdir -p "$BACKUP_BASE/database"
mkdir -p "$BACKUP_BASE/files"
mkdir -p "$BACKUP_BASE/tenants"
mkdir -p "$BACKUP_BASE/handovers"
mkdir -p "$BACKUP_BASE/reports"
mkdir -p "$BACKUP_BASE/restore-tests"

success "Directory structure created"

# =============================================================================
# Permissions
# =============================================================================

info "Setting ownership and permissions"

# Application directories
chown -R "$APP_USER:$APP_GROUP" "$APP_BASE"
chmod 750 "$APP_BASE"
chmod 750 "$APP_BASE/app"
chmod 750 "$APP_BASE/scripts"
chmod 750 "$APP_BASE/workers"

# Data directories
chown -R "$APP_USER:$APP_GROUP" "$DATA_BASE"
chmod 750 "$DATA_BASE"
chmod 750 "$DATA_BASE/firms"
chmod 750 "$DATA_BASE/shared"
chmod 750 "$DATA_BASE/quarantine"
chmod 750 "$DATA_BASE/system"
chmod 750 "$DATA_BASE/imports"
chmod 750 "$DATA_BASE/processing"

# Backup directories
chown -R "$APP_USER:$APP_GROUP" "$BACKUP_BASE"
chmod 750 "$BACKUP_BASE"
find "$BACKUP_BASE" -type d -exec chmod 750 {} \;

success "Permissions set (750, owned by $APP_USER)"

# Verify web server cannot access data
WEB_USER="www-data"
if id "$WEB_USER" &>/dev/null; then
    # Test that www-data cannot read the data directory
    if sudo -u "$WEB_USER" ls "$DATA_BASE" &>/dev/null; then
        warn "www-data can read $DATA_BASE - tightening permissions"
        chmod 700 "$DATA_BASE"
    else
        success "Confirmed: $WEB_USER cannot read $DATA_BASE"
    fi
fi

# =============================================================================
# Folder README files
# =============================================================================

info "Creating folder README files"

cat > "$DATA_BASE/shared/README.txt" <<'EOF'
shared/
-------
Platform-level assets that are not firm-specific.
May include global template starters and platform branding assets.
Must not contain any firm data, recordings, or documents.
EOF

cat > "$DATA_BASE/quarantine/README.txt" <<'EOF'
quarantine/
-----------
Files that have failed virus/malware scanning or format validation.
Files are moved here rather than deleted so they can be reviewed by a
platform admin before permanent removal.
Restricted to platform admin access only.
DO NOT process or open files here without first understanding why they
were quarantined.
EOF

cat > "$DATA_BASE/imports/README.txt" <<'EOF'
imports/
--------
Staging area for files arriving from external connectors (SMB, SFTP, FTP,
local agent) before they have been validated and assigned to a firm.
Files should not remain here after processing completes.
Stale files here indicate a failed or stuck import job.
EOF

cat > "$DATA_BASE/processing/README.txt" <<'EOF'
processing/
-----------
Temporary working area for files actively being processed by a queue worker.
A file should only exist here while a worker job is actively working on it.
Stale files here indicate a crashed or stuck worker.
The cleanup job will flag files older than the configured threshold.
EOF

chown -R "$APP_USER:$APP_GROUP" "$DATA_BASE"
success "README files created"

# =============================================================================
# Create firm directory helper function (for use during firm onboarding)
# =============================================================================

FIRM_SETUP_SCRIPT="$APP_BASE/scripts/create_firm_storage.sh"
cat > "$FIRM_SETUP_SCRIPT" <<'FIRMSCRIPT'
#!/usr/bin/env bash
# Usage: create_firm_storage.sh <firm_id>
# Creates the storage directory tree for a new firm.

set -euo pipefail

APP_USER="jfmsrv01"
DATA_BASE="/data/jfmsrv01"

FIRM_ID="${1:-}"
[[ -z "$FIRM_ID" ]] && { echo "Usage: $0 <firm_id>"; exit 1; }

FIRM_DIR="$DATA_BASE/firms/$FIRM_ID"

if [[ -d "$FIRM_DIR" ]]; then
    echo "Firm directory already exists: $FIRM_DIR"
    exit 0
fi

echo "Creating storage for firm: $FIRM_ID"

mkdir -p "$FIRM_DIR/recordings"
mkdir -p "$FIRM_DIR/transcripts"
mkdir -p "$FIRM_DIR/documents"
mkdir -p "$FIRM_DIR/templates"
mkdir -p "$FIRM_DIR/attachments"
mkdir -p "$FIRM_DIR/exports"
mkdir -p "$FIRM_DIR/temp"
mkdir -p "$FIRM_DIR/reports"
mkdir -p "$FIRM_DIR/backups"

chown -R "$APP_USER:$APP_USER" "$FIRM_DIR"
chmod -R 750 "$FIRM_DIR"

# Create firm backup directory
mkdir -p "/backups/jfmsrv01/tenants/$FIRM_ID"
chown -R "$APP_USER:$APP_USER" "/backups/jfmsrv01/tenants/$FIRM_ID"
chmod 750 "/backups/jfmsrv01/tenants/$FIRM_ID"

echo "Storage created: $FIRM_DIR"
echo "  recordings/  transcripts/  documents/  templates/"
echo "  attachments/ exports/      temp/        reports/"
echo "  backups/"
FIRMSCRIPT

chmod 750 "$FIRM_SETUP_SCRIPT"
chown "$APP_USER:$APP_GROUP" "$FIRM_SETUP_SCRIPT"
success "Firm storage helper script created: $FIRM_SETUP_SCRIPT"

# =============================================================================
# Cleanup job
# =============================================================================

info "Creating temp/processing cleanup job"

CLEANUP_SCRIPT="$APP_BASE/scripts/cleanup_temp.sh"
cat > "$CLEANUP_SCRIPT" <<CLEANSCRIPT
#!/usr/bin/env bash
# =============================================================================
# JFM Server 01 - Temp/Processing Cleanup Job
# Runs on a schedule via cron. Cleans stale temp and processing files.
# Safe to run frequently - only removes files older than configured thresholds.
# =============================================================================

set -euo pipefail

DATA_BASE="$DATA_BASE"
LOG_FILE="/var/log/jfmsrv01_cleanup.log"
TEMP_MAX_AGE=$TEMP_MAX_AGE_HOURS
PROCESSING_MAX_AGE=$PROCESSING_MAX_AGE_HOURS
IMPORTS_MAX_AGE=$IMPORTS_MAX_AGE_HOURS

log() { echo "\$(date '+%Y-%m-%d %H:%M:%S') [CLEANUP] \$*" | tee -a "\$LOG_FILE"; }

log "Starting cleanup job"

# Clean per-firm temp folders
TEMP_CLEANED=0
while IFS= read -r -d '' FIRM_DIR; do
    TEMP_DIR="\$FIRM_DIR/temp"
    if [[ -d "\$TEMP_DIR" ]]; then
        COUNT=\$(find "\$TEMP_DIR" -type f -mmin +\$(( TEMP_MAX_AGE * 60 )) 2>/dev/null | wc -l)
        if [[ \$COUNT -gt 0 ]]; then
            find "\$TEMP_DIR" -type f -mmin +\$(( TEMP_MAX_AGE * 60 )) -delete
            log "Cleaned \$COUNT temp files from \$(basename \$FIRM_DIR)/temp"
            TEMP_CLEANED=\$(( TEMP_CLEANED + COUNT ))
        fi
    fi
done < <(find "\$DATA_BASE/firms" -mindepth 1 -maxdepth 1 -type d -print0 2>/dev/null)

# Check processing folder for stale files (flag but do not delete)
STALE_PROCESSING=\$(find "\$DATA_BASE/processing" -type f \
    -mmin +\$(( PROCESSING_MAX_AGE * 60 )) 2>/dev/null | wc -l)
if [[ \$STALE_PROCESSING -gt 0 ]]; then
    log "ALERT: \$STALE_PROCESSING stale file(s) in processing/ - possible crashed worker"
    find "\$DATA_BASE/processing" -type f \
        -mmin +\$(( PROCESSING_MAX_AGE * 60 )) 2>/dev/null | \
        while read -r F; do log "  STALE: \$F"; done
    # Write alert marker for diagnostics panel to detect
    echo "\$(date '+%Y-%m-%d %H:%M:%S') STALE_PROCESSING \$STALE_PROCESSING" \
        >> "\$DATA_BASE/system/alerts.log"
fi

# Check imports folder for stale files
STALE_IMPORTS=\$(find "\$DATA_BASE/imports" -type f \
    -mmin +\$(( IMPORTS_MAX_AGE * 60 )) 2>/dev/null | wc -l)
if [[ \$STALE_IMPORTS -gt 0 ]]; then
    log "ALERT: \$STALE_IMPORTS stale file(s) in imports/ - possible failed import job"
    echo "\$(date '+%Y-%m-%d %H:%M:%S') STALE_IMPORTS \$STALE_IMPORTS" \
        >> "\$DATA_BASE/system/alerts.log"
fi

log "Cleanup complete. Temp files removed: \$TEMP_CLEANED | Processing stale: \$STALE_PROCESSING | Import stale: \$STALE_IMPORTS"
CLEANSCRIPT

chmod 750 "$CLEANUP_SCRIPT"
chown "$APP_USER:$APP_GROUP" "$CLEANUP_SCRIPT"

# Register cleanup cron job (runs every 30 minutes)
CRON_LINE="*/30 * * * * $APP_USER $CLEANUP_SCRIPT >> /var/log/jfmsrv01_cleanup.log 2>&1"
CRON_FILE="/etc/cron.d/jfmsrv01-cleanup"
echo "$CRON_LINE" > "$CRON_FILE"
chmod 644 "$CRON_FILE"
success "Cleanup cron job created (runs every 30 minutes): $CRON_FILE"

# Create log file with correct ownership
touch /var/log/jfmsrv01_cleanup.log
chown "$APP_USER:$APP_GROUP" /var/log/jfmsrv01_cleanup.log

# =============================================================================
# ClamAV integration script
# =============================================================================

info "Creating ClamAV scan helper"

SCAN_SCRIPT="$APP_BASE/scripts/scan_file.sh"
cat > "$SCAN_SCRIPT" <<'SCANSCRIPT'
#!/usr/bin/env bash
# =============================================================================
# JFM Server 01 - File Virus Scan Helper
# Usage: scan_file.sh <filepath> <firm_id>
# Returns: 0 = clean, 1 = infected/quarantined, 2 = error
# Called by the application before accepting any uploaded file.
# =============================================================================

set -euo pipefail

FILE_PATH="${1:-}"
FIRM_ID="${2:-unknown}"
QUARANTINE_DIR="/data/jfmsrv01/quarantine"
LOG_FILE="/var/log/jfmsrv01_scan.log"

log() { echo "$(date '+%Y-%m-%d %H:%M:%S') [SCAN] $*" | tee -a "$LOG_FILE"; }

[[ -z "$FILE_PATH" ]] && { echo "Usage: $0 <filepath> <firm_id>"; exit 2; }
[[ ! -f "$FILE_PATH" ]] && { log "ERROR: File not found: $FILE_PATH"; exit 2; }

log "Scanning: $FILE_PATH (firm: $FIRM_ID)"

SCAN_OUTPUT=$(clamscan --no-summary "$FILE_PATH" 2>&1)
SCAN_EXIT=$?

if [[ $SCAN_EXIT -eq 0 ]]; then
    log "CLEAN: $FILE_PATH"
    exit 0
elif [[ $SCAN_EXIT -eq 1 ]]; then
    # Infected - move to quarantine
    FILENAME=$(basename "$FILE_PATH")
    QUARANTINE_PATH="$QUARANTINE_DIR/$(date +%Y%m%d_%H%M%S)_firm${FIRM_ID}_${FILENAME}"
    mv "$FILE_PATH" "$QUARANTINE_PATH"
    log "INFECTED: Moved to quarantine: $QUARANTINE_PATH"
    log "ClamAV output: $SCAN_OUTPUT"
    # Write alert for diagnostics
    echo "$(date '+%Y-%m-%d %H:%M:%S') INFECTED firm=$FIRM_ID file=$FILENAME quarantine=$QUARANTINE_PATH" \
        >> "/data/jfmsrv01/system/alerts.log"
    exit 1
else
    log "SCAN ERROR: $FILE_PATH | Exit: $SCAN_EXIT | Output: $SCAN_OUTPUT"
    exit 2
fi
SCANSCRIPT

chmod 750 "$SCAN_SCRIPT"
chown "$APP_USER:$APP_GROUP" "$SCAN_SCRIPT"

# ClamAV freshclam cron (daily definition update check)
cat > /etc/cron.d/jfmsrv01-clamav <<'EOF'
# Update ClamAV definitions daily at 02:30
30 2 * * * root /usr/bin/freshclam --quiet >> /var/log/clamav/freshclam.log 2>&1
EOF
chmod 644 /etc/cron.d/jfmsrv01-clamav
success "ClamAV scan helper and daily update cron created"

# =============================================================================
# Log files
# =============================================================================

info "Creating application log files"
touch /var/log/jfmsrv01_cleanup.log
touch /var/log/jfmsrv01_scan.log
chown "$APP_USER:$APP_GROUP" \
    /var/log/jfmsrv01_cleanup.log \
    /var/log/jfmsrv01_scan.log
chmod 640 \
    /var/log/jfmsrv01_cleanup.log \
    /var/log/jfmsrv01_scan.log

# Add to logrotate
cat >> /etc/logrotate.d/jfmsrv01 <<EOF

/var/log/jfmsrv01_cleanup.log
/var/log/jfmsrv01_scan.log {
    daily
    missingok
    rotate 30
    compress
    delaycompress
    notifempty
    create 640 $APP_USER $APP_GROUP
}
EOF
success "Log files created and added to logrotate"

# =============================================================================
# Verify layout
# =============================================================================

info "Verifying directory layout"
echo ""
echo "Data directory layout:"
tree -L 3 "$DATA_BASE" 2>/dev/null || find "$DATA_BASE" -maxdepth 3 -type d | sort
echo ""
echo "Backup directory layout:"
tree -L 2 "$BACKUP_BASE" 2>/dev/null || find "$BACKUP_BASE" -maxdepth 2 -type d | sort
echo ""

# Permission check
echo "Permission verification:"
ls -la "$DATA_BASE/"
echo ""

success "Storage layout complete"
info "Next step: run 02_app_skeleton.sh"
echo ""
echo "Firm storage helper: $FIRM_SETUP_SCRIPT"
echo "Usage: sudo -u $APP_USER $FIRM_SETUP_SCRIPT firm_0001"
/jfmsrv01-build/02_app_skeleton.sh
#!/usr/bin/env bash
# =============================================================================
# JFM Server 01 - Phase 3
# Application Skeleton Setup
#
# Creates the Laravel application with all five mandatory foundations:
#   1. Feature flags
#   2. Tenant settings
#   3. Storage abstraction
#   4. AI abstraction (with privacy tier enforcement)
#   5. Event-based modules
#
# Also installs:
#   - Multi-tenant foundation (firms, platform structure)
#   - Users / roles / permissions
#   - Notification system foundation
#   - Basic matter/client module
#   - Queue worker configuration (Supervisor)
#   - Environment file
#
# Run as root after 01_storage_layout.sh
# =============================================================================

set -euo pipefail

APP_USER="jfmsrv01"
APP_BASE="/opt/jfmsrv01/app"
DATA_BASE="/data/jfmsrv01"
PHP_VERSION="8.3"

CREDS_FILE="/root/jfmsrv01_credentials.txt"
[[ -f "$CREDS_FILE" ]] && source "$CREDS_FILE" || {
    echo "Credentials file not found at $CREDS_FILE"
    echo "Set DB_PASSWORD and REDIS_PASSWORD manually below."
    DB_PASSWORD="${DB_PASSWORD:-CHANGE_ME}"
    REDIS_PASSWORD="${REDIS_PASSWORD:-CHANGE_ME}"
}

APP_KEY=$(php -r "echo 'base64:'.base64_encode(random_bytes(32));")

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} $*"; }
error()   { echo -e "${RED}[ERROR]${NC} $*"; exit 1; }

[[ $EUID -ne 0 ]] && error "Run as root."

info "Setting up Laravel application skeleton"

# =============================================================================
# Install Laravel
# =============================================================================

info "Creating Laravel project at $APP_BASE"
if [[ ! -f "$APP_BASE/artisan" ]]; then
    sudo -u "$APP_USER" composer create-project laravel/laravel "$APP_BASE" \
        --prefer-dist --no-interaction 2>&1 | tail -5
    success "Laravel project created"
else
    success "Laravel project already exists"
fi

cd "$APP_BASE"

# =============================================================================
# Install packages
# =============================================================================

info "Installing Composer packages"
sudo -u "$APP_USER" composer require \
    spatie/laravel-permission \
    spatie/laravel-activitylog \
    spatie/laravel-settings \
    predis/predis \
    --no-interaction 2>&1 | tail -5
success "Core packages installed"

# =============================================================================
# Environment file
# =============================================================================

info "Creating .env file"
cat > "$APP_BASE/.env" <<EOF
APP_NAME="JFM Server 01"
APP_ENV=production
APP_KEY=$APP_KEY
APP_DEBUG=false
APP_URL=https://jfmsrv011

LOG_CHANNEL=daily
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=warning

DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=${DB_NAME:-jfmsrv01}
DB_USERNAME=${DB_USER:-jfmsrv01}
DB_PASSWORD=${DB_PASSWORD}

BROADCAST_DRIVER=log
CACHE_DRIVER=redis
FILESYSTEM_DISK=local
QUEUE_CONNECTION=redis
SESSION_DRIVER=redis
SESSION_LIFETIME=120

REDIS_HOST=127.0.0.1
REDIS_PASSWORD=${REDIS_PASSWORD}
REDIS_PORT=6379

MAIL_MAILER=smtp
MAIL_HOST=127.0.0.1
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="noreply@jfmsrv011"
MAIL_FROM_NAME="\${APP_NAME}"

# Storage paths
APP_STORAGE_BASE=$DATA_BASE/firms
APP_QUARANTINE_PATH=$DATA_BASE/quarantine
APP_PROCESSING_PATH=$DATA_BASE/processing
APP_IMPORTS_PATH=$DATA_BASE/imports
APP_SHARED_PATH=$DATA_BASE/shared

# ClamAV
CLAMAV_ENABLED=true
CLAMAV_SCAN_SCRIPT=/opt/jfmsrv01/scripts/scan_file.sh

# Deployment mode: multi_tenant | single_tenant_hosted | single_tenant_onsite
DEPLOYMENT_MODE=single_tenant_onsite
EOF

chown "$APP_USER:$APP_USER" "$APP_BASE/.env"
chmod 640 "$APP_BASE/.env"
success ".env file created"

# =============================================================================
# Storage symlink
# =============================================================================

sudo -u "$APP_USER" php artisan storage:link --quiet || true

# Fix storage permissions
chown -R "$APP_USER:$APP_USER" "$APP_BASE/storage" "$APP_BASE/bootstrap/cache"
chmod -R 775 "$APP_BASE/storage" "$APP_BASE/bootstrap/cache"

# =============================================================================
# Database migrations - foundations
# =============================================================================

info "Creating foundation migrations"

# 1. Firms / Tenants
cat > "$APP_BASE/database/migrations/$(date +%Y_%m_%d)_000001_create_firms_table.php" <<'PHP'
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {
    public function up(): void
    {
        Schema::create('firms', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('slug')->unique();
            $table->string('status')->default('active'); // active, suspended, trial
            $table->string('deployment_mode')->default('hosted'); // hosted, onsite, dedicated
            $table->string('plan')->nullable();
            $table->string('domain')->nullable()->unique();
            $table->string('subdomain')->nullable()->unique();
            $table->string('logo_path')->nullable();
            $table->string('primary_colour')->default('#1a56db');
            $table->string('secondary_colour')->default('#f3f4f6');
            $table->string('timezone')->default('Australia/Perth');
            $table->string('country')->default('AU');
            $table->string('state')->nullable();
            $table->string('jurisdiction')->default('WA'); // default legal jurisdiction
            $table->string('storage_path')->nullable(); // override per-firm storage base
            $table->string('storage_driver')->default('local'); // local, smb, sftp, s3
            $table->integer('storage_quota_gb')->default(50);
            $table->bigInteger('storage_used_bytes')->default(0);
            $table->json('settings')->nullable(); // firm-level config overrides
            $table->json('ai_settings')->nullable(); // AI provider preferences
            $table->string('ai_privacy_tier')->default('2'); // 1=onprem, 2=private, 3=shared
            $table->boolean('mfa_required')->default(false);
            $table->string('retention_days')->default('2555'); // ~7 years default
            $table->timestamp('trial_ends_at')->nullable();
            $table->timestamp('suspended_at')->nullable();
            $table->timestamps();
            $table->softDeletes();
        });
    }

    public function down(): void { Schema::dropIfExists('firms'); }
};
PHP

# 2. Users (extend default, add firm_id + role)
cat > "$APP_BASE/database/migrations/$(date +%Y_%m_%d)_000002_add_firm_id_to_users_table.php" <<'PHP'
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {
    public function up(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->foreignId('firm_id')->nullable()->constrained('firms')->nullOnDelete();
            $table->boolean('is_platform_admin')->default(false);
            $table->string('status')->default('active');
            $table->string('mfa_secret')->nullable();
            $table->boolean('mfa_enabled')->default(false);
            $table->timestamp('last_login_at')->nullable();
            $table->string('last_login_ip')->nullable();
            $table->index('firm_id');
        });
    }

    public function down(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn([
                'firm_id','is_platform_admin','status',
                'mfa_secret','mfa_enabled','last_login_at','last_login_ip'
            ]);
        });
    }
};
PHP

# 3. Feature flags
cat > "$APP_BASE/database/migrations/$(date +%Y_%m_%d)_000003_create_feature_flags_table.php" <<'PHP'
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {
    public function up(): void
    {
        Schema::create('feature_flags', function (Blueprint $table) {
            $table->id();
            // Scope: platform (global), firm, role, user
            $table->string('scope')->default('platform');
            $table->foreignId('firm_id')->nullable()->constrained('firms')->cascadeOnDelete();
            $table->foreignId('user_id')->nullable()->constrained('users')->cascadeOnDelete();
            $table->string('role')->nullable();
            $table->string('feature'); // e.g. 'transcription', 'templates', 'ai_review'
            $table->boolean('enabled')->default(false);
            $table->json('config')->nullable(); // feature-specific config
            $table->timestamps();
            $table->unique(['scope','firm_id','user_id','role','feature'], 'feature_flags_unique');
            $table->index(['firm_id','feature']);
            $table->index(['scope','feature']);
        });

        // Global feature definitions
        Schema::create('feature_definitions', function (Blueprint $table) {
            $table->id();
            $table->string('key')->unique();
            $table->string('label');
            $table->text('description')->nullable();
            $table->string('module')->nullable(); // which module this belongs to
            $table->boolean('default_enabled')->default(false);
            $table->json('plans')->nullable(); // which plans include this feature
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('feature_flags');
        Schema::dropIfExists('feature_definitions');
    }
};
PHP

# 4. Matters / Clients
cat > "$APP_BASE/database/migrations/$(date +%Y_%m_%d)_000004_create_matters_table.php" <<'PHP'
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {
    public function up(): void
    {
        Schema::create('matters', function (Blueprint $table) {
            $table->id();
            $table->foreignId('firm_id')->constrained('firms')->cascadeOnDelete();
            $table->string('matter_number')->nullable();
            $table->string('client_name');
            $table->string('client_reference')->nullable();
            $table->string('matter_type')->nullable(); // will, conveyancing, litigation, etc.
            $table->text('description')->nullable();
            $table->foreignId('assigned_lawyer_id')->nullable()->constrained('users')->nullOnDelete();
            $table->string('status')->default('open'); // open, closed, archived
            $table->date('opened_date')->nullable();
            $table->date('closed_date')->nullable();
            $table->string('jurisdiction')->nullable();
            $table->json('metadata')->nullable();
            $table->timestamps();
            $table->softDeletes();
            $table->index(['firm_id','status']);
            $table->index(['firm_id','matter_number']);
            $table->unique(['firm_id','matter_number']);
        });
    }

    public function down(): void { Schema::dropIfExists('matters'); }
};
PHP

# 5. Notifications
cat > "$APP_BASE/database/migrations/$(date +%Y_%m_%d)_000005_create_notifications_table.php" <<'PHP'
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {
    public function up(): void
    {
        Schema::create('platform_notifications', function (Blueprint $table) {
            $table->id();
            $table->foreignId('firm_id')->nullable()->constrained('firms')->cascadeOnDelete();
            $table->foreignId('user_id')->nullable()->constrained('users')->cascadeOnDelete();
            $table->string('type'); // transcription_complete, job_failed, backup_failed, etc.
            $table->string('level')->default('info'); // info, warning, error, success
            $table->string('title');
            $table->text('message');
            $table->json('data')->nullable(); // extra context (job_id, file_name, etc.)
            $table->string('link')->nullable(); // URL to relevant resource
            $table->boolean('read')->default(false);
            $table->timestamp('read_at')->nullable();
            $table->timestamp('expires_at')->nullable();
            $table->timestamps();
            $table->index(['firm_id','user_id','read']);
            $table->index(['firm_id','type']);
        });
    }

    public function down(): void { Schema::dropIfExists('platform_notifications'); }
};
PHP

# 6. Audit log
cat > "$APP_BASE/database/migrations/$(date +%Y_%m_%d)_000006_create_audit_logs_table.php" <<'PHP'
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {
    public function up(): void
    {
        Schema::create('audit_logs', function (Blueprint $table) {
            $table->id();
            $table->foreignId('firm_id')->nullable()->constrained('firms')->nullOnDelete();
            $table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete();
            $table->boolean('is_platform_admin_action')->default(false);
            $table->boolean('is_impersonation')->default(false);
            $table->foreignId('impersonating_admin_id')->nullable()->constrained('users')->nullOnDelete();
            $table->string('event'); // e.g. 'recording.uploaded', 'document.approved'
            $table->string('auditable_type')->nullable(); // model class
            $table->unsignedBigInteger('auditable_id')->nullable();
            $table->json('old_values')->nullable();
            $table->json('new_values')->nullable();
            $table->json('metadata')->nullable(); // IP, user agent, etc.
            $table->string('ip_address')->nullable();
            $table->string('user_agent')->nullable();
            $table->timestamps();
            $table->index(['firm_id','event']);
            $table->index(['firm_id','user_id']);
            $table->index(['auditable_type','auditable_id']);
        });
    }

    public function down(): void { Schema::dropIfExists('audit_logs'); }
};
PHP

success "Foundation migrations created"

# =============================================================================
# Core service providers and foundations
# =============================================================================

info "Creating TenantScope global scope"
mkdir -p "$APP_BASE/app/Models/Scopes"
cat > "$APP_BASE/app/Models/Scopes/TenantScope.php" <<'PHP'
<?php
namespace App\Models\Scopes;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;

/**
 * TenantScope - Automatically filters all queries by the current firm.
 * Applied to all tenant-owned models to prevent cross-firm data leakage.
 */
class TenantScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        if ($firmId = app('current_firm_id')) {
            $builder->where($model->getTable() . '.firm_id', $firmId);
        }
    }
}
PHP

info "Creating Firm model"
cat > "$APP_BASE/app/Models/Firm.php" <<'PHP'
<?php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Firm extends Model
{
    use SoftDeletes;

    protected $fillable = [
        'name','slug','status','deployment_mode','plan','domain','subdomain',
        'logo_path','primary_colour','secondary_colour','timezone','country',
        'state','jurisdiction','storage_path','storage_driver','storage_quota_gb',
        'storage_used_bytes','settings','ai_settings','ai_privacy_tier',
        'mfa_required','retention_days','trial_ends_at','suspended_at',
    ];

    protected $casts = [
        'settings'        => 'array',
        'ai_settings'     => 'array',
        'mfa_required'    => 'boolean',
        'trial_ends_at'   => 'datetime',
        'suspended_at'    => 'datetime',
    ];

    public function users()       { return $this->hasMany(User::class); }
    public function matters()     { return $this->hasMany(Matter::class); }
    public function featureFlags(){ return $this->hasMany(FeatureFlag::class); }

    public function hasFeature(string $feature): bool
    {
        return \App\Services\FeatureFlagService::firmHasFeature($this->id, $feature);
    }

    public function storagePath(string $subfolder = ''): string
    {
        $base = $this->storage_path ?: config('app.storage_base') . '/' . $this->id;
        return $subfolder ? rtrim($base, '/') . '/' . ltrim($subfolder, '/') : $base;
    }

    public function isStorageQuotaExceeded(): bool
    {
        return $this->storage_used_bytes >= ($this->storage_quota_gb * 1024 * 1024 * 1024);
    }

    public function storageUsedPercent(): float
    {
        $quota = $this->storage_quota_gb * 1024 * 1024 * 1024;
        return $quota > 0 ? round(($this->storage_used_bytes / $quota) * 100, 1) : 0;
    }
}
PHP

info "Creating storage abstraction service"
mkdir -p "$APP_BASE/app/Services"
cat > "$APP_BASE/app/Services/StorageService.php" <<'PHP'
<?php
namespace App\Services;

use App\Models\Firm;
use Illuminate\Support\Facades\Log;
use RuntimeException;

/**
 * StorageService - All firm file operations must go through this service.
 * Enforces tenant isolation and path traversal protection.
 * Never hard-code file paths in business logic - use this service.
 */
class StorageService
{
    private Firm $firm;
    private string $basePath;

    public function __construct(Firm $firm)
    {
        $this->firm = $firm;
        $this->basePath = $this->resolveFirmBase($firm);
    }

    private function resolveFirmBase(Firm $firm): string
    {
        $base = $firm->storage_path
            ?: rtrim(config('app.storage_base', '/data/jfmsrv01/firms'), '/') . '/' . $firm->id;
        return rtrim($base, '/');
    }

    /**
     * Resolve a path within this firm's storage.
     * Throws if the resolved path would escape the firm's base directory.
     */
    public function path(string $subfolder, string $filename = ''): string
    {
        $resolved = $this->basePath . '/' . ltrim($subfolder, '/');
        if ($filename) {
            $resolved .= '/' . ltrim($filename, '/');
        }

        // Normalise and validate - prevent path traversal
        $real = realpath(dirname($resolved));
        if ($real === false) {
            // Directory doesn't exist yet - check the path string directly
            $normalised = $this->normalisePath($resolved);
        } else {
            $normalised = $real . '/' . basename($resolved);
        }

        if (!str_starts_with($normalised, $this->basePath)) {
            Log::error('Path traversal attempt blocked', [
                'firm_id'   => $this->firm->id,
                'subfolder' => $subfolder,
                'filename'  => $filename,
                'resolved'  => $resolved,
            ]);
            // Audit log
            \App\Services\AuditService::log('storage.path_traversal_blocked', null, [
                'subfolder' => $subfolder,
                'filename'  => $filename,
            ]);
            throw new RuntimeException('Invalid storage path: access denied.');
        }

        return $normalised;
    }

    private function normalisePath(string $path): string
    {
        $parts = explode('/', $path);
        $result = [];
        foreach ($parts as $part) {
            if ($part === '..') {
                array_pop($result);
            } elseif ($part !== '.') {
                $result[] = $part;
            }
        }
        return implode('/', $result);
    }

    public function recordingsPath(string $filename = ''): string
    {
        return $this->path('recordings', $filename);
    }

    public function transcriptsPath(string $filename = ''): string
    {
        return $this->path('transcripts', $filename);
    }

    public function documentsPath(string $filename = ''): string
    {
        return $this->path('documents', $filename);
    }

    public function templatesPath(string $filename = ''): string
    {
        return $this->path('templates', $filename);
    }

    public function tempPath(string $filename = ''): string
    {
        return $this->path('temp', $filename);
    }

    public function ensureDirectoryExists(string $path): void
    {
        if (!is_dir($path)) {
            mkdir($path, 0750, true);
            chown($path, 'jfmsrv01');
        }
    }

    public function storageUsedBytes(): int
    {
        if (!is_dir($this->basePath)) return 0;
        $output = shell_exec("du -sb " . escapeshellarg($this->basePath) . " 2>/dev/null");
        return (int) explode("\t", trim($output ?? ''))[0];
    }

    public function updateStorageUsage(): void
    {
        $bytes = $this->storageUsedBytes();
        $this->firm->update(['storage_used_bytes' => $bytes]);
    }
}
PHP

info "Creating AI abstraction service"
cat > "$APP_BASE/app/Services/AiService.php" <<'PHP'
<?php
namespace App\Services;

use App\Models\Firm;
use RuntimeException;

/**
 * AiService - All AI provider calls must go through this router.
 * Enforces firm privacy tier restrictions.
 * Never call AI providers directly from business logic.
 *
 * Privacy tiers:
 *   1 = On-premise / self-hosted only (no data leaves firm infrastructure)
 *   2 = Private API with no-training guarantee (data processing agreement required)
 *   3 = Shared API (standard terms - use only if firm has approved)
 */
class AiService
{
    // Task types
    const TASK_TRANSCRIPTION       = 'transcription';
    const TASK_TRANSCRIPT_CLEANUP  = 'transcript_cleanup';
    const TASK_CLASSIFICATION      = 'document_classification';
    const TASK_TEMPLATE_SELECTION  = 'template_selection';
    const TASK_LEGAL_REVIEW        = 'legal_review';
    const TASK_EMAIL_DRAFT         = 'email_draft';
    const TASK_SECOND_CHECK        = 'second_ai_check';
    const TASK_SUMMARISATION       = 'summarisation';

    // Privacy tiers
    const TIER_ONPREMISE = '1';
    const TIER_PRIVATE   = '2';
    const TIER_SHARED    = '3';

    private Firm $firm;
    private string $permittedTier;

    public function __construct(Firm $firm)
    {
        $this->firm = $firm;
        $this->permittedTier = $firm->ai_privacy_tier ?? self::TIER_PRIVATE;
    }

    /**
     * Route a task to the appropriate AI provider, respecting the firm's tier.
     */
    public function route(string $task, array $payload, ?string $preferredProvider = null): array
    {
        $provider = $this->selectProvider($task, $preferredProvider);
        $this->enforcePrivacyTier($provider);
        $this->logUsage($task, $provider);
        return $this->dispatch($provider, $task, $payload);
    }

    private function selectProvider(string $task, ?string $preferred): array
    {
        // Future: load from global AI provider definitions table
        // For MVP: use configured defaults
        $defaults = config('ai.providers', []);
        $taskDefaults = config("ai.task_defaults.$task", []);

        if ($preferred && isset($defaults[$preferred])) {
            return $defaults[$preferred];
        }
        if (!empty($taskDefaults) && isset($defaults[$taskDefaults['provider']])) {
            return $defaults[$taskDefaults['provider']];
        }

        throw new RuntimeException("No AI provider configured for task: $task");
    }

    private function enforcePrivacyTier(array $provider): void
    {
        $providerTier = $provider['privacy_tier'] ?? self::TIER_SHARED;

        if ($providerTier > $this->permittedTier) {
            AuditService::log('ai.provider_tier_blocked', null, [
                'firm_id'        => $this->firm->id,
                'provider'       => $provider['name'] ?? 'unknown',
                'provider_tier'  => $providerTier,
                'permitted_tier' => $this->permittedTier,
            ]);
            throw new RuntimeException(
                "AI provider tier ($providerTier) exceeds firm's permitted tier ({$this->permittedTier}). " .
                "Update the firm's AI privacy tier setting to allow this provider."
            );
        }
    }

    private function logUsage(string $task, array $provider): void
    {
        // Future: write to ai_usage_logs table for cost metering
    }

    private function dispatch(array $provider, string $task, array $payload): array
    {
        // Future: implement provider adapters (OpenAI, Anthropic, Whisper, local, etc.)
        // For MVP: return placeholder
        return [
            'provider' => $provider['name'] ?? 'not_configured',
            'task'     => $task,
            'status'   => 'pending_provider_configuration',
        ];
    }
}
PHP

info "Creating feature flag service"
cat > "$APP_BASE/app/Services/FeatureFlagService.php" <<'PHP'
<?php
namespace App\Services;

use App\Models\FeatureFlag;
use Illuminate\Support\Facades\Cache;

/**
 * FeatureFlagService - Checks whether features are enabled.
 * Hierarchy: user > role > firm > global
 * A disabled feature must be hidden from menus, blocked at routes,
 * and must not run background jobs.
 */
class FeatureFlagService
{
    public static function firmHasFeature(int $firmId, string $feature): bool
    {
        return Cache::remember("feature.$firmId.$feature", 300, function () use ($firmId, $feature) {
            // Check firm-level flag
            $flag = FeatureFlag::where('scope', 'firm')
                ->where('firm_id', $firmId)
                ->where('feature', $feature)
                ->first();

            if ($flag) return $flag->enabled;

            // Fall back to global/platform flag
            $global = FeatureFlag::where('scope', 'platform')
                ->whereNull('firm_id')
                ->where('feature', $feature)
                ->first();

            if ($global) return $global->enabled;

            // Fall back to feature definition default
            $def = \App\Models\FeatureDefinition::where('key', $feature)->first();
            return $def ? $def->default_enabled : false;
        });
    }

    public static function userHasFeature(int $userId, int $firmId, string $feature): bool
    {
        // Check user-level override first
        $flag = FeatureFlag::where('scope', 'user')
            ->where('user_id', $userId)
            ->where('feature', $feature)
            ->first();

        if ($flag) return $flag->enabled;

        return static::firmHasFeature($firmId, $feature);
    }

    public static function clearCache(int $firmId, string $feature): void
    {
        Cache::forget("feature.$firmId.$feature");
    }
}
PHP

info "Creating audit service"
cat > "$APP_BASE/app/Services/AuditService.php" <<'PHP'
<?php
namespace App\Services;

use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Request;
use App\Models\AuditLog;

/**
 * AuditService - All important actions must be logged here.
 * Tenant-scoped. Never log secrets, passwords, or API keys.
 */
class AuditService
{
    public static function log(
        string $event,
        ?int $auditableId = null,
        array $metadata = [],
        string $auditableType = '',
        array $oldValues = [],
        array $newValues = []
    ): void {
        try {
            $user = Auth::user();
            AuditLog::create([
                'firm_id'                   => $user?->firm_id ?? app('current_firm_id'),
                'user_id'                   => $user?->id,
                'is_platform_admin_action'  => $user?->is_platform_admin ?? false,
                'is_impersonation'          => session('is_impersonating', false),
                'impersonating_admin_id'    => session('impersonating_admin_id'),
                'event'                     => $event,
                'auditable_type'            => $auditableType,
                'auditable_id'              => $auditableId,
                'old_values'                => $oldValues ?: null,
                'new_values'                => $newValues ?: null,
                'metadata'                  => $metadata ?: null,
                'ip_address'                => Request::ip(),
                'user_agent'                => Request::userAgent(),
            ]);
        } catch (\Throwable $e) {
            // Audit log must never crash the application
            \Illuminate\Support\Facades\Log::error('AuditService failed: ' . $e->getMessage());
        }
    }
}
PHP

info "Creating event definitions"
mkdir -p "$APP_BASE/app/Events"
for EVENT in \
    "AudioUploaded" "AudioImported" \
    "TranscriptionStarted" "TranscriptionCompleted" \
    "TranscriptEdited" "TranscriptApproved" \
    "DocumentTypeDetected" "TemplateSelected" \
    "DocumentGenerated" "DocumentApproved" \
    "AiReviewRequested" "AiReviewCompleted" \
    "EmailDraftCreated" "EmailSent" \
    "BackupCompleted" "BackupFailed" \
    "JobFailed" "NotificationTriggered" \
    "RetentionExpiryWarning" \
    "SupportImpersonationStarted" "SupportImpersonationEnded"; do
cat > "$APP_BASE/app/Events/${EVENT}.php" <<PHPEOF
<?php
namespace App\\Events;

use Illuminate\\Foundation\\Events\\Dispatchable;
use Illuminate\\Queue\\SerializesModels;

class ${EVENT}
{
    use Dispatchable, SerializesModels;

    public function __construct(
        public readonly int \$firmId,
        public readonly array \$payload = []
    ) {}
}
PHPEOF
done
success "Event classes created (21 events)"

# =============================================================================
# Supervisor queue worker config
# =============================================================================

info "Creating Supervisor queue worker configuration"
cat > /etc/supervisor/conf.d/jfmsrv01-worker.conf <<EOF
[program:jfmsrv01-worker]
process_name=%(program_name)s_%(process_num)02d
command=php $APP_BASE/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600 --queue=high,default,low
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=$APP_USER
numprocs=4
redirect_stderr=true
stdout_logfile=$APP_BASE/storage/logs/worker.log
stdout_logfile_maxbytes=50MB
stdout_logfile_backups=10
stopwaitsecs=3600

[program:jfmsrv01-scheduler]
command=php $APP_BASE/artisan schedule:work
autostart=true
autorestart=true
user=$APP_USER
numprocs=1
redirect_stderr=true
stdout_logfile=$APP_BASE/storage/logs/scheduler.log
EOF

supervisorctl reread && supervisorctl update || warn "Supervisor reload failed - check manually"
success "Supervisor queue workers configured (4 workers)"

# =============================================================================
# Run migrations
# =============================================================================

info "Running database migrations"
sudo -u "$APP_USER" php "$APP_BASE/artisan" migrate --force 2>&1 | tail -10
success "Migrations complete"

# =============================================================================
# Seed feature definitions
# =============================================================================

info "Seeding feature definitions"
cat > "$APP_BASE/database/seeders/FeatureDefinitionSeeder.php" <<'PHP'
<?php
namespace Database\Seeders;

use Illuminate\Database\Seeder;
use App\Models\FeatureDefinition;

class FeatureDefinitionSeeder extends Seeder
{
    public function run(): void
    {
        $features = [
            // Level 1
            ['key'=>'transcription',         'label'=>'Transcription',               'module'=>'transcription',  'default_enabled'=>true,  'plans'=>['basic','standard','professional','enterprise']],
            ['key'=>'transcript_viewer',      'label'=>'Transcript Viewer/Editor',    'module'=>'transcription',  'default_enabled'=>true,  'plans'=>['basic','standard','professional','enterprise']],
            // Level 2
            ['key'=>'templates',             'label'=>'Word Templates',              'module'=>'templates',      'default_enabled'=>false, 'plans'=>['standard','professional','enterprise']],
            ['key'=>'document_generation',   'label'=>'Document Generation',         'module'=>'templates',      'default_enabled'=>false, 'plans'=>['standard','professional','enterprise']],
            ['key'=>'document_watermark',    'label'=>'Draft Watermarking',          'module'=>'templates',      'default_enabled'=>false, 'plans'=>['standard','professional','enterprise']],
            // Level 3
            ['key'=>'file_connectors',       'label'=>'SMB/SFTP/FTP Connectors',    'module'=>'connectors',     'default_enabled'=>false, 'plans'=>['professional','enterprise']],
            ['key'=>'folder_watcher',        'label'=>'Watched Folder Import',       'module'=>'connectors',     'default_enabled'=>false, 'plans'=>['professional','enterprise']],
            ['key'=>'local_agent',           'label'=>'Local Connector Agent',       'module'=>'connectors',     'default_enabled'=>false, 'plans'=>['professional','enterprise']],
            // Level 4
            ['key'=>'ai_cleanup',            'label'=>'AI Transcript Cleanup',       'module'=>'ai',             'default_enabled'=>false, 'plans'=>['professional','enterprise']],
            ['key'=>'ai_classification',     'label'=>'AI Document Classification',  'module'=>'ai',             'default_enabled'=>false, 'plans'=>['professional','enterprise']],
            ['key'=>'ai_legal_review',       'label'=>'AI Legal Review',             'module'=>'ai',             'default_enabled'=>false, 'plans'=>['enterprise']],
            ['key'=>'ai_second_checker',     'label'=>'Second AI Checker',           'module'=>'ai',             'default_enabled'=>false, 'plans'=>['enterprise']],
            // Level 5
            ['key'=>'email_integration',     'label'=>'Email Integration',           'module'=>'email',          'default_enabled'=>false, 'plans'=>['professional','enterprise']],
            ['key'=>'email_ai_reply',        'label'=>'AI Email Drafting',           'module'=>'email',          'default_enabled'=>false, 'plans'=>['enterprise']],
            // Level 6
            ['key'=>'live_dictation',        'label'=>'Live Browser Dictation',      'module'=>'dictation',      'default_enabled'=>false, 'plans'=>['enterprise']],
            // Level 7
            ['key'=>'voip_dictation',        'label'=>'VoIP/SIP Dictation',          'module'=>'voip',           'default_enabled'=>false, 'plans'=>['enterprise']],
            // Platform
            ['key'=>'matters',               'label'=>'Matter/Client Management',    'module'=>'matters',        'default_enabled'=>true,  'plans'=>['basic','standard','professional','enterprise']],
            ['key'=>'audit_log',             'label'=>'Audit Log',                   'module'=>'audit',          'default_enabled'=>true,  'plans'=>['basic','standard','professional','enterprise']],
            ['key'=>'notifications',         'label'=>'Notifications',               'module'=>'notifications',  'default_enabled'=>true,  'plans'=>['basic','standard','professional','enterprise']],
            ['key'=>'backups',               'label'=>'Backups',                     'module'=>'backups',        'default_enabled'=>true,  'plans'=>['basic','standard','professional','enterprise']],
        ];

        foreach ($features as $feature) {
            FeatureDefinition::updateOrCreate(
                ['key' => $feature['key']],
                array_merge($feature, ['plans' => json_encode($feature['plans'])])
            );
        }
    }
}
PHP

sudo -u "$APP_USER" php "$APP_BASE/artisan" db:seed --class=FeatureDefinitionSeeder --force 2>&1 | tail -5
success "Feature definitions seeded"

# =============================================================================
# Fix final permissions
# =============================================================================

chown -R "$APP_USER:$APP_USER" "$APP_BASE"
chmod -R 755 "$APP_BASE"
chmod 640 "$APP_BASE/.env"
find "$APP_BASE/storage" -type d -exec chmod 775 {} \;
find "$APP_BASE/bootstrap/cache" -type d -exec chmod 775 {} \;

# =============================================================================
# Summary
# =============================================================================

success "=== APPLICATION SKELETON COMPLETE ==="
echo ""
echo "Five mandatory foundations installed:"
echo "  1. Feature flags     - FeatureFlagService + feature_flags table"
echo "  2. Tenant settings   - Firm model + firms table"
echo "  3. Storage abstraction - StorageService with path traversal protection"
echo "  4. AI abstraction    - AiService with privacy tier enforcement"
echo "  5. Event system      - 21 event classes created"
echo ""
echo "Also installed:"
echo "  - Multi-tenant foundation (Platform -> Firm -> User)"
echo "  - Users + roles (spatie/laravel-permission)"
echo "  - Audit log (AuditService + audit_logs table)"
echo "  - Notifications (platform_notifications table)"
echo "  - Matter/client management (matters table)"
echo "  - Queue workers (4 Supervisor workers)"
echo "  - 20 feature definitions seeded"
echo ""
echo "Next step: run 03_admin_safety_tools.sh"
/jfmsrv01-build/03_admin_safety_tools.sh
#!/usr/bin/env bash
# =============================================================================
# JFM Server 01 - Phase 4
# Admin Safety Tools Setup
#
# Creates all developer/admin safety tooling:
#   - Backup scripts (source, database, file, tenant export)
#   - Changelog file
#   - Handover file
#   - Route list report
#   - Schema summary report
#   - Error log tail report
#   - Smoke test script
#   - Admin diagnostics data endpoint
#   - Patch workflow script (backup -> apply -> smoke test -> log)
#
# Run as root after 02_app_skeleton.sh
# =============================================================================

set -euo pipefail

APP_USER="jfmsrv01"
APP_BASE="/opt/jfmsrv01/app"
SCRIPTS_BASE="/opt/jfmsrv01/scripts"
DATA_BASE="/data/jfmsrv01"
BACKUP_BASE="/backups/jfmsrv01"
PHP_VERSION="8.3"

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} $*"; }
error()   { echo -e "${RED}[ERROR]${NC} $*"; exit 1; }

[[ $EUID -ne 0 ]] && error "Run as root."
mkdir -p "$SCRIPTS_BASE"

# =============================================================================
# Changelog file
# =============================================================================

info "Creating changelog file"
CHANGELOG="$SCRIPTS_BASE/CHANGELOG.md"
if [[ ! -f "$CHANGELOG" ]]; then
cat > "$CHANGELOG" <<EOF
# JFM Server 01 - Changelog

## $(date +%Y-%m-%d) - Initial Setup

### Added
- Server provisioned: Ubuntu 24.04 LTS
- Phase 0: System hardening, firewall, SSH, fail2ban
- Phase 1: Nginx, PHP $PHP_VERSION, PostgreSQL, Redis, ClamAV, ffmpeg, LibreOffice
- Phase 2: Storage layout, firm folder structure, ClamAV scan helper, cleanup cron
- Phase 3: Laravel application skeleton
  - Five mandatory foundations: feature flags, tenant settings, storage abstraction,
    AI abstraction (with privacy tier enforcement), event-based modules
  - Multi-tenant architecture (Platform -> Firm -> User)
  - Audit log, notifications, matter/client management
  - 21 event classes, 20 feature definitions
- Phase 4: Admin safety tools (this entry)

### Architecture decisions
- Database: PostgreSQL (row-level security, better JSON, stronger integrity)
- Queue: Redis with AOF persistence and noeviction policy
- Storage: Shared disk with per-firm logical folders, path traversal protection
- AI: Privacy tier model (1=onprem, 2=private, 3=shared), firm-level enforcement
- Tenancy: Shared database + firm_id on all tenant tables, separate firm folders
EOF
fi
chown "$APP_USER:$APP_USER" "$CHANGELOG"
success "Changelog created: $CHANGELOG"

# =============================================================================
# Handover file
# =============================================================================

info "Creating handover file"
HANDOVER="$SCRIPTS_BASE/HANDOVER.md"
cat > "$HANDOVER" <<EOF
# JFM Server 01 - Handover Document

**Last updated:** $(date '+%Y-%m-%d %H:%M:%S')
**Server:** $(hostname)
**Platform version:** MVP Phase 4

---

## Current state

The server has completed Phases 0-4:
- Server provisioned and hardened
- All services installed (Nginx, PostgreSQL, Redis, ClamAV, PHP, Python, ffmpeg)
- Laravel application skeleton created with all five mandatory foundations
- Database migrations run, feature definitions seeded

## What has been built

### Five mandatory foundations
1. **Feature flags** - \`FeatureFlagService\`, \`feature_flags\` table, 20 feature definitions
2. **Tenant settings** - \`Firm\` model, \`firms\` table, per-firm config
3. **Storage abstraction** - \`StorageService\` with path traversal protection
4. **AI abstraction** - \`AiService\` with privacy tier enforcement (tiers 1/2/3)
5. **Event system** - 21 event classes under \`app/Events/\`

### Other foundations
- Multi-tenant architecture: Platform -> Firm -> User
- Audit log: \`AuditService\`, \`audit_logs\` table
- Notifications: \`platform_notifications\` table
- Matter/client management: \`matters\` table
- Queue workers: 4 Supervisor workers + scheduler

## What needs to be built next (Phase 5)

- Manual audio upload form (firm-scoped, with ClamAV scan)
- Transcription job model and migration
- Queue job: \`ProcessTranscriptionJob\`
- Transcript model + viewer/editor UI
- Basic admin theme (shared, not page-specific CSS)

## Key file locations

| Path | Purpose |
|------|---------|
| \`/opt/jfmsrv01/app\` | Laravel application |
| \`/opt/jfmsrv01/app/.env\` | Environment configuration |
| \`/data/jfmsrv01/firms/\` | Per-firm file storage |
| \`/data/jfmsrv01/quarantine/\` | Quarantined files (failed virus scan) |
| \`/backups/jfmsrv01/\` | Backup destination |
| \`/opt/jfmsrv01/scripts/\` | Admin scripts |
| \`/etc/supervisor/conf.d/jfmsrv01-worker.conf\` | Queue worker config |
| \`/etc/nginx/sites-available/jfmsrv01\` | Nginx config |
| \`/root/jfmsrv01_credentials.txt\` | Generated passwords (root only) |

## Key commands

\`\`\`bash
# Check queue workers
supervisorctl status

# Run migrations
sudo -u jfmsrv01 php /opt/jfmsrv01/app/artisan migrate

# Run smoke test
sudo bash /opt/jfmsrv01/scripts/smoke_test.sh

# Create backup
sudo bash /opt/jfmsrv01/scripts/backup.sh source

# View error log
tail -50 /opt/jfmsrv01/app/storage/logs/laravel.log

# Create firm storage
sudo -u jfmsrv01 /opt/jfmsrv01/scripts/create_firm_storage.sh firm_0001
\`\`\`

## Architecture notes

- All file access must use \`StorageService\` - never hard-code paths
- All AI calls must use \`AiService\` - it enforces the firm's privacy tier
- All feature checks must use \`FeatureFlagService\` - check before showing UI or running jobs
- All important actions must call \`AuditService::log()\`
- Every tenant-owned model must have \`firm_id\` and use \`TenantScope\`
- Logs must never contain secrets, passwords, or API keys at any level

## Deployment mode

Current: \`single_tenant_onsite\` (set \`DEPLOYMENT_MODE\` in \`.env\`)
Options: \`multi_tenant\` | \`single_tenant_hosted\` | \`single_tenant_onsite\`
EOF

chown "$APP_USER:$APP_USER" "$HANDOVER"
success "Handover file created: $HANDOVER"

# =============================================================================
# Backup script
# =============================================================================

info "Creating backup script"
cat > "$SCRIPTS_BASE/backup.sh" <<'BACKUPSCRIPT'
#!/usr/bin/env bash
# =============================================================================
# JFM Server 01 - Backup Script
# Usage: backup.sh [source|database|files|tenant <firm_id>|full]
# =============================================================================

set -euo pipefail

APP_BASE="/opt/jfmsrv01/app"
DATA_BASE="/data/jfmsrv01"
BACKUP_BASE="/backups/jfmsrv01"
SCRIPTS_BASE="/opt/jfmsrv01/scripts"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
LOG_FILE="$BACKUP_BASE/reports/backup_${TIMESTAMP}.log"
RETENTION_DAYS=30

mkdir -p "$BACKUP_BASE/reports"

log() { echo "$(date '+%Y-%m-%d %H:%M:%S') $*" | tee -a "$LOG_FILE"; }
success() { log "[OK] $*"; }
error()   { log "[ERROR] $*"; exit 1; }

BACKUP_TYPE="${1:-full}"
log "Starting backup: type=$BACKUP_TYPE timestamp=$TIMESTAMP"

do_source_backup() {
    log "Source backup starting"
    DEST="$BACKUP_BASE/source/source_${TIMESTAMP}.tar.gz"
    tar -czf "$DEST" \
        --exclude="$APP_BASE/node_modules" \
        --exclude="$APP_BASE/vendor" \
        --exclude="$APP_BASE/storage/framework/cache" \
        --exclude="$APP_BASE/storage/framework/sessions" \
        "$APP_BASE" \
        "$SCRIPTS_BASE" \
        /etc/nginx/sites-available/jfmsrv01 \
        /etc/supervisor/conf.d/jfmsrv01-worker.conf \
        2>/dev/null
    SIZE=$(du -sh "$DEST" | cut -f1)
    success "Source backup: $DEST ($SIZE)"

    # Keep last N source backups
    ls -t "$BACKUP_BASE/source/source_"*.tar.gz 2>/dev/null | tail -n +11 | xargs -r rm
}

do_database_backup() {
    log "Database backup starting"
    DEST="$BACKUP_BASE/database/db_${TIMESTAMP}.sql.gz"
    CREDS="/root/jfmsrv01_credentials.txt"
    [[ -f "$CREDS" ]] && source "$CREDS"
    PGPASSWORD="${DB_PASSWORD:-}" pg_dump \
        -U "${DB_USER:-jfmsrv01}" \
        -h 127.0.0.1 \
        "${DB_NAME:-jfmsrv01}" | gzip > "$DEST"
    SIZE=$(du -sh "$DEST" | cut -f1)
    success "Database backup: $DEST ($SIZE)"

    # Schema summary
    SCHEMA_DEST="$BACKUP_BASE/database/schema_${TIMESTAMP}.txt"
    PGPASSWORD="${DB_PASSWORD:-}" psql \
        -U "${DB_USER:-jfmsrv01}" \
        -h 127.0.0.1 \
        "${DB_NAME:-jfmsrv01}" \
        -c "\dt" > "$SCHEMA_DEST" 2>&1
    success "Schema summary: $SCHEMA_DEST"

    # Keep last 30 database backups
    ls -t "$BACKUP_BASE/database/db_"*.sql.gz 2>/dev/null | tail -n +31 | xargs -r rm
}

do_files_backup() {
    log "File backup starting (incremental)"
    DEST="$BACKUP_BASE/files/files_${TIMESTAMP}.tar.gz"
    tar -czf "$DEST" \
        --exclude="$DATA_BASE/processing" \
        --exclude="$DATA_BASE/imports" \
        "$DATA_BASE/firms" \
        "$DATA_BASE/shared" \
        2>/dev/null || true
    SIZE=$(du -sh "$DEST" | cut -f1)
    success "File backup: $DEST ($SIZE)"
}

do_tenant_backup() {
    FIRM_ID="${2:-}"
    [[ -z "$FIRM_ID" ]] && { log "Usage: backup.sh tenant <firm_id>"; exit 1; }
    log "Tenant backup: $FIRM_ID"
    FIRM_DIR="$DATA_BASE/firms/$FIRM_ID"
    [[ ! -d "$FIRM_DIR" ]] && error "Firm directory not found: $FIRM_DIR"
    mkdir -p "$BACKUP_BASE/tenants/$FIRM_ID"
    DEST="$BACKUP_BASE/tenants/$FIRM_ID/backup_${TIMESTAMP}.tar.gz"
    tar -czf "$DEST" "$FIRM_DIR" 2>/dev/null
    SIZE=$(du -sh "$DEST" | cut -f1)
    success "Tenant backup: $DEST ($SIZE)"
}

do_handover_backup() {
    log "Backing up handover and changelog"
    cp "$SCRIPTS_BASE/CHANGELOG.md" "$BACKUP_BASE/handovers/CHANGELOG_${TIMESTAMP}.md" 2>/dev/null || true
    cp "$SCRIPTS_BASE/HANDOVER.md"  "$BACKUP_BASE/handovers/HANDOVER_${TIMESTAMP}.md"  2>/dev/null || true
    success "Handover files backed up"
}

case "$BACKUP_TYPE" in
    source)   do_source_backup ;;
    database) do_database_backup ;;
    files)    do_files_backup ;;
    tenant)   do_tenant_backup "$@" ;;
    full)
        do_source_backup
        do_database_backup
        do_files_backup
        do_handover_backup
        ;;
    *) error "Unknown backup type: $BACKUP_TYPE. Use: source|database|files|tenant <id>|full" ;;
esac

log "Backup complete: $BACKUP_TYPE"
BACKUPSCRIPT

chmod 750 "$SCRIPTS_BASE/backup.sh"

# =============================================================================
# Route list report
# =============================================================================

info "Creating route list report script"
cat > "$SCRIPTS_BASE/route_list.sh" <<EOF
#!/usr/bin/env bash
# Generates current route list for handover/review
cd $APP_BASE
php artisan route:list --columns=method,uri,name,action 2>/dev/null | tee /tmp/route_list_\$(date +%Y%m%d).txt
echo "Saved to: /tmp/route_list_\$(date +%Y%m%d).txt"
EOF
chmod 750 "$SCRIPTS_BASE/route_list.sh"

# =============================================================================
# Error log tail report
# =============================================================================

info "Creating log tail script"
cat > "$SCRIPTS_BASE/log_tail.sh" <<EOF
#!/usr/bin/env bash
# Shows last N lines of application error log
LINES="\${1:-50}"
LOG="$APP_BASE/storage/logs/laravel.log"
[[ -f "\$LOG" ]] && tail -n "\$LINES" "\$LOG" || echo "No log file found at \$LOG"
EOF
chmod 750 "$SCRIPTS_BASE/log_tail.sh"

# =============================================================================
# Smoke test script
# =============================================================================

info "Creating smoke test script"
cat > "$SCRIPTS_BASE/smoke_test.sh" <<'SMOKETEST'
#!/usr/bin/env bash
# =============================================================================
# JFM Server 01 - Smoke Test
# Run before and after every patch.
# Exit code 0 = all pass. Non-zero = failures detected.
# =============================================================================

set -euo pipefail

APP_BASE="/opt/jfmsrv01/app"
DATA_BASE="/data/jfmsrv01"
PASS=0; FAIL=0; WARN=0
REPORT_FILE="/tmp/smoke_test_$(date +%Y%m%d_%H%M%S).txt"

p() { echo "[PASS] $*"; echo "[PASS] $*" >> "$REPORT_FILE"; ((PASS++)); }
f() { echo "[FAIL] $*"; echo "[FAIL] $*" >> "$REPORT_FILE"; ((FAIL++)); }
w() { echo "[WARN] $*"; echo "[WARN] $*" >> "$REPORT_FILE"; ((WARN++)); }

echo "=== Smoke Test $(date) ===" | tee "$REPORT_FILE"

# PHP
php -v &>/dev/null && p "PHP available" || f "PHP not available"

# Artisan
[[ -f "$APP_BASE/artisan" ]] && p "artisan exists" || f "artisan not found"

# .env
[[ -f "$APP_BASE/.env" ]] && p ".env exists" || f ".env not found"

# Database connection
php "$APP_BASE/artisan" db:show &>/dev/null && p "Database connection OK" || f "Database connection FAILED"

# Migrations
PENDING=$(php "$APP_BASE/artisan" migrate:status 2>/dev/null | grep -c "Pending" || true)
[[ "$PENDING" -eq 0 ]] && p "No pending migrations" || w "$PENDING pending migrations"

# Storage paths
[[ -d "$DATA_BASE/firms" ]]      && p "firms/ directory exists"      || f "firms/ directory MISSING"
[[ -d "$DATA_BASE/quarantine" ]] && p "quarantine/ directory exists"  || f "quarantine/ directory MISSING"
[[ -d "$DATA_BASE/processing" ]] && p "processing/ directory exists"  || f "processing/ directory MISSING"

# Permissions
OWNER=$(stat -c '%U' "$DATA_BASE" 2>/dev/null || echo "unknown")
[[ "$OWNER" == "jfmsrv01" ]] && p "Data dir owned by jfmsrv01" || f "Data dir owner wrong: $OWNER"

# Redis
redis-cli -a "$(grep REDIS_PASSWORD /opt/jfmsrv01/app/.env | cut -d= -f2)" ping 2>/dev/null | grep -q PONG && \
    p "Redis responding" || f "Redis not responding"

# Nginx
nginx -t 2>/dev/null && p "Nginx config valid" || f "Nginx config INVALID"
systemctl is-active nginx &>/dev/null && p "Nginx running" || f "Nginx not running"

# Queue workers
WORKERS=$(supervisorctl status jfmsrv01-worker: 2>/dev/null | grep -c RUNNING || true)
[[ "$WORKERS" -gt 0 ]] && p "Queue workers running ($WORKERS)" || w "No queue workers running"

# ClamAV
systemctl is-active clamav-daemon &>/dev/null && p "ClamAV daemon running" || w "ClamAV daemon not running"
CLAM_AGE=$(find /var/lib/clamav -name "*.cvd" -o -name "*.cld" 2>/dev/null | \
    xargs stat -c '%Y' 2>/dev/null | sort -n | tail -1 || echo 0)
NOW=$(date +%s)
AGE_DAYS=$(( (NOW - ${CLAM_AGE:-0}) / 86400 ))
[[ "$AGE_DAYS" -lt 3 ]] && p "ClamAV definitions up to date (${AGE_DAYS}d old)" || w "ClamAV definitions may be stale (${AGE_DAYS}d old)"

# Stale processing files
STALE_PROCESSING=$(find "$DATA_BASE/processing" -type f -mmin +120 2>/dev/null | wc -l)
[[ "$STALE_PROCESSING" -eq 0 ]] && p "No stale processing files" || w "$STALE_PROCESSING stale file(s) in processing/"

# Log file errors (last 100 lines)
RECENT_ERRORS=$(tail -100 "$APP_BASE/storage/logs/laravel.log" 2>/dev/null | grep -c "\.ERROR:" || true)
[[ "$RECENT_ERRORS" -eq 0 ]] && p "No recent errors in application log" || w "$RECENT_ERRORS error(s) in recent log"

# Secret check - ensure no secrets leaked into log
if grep -q "password\|api_key\|secret" "$APP_BASE/storage/logs/laravel.log" 2>/dev/null; then
    f "Possible secret in application log - review immediately"
else
    p "No obvious secrets in application log"
fi

# Firewall
ufw status | grep -q "Status: active" && p "Firewall active" || w "Firewall not active"

# TLS certificate expiry
DOMAIN=$(grep server_name /etc/nginx/sites-available/jfmsrv01 2>/dev/null | \
    grep -v "_" | awk '{print $2}' | tr -d ';' | head -1)
if [[ -n "$DOMAIN" && "$DOMAIN" != "_" ]]; then
    EXPIRY=$(echo | timeout 5 openssl s_client -servername "$DOMAIN" \
        -connect "$DOMAIN:443" 2>/dev/null | \
        openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2 || echo "")
    if [[ -n "$EXPIRY" ]]; then
        EXPIRY_EPOCH=$(date -d "$EXPIRY" +%s 2>/dev/null || echo 0)
        DAYS_LEFT=$(( (EXPIRY_EPOCH - NOW) / 86400 ))
        [[ "$DAYS_LEFT" -gt 14 ]] && p "TLS cert expires in ${DAYS_LEFT} days" || \
            w "TLS cert expires in ${DAYS_LEFT} days - RENEW SOON"
    fi
fi

echo ""
echo "=== Results: $PASS passed | $FAIL failed | $WARN warnings ==="
echo "Report saved: $REPORT_FILE"
cp "$REPORT_FILE" "/backups/jfmsrv01/reports/" 2>/dev/null || true

[[ "$FAIL" -eq 0 ]] && exit 0 || exit 1
SMOKETEST

chmod 750 "$SCRIPTS_BASE/smoke_test.sh"
success "Smoke test script created"

# =============================================================================
# Patch workflow script
# =============================================================================

info "Creating patch workflow script"
cat > "$SCRIPTS_BASE/patch.sh" <<'PATCHSCRIPT'
#!/usr/bin/env bash
# =============================================================================
# JFM Server 01 - Patch Workflow
# Run this before and after every patch.
#
# Usage:
#   patch.sh pre  "Description of patch"    # Before applying changes
#   patch.sh post "Description of patch"   # After applying changes
# =============================================================================

set -euo pipefail

APP_BASE="/opt/jfmsrv01/app"
SCRIPTS_BASE="/opt/jfmsrv01/scripts"

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} $*"; }
error()   { echo -e "${RED}[ERROR]${NC} $*"; exit 1; }

STAGE="${1:-pre}"
DESCRIPTION="${2:-No description provided}"

case "$STAGE" in
    pre)
        info "=== PRE-PATCH: $DESCRIPTION ==="
        info "Step 1/3: Source backup"
        bash "$SCRIPTS_BASE/backup.sh" source
        info "Step 2/3: Pre-patch smoke test"
        bash "$SCRIPTS_BASE/smoke_test.sh" || warn "Pre-patch smoke test had failures - proceed with caution"
        info "Step 3/3: Ready"
        success "Pre-patch complete. Apply your changes now, then run: patch.sh post \"$DESCRIPTION\""
        ;;
    post)
        info "=== POST-PATCH: $DESCRIPTION ==="
        info "Step 1/5: Clear caches"
        sudo -u jfmsrv01 php "$APP_BASE/artisan" config:clear
        sudo -u jfmsrv01 php "$APP_BASE/artisan" cache:clear
        sudo -u jfmsrv01 php "$APP_BASE/artisan" view:clear
        sudo -u jfmsrv01 php "$APP_BASE/artisan" route:clear
        sudo -u jfmsrv01 php "$APP_BASE/artisan" config:cache
        sudo -u jfmsrv01 php "$APP_BASE/artisan" route:cache

        info "Step 2/5: Run migrations if any"
        sudo -u jfmsrv01 php "$APP_BASE/artisan" migrate --force

        info "Step 3/5: Reload services"
        supervisorctl restart all || warn "Supervisor restart failed"
        systemctl reload nginx || warn "Nginx reload failed"

        info "Step 4/5: Post-patch smoke test"
        bash "$SCRIPTS_BASE/smoke_test.sh" || {
            warn "POST-PATCH SMOKE TEST FAILED"
            warn "Review failures above. If critical, restore from backup."
            warn "Backup location: /backups/jfmsrv01/source/"
        }

        info "Step 5/5: Update changelog"
        DATE=$(date '+%Y-%m-%d')
        echo "" >> "$SCRIPTS_BASE/CHANGELOG.md"
        echo "## $DATE - $DESCRIPTION" >> "$SCRIPTS_BASE/CHANGELOG.md"
        echo "" >> "$SCRIPTS_BASE/CHANGELOG.md"
        echo "### Changed" >> "$SCRIPTS_BASE/CHANGELOG.md"
        echo "- $DESCRIPTION" >> "$SCRIPTS_BASE/CHANGELOG.md"
        echo "" >> "$SCRIPTS_BASE/CHANGELOG.md"
        echo "### Smoke test" >> "$SCRIPTS_BASE/CHANGELOG.md"
        echo "- Run post-patch: $(date '+%Y-%m-%d %H:%M:%S')" >> "$SCRIPTS_BASE/CHANGELOG.md"

        success "Post-patch complete. Remember to update HANDOVER.md if architecture changed."
        ;;
    *)
        error "Usage: patch.sh [pre|post] \"Description\""
        ;;
esac
PATCHSCRIPT

chmod 750 "$SCRIPTS_BASE/patch.sh"
success "Patch workflow script created"

# =============================================================================
# Diagnostics data script (feeds admin diagnostics panel)
# =============================================================================

info "Creating diagnostics script"
cat > "$SCRIPTS_BASE/diagnostics.sh" <<'DIAGSCRIPT'
#!/usr/bin/env bash
# =============================================================================
# Outputs a JSON diagnostics report for the admin panel.
# Called by the Laravel DiagnosticsController.
# =============================================================================

APP_BASE="/opt/jfmsrv01/app"
DATA_BASE="/data/jfmsrv01"

REDIS_PASS=$(grep REDIS_PASSWORD "$APP_BASE/.env" 2>/dev/null | cut -d= -f2 || echo "")

# Disk usage
DATA_USED=$(df -B1 "$DATA_BASE" 2>/dev/null | tail -1 | awk '{print $3}')
DATA_AVAIL=$(df -B1 "$DATA_BASE" 2>/dev/null | tail -1 | awk '{print $4}')
OS_USED=$(df -B1 / 2>/dev/null | tail -1 | awk '{print $3}')
OS_AVAIL=$(df -B1 / 2>/dev/null | tail -1 | awk '{print $4}')

# Queue depth
QUEUE_DEPTH=$(redis-cli -a "$REDIS_PASS" llen queues:default 2>/dev/null || echo 0)
QUEUE_HIGH=$(redis-cli -a "$REDIS_PASS" llen queues:high 2>/dev/null || echo 0)

# Workers
WORKERS=$(supervisorctl status 2>/dev/null | grep -c RUNNING || echo 0)

# ClamAV definition age
CLAM_FILE=$(find /var/lib/clamav -name "*.cvd" -o -name "*.cld" 2>/dev/null | \
    xargs stat -c '%Y' 2>/dev/null | sort -n | tail -1 || echo 0)
NOW=$(date +%s)
CLAM_AGE_HOURS=$(( (NOW - ${CLAM_FILE:-$NOW}) / 3600 ))

# Stale files
STALE_PROCESSING=$(find "$DATA_BASE/processing" -type f -mmin +120 2>/dev/null | wc -l)
STALE_IMPORTS=$(find "$DATA_BASE/imports" -type f -mmin +60 2>/dev/null | wc -l)

# Recent log errors
RECENT_ERRORS=$(tail -200 "$APP_BASE/storage/logs/laravel.log" 2>/dev/null | grep -c "\.ERROR:" || echo 0)

# Alerts
ALERTS=0
[[ -f "$DATA_BASE/system/alerts.log" ]] && \
    ALERTS=$(grep -c "$(date +%Y-%m-%d)" "$DATA_BASE/system/alerts.log" 2>/dev/null || echo 0)

cat <<JSON
{
  "timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
  "hostname": "$(hostname)",
  "disk": {
    "data_used_bytes": ${DATA_USED:-0},
    "data_avail_bytes": ${DATA_AVAIL:-0},
    "os_used_bytes": ${OS_USED:-0},
    "os_avail_bytes": ${OS_AVAIL:-0}
  },
  "queue": {
    "depth_default": ${QUEUE_DEPTH:-0},
    "depth_high": ${QUEUE_HIGH:-0},
    "workers_running": ${WORKERS:-0}
  },
  "clamav": {
    "definition_age_hours": ${CLAM_AGE_HOURS:-999},
    "daemon_active": $(systemctl is-active clamav-daemon &>/dev/null && echo true || echo false)
  },
  "stale_files": {
    "processing": ${STALE_PROCESSING:-0},
    "imports": ${STALE_IMPORTS:-0}
  },
  "log_errors_recent": ${RECENT_ERRORS:-0},
  "alerts_today": ${ALERTS:-0},
  "services": {
    "nginx":      "$(systemctl is-active nginx 2>/dev/null || echo unknown)",
    "postgresql": "$(systemctl is-active postgresql 2>/dev/null || echo unknown)",
    "redis":      "$(systemctl is-active redis-server 2>/dev/null || echo unknown)",
    "clamav":     "$(systemctl is-active clamav-daemon 2>/dev/null || echo unknown)",
    "supervisor": "$(systemctl is-active supervisor 2>/dev/null || echo unknown)"
  }
}
JSON
DIAGSCRIPT

chmod 750 "$SCRIPTS_BASE/diagnostics.sh"

# =============================================================================
# Set ownership and run initial smoke test
# =============================================================================

chown -R "$APP_USER:$APP_USER" "$SCRIPTS_BASE"
chmod 750 "$SCRIPTS_BASE"

success "=== PHASE 4 COMPLETE: Admin safety tools installed ==="
echo ""
echo "Scripts created in $SCRIPTS_BASE:"
echo "  backup.sh          - Backup (source|database|files|tenant <id>|full)"
echo "  smoke_test.sh      - Full smoke test (run before/after every patch)"
echo "  patch.sh           - Patch workflow (pre + post)"
echo "  diagnostics.sh     - JSON diagnostics for admin panel"
echo "  route_list.sh      - Route list report"
echo "  log_tail.sh        - Error log tail"
echo "  create_firm_storage.sh  - Create storage dirs for a new firm"
echo "  CHANGELOG.md       - Change history"
echo "  HANDOVER.md        - Current state and handover notes"
echo ""
info "Running initial smoke test..."
bash "$SCRIPTS_BASE/smoke_test.sh" || warn "Some checks failed - review above"
echo ""
echo "Next step: run 04_first_firm.sh to create your first firm"
/jfmsrv01-build/04_first_firm.sh
#!/usr/bin/env bash
# =============================================================================
# JFM Server 01 - First Firm Setup
# Creates your first firm/tenant and platform admin user.
#
# Run as root after 03_admin_safety_tools.sh
# =============================================================================

set -euo pipefail

APP_USER="jfmsrv01"
APP_BASE="/opt/jfmsrv01/app"
SCRIPTS_BASE="/opt/jfmsrv01/scripts"

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} $*"; }
error()   { echo -e "${RED}[ERROR]${NC} $*"; exit 1; }

[[ $EUID -ne 0 ]] && error "Run as root."

# =============================================================================
# Collect details interactively
# =============================================================================

echo ""
echo -e "${BLUE}=== First Firm Setup ===${NC}"
echo "This creates your first firm and a platform admin user."
echo "Press ENTER to accept defaults shown in [brackets]."
echo ""

read -rp "Firm name [Demo Law Firm]: " FIRM_NAME
FIRM_NAME="${FIRM_NAME:-Demo Law Firm}"

read -rp "Firm slug (URL-safe, no spaces) [demo-law-firm]: " FIRM_SLUG
FIRM_SLUG="${FIRM_SLUG:-demo-law-firm}"

read -rp "Jurisdiction [WA]: " JURISDICTION
JURISDICTION="${JURISDICTION:-WA}"

read -rp "Platform admin email [admin@jfmsrv011]: " ADMIN_EMAIL
ADMIN_EMAIL="${ADMIN_EMAIL:-admin@jfmsrv011}"

read -rp "Platform admin name [Platform Admin]: " ADMIN_NAME
ADMIN_NAME="${ADMIN_NAME:-Platform Admin}"

# Generate a secure initial password
ADMIN_PASSWORD=$(openssl rand -base64 18 | tr -dc 'a-zA-Z0-9' | head -c 20)
echo ""
info "Platform admin password (change after first login): $ADMIN_PASSWORD"
echo ""

# =============================================================================
# Create firm and admin via Artisan tinker
# =============================================================================

info "Creating firm: $FIRM_NAME"

sudo -u "$APP_USER" php "$APP_BASE/artisan" tinker --execute="
use App\Models\Firm;
use App\Models\User;
use App\Models\FeatureFlag;
use Illuminate\Support\Facades\Hash;

// Create firm
\$firm = Firm::updateOrCreate(
    ['slug' => '$FIRM_SLUG'],
    [
        'name'         => '$FIRM_NAME',
        'slug'         => '$FIRM_SLUG',
        'status'       => 'active',
        'jurisdiction' => '$JURISDICTION',
        'country'      => 'AU',
        'timezone'     => 'Australia/Perth',
        'plan'         => 'enterprise',
        'ai_privacy_tier' => '2',
        'storage_quota_gb' => 50,
        'mfa_required' => false,
        'settings'     => [],
    ]
);
echo 'Firm created: ID=' . \$firm->id . PHP_EOL;

// Create platform admin (not attached to a firm)
\$admin = User::updateOrCreate(
    ['email' => '$ADMIN_EMAIL'],
    [
        'name'              => '$ADMIN_NAME',
        'email'             => '$ADMIN_EMAIL',
        'password'          => Hash::make('$ADMIN_PASSWORD'),
        'firm_id'           => null,
        'is_platform_admin' => true,
        'status'            => 'active',
        'email_verified_at' => now(),
    ]
);
echo 'Platform admin created: ID=' . \$admin->id . PHP_EOL;

// Create firm admin user
\$firmAdmin = User::updateOrCreate(
    ['email' => 'firmadmin@' . '$FIRM_SLUG' . '.local'],
    [
        'name'              => '$FIRM_NAME Admin',
        'email'             => 'firmadmin@' . '$FIRM_SLUG' . '.local',
        'password'          => Hash::make('$ADMIN_PASSWORD'),
        'firm_id'           => \$firm->id,
        'is_platform_admin' => false,
        'status'            => 'active',
        'email_verified_at' => now(),
    ]
);
echo 'Firm admin created: ID=' . \$firmAdmin->id . PHP_EOL;

// Enable Level 1 features for this firm
\$level1Features = ['transcription', 'transcript_viewer', 'matters', 'audit_log', 'notifications', 'backups'];
foreach (\$level1Features as \$feature) {
    FeatureFlag::updateOrCreate(
        ['scope' => 'firm', 'firm_id' => \$firm->id, 'feature' => \$feature],
        ['enabled' => true]
    );
}
echo 'Level 1 features enabled for firm' . PHP_EOL;

echo 'Done.' . PHP_EOL;
" 2>&1

success "Firm and users created"

# Create storage directories for the new firm
info "Creating storage directories for firm"
FIRM_ID=$(sudo -u "$APP_USER" php "$APP_BASE/artisan" tinker --execute="
echo App\Models\Firm::where('slug','$FIRM_SLUG')->value('id');
" 2>/dev/null | tail -1)

if [[ -n "$FIRM_ID" && "$FIRM_ID" =~ ^[0-9]+$ ]]; then
    bash "$SCRIPTS_BASE/create_firm_storage.sh" "$FIRM_ID"
    success "Storage created for firm ID: $FIRM_ID"
else
    warn "Could not determine firm ID automatically. Run manually:"
    warn "  sudo -u $APP_USER $SCRIPTS_BASE/create_firm_storage.sh <firm_id>"
fi

# =============================================================================
# Save setup summary
# =============================================================================

SUMMARY_FILE="/root/first_firm_setup.txt"
cat > "$SUMMARY_FILE" <<EOF
# JFM Server 01 - First Firm Setup
# Created: $(date)

Firm name:    $FIRM_NAME
Firm slug:    $FIRM_SLUG
Firm ID:      ${FIRM_ID:-unknown}
Jurisdiction: $JURISDICTION
Plan:         enterprise (all features available)

Platform admin:
  Email:    $ADMIN_EMAIL
  Password: $ADMIN_PASSWORD (CHANGE ON FIRST LOGIN)

Firm admin:
  Email:    firmadmin@${FIRM_SLUG}.local
  Password: $ADMIN_PASSWORD (CHANGE ON FIRST LOGIN)

NOTE: Both accounts use the same initial password.
Change both passwords immediately after first login.
EOF
chmod 600 "$SUMMARY_FILE"
success "Setup summary saved to $SUMMARY_FILE (root-readable only)"

# =============================================================================
# Final summary
# =============================================================================

success "=== FIRST FIRM SETUP COMPLETE ==="
echo ""
echo -e "${GREEN}Your platform is ready.${NC}"
echo ""
echo "Firm:             $FIRM_NAME ($FIRM_SLUG)"
echo "Platform admin:   $ADMIN_EMAIL"
echo "Initial password: $ADMIN_PASSWORD"
echo ""
echo -e "${YELLOW}Change both user passwords immediately after first login.${NC}"
echo ""
echo "Build status:"
echo "  [x] Phase 0 - Server provisioning and hardening"
echo "  [x] Phase 1 - Base services (Nginx, PostgreSQL, Redis, ClamAV, PHP, Python)"
echo "  [x] Phase 2 - Storage layout and cleanup jobs"
echo "  [x] Phase 3 - Application skeleton (5 foundations, migrations, events)"
echo "  [x] Phase 4 - Admin safety tools (backup, smoke test, patch workflow)"
echo "  [x] Phase 5 - First firm created"
echo ""
echo "  [ ] Phase 5 (app) - Manual audio upload and transcription MVP"
echo "  [ ] Phase 6 - Word template manager and document generation"
echo ""
echo "Next: refer to the master plan Phase 1 build list to continue."
echo "Run smoke test at any time: sudo bash $SCRIPTS_BASE/smoke_test.sh"
Auto-generated by publish_handoff.sh | 20260530_161140