SmartCane/CODE_MERGE_GUIDE_LARAVEL.md
Timon a1692817fe Add environment configuration for PyTorch and harvest dependencies
- Created `environment_pytorch.yml` to define the conda environment for PyTorch with GPU support, including necessary packages like GDAL and SentinelHub.
- Added `requirements_harvest.txt` for Python package dependencies related to data harvesting, including PyTorch, Pandas, NumPy, and others.
2026-02-24 15:12:18 +01:00

36 KiB

SmartCane Code Merge: Implementation Handoff for Developer

For: Martins moltbot

What: Complete implementation guide for merging code-improvements branch into master

Target Repo: https://bitbucket.org/sobitnl/smartcane/src/master/

Date: February 24, 2026


New folder structure, new scripts (R and Python) so new .sh and cron, new buttons (client type and area unit). Add kpi excel to email (in addition to the word file). New packages to be installed. nice to have - group projects by country.

📋 TASKS (AI generated, ik weet niet of ze ergens op slaan...)

This handoff includes everything you need to do before handing over to production:

  1. Database migration
  2. Update Project model
  3. Update jobs (2 files)
  4. Update ProjectManager form
  5. Add form fields to Blade template
  6. Update MailingForm
  7. Create 5 shell script wrappers
  8. Setup Linux cron jobs
  9. Run verification tests
  10. Country-based project organization (MZ/UG/TZ)
  11. Download scheduling (auto-stagger per project)
  12. Project search feature
  13. Harvest prediction setup
  14. Area unit selection UI (hectare/acre)

📋 WHAT EACH STEP MEANS

Step 1-6: Laravel Changes

  • Steps 1-3: Database + models + jobs (backend logic)
  • Steps 4-6: Forms + email logic (UI + user-facing)

Step 7: Shell Script Wrappers

Create 5 bash shell scripts that wrap R and Python scripts. These let Laravel jobs call R/Python scripts easily.

Example: When Laravel needs to create field TIFFs, it runs:

./10_create_per_field_tiffs.sh --project=angata --area_unit=hectare

This script then exports the area unit as an environment variable and calls the R script.

Step 8: Setup Cron

Add scheduled job to Linux to run /10_planet_download.sh every day at staggered times (00:01, 00:11, etc.) for each project.

Step 10: Verify Everything Works

Run manual tests to confirm all changes work correctly.


🎯 Client Type Overview (Read This First)

SmartCane supports two project types. Which one you implement determines which scripts to use:

Project Type Example Scripts Used Purpose
agronomic_support aura, chemba, esa not 21, 22, 23, 31; but the rest yes Weekly field health analysis, CI trends, uniformity alerts
cane_supply angata, simba Additional: 21, 22, 23, 31 Field health + harvest date prediction (ML-based)

Translation:

  • Choose agronomic_support if you want field-level vegetation analysis only
  • Choose cane_supply if you also want harvest date forecasting (requires ML models)

All the code below supports both types. The database column client_type controls which scripts actually run.


STEP 1: Database Migration

Create file: database/migrations/2025_02_24_add_client_type_and_area_unit_to_projects_table.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('projects', function (Blueprint $table) {
            $table->enum('client_type', ['agronomic_support', 'cane_supply'])
                ->default('agronomic_support')
                ->after('download_path');
            
            $table->enum('preferred_area_unit', ['hectare', 'acre'])
                ->default('hectare')
                ->after('client_type');
        });
    }

    public function down(): void
    {
        Schema::table('projects', function (Blueprint $table) {
            $table->dropColumn(['client_type', 'preferred_area_unit']);
        });
    }
};

Run migration:

php artisan migrate

Expected: Two new columns added to projects table.


STEP 2: Update Project Model

File: laravel_app/app/Models/Project.php

2.1: Update $fillable array

Add these two lines to the fillable declaration:

protected $fillable = [
    'name',
    'download_path',
    'client_type',              // ← ADD THIS
    'preferred_area_unit',      // ← ADD THIS
    'mail_template',
    'mail_subject',
    'mail_frequency',
    'mail_day',
    'mail_scheduled',
    'pivot_json_path',
    'span_json_path',
    'harvest_json_path',
    'min_harvest_date',
    'borders',
];

2.2: Add new method at bottom of Project class

/**
 * Get the latest KPI Excel file for this project
 */
public function getLatestKpiFile(): ?string
{
    $kpiPath = $this->download_path . '/reports/kpis/';
    
    try {
        $files = Storage::files($kpiPath);
    } catch (\Exception $e) {
        return null;
    }
    
    return collect($files)
        ->filter(fn($f) => Str::endsWith($f, '.xlsx'))
        ->sortByDesc(fn($f) => Storage::lastModified($f))
        ->first();
}

