garden plan web app

This commit is contained in:
Jonas H
2026-01-27 10:05:53 +01:00
commit 10bf17c8a2
11 changed files with 2554 additions and 0 deletions

12
.gitignore vendored Normal file
View File

@@ -0,0 +1,12 @@
# Rust
/target/
Cargo.lock
**/*.rs.bk
*.pdb
# IDE
.vscode/
.idea/
*.swp
*.swo
*~

362
CLAUDE.md Normal file
View 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
View 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
View 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
View 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

View 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
View 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
View 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
1 Date Week Activity Crop Variety Bed Quantity Harvest Period Notes
2 2026-03-09 Week 10 Greenhouse Sow Tomatillo Toma Verde Middle 3 plants July-October Sow in trays with heating mat
3 2026-03-09 Week 10 Greenhouse Sow Cilantro Santo Middle 6 plants May-July Sow in trays with heating mat - bolts in summer heat
4 2026-03-09 Week 10 Greenhouse Sow Tomato Mixed varieties Greenhouse 4 plants June-October Sow in trays with heating mat
5 2026-04-06 Week 14 Direct Sow Arugula Rocket Middle 50 plants May-June Early spring crop - harvest before main crops
6 2026-04-06 Week 14 Direct Sow Arugula Rocket South 50 plants May-June Early spring crop - harvest before zucchini
7 2026-04-13 Week 15 Direct Sow Radicchio Palla Rossa South 12 plants May-early June Early harvest before zucchini spreads
8 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
9 2026-04-13 Week 15 Direct Sow Carrots Nantes North 50 plants July Single row - first succession
10 2026-05-04 Week 18 Transplant Tomatillo Toma Verde Middle 3 plants July-October Add supports immediately
11 2026-05-04 Week 18 Transplant Cilantro Santo Middle 6 plants May-July Around tomatillo bases - will bolt by July
12 2026-05-04 Week 18 Direct Plant Nasturtium Mixed varieties Middle 8 plants June-October Bed edges - edible flowers, pest deterrent
13 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
14 2026-05-04 Week 18 Direct Sow Zucchini Black Beauty South 3 seeds per spot June-September Thin to 2 plants
15 2026-03-09 Week 10 Greenhouse Sow Onion Sturon North 36 plants July-August Sow in trays with heating mat
16 2026-04-27 Week 17 Transplant Onion Sturon North 36 plants July-August Transplant 10cm apart in 2 rows
17 2026-04-27 Week 17 Transplant Tomato Mixed varieties Greenhouse 4 plants June-October Transplant to greenhouse after last frost risk
18 2026-05-18 Week 20 Direct Plant Winter Squash Uchiki Kuri South 2 plants September Will trail outside bed
19 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
20 2026-07-06 Week 27 Direct Sow Carrots Nantes Middle 40 plants October-November Plant where cilantro was - fall harvest
21 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
22 2026-07-27 Week 30 Direct Sow Radicchio Palla Rossa North 8 plants November-December Fall harvest in partial shade
23 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
24 2026-08-17 Week 33 Direct Sow Mustard Greens Red Giant South 20 plants October-March 2027 Purple-red leaves, spicy, cold-hardy
25 2026-08-17 Week 33 Direct Sow Tatsoi Rosette South 15 plants October-March 2027 Spoon-shaped leaves, mild mustard flavor, very cold-hardy
26 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
27 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
28 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
29 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
30 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
31 2026-11-09 Week 45 Mulch All beds Straw/leaves All Heavy layer N/A Winter protection
32 2027-06-21 Week 25 Harvest Garlic All South 40 bulbs N/A Cure for 2 weeks

View 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
View 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

File diff suppressed because it is too large Load Diff