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:
- Database migration
- Update Project model
- Update jobs (2 files)
- Update ProjectManager form
- Add form fields to Blade template
- Update MailingForm
- Create 5 shell script wrappers
- Setup Linux cron jobs
- Run verification tests
- Country-based project organization (MZ/UG/TZ)
- Download scheduling (auto-stagger per project)
- Project search feature
- Harvest prediction setup
- 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_supportif you want field-level vegetation analysis only - Choose
cane_supplyif 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:
-
Job Dispatchers (if creating automated job queues)
- File: Any job class that orchestrates multiple scripts
- Check:
if ($project->client_type === 'cane_supply')
-
Manual UI (if running scripts from Laravel UI)
- File: ProjectManager Blade template or controller
- Show: Only harvest buttons for cane_supply projects
-
Scheduled Tasks (for weekly/baseline runs)
- File:
/etc/cron.d/smartcane_downloadsor Laravel Kernel.php - Condition: Only queue harvest scripts for cane_supply projects
- File:
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:
Note
: Database migration for
preferred_area_unitcolumn already created in STEP 1. The column is added alongsideclient_typein the same migration with proper rollback handling.
Step 1: Update Project model (laravel_app/app/Models/Project.php)
protected $fillable = [
// ... existing fields ...
'preferred_area_unit', // ADD THIS
];
Step 2: 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 3: 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.Rdynamically 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)
- Backup project folder externally (optional)
- Delete project from Laravel UI (removes all data)
- Recreate project with new form fields (client_type + area_unit selector)
- Redownload satellite data for relevant date range
- Run full R pipeline (Scripts 10-80) to generate KPI reports
Large Projects
Projects: angata
Option B: Preserve merged_tif ✅ Recommended (No redownload)
- Backup
merged_tif/folder externally - Keep ONLY:
merged_tif/folder - Delete: everything else (field_tiles/, reports/, logs/, etc.)
- 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