STEP 3: Update Jobs

3.1: ProjectDownloadTiffJob.php

File: laravel_app/app/Jobs/ProjectDownloadTiffJob.php

Find this line (around line 73):

$path = $project->download_path . '/merged_final_tif/' . $filename;

Replace with:

$path = $project->download_path . '/field_tiles_CI/' . $filename;

3.2: ProjectMosiacGeneratorJob.php

File: laravel_app/app/Jobs/ProjectMosiacGeneratorJob.php

Find the $command array (around lines 50-60):

$command = [
    sprintf('%sbuild_mosaic.sh', $projectFolder),
    sprintf('--end_date=%s', $this->mosaic->end_date->format('Y-m-d')),
    sprintf('--offset=%s', $this->mosaic->offset),
    sprintf('--data_dir=%s', $this->mosaic->project->download_path),
    sprintf('--file_name_tif=%s', basename($this->mosaic->path)),
];

Replace with:

$command = [
    sprintf('%s40_mosaic_creation_per_field.sh', $projectFolder),
    sprintf('--project=%s', $project->name),
    sprintf('--end_date=%s', $this->mosaic->end_date->format('Y-m-d')),
    sprintf('--offset=%s', $this->mosaic->offset),
];

Also improve exception handling. Find the try/catch block and replace with:

try {
    $process = ProcessNew::timeout(300)
        ->env(['PATH' => $currentPath.':/usr/local/Cellar/pandoc/3.1.8/bin/pandoc'])
        ->start($command, function (string $type, string $output) use ($project) {
            ProjectLogger::log($project, $output);
            $this->throwIfOutputContainsError($output);
        });
    $results = $process->wait();
    if ($results->successful()) {
        $this->mosaic->setStatusSuccess();
    }
} catch (\RuntimeException|\Symfony\Component\Process\Exception\ProcessTimedOutException|\Symfony\Component\Process\Exception\ProcessFailedException $e) {
    ProjectLogger::log($project, "MOSAIC JOB ERROR: " . $e->getMessage());
    $this->mosaic->setStatusFailed();
    throw $e;
}

STEP 4: Update ProjectManager Form

File: laravel_app/app/Livewire/Projects/ProjectManager.php

4.1: Update createProject() method

Find the method and replace with this version that creates directories:

public function createProject()
{
    $projectIdentifier = $this->formData['id'] ?? null;
    Validator::make(
        ['name' => $this->formData['name']],
        ['name' => ['required', Rule::unique('projects')->ignore($projectIdentifier), 'string', 'max:255']]
    )->validate();
    
    $basePath = $this->makeValidDirectoryName($this->formData['name']);
    
    // Create base directory
    Storage::makeDirectory($basePath, recursive: true);
    
    // Create project record
    $project = Project::create([
        'name' => $this->formData['name'],
        'download_path' => $basePath,
        'client_type' => $this->formData['client_type'] ?? 'agronomic_support',
        'preferred_area_unit' => $this->formData['preferred_area_unit'] ?? 'hectare',
    ]);
    
    // Create required subdirectories
    $subdirectories = [
        'Data',
        'merged_tif',
        'field_tiles',
        'field_tiles_CI',
        'daily_vals',
        'weekly_mosaic',
        'reports',
        'reports/kpis',
        'logs',
        'RGB',
    ];
    
    foreach ($subdirectories as $subdir) {
        Storage::makeDirectory($basePath . '/' . $subdir, recursive: true);
    }
    
    return redirect()->route('project.show', [$project->name, 'settings']);
}

4.2: Update resetFormData() method

Add the two new fields:

private function resetFormData()
{
    $this->formData = [
        'name' => '',
        'client_type' => 'agronomic_support',          // ← ADD THIS
        'preferred_area_unit' => 'hectare',            // ← ADD THIS
        'mail_template' => '',
        'mail_subject' => '',
        'mail_frequency' => '',
        'mail_day' => '',
        'mail_scheduled' => false,
        // ... rest of existing fields
    ];
}

STEP 5: Add Form Fields to Blade Template

File: Your project create/edit form Blade template (e.g., resources/views/livewire/projects/project-manager.blade.php)

Add these two fields immediately after the project name field:

