# 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:
```bash
./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
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:
```bash
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:
```php
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
```php
/**
* 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):
```php
$path = $project->download_path . '/merged_final_tif/' . $filename;
```
Replace with:
```php
$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):
```php
$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:
```php
$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:
```php
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:
```php
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:
```php
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**:
```blade
```
---
### 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:
```php
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`:
```php
// 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
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`
```bash
#!/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`
```bash
#!/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`
```bash
#!/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`
```bash
#!/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`
```bash
#!/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
```bash
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
```bash
# 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:
```bash
pwd # Run in project root
```
---
## STEP 9: Verification (15 min)
Run these checks to confirm all changes work:
### 10.1: Database
```bash
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
```bash
./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
```bash
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)
```bash
sudo systemctl restart cron
grep "smartcane" /var/log/syslog
# ✅ Expected: cron job loaded successfully
```
---
## ✅ VERIFICATION CHECKLIST
After all changes:
```bash
# 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
```
```bash
# 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
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`:**
```php
protected $fillable = [
'name',
'country',
'country_code',
'download_path',
'client_type',
// ... rest
];
```
**Update ProjectManager.php:**
```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:**
```blade
```
---
## 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`:**
```bash
# 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)
```powershell
# 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`:**
```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`:**
```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:**
```blade
@forelse($this->projects as $project)
{{ $project->name }}
{{ $project->download_path }}
{{ $project->client_type }}
@empty
No projects found
@endforelse
{{ $this->projects->links() }}
```
---
## STEP 13: Harvest Date Prediction Setup
**Why**: Enable harvest date forecasting for cane_supply projects
**Create conda environment:**
```bash
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):**
```bash
python python_app/22_harvest_baseline_prediction.py angata
python python_app/23_convert_harvest_format.py angata
```
**Schedule weekly prediction:**
```bash
# 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_unit` column already created in **STEP 1**. The column is added alongside `client_type` in the same migration with proper rollback handling.
**Step 1: Update Project model** (`laravel_app/app/Models/Project.php`)
```php
protected $fillable = [
// ... existing fields ...
'preferred_area_unit', // ADD THIS
];
```
**Step 2: Add form UI** (Blade template)
```blade
```
**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.R` dynamically before script execution
- **Option B**: Pass as environment variable to scripts (R scripts read `Sys.getenv("AREA_UNIT")`)
Example (PowerShell wrapper):
```powershell
$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
---