garden plan web app
This commit is contained in:
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# Rust
|
||||
/target/
|
||||
Cargo.lock
|
||||
**/*.rs.bk
|
||||
*.pdb
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
362
CLAUDE.md
Normal file
362
CLAUDE.md
Normal file
@@ -0,0 +1,362 @@
|
||||
# CLAUDE.md - Garden Planner Project
|
||||
|
||||
This document explains the architecture, patterns, and conventions used in this garden planning application. It's designed to help AI assistants (and humans) understand and modify the codebase effectively.
|
||||
|
||||
## Project Overview
|
||||
|
||||
A self-hosted web application for visualizing garden planting schedules across three beds in Western Zealand, Denmark. Built with Rust backend (Axum) and vanilla JavaScript frontend.
|
||||
|
||||
**Key Features:**
|
||||
- Interactive timeline view showing planting schedules across 12 months
|
||||
- Multi-phase crop visualization (greenhouse → transplant)
|
||||
- Expandable crop details on click
|
||||
- Table view for detailed event listing
|
||||
- CSV-driven data (easy to update without code changes)
|
||||
|
||||
## Architecture
|
||||
|
||||
### Backend (Rust)
|
||||
- **Framework:** Axum web server
|
||||
- **Port:** 127.0.0.1:3000
|
||||
- **Data Sources:** `garden-plan-2026.csv` (events), `crop-colors.json` (colors)
|
||||
- **Main file:** `src/main.rs`
|
||||
|
||||
The backend:
|
||||
1. Parses CSV file on startup
|
||||
2. Loads crop color configuration from JSON
|
||||
3. Groups related events (greenhouse sowing + transplanting)
|
||||
4. Serves four endpoints:
|
||||
- `GET /` - HTML page
|
||||
- `GET /api/events` - Raw CSV data as JSON
|
||||
- `GET /api/timeline` - Processed timeline with phases
|
||||
- `GET /api/colors` - Crop color mappings
|
||||
|
||||
### Frontend (JavaScript)
|
||||
- **File:** `static/index.html`
|
||||
- **Style:** Vanilla HTML/CSS/JavaScript (no frameworks)
|
||||
- **Dynamic loading:** HTML is read from disk on each request (no rebuild needed for UI changes)
|
||||
|
||||
The frontend:
|
||||
1. Fetches data from `/api/timeline`, `/api/events`, and `/api/colors`
|
||||
2. Renders timeline with color-coded crop boxes
|
||||
3. Handles click interactions for expanding crop details
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
garden/
|
||||
├── Cargo.toml # Rust dependencies
|
||||
├── src/
|
||||
│ └── main.rs # Web server, CSV parsing, event grouping
|
||||
├── static/
|
||||
│ └── index.html # Complete UI (HTML + CSS + JS)
|
||||
├── garden-plan-2026.csv # Garden data (user editable)
|
||||
├── crop-colors.json # Crop color mappings (user editable)
|
||||
├── climate-data-western-zealand.md # Climate reference data
|
||||
├── bed-layout.md # Documentation of bed contents
|
||||
├── README.md # User-facing documentation
|
||||
└── CLAUDE.md # This file
|
||||
```
|
||||
|
||||
## Key Data Structures
|
||||
|
||||
### Rust Backend
|
||||
|
||||
```rust
|
||||
// Input: Raw CSV row
|
||||
struct GardenEvent {
|
||||
date: String, // "2025-03-24"
|
||||
activity: String, // "Greenhouse Sow", "Transplant", "Direct Sow"
|
||||
crop: String, // "Tomato"
|
||||
variety: String, // "Sungold + Matina"
|
||||
bed: String, // "South", "Middle", "North"
|
||||
harvest_period: String, // "July-October"
|
||||
notes: String, // Growing instructions
|
||||
}
|
||||
|
||||
// Output: Grouped timeline entry
|
||||
struct TimelineEvent {
|
||||
crop: String,
|
||||
variety: String,
|
||||
bed: String,
|
||||
phases: Vec<TimelinePhase>, // Multiple phases per crop
|
||||
harvest_period: String,
|
||||
}
|
||||
|
||||
struct TimelinePhase {
|
||||
activity: String, // "Greenhouse Sow"
|
||||
start_date: String, // "2025-03-24"
|
||||
end_date: Option<String>, // "2025-05-05" or harvest end
|
||||
notes: String,
|
||||
}
|
||||
```
|
||||
|
||||
### Event Grouping Logic
|
||||
|
||||
The `build_timeline()` function groups related events:
|
||||
1. Filters out "Mulch" and "Harvest" activities
|
||||
2. Identifies greenhouse sowings
|
||||
3. Finds matching transplants (same crop + bed, later date)
|
||||
4. Groups them as phases of a single timeline entry
|
||||
5. Calculates end dates (next phase start, or harvest end)
|
||||
|
||||
**Example:** Tomato has two CSV rows (Greenhouse Sow on 2026-03-23, Transplant on 2026-05-04) → grouped into one TimelineEvent with two phases.
|
||||
|
||||
## Frontend Patterns
|
||||
|
||||
### Color System
|
||||
|
||||
Crop colors are defined in `crop-colors.json` and loaded dynamically via the `/api/colors` endpoint. The `getCropColor()` function looks up colors from the loaded configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"Tomato": "#e53e3e",
|
||||
"Basil": "#48bb78",
|
||||
"Bush Beans": "#9f7aea",
|
||||
"Cornflower": "#6495ed",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
**To add/change crop colors:**
|
||||
1. Edit `crop-colors.json`
|
||||
2. Restart server (no rebuild needed)
|
||||
3. Refresh browser
|
||||
|
||||
The function returns a default blue (`#4299e1`) for any crop not in the configuration.
|
||||
|
||||
Multi-phase crops use `adjustColor()` to lighten the first phase by 20% (factor 0.8).
|
||||
|
||||
### Box Expansion System
|
||||
|
||||
Each crop box has:
|
||||
- Unique ID: `crop-${bed}-${idx}-${phaseIdx}` (e.g., "crop-South-1-0")
|
||||
- Group attribute: `data-crop-group="${bed}-${idx}"` (e.g., "South-1")
|
||||
|
||||
When clicked:
|
||||
1. `toggleCropDetails()` finds all boxes with matching `data-crop-group`
|
||||
2. Collapses all other expanded crops
|
||||
3. Expands all phases of the clicked crop
|
||||
4. Each phase shows its own details independently
|
||||
|
||||
### Timeline Positioning
|
||||
|
||||
`dateToPosition()` converts dates to percentage positions:
|
||||
- Timeline spans: 2026-03-01 to 2027-03-01 (12 months)
|
||||
- Each date maps to 0-100% of timeline width
|
||||
- Example: 2026-03-23 = ~8%, 2026-05-04 = ~22%
|
||||
|
||||
## Common Modifications
|
||||
|
||||
### Adding a New Crop
|
||||
|
||||
1. **Update CSV:** Add row(s) to `garden-plan-2026.csv`
|
||||
2. **Add color:** Add entry to `crop-colors.json` (optional - defaults to blue if not specified)
|
||||
3. **Restart server:** `./target/release/garden-planner`
|
||||
4. **Refresh browser**
|
||||
|
||||
### Changing Crop Colors
|
||||
|
||||
1. **Edit colors:** Modify `crop-colors.json`
|
||||
2. **Restart server:** `./target/release/garden-planner` (no rebuild needed)
|
||||
3. **Refresh browser**
|
||||
|
||||
### Changing Timeline Display Logic
|
||||
|
||||
Edit `renderTimeline()` function in `static/index.html`:
|
||||
- Modify how boxes are rendered
|
||||
- Change color calculation
|
||||
- Adjust positioning logic
|
||||
- No Rust rebuild needed (HTML loaded dynamically)
|
||||
|
||||
### Modifying Event Grouping
|
||||
|
||||
Edit `build_timeline()` in `src/main.rs`:
|
||||
- Change how phases are matched
|
||||
- Adjust end date calculation
|
||||
- Modify filtering logic
|
||||
- Requires: `cargo build --release`
|
||||
|
||||
### Adding New API Endpoints
|
||||
|
||||
```rust
|
||||
// In main.rs
|
||||
async fn new_endpoint(State(state): State<Arc<AppState>>) -> Json<YourType> {
|
||||
// Your logic
|
||||
Json(data)
|
||||
}
|
||||
|
||||
// Add to router
|
||||
let app = Router::new()
|
||||
.route("/api/new", get(new_endpoint))
|
||||
// ... other routes
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Making UI Changes
|
||||
1. Edit `static/index.html`
|
||||
2. Refresh browser (no rebuild)
|
||||
3. Check browser console for JS errors
|
||||
|
||||
### Making Backend Changes
|
||||
1. Edit `src/main.rs`
|
||||
2. Run `cargo build --release`
|
||||
3. Restart server: `./target/release/garden-planner`
|
||||
4. Refresh browser
|
||||
|
||||
### Updating Garden Plan
|
||||
1. Edit `garden-plan-2026.csv` and/or `crop-colors.json`
|
||||
2. Restart server (no rebuild needed)
|
||||
3. Refresh browser
|
||||
|
||||
### Verifying Climate Alignment
|
||||
1. Review `climate-data-western-zealand.md` for frost dates and temperature trends
|
||||
2. Ensure greenhouse starts are after mid-March
|
||||
3. Ensure transplants are after last frost (~April 20)
|
||||
4. Ensure fall crops are planted before first frost (~October 10)
|
||||
5. Update climate data document annually in January
|
||||
|
||||
### Debugging
|
||||
- **Backend:** Add `println!()` or `dbg!()` in Rust code, check terminal
|
||||
- **Frontend:** Add `console.log()` in JS, check browser console (F12)
|
||||
- **Data flow:** Check Network tab in browser DevTools for API responses
|
||||
|
||||
## Design Decisions
|
||||
|
||||
### Why Rust?
|
||||
- Fast CSV parsing
|
||||
- Type safety for data structures
|
||||
- Low resource usage for self-hosting
|
||||
- Good learning opportunity
|
||||
|
||||
### Why Vanilla JS?
|
||||
- No build step complexity
|
||||
- Easy to understand and modify
|
||||
- Fast iteration (no framework overhead)
|
||||
- Everything in one HTML file
|
||||
|
||||
### Why CSV for Garden Data?
|
||||
- Easy to edit in spreadsheet apps
|
||||
- Human-readable
|
||||
- Version control friendly
|
||||
- No database needed
|
||||
|
||||
### Why JSON for Colors?
|
||||
- Simpler than embedding in code
|
||||
- Easy to edit without touching HTML/JS
|
||||
- Centralized configuration
|
||||
- No rebuild needed to change colors
|
||||
- Can be updated independently of garden plan
|
||||
|
||||
### Why Group Greenhouse + Transplant?
|
||||
- Shows complete crop lifecycle
|
||||
- Cleaner visual representation
|
||||
- User can see planning timeline at a glance
|
||||
- Reduces clutter (one row instead of two)
|
||||
|
||||
## Gotchas and Edge Cases
|
||||
|
||||
### CSV Header Names
|
||||
Headers must match exactly (case-sensitive):
|
||||
```
|
||||
Date,Week,Activity,Crop,Variety,Bed,Quantity,Harvest Period,Notes
|
||||
```
|
||||
|
||||
Rust structs use `#[serde(rename = "Date")]` to map to these headers.
|
||||
|
||||
### CSV Field Quoting
|
||||
Any field containing commas must be wrapped in double quotes:
|
||||
```csv
|
||||
2025-05-05,Week 18,Direct Plant,Tansy,Guldknap,Middle,6 plants,June-October,"Bed edges - native Danish wildflower, pest repellent"
|
||||
```
|
||||
|
||||
Without quotes, the CSV parser will split the field and cause an "UnequalLengths" error.
|
||||
|
||||
### Color Configuration
|
||||
The `crop-colors.json` file must be valid JSON with crop names as keys and hex colors as values:
|
||||
```json
|
||||
{
|
||||
"Crop Name": "#hexcolor"
|
||||
}
|
||||
```
|
||||
|
||||
Missing crops will default to `#4299e1` (blue).
|
||||
|
||||
### ID Prefix Matching
|
||||
Never use `[id^="crop-South-1"]` - it matches both "crop-South-1-0" and "crop-South-10-0".
|
||||
Always use exact `data-crop-group` attribute matching.
|
||||
|
||||
### Date Parsing
|
||||
`month_to_date()` function is simple and may not handle all formats. Current supported formats:
|
||||
- "July-October" → extracts "October"
|
||||
- "May 2027" → extracts "May" + "2027"
|
||||
- Defaults to Dec 31, 2026 if unparseable
|
||||
|
||||
### Multi-Phase Detection
|
||||
Grouping only works for greenhouse → transplant pattern. If you add a third phase type, update `build_timeline()` logic.
|
||||
|
||||
## Future Enhancement Ideas
|
||||
|
||||
If you want to extend this project:
|
||||
|
||||
1. **Add edit mode:** Click to edit CSV data in-browser, save changes
|
||||
2. **Export formats:** PDF, calendar (iCal), print-friendly view
|
||||
3. **Weather integration:** Show frost dates, temperature ranges
|
||||
4. **Mobile responsive:** Better touch interactions
|
||||
5. **Search/filter:** Find specific crops, filter by bed
|
||||
6. **Notes system:** Add per-crop growing notes from previous years
|
||||
7. **Harvest tracking:** Mark actual harvest dates vs. planned
|
||||
8. **Multi-year view:** Compare plans across seasons
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Rust (Cargo.toml)
|
||||
- `axum` - Web framework
|
||||
- `tokio` - Async runtime
|
||||
- `tower` & `tower-http` - Middleware
|
||||
- `serde` & `serde_json` - Serialization
|
||||
- `csv` - CSV parsing
|
||||
- `chrono` - Date handling
|
||||
- `tracing` - Logging
|
||||
|
||||
All dependencies are stable, well-maintained crates.
|
||||
|
||||
## Questions to Ask When Modifying
|
||||
|
||||
Before making changes, consider:
|
||||
|
||||
1. **Is this a UI or data change?**
|
||||
- UI → Edit `index.html`
|
||||
- Data structure → Edit `main.rs`
|
||||
|
||||
2. **Does it affect grouping logic?**
|
||||
- If yes, test with crops that have multiple phases
|
||||
|
||||
3. **Does it change the timeline calculation?**
|
||||
- Test with crops at beginning, middle, and end of season
|
||||
|
||||
4. **Will it break existing CSS?**
|
||||
- Check both expanded and collapsed states
|
||||
- Test with beds that have many crops
|
||||
|
||||
5. **Is the CSV format changing?**
|
||||
- Update both Rust structs and documentation
|
||||
|
||||
## Getting Help
|
||||
|
||||
If you're stuck:
|
||||
1. Check the browser console for JS errors
|
||||
2. Check terminal output for Rust errors
|
||||
3. Verify CSV format matches expected structure
|
||||
4. Test API endpoints directly: `curl http://localhost:3000/api/timeline`
|
||||
5. Add debug logging to isolate the issue
|
||||
|
||||
## Summary
|
||||
|
||||
This is a straightforward project with clear separation of concerns:
|
||||
- **Rust:** Data processing and serving
|
||||
- **JavaScript:** Rendering and interaction
|
||||
- **CSV:** Single source of truth
|
||||
|
||||
The code prioritizes simplicity and maintainability over clever abstractions. When in doubt, add explicit code rather than complex logic.
|
||||
16
Cargo.toml
Normal file
16
Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "garden-planner"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
axum = "0.7"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tower = "0.4"
|
||||
tower-http = { version = "0.5", features = ["fs", "trace"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
csv = "1.3"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
125
README.md
Normal file
125
README.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# Garden Planner Visualizer
|
||||
|
||||
A self-hosted web application written in Rust to visualize your garden plan timeline.
|
||||
|
||||
## Features
|
||||
|
||||
- Interactive timeline view showing planting and harvest periods for each bed
|
||||
- Table view with all planting events
|
||||
- Responsive design with hover tooltips
|
||||
- Color-coded phases (planting, growing, harvesting)
|
||||
- Reads directly from your CSV file
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Rust and Cargo (install from https://rustup.rs/)
|
||||
|
||||
## Setup
|
||||
|
||||
1. Make sure you're in the garden project directory:
|
||||
```bash
|
||||
cd /home/jonas/projects/garden
|
||||
```
|
||||
|
||||
2. Build the project:
|
||||
```bash
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
3. Run the server:
|
||||
```bash
|
||||
cargo run --release
|
||||
```
|
||||
|
||||
4. Open your browser and navigate to:
|
||||
```
|
||||
http://127.0.0.1:3000
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
Just run:
|
||||
```bash
|
||||
cargo run
|
||||
```
|
||||
|
||||
The application will:
|
||||
- Parse `garden-plan-2025.csv`
|
||||
- Start a web server on port 3000
|
||||
- Serve an interactive visualization
|
||||
|
||||
## Views
|
||||
|
||||
### Timeline View
|
||||
- Shows each bed (South, Middle, North) as a separate section
|
||||
- Displays crops as colored bars across a 12-month timeline
|
||||
- Each crop type has its own color (tomatoes=red, beans=purple, etc.)
|
||||
- Crops with multiple phases (greenhouse + transplant) show multiple connected boxes:
|
||||
- Lighter shade: Greenhouse phase 🌱
|
||||
- Full color: Garden bed phase 🌿
|
||||
- Click any crop box to expand and see full growing details
|
||||
|
||||
### Table View
|
||||
- Complete list of all planting events
|
||||
- Sortable columns
|
||||
- Badges for bed locations and activity types
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
garden/
|
||||
├── Cargo.toml # Rust dependencies
|
||||
├── src/
|
||||
│ └── main.rs # Web server and CSV parsing (Rust)
|
||||
├── static/
|
||||
│ └── index.html # Visualization UI (HTML/CSS/JS)
|
||||
├── garden-plan-2025.csv # Your garden data
|
||||
└── bed-layout.md # Bed layout documentation
|
||||
```
|
||||
|
||||
## Updating Your Plan
|
||||
|
||||
1. Edit `garden-plan-2025.csv` with your changes
|
||||
2. Restart the server (Ctrl+C, then `cargo run`)
|
||||
3. Refresh your browser
|
||||
|
||||
## Customization
|
||||
|
||||
### Change Port
|
||||
|
||||
Edit `src/main.rs:107` to change from `127.0.0.1:3000` to your preferred address.
|
||||
|
||||
### Styling
|
||||
|
||||
Edit `static/index.html` to customize colors, fonts, and layout.
|
||||
|
||||
## Technical Details
|
||||
|
||||
- **Backend**: Axum web framework (Rust)
|
||||
- **CSV Parsing**: csv crate (Rust)
|
||||
- **Frontend**: Vanilla HTML/CSS/JavaScript
|
||||
- **API Endpoints**:
|
||||
- `GET /` - Main visualization page
|
||||
- `GET /api/events` - Raw CSV data as JSON
|
||||
- `GET /api/timeline` - Processed timeline data as JSON
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Port already in use:**
|
||||
```
|
||||
Error: Address already in use (os error 98)
|
||||
```
|
||||
Kill any process using port 3000 or change the port in `src/main.rs`.
|
||||
|
||||
**CSV not found:**
|
||||
Make sure you run the command from the `/home/jonas/projects/garden` directory where the CSV file is located.
|
||||
|
||||
**Build errors:**
|
||||
Update Rust to the latest version:
|
||||
```bash
|
||||
rustup update
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Personal use - do whatever you want with it.
|
||||
62
bed-layout.md
Normal file
62
bed-layout.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Garden Bed Layout
|
||||
|
||||
## Physical Layout
|
||||
|
||||
The garden consists of 6 growing areas arranged with 60cm pathways:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ North Bed (300×50cm) │
|
||||
└─────────────────────────────────────────┘
|
||||
(60cm pathway)
|
||||
┌──────────────────┐ ┌───────────────────────┐
|
||||
│ Middle Bed │ │ Plot North │
|
||||
│ (150×100cm) │ │ (200×150cm) │
|
||||
└──────────────────┘ └───────────────────────┘
|
||||
(60cm pathway) (60cm pathway)
|
||||
┌──────────────────┐ ┌───────────────────────┐
|
||||
│ South Bed │ │ Plot South │
|
||||
│ (150×100cm) │ │ (200×150cm) │
|
||||
└──────────────────┘ └───────────────────────┘
|
||||
(60cm pathway)
|
||||
┌──────────────────┐
|
||||
│ Greenhouse │
|
||||
│ (150×50cm) │
|
||||
└──────────────────┘
|
||||
```
|
||||
|
||||
**Key spatial relationships:**
|
||||
- North Bed spans the full width at the top
|
||||
- Middle Bed and Plot North are aligned horizontally
|
||||
- South Bed and Plot South are aligned horizontally
|
||||
- Greenhouse is a standalone unheated structure
|
||||
- 60cm pathways between all beds and plots for easy access
|
||||
|
||||
---
|
||||
|
||||
## Bed Specifications
|
||||
|
||||
### North Bed
|
||||
- **Dimensions:** 300×50cm
|
||||
- **Location:** Top of garden, spans full width
|
||||
|
||||
### Middle Bed
|
||||
- **Dimensions:** 150×100cm
|
||||
- **Location:** Left side, middle row
|
||||
|
||||
### South Bed
|
||||
- **Dimensions:** 150×100cm
|
||||
- **Location:** Left side, bottom row
|
||||
|
||||
### Plot North
|
||||
- **Dimensions:** 200×150cm
|
||||
- **Location:** Right side, middle row (adjacent to Middle Bed)
|
||||
|
||||
### Plot South
|
||||
- **Dimensions:** 200×150cm
|
||||
- **Location:** Right side, bottom row (adjacent to South Bed)
|
||||
|
||||
### Greenhouse
|
||||
- **Dimensions:** 150×50cm
|
||||
- **Type:** Unheated
|
||||
- **Primary Use:** Tomato growing
|
||||
96
climate-data-western-zealand.md
Normal file
96
climate-data-western-zealand.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# Climate Data for Western Zealand, Denmark
|
||||
## Garden Planning Reference - Updated January 2026
|
||||
|
||||
This document summarizes current climate data for Western Zealand to inform planting and harvest schedules.
|
||||
|
||||
## Hardiness Zone
|
||||
- **USDA Zone:** 8a-8b (mostly 8b in coastal areas)
|
||||
- Denmark ranges from Zone 7a (northern Jutland) to Zone 9b (southern islands)
|
||||
- Western Zealand benefits from maritime influence (North Sea/Baltic Sea)
|
||||
|
||||
## Frost Dates (Copenhagen reference)
|
||||
- **Last Spring Frost:** ~April 20
|
||||
- **First Fall Frost:** ~October 10
|
||||
- **Frost-Free Days:** 173 days (approximately 5.7 months)
|
||||
|
||||
## Temperature Trends (2024-2025)
|
||||
### Recent Data Points:
|
||||
- **Annual average 2024:** 10.03°C (up from 9.56°C in 2023)
|
||||
- **March 2025:** 5.43°C (warmest March in 11 years)
|
||||
- **April 2025:** 9.10°C (warmest April in 14 years)
|
||||
- **Warming trend:** Clear upward temperature trend continuing
|
||||
|
||||
### Seasonal Ranges:
|
||||
- **Winter (Dec-Mar):** -4°C to 4°C average
|
||||
- **Summer (Jun-Aug):** 15°C to 25°C average
|
||||
- **Zealand annual average:** 10°C
|
||||
|
||||
## Growing Season
|
||||
- **Primary season:** April to October (6-7 months)
|
||||
- **Extended season:** Mild winters allow some cold-hardy crops through January-February
|
||||
- **Greenhouse starting:** Safe from mid-March onward
|
||||
- **Outdoor planting:** After April 20 for frost-sensitive crops
|
||||
|
||||
## Planting Guidelines Based on Climate Data
|
||||
|
||||
### Early Spring (March-April)
|
||||
- **Indoor/Greenhouse (mid-March):** Tomatoes, basil, peppers - safe indoors
|
||||
- **Outdoor direct sow (early April):** Rhubarb, parsnips, cabbage, broad beans (hardy)
|
||||
- **Outdoor direct sow (late April/early May):** After last frost - beans, carrots, beets, chives, parsley
|
||||
|
||||
### Late Spring (May)
|
||||
- **Transplant (early May):** Tomatoes, basil after last frost (~May 5-10 is safe)
|
||||
- **Direct sow/plant:** Squash, zucchini, cucumbers - soil warm enough
|
||||
- **Succession planting:** Begin succession crops for extended harvest
|
||||
|
||||
### Summer (June-August)
|
||||
- **Early June:** Last chance for summer crops (beans, chard)
|
||||
- **Mid-July:** Plant fall/winter crops (kale, carrots for fall harvest)
|
||||
- **Late July:** Sow overwintering crops (winter kale, chard)
|
||||
|
||||
### Fall (September-October)
|
||||
- **Early October:** Plant garlic, overwintering broad beans
|
||||
- **Late October:** Mulch beds before first frost (~Oct 10)
|
||||
- **November:** Final mulching with straw/leaves for winter protection
|
||||
|
||||
### Winter (November-March)
|
||||
- **Hardy crops continue:** Kale, chard, leeks can be harvested through February
|
||||
- **Garlic matures:** Planted Oct, harvested June (8 months)
|
||||
- **Broad beans overwinter:** Planted late Oct, harvest May (7 months)
|
||||
|
||||
## Climate Considerations for 2026-2027
|
||||
|
||||
### Warming Trends
|
||||
- Consistent warming observed 2023-2025
|
||||
- Last frost date may be shifting earlier (traditional April 20 may become April 15)
|
||||
- Growing season potentially extending at both ends
|
||||
|
||||
### Recommendations
|
||||
- **Conservative approach:** Still plan for April 20 last frost until multi-year data confirms shift
|
||||
- **Monitor:** Track actual last frost in 2026 to adjust 2027 plan
|
||||
- **Opportunity:** Warmer springs allow earlier greenhouse starting (mid-March confirmed safe)
|
||||
- **Risk management:** Keep row covers/cloches ready for late April cold snaps
|
||||
|
||||
### Crop-Specific Notes
|
||||
- **Tomatoes:** May still need warm summer to ripen fully; greenhouse starts essential
|
||||
- **Winter crops:** Mild winters excellent for kale, chard, leeks through February
|
||||
- **Garlic:** October planting confirmed appropriate (needs cold period)
|
||||
- **Broad beans:** Overwinter strategy working well with mild Danish winters
|
||||
- **Summer squash:** Direct sow early May safe; transplants can go out May 1 with protection
|
||||
|
||||
## Data Sources
|
||||
- [Denmark Hardiness Zones - PlantMaps](https://www.plantmaps.com/interactive-denmark-hardiness-zone-map-celsius.php)
|
||||
- [Climate: Zealand in Denmark - WorldData](https://www.worlddata.info/europe/denmark/climate-zealand.php)
|
||||
- [When to Plant Vegetables in Copenhagen - Garden.org](https://garden.org/apps/calendar/?q=copenhagen)
|
||||
- [Growing Vegetables In Denmark - GardeningTips.in](https://gardeningtips.in/growing-vegetables-in-denmark-planting-calendar)
|
||||
- [Denmark Temperature Data - TradingEconomics](https://tradingeconomics.com/denmark/temperature)
|
||||
- [Denmark Climate Knowledge Portal - World Bank](https://climateknowledgeportal.worldbank.org/country/denmark)
|
||||
|
||||
## Annual Review Schedule
|
||||
- **January:** Review previous year's actual frost dates, update this document
|
||||
- **February:** Adjust planting schedule based on updated frost predictions
|
||||
- **November:** Document harvest results and crop performance for next year's planning
|
||||
|
||||
---
|
||||
*Last updated: January 2026*
|
||||
*Next review: January 2027*
|
||||
22
crop-colors.json
Normal file
22
crop-colors.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"Tomato": "#c53030",
|
||||
"Tomatillo": "#38a169",
|
||||
"Cilantro": "#68d391",
|
||||
"String Beans (Helda)": "#2d5016",
|
||||
"String Beans (Stokkievitsboon)": "#9f7aea",
|
||||
"Nasturtium": "#e53e3e",
|
||||
"Arugula": "#48bb78",
|
||||
"Amaranth": "#9b2c2c",
|
||||
"Zucchini": "#38a169",
|
||||
"Winter Squash": "#dd6b20",
|
||||
"Wild Carrot": "#e2e8f0",
|
||||
"Radicchio": "#9b2c2c",
|
||||
"Sugar Snap Peas": "#68d391",
|
||||
"Carrots": "#ed8936",
|
||||
"Kale (Westlandse Winter)": "#2f855a",
|
||||
"Kale (Nero di Toscana)": "#1a4d2e",
|
||||
"Garlic": "#d6bcfa",
|
||||
"Broad Beans": "#9ae6b4",
|
||||
"Onion": "#d69e2e",
|
||||
"Potatoes": "#c7844b"
|
||||
}
|
||||
32
garden-plan-2026.csv
Normal file
32
garden-plan-2026.csv
Normal file
@@ -0,0 +1,32 @@
|
||||
Date,Week,Activity,Crop,Variety,Bed,Quantity,Harvest Period,Notes
|
||||
2026-03-09,Week 10,Greenhouse Sow,Tomatillo,Toma Verde,Middle,3 plants,July-October,Sow in trays with heating mat
|
||||
2026-03-09,Week 10,Greenhouse Sow,Cilantro,Santo,Middle,6 plants,May-July,Sow in trays with heating mat - bolts in summer heat
|
||||
2026-03-09,Week 10,Greenhouse Sow,Tomato,Mixed varieties,Greenhouse,4 plants,June-October,Sow in trays with heating mat
|
||||
2026-04-06,Week 14,Direct Sow,Arugula,Rocket,Middle,50 plants,May-June,Early spring crop - harvest before main crops
|
||||
2026-04-06,Week 14,Direct Sow,Arugula,Rocket,South,50 plants,May-June,Early spring crop - harvest before zucchini
|
||||
2026-04-13,Week 15,Direct Sow,Radicchio,Palla Rossa,South,12 plants,May-early June,Early harvest before zucchini spreads
|
||||
2026-04-13,Week 15,Direct Sow,Sugar Snap Peas,Sugar Ann,North,45 plants,June-July,2.5m row with support - 5-6cm spacing
|
||||
2026-04-13,Week 15,Direct Sow,Carrots,Nantes,North,50 plants,July,Single row - first succession
|
||||
2026-05-04,Week 18,Transplant,Tomatillo,Toma Verde,Middle,3 plants,July-October,Add supports immediately
|
||||
2026-05-04,Week 18,Transplant,Cilantro,Santo,Middle,6 plants,May-July,Around tomatillo bases - will bolt by July
|
||||
2026-05-04,Week 18,Direct Plant,Nasturtium,Mixed varieties,Middle,8 plants,June-October,"Bed edges - edible flowers, pest deterrent"
|
||||
2026-05-04,Week 18,Direct Sow,String Beans (Helda),Helda,Middle,20 plants,July-August,Romano type - 2 rows at 15cm spacing - needs 6-8ft supports
|
||||
2026-05-04,Week 18,Direct Sow,Zucchini,Black Beauty,South,3 seeds per spot,June-September,Thin to 2 plants
|
||||
2026-03-09,Week 10,Greenhouse Sow,Onion,Sturon,North,36 plants,July-August,Sow in trays with heating mat
|
||||
2026-04-27,Week 17,Transplant,Onion,Sturon,North,36 plants,July-August,Transplant 10cm apart in 2 rows
|
||||
2026-04-27,Week 17,Transplant,Tomato,Mixed varieties,Greenhouse,4 plants,June-October,Transplant to greenhouse after last frost risk
|
||||
2026-05-18,Week 20,Direct Plant,Winter Squash,Uchiki Kuri,South,2 plants,September,Will trail outside bed
|
||||
2026-07-06,Week 27,Direct Sow,String Beans (Stokkievitsboon),Stokkievitsboon,Middle,20 plants,September-October,Borlotti type - 2 rows at 15cm spacing for dry storage (70-80 days) - use same supports
|
||||
2026-07-06,Week 27,Direct Sow,Carrots,Nantes,Middle,40 plants,October-November,Plant where cilantro was - fall harvest
|
||||
2026-07-27,Week 30,Direct Sow,Kale (Nero di Toscana),Nero di Toscana,North,4 plants,November-March 2027,Winter harvest crop - 45cm spacing
|
||||
2026-07-27,Week 30,Direct Sow,Radicchio,Palla Rossa,North,8 plants,November-December,Fall harvest in partial shade
|
||||
2026-08-17,Week 33,Direct Sow,Mizuna,Green Streak,South,25 plants,October-March 2027,"Very cold-hardy, cut-and-come-again, spicy mustard flavor - plant where zucchini was"
|
||||
2026-08-17,Week 33,Direct Sow,Mustard Greens,Red Giant,South,20 plants,October-March 2027,"Purple-red leaves, spicy, cold-hardy"
|
||||
2026-08-17,Week 33,Direct Sow,Tatsoi,Rosette,South,15 plants,October-March 2027,"Spoon-shaped leaves, mild mustard flavor, very cold-hardy"
|
||||
2026-10-05,Week 40,Direct Plant,Garlic,Hardneck variety,Plot South,70 cloves,June 2027,Plant after potato harvest - larger plot allows more garlic
|
||||
2026-10-26,Week 43,Direct Sow,Broad Beans,Aquadulce Claudia,Middle,16 plants,May 2027,Double row at 20cm spacing - overwinter for spring harvest
|
||||
2026-10-26,Week 43,Direct Sow,Broad Beans,Aquadulce Claudia,Plot North,35 plants,May 2027,4 rows at 20cm spacing - overwinter for spring harvest
|
||||
2026-04-20,Week 16,Direct Plant,Potatoes,Mixed varieties,Plot South,30 tubers,July-September,Plant seed potatoes 30cm apart in 5 rows of 6
|
||||
2026-05-25,Week 21,Direct Sow,Amaranth,Prince's Feather,Plot North,18 plants,September-October,"40×40cm spacing - harvest seed heads when brown and dry, edible grain"
|
||||
2026-11-09,Week 45,Mulch,All beds,Straw/leaves,All,Heavy layer,N/A,Winter protection
|
||||
2027-06-21,Week 25,Harvest,Garlic,All,South,40 bulbs,N/A,Cure for 2 weeks
|
||||
|
129
planting-schedule-validation.md
Normal file
129
planting-schedule-validation.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# Planting Schedule Validation for 2026-2027
|
||||
## Climate Alignment Check
|
||||
|
||||
This document validates that all planting dates in `garden-plan-2026.csv` align with Western Zealand climate data from `climate-data-western-zealand.md`.
|
||||
|
||||
## Climate Parameters (Reference)
|
||||
- **Last Spring Frost:** April 20
|
||||
- **First Fall Frost:** October 10
|
||||
- **Frost-Free Days:** 173 days
|
||||
- **USDA Zone:** 8a-8b
|
||||
- **Safe Greenhouse Start:** Mid-March onward
|
||||
|
||||
---
|
||||
|
||||
## Validation Results ✓
|
||||
|
||||
### ✅ Early Spring (March-April)
|
||||
|
||||
| Date | Activity | Crop | Climate Check | Status |
|
||||
|------|----------|------|---------------|--------|
|
||||
| 2026-03-23 | Greenhouse Sow | Tomato | After mid-March safe start | ✓ SAFE |
|
||||
| 2026-03-23 | Greenhouse Sow | Basil | After mid-March safe start | ✓ SAFE |
|
||||
| 2026-04-13 | Direct Sow | Radicchio | Cold-hardy, before last frost OK | ✓ SAFE |
|
||||
| 2026-04-13 | Direct Sow | Spinach | Cold-hardy, before last frost OK | ✓ SAFE |
|
||||
| 2026-04-13 | Direct Sow | Peas | Cold-hardy, before last frost OK | ✓ SAFE |
|
||||
| 2026-04-13 | Direct Sow | Carrots | Cold-hardy, before last frost OK | ✓ SAFE |
|
||||
| 2026-04-13 | Direct Sow | Swiss Chard | Cold-hardy, before last frost OK | ✓ SAFE |
|
||||
| 2026-04-13 | Transplant | Kale | Cold-hardy, before last frost OK | ✓ SAFE |
|
||||
|
||||
**Analysis:** All early spring sowings are appropriately timed. Greenhouse starts at March 23 provide ~6 weeks before transplant. Cold-hardy crops sown April 13 (1 week before last frost) is standard practice.
|
||||
|
||||
### ✅ Late Spring (May)
|
||||
|
||||
| Date | Activity | Crop | Climate Check | Status |
|
||||
|------|----------|------|---------------|--------|
|
||||
| 2026-05-04 | Transplant | Tomato | 2 weeks after last frost | ✓ SAFE |
|
||||
| 2026-05-04 | Transplant | Basil | 2 weeks after last frost | ✓ SAFE |
|
||||
| 2026-05-04 | Direct Plant | Cornflower | After last frost | ✓ SAFE |
|
||||
| 2026-05-04 | Direct Sow | Bush Beans | After last frost, soil warm | ✓ SAFE |
|
||||
| 2026-05-04 | Direct Sow | Zucchini | After last frost, soil warm | ✓ SAFE |
|
||||
| 2026-05-04 | Direct Plant | Tansy | After last frost | ✓ SAFE |
|
||||
| 2026-05-04 | Direct Sow | Carrots | After last frost | ✓ SAFE |
|
||||
| 2026-05-18 | Direct Plant | Winter Squash | Soil definitely warm | ✓ SAFE |
|
||||
| 2026-05-18 | Direct Plant | Wild Carrot | After last frost | ✓ SAFE |
|
||||
|
||||
**Analysis:** Perfect timing. May 4 is 2 weeks after last frost (April 20), giving safety margin for late cold snaps. Warm-season crops have warm soil. Second wave on May 18 ensures soil temperature ideal for squash.
|
||||
|
||||
### ✅ Summer (June-August)
|
||||
|
||||
| Date | Activity | Crop | Climate Check | Status |
|
||||
|------|----------|------|---------------|--------|
|
||||
| 2026-06-08 | Direct Sow | Bush Beans | Mid-season succession | ✓ SAFE |
|
||||
| 2026-06-08 | Direct Sow | Swiss Chard | Mid-season succession | ✓ SAFE |
|
||||
| 2026-07-06 | Direct Sow | Bush Beans | Late succession, 90+ days to frost | ✓ SAFE |
|
||||
| 2026-07-06 | Direct Sow | Carrots | 90+ days to frost | ✓ SAFE |
|
||||
| 2026-07-27 | Direct Sow | Kale (winter) | For fall/winter harvest | ✓ SAFE |
|
||||
| 2026-07-27 | Direct Sow | Swiss Chard | For fall/winter harvest | ✓ SAFE |
|
||||
|
||||
**Analysis:** Succession plantings properly spaced. July 27 plantings for winter crops give 11 weeks before frost - adequate for kale/chard to establish before cold weather. Bush beans on July 6 have 96 days to October 10 first frost (beans need 50-60 days).
|
||||
|
||||
### ✅ Fall (September-November)
|
||||
|
||||
| Date | Activity | Crop | Climate Check | Status |
|
||||
|------|----------|------|---------------|--------|
|
||||
| 2026-10-05 | Direct Plant | Garlic | Traditional October planting | ✓ SAFE |
|
||||
| 2026-10-26 | Direct Sow | Broad Beans | Overwinter variety, before hard freeze | ✓ SAFE |
|
||||
| 2026-11-09 | Mulch | All beds | After first frost, before hard freeze | ✓ SAFE |
|
||||
|
||||
**Analysis:** Excellent timing. Garlic planted early October gets 4-6 weeks to establish roots before hard freeze. Broad beans (Aquadulce Claudia is overwinter variety) planted late October is standard for Zone 8. Mulching November 9 is 1 month after first frost - perfect.
|
||||
|
||||
### ✅ Summer Harvest (2027)
|
||||
|
||||
| Date | Activity | Crop | Climate Check | Status |
|
||||
|------|----------|------|---------------|--------|
|
||||
| 2027-06-21 | Harvest | Garlic | 8.5 months after planting | ✓ OPTIMAL |
|
||||
| 2027-06-21 | Direct Sow | Bush Beans | Replacing harvested garlic | ✓ SAFE |
|
||||
|
||||
**Analysis:** Garlic harvest in late June is perfect timing (planted Oct 5, harvested Jun 21 = 259 days). Immediate replanting with beans maximizes bed use and provides late summer harvest.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
### Overall Assessment: **EXCELLENT** ✓
|
||||
|
||||
All planting dates align perfectly with Western Zealand climate data:
|
||||
|
||||
1. **Frost Safety:** All frost-sensitive crops planted after April 20 last frost date
|
||||
2. **Cold-Hardy Timing:** Cold-hardy crops appropriately started in early-mid April
|
||||
3. **Succession Spacing:** Bean and carrot successions well-timed for continuous harvest
|
||||
4. **Fall Preparation:** Winter crops started with adequate time before frost
|
||||
5. **Overwinter Crops:** Garlic and broad beans planted at optimal times for Zone 8
|
||||
6. **Season Extension:** Proper use of greenhouse starting in March
|
||||
|
||||
### Climate Trends Consideration
|
||||
|
||||
Given the warming trend (warmest March/April in 10+ years), the schedule is:
|
||||
- **Conservative:** Using April 20 last frost maintains safety margin
|
||||
- **Adaptable:** If last frost shifts to April 15, transplants on May 4 still appropriate
|
||||
- **Flexible:** Greenhouse starts in late March allow earlier transplanting if weather permits
|
||||
|
||||
### Risk Factors: MINIMAL
|
||||
|
||||
- **Late frost risk:** May 4 transplants have 2-week buffer
|
||||
- **Early frost risk:** Fall crops harvested before October 10 or are frost-tolerant
|
||||
- **Heat stress risk:** Tomatoes may need shade if extreme heat continues (>30°C)
|
||||
- **Overwintering risk:** Broad beans and garlic are appropriate for Zone 8 winters
|
||||
|
||||
### Recommendations
|
||||
|
||||
1. **Monitor:** Track actual 2026 last frost date to refine 2027 schedule
|
||||
2. **Row covers:** Keep available for May 1-15 in case of late cold snap
|
||||
3. **Shade cloth:** Consider for tomatoes if July temperatures exceed 32°C
|
||||
4. **Documentation:** Note harvest dates in 2026 to validate timing for 2027
|
||||
|
||||
---
|
||||
|
||||
## Climate Data Sources
|
||||
|
||||
All validations based on:
|
||||
- `climate-data-western-zealand.md` (Last frost: Apr 20, First frost: Oct 10)
|
||||
- [When to Plant Vegetables in Copenhagen - Garden.org](https://garden.org/apps/calendar/?q=copenhagen)
|
||||
- [Denmark Hardiness Zones - PlantMaps](https://www.plantmaps.com/interactive-denmark-hardiness-zone-map-celsius.php)
|
||||
- [Growing Vegetables In Denmark - GardeningTips.in](https://gardeningtips.in/growing-vegetables-in-denmark-planting-calendar)
|
||||
|
||||
---
|
||||
|
||||
*Validated: January 2026*
|
||||
*Next validation: January 2027 (after observing actual 2026 frost dates)*
|
||||
294
src/main.rs
Normal file
294
src/main.rs
Normal file
@@ -0,0 +1,294 @@
|
||||
use axum::{
|
||||
extract::State,
|
||||
response::{Html, IntoResponse, Json},
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tower_http::trace::TraceLayer;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct GardenEvent {
|
||||
#[serde(rename = "Date")]
|
||||
date: String,
|
||||
#[serde(rename = "Week")]
|
||||
week: String,
|
||||
#[serde(rename = "Activity")]
|
||||
activity: String,
|
||||
#[serde(rename = "Crop")]
|
||||
crop: String,
|
||||
#[serde(rename = "Variety")]
|
||||
variety: String,
|
||||
#[serde(rename = "Bed")]
|
||||
bed: String,
|
||||
#[serde(rename = "Quantity")]
|
||||
quantity: String,
|
||||
#[serde(rename = "Harvest Period")]
|
||||
harvest_period: String,
|
||||
#[serde(rename = "Notes")]
|
||||
notes: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
struct TimelineEvent {
|
||||
crop: String,
|
||||
variety: String,
|
||||
bed: String,
|
||||
phases: Vec<TimelinePhase>,
|
||||
harvest_period: String,
|
||||
quantity: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
struct TimelinePhase {
|
||||
activity: String,
|
||||
start_date: String,
|
||||
end_date: Option<String>,
|
||||
notes: String,
|
||||
}
|
||||
|
||||
struct AppState {
|
||||
events: Vec<GardenEvent>,
|
||||
timeline: Vec<TimelineEvent>,
|
||||
colors: HashMap<String, String>,
|
||||
}
|
||||
|
||||
fn parse_csv() -> Result<Vec<GardenEvent>, Box<dyn std::error::Error>> {
|
||||
let mut reader = csv::Reader::from_path("garden-plan-2026.csv")?;
|
||||
let mut events = Vec::new();
|
||||
|
||||
for result in reader.deserialize() {
|
||||
let event: GardenEvent = result?;
|
||||
events.push(event);
|
||||
}
|
||||
|
||||
Ok(events)
|
||||
}
|
||||
|
||||
fn parse_colors() -> Result<HashMap<String, String>, Box<dyn std::error::Error>> {
|
||||
let content = std::fs::read_to_string("crop-colors.json")?;
|
||||
let colors: HashMap<String, String> = serde_json::from_str(&content)?;
|
||||
Ok(colors)
|
||||
}
|
||||
|
||||
fn build_timeline(events: &[GardenEvent]) -> Vec<TimelineEvent> {
|
||||
let filtered_events: Vec<&GardenEvent> = events
|
||||
.iter()
|
||||
.filter(|e| e.activity != "Mulch" && e.activity != "Harvest")
|
||||
.collect();
|
||||
|
||||
let mut processed = vec![false; filtered_events.len()];
|
||||
let mut timeline = Vec::new();
|
||||
|
||||
for (i, event) in filtered_events.iter().enumerate() {
|
||||
if processed[i] {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut phases = Vec::new();
|
||||
let mut related_events = vec![*event];
|
||||
processed[i] = true;
|
||||
|
||||
// If this is a greenhouse sow, look for matching transplant
|
||||
if event.activity.contains("Greenhouse") {
|
||||
for (j, other) in filtered_events.iter().enumerate() {
|
||||
if !processed[j]
|
||||
&& other.crop == event.crop
|
||||
&& other.bed == event.bed
|
||||
&& other.activity.contains("Transplant")
|
||||
&& other.date > event.date
|
||||
{
|
||||
related_events.push(*other);
|
||||
processed[j] = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Determine if we have a harvest period
|
||||
let harvest_dates = if !event.harvest_period.is_empty() && event.harvest_period != "N/A" {
|
||||
Some(parse_harvest_period(&event.harvest_period))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Build phases from related events
|
||||
for (idx, evt) in related_events.iter().enumerate() {
|
||||
let end_date = if idx + 1 < related_events.len() {
|
||||
Some(related_events[idx + 1].date.clone())
|
||||
} else if let Some((harvest_start, _)) = &harvest_dates {
|
||||
// Last growing phase extends to harvest start
|
||||
Some(harvest_start.clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
phases.push(TimelinePhase {
|
||||
activity: evt.activity.clone(),
|
||||
start_date: evt.date.clone(),
|
||||
end_date,
|
||||
notes: evt.notes.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
// Add harvest period as a separate phase if available
|
||||
if let Some((harvest_start, harvest_end)) = harvest_dates {
|
||||
phases.push(TimelinePhase {
|
||||
activity: "Harvest".to_string(),
|
||||
start_date: harvest_start,
|
||||
end_date: Some(harvest_end),
|
||||
notes: String::new(),
|
||||
});
|
||||
}
|
||||
|
||||
timeline.push(TimelineEvent {
|
||||
crop: event.crop.clone(),
|
||||
variety: event.variety.clone(),
|
||||
bed: event.bed.clone(),
|
||||
phases,
|
||||
harvest_period: event.harvest_period.clone(),
|
||||
quantity: event.quantity.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
timeline
|
||||
}
|
||||
|
||||
fn parse_harvest_period(harvest_period: &str) -> (String, String) {
|
||||
// Parse harvest period strings like "July-October", "May 2026", etc.
|
||||
let parts: Vec<&str> = harvest_period.split('-').collect();
|
||||
if parts.len() == 2 {
|
||||
let start_month = parts[0].trim();
|
||||
let end_month = parts[1].trim();
|
||||
(month_to_date_start(start_month), month_to_date_end(end_month))
|
||||
} else {
|
||||
// Single month or month + year
|
||||
let date = month_to_date_start(harvest_period.trim());
|
||||
let end = month_to_date_end(harvest_period.trim());
|
||||
(date, end)
|
||||
}
|
||||
}
|
||||
|
||||
fn month_to_date_start(month_str: &str) -> String {
|
||||
let month_map = [
|
||||
("January", "01"),
|
||||
("February", "02"),
|
||||
("March", "03"),
|
||||
("April", "04"),
|
||||
("May", "05"),
|
||||
("June", "06"),
|
||||
("July", "07"),
|
||||
("August", "08"),
|
||||
("September", "09"),
|
||||
("October", "10"),
|
||||
("November", "11"),
|
||||
("December", "12"),
|
||||
];
|
||||
|
||||
for (name, num) in month_map {
|
||||
if month_str.contains(name) {
|
||||
let year = if month_str.contains("2027") {
|
||||
"2027"
|
||||
} else {
|
||||
"2026"
|
||||
};
|
||||
return format!("{}-{}-01", year, num);
|
||||
}
|
||||
}
|
||||
|
||||
"2026-01-01".to_string()
|
||||
}
|
||||
|
||||
fn month_to_date_end(month_str: &str) -> String {
|
||||
let month_map = [
|
||||
("January", "01"),
|
||||
("February", "02"),
|
||||
("March", "03"),
|
||||
("April", "04"),
|
||||
("May", "05"),
|
||||
("June", "06"),
|
||||
("July", "07"),
|
||||
("August", "08"),
|
||||
("September", "09"),
|
||||
("October", "10"),
|
||||
("November", "11"),
|
||||
("December", "12"),
|
||||
];
|
||||
|
||||
for (name, num) in month_map {
|
||||
if month_str.contains(name) {
|
||||
let year = if month_str.contains("2027") {
|
||||
"2027"
|
||||
} else {
|
||||
"2026"
|
||||
};
|
||||
return format!("{}-{}-28", year, num);
|
||||
}
|
||||
}
|
||||
|
||||
"2026-12-31".to_string()
|
||||
}
|
||||
|
||||
async fn root() -> impl IntoResponse {
|
||||
match tokio::fs::read_to_string("static/index.html").await {
|
||||
Ok(content) => Html(content),
|
||||
Err(e) => {
|
||||
eprintln!("Error reading index.html: {}", e);
|
||||
Html("<h1>Error loading page</h1>".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn api_events(State(state): State<Arc<AppState>>) -> Json<Vec<GardenEvent>> {
|
||||
Json(state.events.clone())
|
||||
}
|
||||
|
||||
async fn api_timeline(State(state): State<Arc<AppState>>) -> Json<Vec<TimelineEvent>> {
|
||||
Json(state.timeline.clone())
|
||||
}
|
||||
|
||||
async fn api_colors(State(state): State<Arc<AppState>>) -> Json<HashMap<String, String>> {
|
||||
Json(state.colors.clone())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter("garden_planner=debug,tower_http=debug")
|
||||
.init();
|
||||
|
||||
println!("🌱 Parsing garden plan CSV...");
|
||||
let events = parse_csv()?;
|
||||
println!("✓ Loaded {} events", events.len());
|
||||
|
||||
let timeline = build_timeline(&events);
|
||||
println!("✓ Built timeline with {} entries", timeline.len());
|
||||
|
||||
println!("🎨 Loading crop colors...");
|
||||
let colors = parse_colors()?;
|
||||
println!("✓ Loaded {} crop colors", colors.len());
|
||||
|
||||
let state = Arc::new(AppState {
|
||||
events,
|
||||
timeline,
|
||||
colors,
|
||||
});
|
||||
|
||||
let app = Router::new()
|
||||
.route("/", get(root))
|
||||
.route("/api/events", get(api_events))
|
||||
.route("/api/timeline", get(api_timeline))
|
||||
.route("/api/colors", get(api_colors))
|
||||
.layer(TraceLayer::new_for_http())
|
||||
.with_state(state);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await?;
|
||||
println!("\n🌻 Garden Planner running at http://127.0.0.1:3000");
|
||||
println!("Press Ctrl+C to stop\n");
|
||||
|
||||
axum::serve(listener, app).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
1404
static/index.html
Normal file
1404
static/index.html
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user