<!-- Client Type (Required) -->
<div class="form-group mb-3">
    <label for="client_type" class="form-label">
        Client Type <span class="text-danger">*</span>
    </label>
    <select wire:model="formData.client_type" id="client_type" class="form-control" required>
        <option value="">-- Select Client Type --</option>
        <option value="agronomic_support">Agronomic Support</option>
        <option value="cane_supply">Cane Supply</option>
    </select>
    @error('formData.client_type') 
        <span class="text-danger small d-block mt-1">{{ $message }}</span> 
    @enderror
</div>

<!-- Preferred Area Unit (Toggle) -->
<div class="form-group mb-3">
    <label class="form-label">
        Preferred Area Unit <span class="text-danger">*</span>
    </label>
    <div class="btn-group btn-group-sm w-100" role="group">
        <input type="radio" class="btn-check" name="preferred_area_unit" 
               id="unit_hectare" value="hectare" 
               wire:model="formData.preferred_area_unit" required />
        <label class="btn btn-outline-primary" for="unit_hectare" style="width: 50%;">
            Hectare (ha)
        </label>
        
        <input type="radio" class="btn-check" name="preferred_area_unit" 
               id="unit_acre" value="acre" 
               wire:model="formData.preferred_area_unit" required />
        <label class="btn btn-outline-primary" for="unit_acre" style="width: 50%;">
            Acre (ac)
        </label>
    </div>
    @error('formData.preferred_area_unit') 
        <span class="text-danger small d-block mt-1">{{ $message }}</span> 
    @enderror
</div>

STEP 6: Update Mailing Form

File: laravel_app/app/Livewire/Forms/MailingForm.php

Find the saveAndSendMailing() static method and update it to auto-attach KPI Excel for cane_supply clients:

public static function saveAndSendMailing($report, $subject, $message, $recipients)
{
    if ($report->documentExists()) {
        $mailing = $report->project->mailings()->create([
            'subject'   => $subject,
            'message'   => $message,
            'report_id' => $report->id,
        ]);

        // Attach main report
        $mailing->attachments()->create([
            'name' => $report->name,
            'path' => $report->path,
        ]);
        
        // Auto-attach KPI Excel for cane_supply projects
        if ($report->project->client_type === 'cane_supply') {
            $kpiFile = $report->project->getLatestKpiFile();
            if ($kpiFile) {
                $mailing->attachments()->create([
                    'name' => 'KPI Summary - ' . date('Y-m-d'),
                    'path' => $kpiFile,
                ]);
            }
        }

        $mailing->recipients()->createMany($recipients);
        Mail::to($mailing->recipients()->pluck('email')->toArray())
            ->send(new \App\Mail\ReportMailer($mailing, $report));
    } else {
        self::sendReportNotFoundNotificationToAdmin($report);
    }
}

STEP 6.5: Understanding Script-Client Type Mapping

Critical for implementation: Different client types require different scripts. This section clarifies which scripts run for which client type.

Script Dependencies by Client Type

All Projects (Both agronomic_support and cane_supply):

10_create_per_field_tiffs.sh        ← Always run
21_convert_ci_rds_to_csv.sh         ← Always run

cane_supply ONLY (Projects with harvest prediction):

22_harvest_baseline_prediction.sh   ← Cane supply only
23_convert_harvest_format.sh        ← Cane supply only
31_harvest_imminent_weekly.sh       ← Cane supply only (weekly)

agronomic_support ONLY (Default projects, farm analysis):

[These 5 scripts are NOT called]

Reference Table

Script Purpose agronomic_support cane_supply
10_create_per_field_tiffs.sh Extract per-field CI from 4-band imagery Always Always
21_convert_ci_rds_to_csv.sh Convert CI data to CSV (for reports) Always Always
22_harvest_baseline_prediction.sh Baseline harvest date ML model Skip Once at setup
23_convert_harvest_format.sh Convert harvest dates to reporting format Skip Once at setup
31_harvest_imminent_weekly.sh Weekly harvest imminence detection Skip Weekly schedule

How Dispatch Works

When a Laravel job needs to run scripts, it should check the project's client_type:

// Example: In a ProjectPipelineJob or similar dispatcher

public function handle()
{
    $project = $this->project;
    
    // ALWAYS run for both client types
    // (Script 10: Create per-field TIFFs)
    // (Script 21: Convert CI to CSV)
    
    // CONDITIONALLY run only for cane_supply
    if ($project->client_type === 'cane_supply') {
        // Script 22: Harvest baseline
        // Script 23: Harvest format conversion
        // Script 31: Weekly harvest imminent (scheduled separately)
    }
}

Implementation Locations

