# SOBIT Linux Server Deployment Architecture This document explains how SmartCane runs on SOBIT (the production Linux server) through Laravel web interface, contrasting with manual dev laptop execution. ## High-Level SOBIT Architecture SOBIT hosts a Laravel-based web application that orchestrates Python and R pipeline execution through a **job queue system**. When a user clicks a button in the web UI, a job is created, pushed to a queue, and workers execute the actual Python/R scripts. ```mermaid %% SOBIT Deployment Architecture flowchart TD A["Web UI
Laravel Dashboard
Project Controller"] B["User Action
e.g., Download Data,
Generate Report"] C["Job Dispatch
ProjectDownloadTiffJob,
ProjectMosaicJob, etc."] D["Queue Worker
Executes Job Handler"] E["Shell Script Wrapper
runpython.sh
20_ci_extraction.sh
90_kpi_report.sh"] F["Python/R Executable
00_download_8band_pu_optimized.py
20_ci_extraction_per_field.R
rmarkdown::render"] G["Output Files
laravel_app/storage/app/{PROJECT}/"] H["Next Job Dispatch
job chaining"] A --> B B --> C C --> D D --> E E --> F F --> G G --> H H -.-> D ``` --- ## Laravel Job Classes & Queue Flow SmartCane uses Laravel's **queue system** to manage asynchronous task execution. Each stage has a corresponding Job class. ### Job Class Hierarchy | Job Class | Shell Script | R/Python Script | Input File | Output File | Next Stage | |-----------|-------------|-----------------|------------|------------|-----------| | **ProjectDownloadTiffJob** | `runpython.sh` | `00_download_8band_pu_optimized.py` | date param | `merged_tif/{DATE}.tif` | ProjectCreateFieldTilesJob | | **ProjectCreateFieldTilesJob** | (direct Rscript) | `10_create_per_field_tiffs.R` | `merged_tif/{DATE}.tif` | `field_tiles/{FIELD}/{DATE}.tif` | ProjectCIExtractionJob | | **ProjectCIExtractionJob** | `20_ci_extraction.sh` | `20_ci_extraction_per_field.R` | `field_tiles/...` | `combined_CI_data.rds` | ProjectGrowthModelJob | | **ProjectGrowthModelJob** | `30_growth_model.sh` | `30_interpolate_growth_model.R` | `combined_CI_data.rds` | `All_pivots_...rds` | ProjectMosaicGeneratorJob | | **ProjectMosaicGeneratorJob** | `40_mosaic_creation.sh` | `40_mosaic_creation_per_field.R` | `field_tiles_CI/...` | `weekly_mosaic/{FIELD}/week_*.tif` | ProjectKPICalculationJob | | **ProjectKPICalculationJob** | `80_calculate_kpis.sh` | `80_calculate_kpis.R` | `weekly_mosaic/...` | `field_analysis_*.xlsx` + RDS | ProjectReportGeneratorJob | | **ProjectReportGeneratorJob** | `90_kpi_report.sh` | `90_*.Rmd` or `91_*.Rmd` render | Excel + RDS | `SmartCane_Report_*.docx` | ✅ Complete | --- ## Execution Flow: From Web UI to Output ### Scenario: User Clicks "Download Latest Data" for Project "Angata" #### Step 1: Web UI Dispatch **File**: `laravel_app/app/Http/Controllers/ProjectController.php` ```php public function downloadData(Request $request, $projectName) { $project = Project::where('name', $projectName)->firstOrFail(); // Dispatch job to queue dispatch(new ProjectDownloadTiffJob($project, $request->date)); return response()->json(['status' => 'Job queued']); } ``` #### Step 2: Job Queue Entry **File**: `laravel_app/app/Jobs/ProjectDownloadTiffJob.php` ```php class ProjectDownloadTiffJob implements ShouldQueue { public $project; public $date; public function __construct(Project $project, $date = null) { $this->project = $project; $this->date = $date ?? Carbon::now()->toDateString(); } public function handle() { // Execute shell script wrapper $command = "bash " . base_path() . "/runpython.sh " . "--date=" . $this->date . " " . "--project_dir=" . $this->project->directory; $process = new Process(explode(' ', $command)); $process->run(); if ($process->isSuccessful()) { // Dispatch next job dispatch(new ProjectCreateFieldTilesJob($this->project, $this->date)); } else { // Log error and notify user Log::error("Download failed for {$this->project->name}: " . $process->getErrorOutput()); } } } ``` #### Step 3: Shell Script Execution (runpython.sh) **File**: `laravel_app/../runpython.sh` ```bash #!/bin/bash # Wrapper for Python download script DATE=${1#--date=} PROJECT_DIR=${2#--project_dir=} cd /path/to/SmartCane_code/python_app # Run Python with project-specific environment python 00_download_8band_pu_optimized.py $PROJECT_DIR --date $DATE if [ $? -eq 0 ]; then echo "Download successful for $PROJECT_DIR on $DATE" exit 0 else echo "Download failed for $PROJECT_DIR on $DATE" exit 1 fi ``` #### Step 4: Python Download (Stage 00) **File**: `python_app/00_download_8band_pu_optimized.py` ```python import sys from datetime import datetime project_dir = sys.argv[1] # "angata" date_str = sys.argv[2] if len(sys.argv) > 2 else datetime.now().strftime('%Y-%m-%d') # Authenticate with Planet API auth = get_planet_credentials() bbox = get_project_bbox(project_dir) # From database or config # Download 4-band TIFF tiff_path = f"laravel_app/storage/app/{project_dir}/merged_tif/{date_str}.tif" download_and_save_tiff(auth, bbox, date_str, tiff_path) print(f"Downloaded to {tiff_path}") ``` #### Step 5: Job Chaining (Automatic) **Back in ProjectDownloadTiffJob.php**: ```php if ($process->isSuccessful()) { // Dispatch next job in pipeline dispatch(new ProjectCreateFieldTilesJob($this->project, $this->date)); } ``` This triggers `ProjectCreateFieldTilesJob`, which calls the next shell script, and so on. --- ## Shell Script Wrappers: Design & Responsibility SOBIT wrapper scripts ensure Python and R scripts run with correct environment variables and working directory. ### Root-Level Shell Scripts **Location**: `c:\Users\timon\Documents\SmartCane_code\` (root) | Script | Purpose | Calls | Environment Setup | |--------|---------|-------|-------------------| | `10_planet_download.sh` | Stage 10 wrapper | `Rscript 10_create_per_field_tiffs.R` | Sets R_LIBS, PYTHONPATH | | `20_ci_extraction.sh` | Stage 20 wrapper | `Rscript 20_ci_extraction_per_field.R` | R environment + data paths | | `30_growth_model.sh` | Stage 30 wrapper | `Rscript 30_interpolate_growth_model.R` | Growth model data path | | `40_mosaic_creation.sh` | Stage 40 wrapper | `Rscript 40_mosaic_creation_per_field.R` | Sentinel config for mosaics | | `80_calculate_kpis.sh` | Stage 80 wrapper | `Rscript 80_calculate_kpis.R` | KPI utility loading | | `90_kpi_report.sh` | Stage 90/91 wrapper | `rmarkdown::render()` via Rscript | RMarkdown dependencies | | `runpython.sh` | Python wrapper | `python 00_download_8band_pu_optimized.py` | Python venv activation | ### Example: `20_ci_extraction.sh` ```bash #!/bin/bash # CI Extraction wrapper for SOBIT set -e # Exit on error # Load environment export R_LIBS="/opt/R/library" export PYTHONPATH="/opt/python/lib/python3.9/site-packages:$PYTHONPATH" PROJECT_DIR=$1 END_DATE=$2 OFFSET=${3:-7} # Change to code directory cd /var/www/smartcane/code # Execute R script Rscript r_app/20_ci_extraction_per_field.R "$PROJECT_DIR" "$END_DATE" "$OFFSET" if [ $? -eq 0 ]; then echo "[$(date)] CI extraction completed for $PROJECT_DIR" # Log success to Laravel job tracking echo "Success" > "laravel_app/storage/logs/${PROJECT_DIR}_stage20_${END_DATE}.log" else echo "[$(date)] CI extraction FAILED for $PROJECT_DIR" exit 1 fi ``` ### Example: `90_kpi_report.sh` ```bash #!/bin/bash # RMarkdown report rendering wrapper set -e PROJECT_DIR=$1 REPORT_DATE=$2 CLIENT_TYPE=$3 # "agronomic_support" or "cane_supply" cd /var/www/smartcane/code # Determine which RMarkdown template to use if [ "$CLIENT_TYPE" = "agronomic_support" ]; then REPORT_TEMPLATE="r_app/90_CI_report_with_kpis_agronomic_support.Rmd" else REPORT_TEMPLATE="r_app/91_CI_report_with_kpis_cane_supply.Rmd" fi # Render RMarkdown to Word Rscript -e " rmarkdown::render( '$REPORT_TEMPLATE', params = list( data_dir = '$PROJECT_DIR', report_date = as.Date('$REPORT_DATE') ), output_file = 'SmartCane_Report_${CLIENT_TYPE}_${PROJECT_DIR}_${REPORT_DATE}.docx', output_dir = 'laravel_app/storage/app/$PROJECT_DIR/reports' ) " echo "[$(date)] Report generation completed for $PROJECT_DIR" ``` --- ## Data Storage on SOBIT All data is stored in Laravel's standard storage directory with project-based subdirectories. ### Storage Structure ``` laravel_app/storage/app/ ├── angata/ # Cane Supply project │ ├── merged_tif/ # Stage 00 Python output │ │ ├── 2026-02-12.tif │ │ └── 2026-02-19.tif │ ├── field_tiles/ # Stage 10 output │ ├── field_tiles_CI/ # Stage 20 output │ ├── Data/ │ │ ├── pivot.geojson # Field boundaries (input) │ │ ├── harvest.xlsx # Harvest dates (input) │ │ ├── extracted_ci/ # Stage 20 CI data │ │ └── growth_model_interpolated/ # Stage 30 data │ ├── weekly_mosaic/ # Stage 40 output │ └── reports/ # Stages 80/90/91 output │ ├── SmartCane_Report_*.docx │ ├── angata_field_analysis_week*.xlsx │ └── kpis/ │ └── *.rds │ ├── aura/ # Agronomic Support project │ └── (same structure as angata) │ ├── chemba/ ├── xinavane/ ├── esa/ └── simba/ ``` ### Permissions Model - **Web server user** (www-data on Linux): Can read/write to all storage subdirectories - **Laravel artisan commands**: Have full access via `Storage::disk('local')` - **Job queue workers**: Execute as www-data, access via storage symlink --- ## Job Queue Configuration **File**: `laravel_app/config/queue.php` ```php 'default' => env('QUEUE_CONNECTION', 'database'), 'connections' => [ 'database' => [ 'driver' => 'database', 'table' => 'jobs', 'queue' => 'default', 'retry_after' => 1800, // 30 min timeout per job 'after_commit' => false, ], 'redis' => [ 'driver' => 'redis', 'connection' => 'default', 'queue' => env('REDIS_QUEUE', 'default'), 'after_commit' => false, ], ], ``` ### Starting Queue Worker On SOBIT, the queue worker runs in the background: ```bash # Manual start (for debugging) php artisan queue:work --queue=default --timeout=1800 # Production (supervisor-managed) # Supervisor config: /etc/supervisor/conf.d/smartcane-worker.conf [program:smartcane-queue-worker] process_name=%(program_name)s_%(process_num)02d command=php /var/www/smartcane/artisan queue:work --queue=default --timeout=1800 autostart=true autorestart=true numprocs=2 redirect_stderr=true stdout_logfile=/var/log/smartcane-queue.log ``` --- ## Error Handling & Retries ### Job Failure Scenario If a shell script returns non-zero exit code: ```php // In job handle() method if (!$process->isSuccessful()) { Log::error("Job failed: " . $process->getErrorOutput()); // Laravel automatically retries if $tries > 1 // Configurable in job class: // public $tries = 3; // public $retryAfter = 300; // 5 minutes between retries } ``` ### Monitoring & Alerts - **Failed Jobs Table**: `jobs_failed` table in Laravel database - **Logs**: `storage/logs/laravel.log` + individual stage logs - **User Notification**: Job status visible in web UI; email alerts can be configured --- ## Deployment vs Dev Laptop: Key Differences | Aspect | **SOBIT (Production)** | **Dev Laptop** | |--------|----------------------|----------------| | **Execution Model** | Async job queue | Synchronous PowerShell | | **User Interaction** | Web UI clicks → jobs | Manual script calls | | **Data Location** | `laravel_app/storage/app/{PROJECT}/` | Same (shared Laravel directory) | | **Error Handling** | Job retries, logs in database | Terminal output only | | **Parallelization** | Multiple queue workers | Single sequential execution | | **Monitoring** | Web dashboard + Laravel logs | Console output only | | **Environment Setup** | Bash scripts set env vars | Manual R/Python environment | | **Scheduling** | Can use Laravel scheduler for automated runs | Manual cron or batch scripts | --- ## Running Full Pipeline on SOBIT via Web UI ### User Workflow 1. **Navigate** to ProjectController dashboard ``` http://sobit-server/projects/angata ``` 2. **Click** "Download Latest Data" button - ProjectDownloadTiffJob queued with current date - Web UI shows "Job submitted" 3. **Queue Worker** (background process) executes jobs in sequence - Downloads TIFF (Stage 00) - Dispatches Stage 10 job - Creates field tiles - Dispatches Stage 20 job - (etc. through Stage 91) 4. **Monitor** progress via Dashboard - Job history tab shows completed jobs - Report links appear when Stage 91 completes - Download Word/Excel from reports section ### Command-Line Submission (Alternative) Developer can manually trigger jobs via Laravel artisan: ```bash # SSH into SOBIT ssh user@sobit-server # Manually dispatch job php artisan smartcane:process-pipeline angata --date=2026-02-19 --async # Or using job dispatch directly php artisan queue:work --queue=default ``` --- ## Troubleshooting SOBIT Deployment ### Issue: Job Stuck in Queue ```bash # Check job queue depth SELECT COUNT(*) FROM jobs WHERE queue = 'default'; # Retry failed jobs php artisan queue:retry --all # Clear old jobs php artisan queue:clear ``` ### Issue: Shell Script Can't Find R/Python **Cause**: Environment variables not set in shell wrapper. **Fix**: Add to shell script: ```bash export PATH="/opt/R/bin:/opt/python/bin:$PATH" export R_HOME="/opt/R" source /opt/python/venv/bin/activate ``` ### Issue: Permission Denied on Storage Files **Cause**: Files created by web server, permission mismatch. **Fix**: ```bash sudo chown -R www-data:www-data laravel_app/storage/app/* sudo chmod -R 755 laravel_app/storage/app/ ``` --- ## Next Steps - See [ARCHITECTURE_DATA_FLOW.md](ARCHITECTURE_DATA_FLOW.md) for stage details - See [CLIENT_TYPE_ARCHITECTURE.md](CLIENT_TYPE_ARCHITECTURE.md) for how jobs route based on project type - See [DEV_LAPTOP_EXECUTION.md](DEV_LAPTOP_EXECUTION.md) for alternative manual execution model - See [ARCHITECTURE_INTEGRATION_GUIDE.md](ARCHITECTURE_INTEGRATION_GUIDE.md) for choosing SOBIT vs dev laptop