Where to check client_type:

  1. Job Dispatchers (if creating automated job queues)

    • File: Any job class that orchestrates multiple scripts
    • Check: if ($project->client_type === 'cane_supply')
  2. Manual UI (if running scripts from Laravel UI)

    • File: ProjectManager Blade template or controller
    • Show: Only harvest buttons for cane_supply projects
  3. Scheduled Tasks (for weekly/baseline runs)

    • File: /etc/cron.d/smartcane_downloads or Laravel Kernel.php
    • Condition: Only queue harvest scripts for cane_supply projects

Example: Conditional Job Dispatch

If you're creating a job to run the full pipeline, use this pattern:

<?php

namespace App\Jobs;

use App\Models\Project;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;

class ProjectPipelineDispatcher implements ShouldQueue
{
    use Queueable;

    public function __construct(public Project $project) {}

    public function handle()
    {
        // Scripts that run for ALL projects
        $this->runScript('10_create_per_field_tiffs.sh');
        $this->runScript('21_convert_ci_rds_to_csv.sh');

        // Scripts that run ONLY for cane_supply
        if ($this->project->client_type === 'cane_supply') {
            $this->runScript('22_harvest_baseline_prediction.sh');    // Once at setup
            $this->runScript('23_convert_harvest_format.sh');          // Once at setup
            // 31_harvest_imminent_weekly.sh runs separately on schedule
        }
    }

    private function runScript(string $scriptName)
    {
        $command = "bash {$scriptName} --project={$this->project->name} --area_unit={$this->project->preferred_area_unit}";
        \Illuminate\Support\Facades\Process::run($command);
    }
}

Why This Matters

  • Cost: Harvest scripts use high-impact ML models. Running them for agronomic_support projects wastes resources.
  • Clarity: Future developers need to understand the business logic (harvest prediction = cane_supply only).
  • Maintenance: If new scripts are added (e.g., SAR analysis), this mapping gets updated in one place.

STEP 7: Create Shell Script Wrappers

What: Create 5 bash shell scripts in the project root
Why: These allow Laravel jobs to call R and Python scripts with proper environment variables
Note: Make each executable with chmod +x after creation

When to call each script (see Step 6.5 above):

  • Scripts 10, 21: Always (for all projects)
  • Scripts 22, 23, 31: Only if client_type === 'cane_supply'

Create these 5 shell scripts in the project root:


Script 1: 10_create_per_field_tiffs.sh

#!/bin/bash
# Create per-field TIFFs from merged 4-band imagery
# Usage: ./10_create_per_field_tiffs.sh --project=angata

set -e

PROJECT=""
AREA_UNIT="hectare"  # Default

while [[ $# -gt 0 ]]; do
    case $1 in
        --project=*) PROJECT="${1#*=}" ;;
        --area_unit=*) AREA_UNIT="${1#*=}" ;;
        --*) ;;  # Ignore other args
    esac
    shift
done

[ -z "$PROJECT" ] && { echo "ERROR: --project required"; exit 1; }

cd "$(dirname "$0")/r_app"
export AREA_UNIT="$AREA_UNIT"
Rscript -e "PROJECT='$PROJECT'; source('parameters_project.R'); source('10_create_per_field_tiffs.R')"

Script 2: 21_convert_ci_rds_to_csv.sh

#!/bin/bash
# Convert CI RDS file to CSV format
# Usage: ./21_convert_ci_rds_to_csv.sh --project=angata

set -e

PROJECT=""
AREA_UNIT="hectare"

while [[ $# -gt 0 ]]; do
    case $1 in
        --project=*) PROJECT="${1#*=}" ;;
        --area_unit=*) AREA_UNIT="${1#*=}" ;;
        --*) ;;
    esac
    shift
done

[ -z "$PROJECT" ] && { echo "ERROR: --project required"; exit 1; }

cd "$(dirname "$0")/r_app"
export AREA_UNIT="$AREA_UNIT"
Rscript -e "PROJECT='$PROJECT'; source('parameters_project.R'); source('21_convert_ci_rds_to_csv.R')"

Script 3: 22_harvest_baseline_prediction.sh

#!/bin/bash
# Baseline harvest date prediction
# Usage: ./22_harvest_baseline_prediction.sh --project=angata

set -e

PROJECT=""
AREA_UNIT="hectare"

while [[ $# -gt 0 ]]; do
    case $1 in
        --project=*) PROJECT="${1#*=}" ;;
        --area_unit=*) AREA_UNIT="${1#*=}" ;;
        --*) ;;
    esac
    shift
done

[ -z "$PROJECT" ] && { echo "ERROR: --project required"; exit 1; }

cd "$(dirname "$0")/python_app"
export AREA_UNIT="$AREA_UNIT"

if command -v conda &> /dev/null; then
    conda run -n pytorch_gpu python 22_harvest_baseline_prediction.py "$PROJECT" 2>&1 || \
    conda run -n pytorch_cpu python 22_harvest_baseline_prediction.py "$PROJECT" 2>&1
else
    python 22_harvest_baseline_prediction.py "$PROJECT"
fi

Script 4: 23_convert_harvest_format.sh

#!/bin/bash
# Convert harvest date format
# Usage: ./23_convert_harvest_format.sh --project=angata

set -e

PROJECT=""
AREA_UNIT="hectare"

while [[ $# -gt 0 ]]; do
    case $1 in
        --project=*) PROJECT="${1#*=}" ;;
        --area_unit=*) AREA_UNIT="${1#*=}" ;;
        --*) ;;
    esac
    shift
done

[ -z "$PROJECT" ] && { echo "ERROR: --project required"; exit 1; }

cd "$(dirname "$0")/python_app"
export AREA_UNIT="$AREA_UNIT"

if command -v conda &> /dev/null; then
    conda run -n pytorch_gpu python 23_convert_harvest_format.py "$PROJECT" 2>&1 || \
    conda run -n pytorch_cpu python 23_convert_harvest_format.py "$PROJECT" 2>&1
else
    python 23_convert_harvest_format.py "$PROJECT"
fi

Script 5: 31_harvest_imminent_weekly.sh

#!/bin/bash
# Weekly harvest prediction (imminent detection)
# Usage: ./31_harvest_imminent_weekly.sh --project=angata

set -e

PROJECT=""
AREA_UNIT="hectare"

while [[ $# -gt 0 ]]; do
    case $1 in
        --project=*) PROJECT="${1#*=}" ;;
        --area_unit=*) AREA_UNIT="${1#*=}" ;;
        --*) ;;
    esac
    shift
done

[ -z "$PROJECT" ] && { echo "ERROR: --project required"; exit 1; }

cd "$(dirname "$0")/python_app"
export AREA_UNIT="$AREA_UNIT"

if command -v conda &> /dev/null; then
    conda run -n pytorch_gpu python 31_harvest_imminent_weekly.py "$PROJECT" 2>&1 || \
    conda run -n pytorch_cpu python 31_harvest_imminent_weekly.py "$PROJECT" 2>&1
else
    python 31_harvest_imminent_weekly.py "$PROJECT"
fi

STEP 8: Make Scripts Executable

chmod +x 10_create_per_field_tiffs.sh
chmod +x 21_convert_ci_rds_to_csv.sh
chmod +x 22_harvest_baseline_prediction.sh
chmod +x 23_convert_harvest_format.sh
chmod +x 31_harvest_imminent_weekly.sh

STEP 8: Setup Linux Cron Jobs (5 min)

What: Add scheduled jobs to run satellite downloads daily
Where: Add to /etc/cron.d/smartcane_downloads
When: Runs at 00:01 each day, staggered by 10 minutes per project

# SmartCane Satellite Download - Staggered per project
# Runs at 00:01 each day, offset by 10 minutes per project

1 0 * * * /path/to/smartcane/10_planet_download.sh --project=angata 2>&1 | logger
11 0 * * * /path/to/smartcane/10_planet_download.sh --project=chemba 2>&1 | logger
21 0 * * * /path/to/smartcane/10_planet_download.sh --project=xinavane 2>&1 | logger
31 0 * * * /path/to/smartcane/10_planet_download.sh --project=esa 2>&1 | logger
41 0 * * * /path/to/smartcane/10_planet_download.sh --project=simba 2>&1 | logger
51 0 * * * /path/to/smartcane/10_planet_download.sh --project=aura 2>&1 | logger

Note: Replace /path/to/smartcane/ with absolute path. Find it with:

pwd  # Run in project root

STEP 9: Verification (15 min)

Run these checks to confirm all changes work:

10.1: Database

php artisan migrate
# ✅ Expected: No errors, 2 new columns added

10.2: Create Project via UI

1. Visit project create form
2. Enter project name (e.g., "test_project")
3. Select Client Type: "Cane Supply"
4. Select Preferred Area Unit: "Acre (ac)"
5. Click Save

Expected:

  • Project created with client_type="cane_supply", preferred_area_unit="acre"
  • Directories created: Data/, field_tiles/, field_tiles_CI/, reports/kpis/, etc.
  • Project appears in list with correct settings

10.3: Test Shell Wrapper

./10_create_per_field_tiffs.sh --project=test_project --area_unit=acre
# ✅ Expected: R script runs without error

10.4: Test Area Unit Env Var

cd r_app
export AREA_UNIT="acre"
Rscript -e "source('parameters_project.R'); print(AREA_UNIT_PREFERENCE)"
# ✅ Expected: [1] "acre"

10.5: Test Cron (if configured)

sudo systemctl restart cron
grep "smartcane" /var/log/syslog
# ✅ Expected: cron job loaded successfully

VERIFICATION CHECKLIST

After all changes:

# 1. Database migration ran successfully
php artisan migrate

# 2. New columns exist
php artisan tinker
>>> DB::select("DESCRIBE projects WHERE Field IN ('client_type', 'preferred_area_unit')")

Expected output:

client_type: enum AGRONOMIC_SUPPORT/CANE_SUPPLY
preferred_area_unit: enum HECTARE/ACRE
# 3. Create new project via UI
# → Visit project create form
# → Verify "Client Type" dropdown appears
# → Verify "Preferred Area Unit" toggle appears
# → Select values and save

# 4. Verify directories created
ls -la laravel_app/storage/app/{your_new_project}/
# → Should have: Data/, field_tiles/, field_tiles_CI/, reports/, etc.

# 5. Test shell wrapper
./10_create_per_field_tiffs.sh --project={your_project}
# → Should run R script without error

# 6. Test cron job (if Linux cron set up)
sudo systemctl restart cron  # Or relevant service for your distro

These must be completed before production deployment:

  • Country-based project organization (MZ/UG/TZ folders)
  • Download scheduling (staggered by project)
  • Project search feature
  • Harvest prediction setup
  • Area unit selection UI

STEP 10: Country-Based Organization

Why: Organize projects by geographic location (MZ/UG/TZ folders)

Create Migration: database/migrations/YYYY_MM_DD_add_country_to_projects_table.php

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;

return new class extends Migration
{
    public function up(): void
    {
        Schema::table('projects', function (Blueprint $table) {
            $table->string('country')->default('Mozambique')->after('name');
            $table->string('country_code', 2)->default('MZ')->after('country');
        });
        
        // Update existing projects
        DB::table('projects')->where('name', 'angata')->update(['country' => 'Mozambique', 'country_code' => 'MZ']);
        DB::table('projects')->where('name', 'aura')->update(['country' => 'Mozambique', 'country_code' => 'MZ']);
        DB::table('projects')->where('name', 'chemba')->update(['country' => 'Mozambique', 'country_code' => 'MZ']);
        DB::table('projects')->where('name', 'xinavane')->update(['country' => 'Tanzania', 'country_code' => 'TZ']);
        DB::table('projects')->where('name', 'esa')->update(['country' => 'Kenya', 'country_code' => 'KQ']);
        DB::table('projects')->where('name', 'simba')->update(['country' => 'Uganda', 'country_code' => 'UG']);
        DB::table('projects')->where('name', 'john')->update(['country' => 'Uganda', 'country_code' => 'UG']);
        DB::table('projects')->where('name', 'huss')->update(['country' => 'Tanzania', 'country_code' => 'TZ']);
    }

    public function down(): void
    {
        Schema::table('projects', function (Blueprint $table) {
            $table->dropColumn(['country', 'country_code']);
        });
    }
};

Update Project.php $fillable:

protected $fillable = [
    'name',
    'country',
    'country_code',
    'download_path',
    'client_type',
    // ... rest
];

Update ProjectManager.php:

public array $countries = [
    'MZ' => 'Mozambique',
    'TZ' => 'Tanzania',
    'UG' => 'Uganda',
    'KQ' => 'Kenya',
    'SA' => 'South Africa',
    'ZW' => 'Zimbabwe',
    'BR' => 'Brazil',
    'MX' => 'Mexico',
    'IN' => 'India',
];

public function createProject()
{
    $projectIdentifier = $this->formData['id'] ?? null;
    Validator::make(
        ['name' => $this->formData['name']],
        ['name' => ['required', Rule::unique('projects')->ignore($projectIdentifier)]]
    )->validate();
    
    $projectPath = $this->formData['country_code'] . '/' . $this->formData['name'];
    Storage::makeDirectory($projectPath, recursive: true);
    
    $project = Project::create([
        'name' => $this->formData['name'],
        'country' => $this->formData['country'],
        'country_code' => $this->formData['country_code'],
        'download_path' => $projectPath,
        'client_type' => $this->formData['client_type'] ?? 'agronomic_support',
    ]);
    return redirect()->route('project.show', [$project->name, 'settings']);
}

Add to Blade template:

<div class="form-group mb-3">
    <label for="country" class="form-label">Country</label>
    <select wire:model="formData.country" id="country" class="form-control" required>
        @foreach($countries as $code => $name)
            <option value="{{ $name }}" @selected($formData['country'] === $name)>
                {{ $name }} ({{ $code }})
            </option>
        @endforeach
    </select>
</div>

<div class="form-group mb-3">
    <label for="country_code" class="form-label">Country Code (Auto-populated)</label>
    <input type="text" wire:model="formData.country_code" id="country_code" readonly class="form-control" />
</div>

STEP 11: Download Scheduling

Why: Avoid API rate limits by staggering downloads per project at 00:01

Option A: Linux Cron (If server is Linux)

Add to /etc/cron.d/smartcane_downloads:

# Stagger downloads by 10 minutes per project
1 0 * * * /usr/bin/python /home/user/smartcane/python_app/00_download_8band_pu_optimized.py angata 2>&1 | logger
15 0 * * * /usr/bin/python /home/user/smartcane/python_app/00_download_8band_pu_optimized.py chemba 2>&1 | logger
25 0 * * * /usr/bin/python /home/user/smartcane/python_app/00_download_8band_pu_optimized.py xinavane 2>&1 | logger
35 0 * * * /usr/bin/python /home/user/smartcane/python_app/00_download_8band_pu_optimized.py esa 2>&1 | logger
45 0 * * * /usr/bin/python /home/user/smartcane/python_app/00_download_8band_pu_optimized.py simba 2>&1 | logger
0 1 * * * /usr/bin/python /home/user/smartcane/python_app/00_download_8band_pu_optimized.py aura 2>&1 | logger

Option B: Windows Task Scheduler (If server is Windows)

# Create task for each project
$taskName = "SmartCane-Download-angata"
$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NoProfile -WindowStyle Hidden -Command python C:\smartcane\python_app\00_download_8band_pu_optimized.py angata"
$trigger = New-ScheduledTaskTrigger -Daily -At 00:01
Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -RunLevel Highest

Option C: Laravel Task Scheduler

Add to laravel_app/app/Console/Kernel.php:

protected function schedule(Schedule $schedule)
{
    $schedule->exec('python python_app/00_download_8band_pu_optimized.py angata')
        ->dailyAt('00:01');
    $schedule->exec('python python_app/00_download_8band_pu_optimized.py chemba')
        ->dailyAt('00:15');
    $schedule->exec('python python_app/00_download_8band_pu_optimized.py xinavane')
        ->dailyAt('00:25');
    $schedule->exec('python python_app/00_download_8band_pu_optimized.py esa')
        ->dailyAt('00:35');
    $schedule->exec('python python_app/00_download_8band_pu_optimized.py simba')
        ->dailyAt('00:45');
    $schedule->exec('python python_app/00_download_8band_pu_optimized.py aura')
        ->dailyAt('01:00');
}

STEP 12: Project Search Feature

Why: Find projects quickly if there are many

Add to laravel_app/app/Livewire/Projects/ProjectList.php:

public string $searchQuery = '';

public function getProjectsProperty()
{
    $query = Project::query();
    
    if (!empty($this->searchQuery)) {
        $query->where('name', 'like', '%' . $this->searchQuery . '%')
              ->orWhere('download_path', 'like', '%' . $this->searchQuery . '%');
    }
    
    return $query->orderBy('name')->paginate(15);
}

Add to Blade template:

<div class="search-bar mb-4">
    <input type="text" 
           wire:model.live="searchQuery" 
           placeholder="Search projects..." 
           class="form-control" />
</div>

<div class="projects-list">
    @forelse($this->projects as $project)
        <div class="project-card">
            <h3>{{ $project->name }}</h3>
            <p>{{ $project->download_path }}</p>
            <span class="badge badge-info">{{ $project->client_type }}</span>
        </div>
    @empty
        <p>No projects found</p>
    @endforelse
</div>

{{ $this->projects->links() }}

STEP 13: Harvest Date Prediction Setup

Why: Enable harvest date forecasting for cane_supply projects

Create conda environment:

conda env create -f python_app/environment_pytorch.yml

# Activate
conda activate pytorch_gpu

# Or CPU-only if no GPU
conda create -n pytorch_cpu python=3.10 pytorch::pytorch torchvision torchaudio -c pytorch
conda activate pytorch_cpu

Run baseline prediction (once):

python python_app/22_harvest_baseline_prediction.py angata
python python_app/23_convert_harvest_format.py angata

Schedule weekly prediction:

# Add to cron (Linux)
0 23 * * 0 conda run -n pytorch_gpu python /home/user/smartcane/python_app/31_harvest_imminent_weekly.py angata 2>&1 | logger

# Or Task Scheduler (Windows)
# Similar to download scheduling above, but Sunday 23:00

STEP 14: Area Unit Selection UI (Hectares vs Acres)

Why: Allow projects to choose their preferred area unit (hectares or acres) in reports and dashboards

Status: R/Python business logic complete. Database schema + UI implementation pending.

What's already done (in this codebase):

  • Unified area calculation function in 80_utils_common.R: calculate_area_from_geometry()
  • Area unit preference in parameters_project.R: AREA_UNIT_PREFERENCE (default: "hectare")
  • Helper function: get_area_unit_label() for dynamic "ha" or "ac" display
  • Refactored scripts 80/90/91 to use unified function and support user's area preference
  • Area now included in KPI outputs (CSV/RDS/Excel) from script 80
  • Scripts 90/91 read area from KPI files instead of recalculating

What needs implementation:

Step 1: Create database migration

<?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('projects', function (Blueprint $table) {
            $table->enum('preferred_area_unit', ['hectare', 'acre'])
                ->default('hectare')
                ->after('client_type');
        });
    }

    public function down(): void
    {
        Schema::table('projects', function (Blueprint $table) {
            $table->dropColumn('preferred_area_unit');
        });
    }
};

Step 2: Update Project model (laravel_app/app/Models/Project.php)

protected $fillable = [
    // ... existing fields ...
    'preferred_area_unit',  // ADD THIS
];

Step 3: Add form UI (Blade template)

<div class="form-group mb-3">
    <label for="preferred_area_unit" class="form-label">Area Unit Preference <span class="text-danger">*</span></label>
    <div class="btn-group btn-group-sm" role="group">
        <input type="radio" class="btn-check" name="preferred_area_unit" 
               id="unit_hectare" value="hectare" 
               wire:model="formData.preferred_area_unit" />
        <label class="btn btn-outline-primary" for="unit_hectare">Hectares (ha)</label>
        
        <input type="radio" class="btn-check" name="preferred_area_unit" 
               id="unit_acre" value="acre" 
               wire:model="formData.preferred_area_unit" />
        <label class="btn btn-outline-primary" for="unit_acre">Acres (ac)</label>
    </div>
    @error('formData.preferred_area_unit') 
        <span class="text-danger small">{{ $message }}</span> 
    @enderror
</div>

Step 4: Pass preference to R scripts (in job/shell wrapper)

When launching R scripts, read project->preferred_area_unit and either:

  • Option A: Write to parameters_project.R dynamically before script execution
  • Option B: Pass as environment variable to scripts (R scripts read Sys.getenv("AREA_UNIT"))

Example (PowerShell wrapper):

$areaUnit = $project->preferred_area_unit  # From database
$env:AREA_UNIT = $areaUnit  # Set environment variable
& "C:\Program Files\R\R-4.4.3\bin\x64\Rscript.exe" r_app/80_calculate_kpis.R $project 

Testing checklist:

  • Database migration runs successfully
  • Project form shows area unit radio buttons
  • Can select and save area unit preference
  • Area unit persists in database
  • Run script 80 with one project set to "hectare", observe KPI output
  • Run script 80 with another project set to "acre", compare outputs
  • Reports display area in user's chosen unit

Notes:

  • Default preference: "hectare" (metric standard)
  • Conversion factor: 0.404686 (1 hectare = 0.404686 acres)
  • All area calculations use EPSG:6933 (equal-area projection) for accuracy
  • Area column in KPI outputs named dynamically: "Area_ha" or "Area_ac"

SECTION C: POST-MERGE DATA MIGRATION STRATEGY

Approach: Per-Project Options

Small Projects

Projects: aura, chemba, xinavane, esa, simba, john, huss, tpc, miwani, etc.

Option A: Delete & Redownload Recommended (Fastest setup)

  1. Backup project folder externally (optional)
  2. Delete project from Laravel UI (removes all data)
  3. Recreate project with new form fields (client_type + area_unit selector)
  4. Redownload satellite data for relevant date range
  5. Run full R pipeline (Scripts 10-80) to generate KPI reports

Large Projects

Projects: angata

Option B: Preserve merged_tif Recommended (No redownload)

  1. Backup merged_tif/ folder externally
  2. Keep ONLY: merged_tif/ folder
  3. Delete: everything else (field_tiles/, reports/, logs/, etc.)
  4. Run Scripts 10-80 from existing merged_tif data
    • Regenerates field_tiles_CI/, reports/kpis/, weekly_mosaic/, etc.
    • No satellite redownload needed
    • KPI reports automatically use project's preferred_area_unit