commit 10bf17c8a2fc93a27035b99b2b396d1f100802f3 Author: Jonas H Date: Tue Jan 27 10:05:53 2026 +0100 garden plan web app diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a756273 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# Rust +/target/ +Cargo.lock +**/*.rs.bk +*.pdb + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..a6cf61b --- /dev/null +++ b/CLAUDE.md @@ -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, // Multiple phases per crop + harvest_period: String, +} + +struct TimelinePhase { + activity: String, // "Greenhouse Sow" + start_date: String, // "2025-03-24" + end_date: Option, // "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>) -> Json { + // 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. diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..aafc785 --- /dev/null +++ b/Cargo.toml @@ -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"] } diff --git a/README.md b/README.md new file mode 100644 index 0000000..28cb52f --- /dev/null +++ b/README.md @@ -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. diff --git a/bed-layout.md b/bed-layout.md new file mode 100644 index 0000000..20cef5d --- /dev/null +++ b/bed-layout.md @@ -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 diff --git a/climate-data-western-zealand.md b/climate-data-western-zealand.md new file mode 100644 index 0000000..0a6edac --- /dev/null +++ b/climate-data-western-zealand.md @@ -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* diff --git a/crop-colors.json b/crop-colors.json new file mode 100644 index 0000000..c06c700 --- /dev/null +++ b/crop-colors.json @@ -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" +} diff --git a/garden-plan-2026.csv b/garden-plan-2026.csv new file mode 100644 index 0000000..33039a0 --- /dev/null +++ b/garden-plan-2026.csv @@ -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 diff --git a/planting-schedule-validation.md b/planting-schedule-validation.md new file mode 100644 index 0000000..c20a542 --- /dev/null +++ b/planting-schedule-validation.md @@ -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)* diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..befa82b --- /dev/null +++ b/src/main.rs @@ -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, + harvest_period: String, + quantity: String, +} + +#[derive(Debug, Clone, Serialize)] +struct TimelinePhase { + activity: String, + start_date: String, + end_date: Option, + notes: String, +} + +struct AppState { + events: Vec, + timeline: Vec, + colors: HashMap, +} + +fn parse_csv() -> Result, Box> { + 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, Box> { + let content = std::fs::read_to_string("crop-colors.json")?; + let colors: HashMap = serde_json::from_str(&content)?; + Ok(colors) +} + +fn build_timeline(events: &[GardenEvent]) -> Vec { + 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("

Error loading page

".to_string()) + } + } +} + +async fn api_events(State(state): State>) -> Json> { + Json(state.events.clone()) +} + +async fn api_timeline(State(state): State>) -> Json> { + Json(state.timeline.clone()) +} + +async fn api_colors(State(state): State>) -> Json> { + Json(state.colors.clone()) +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + 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(()) +} diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..b765bda --- /dev/null +++ b/static/index.html @@ -0,0 +1,1404 @@ + + + + + + Garden Planner 2026-2027 + + + +
+

Garden Planner 2026-2027

+

Western Zealand, Denmark // Three Bed System

+ +
+ + +
+ +
+
+
+
+ Click any crop box to see growing details +
+
+
+ +
+
+
+
+ Timeline + +
+ +
+
+
+
+
+
+
+ March 2026 + March 1, 2026 + February 2027 +
+
+
+ +
+
+ +
+
▲ North Bed
+
300×50cm
+
+ + +
+ + +
+
■ Middle Bed
+
150×100cm
+
+ + +
+
◇ Plot North
+
150×100cm
+
+ + +
+ + +
+
+ + +
+
▼ South Bed
+
150×100cm
+
+ + +
+
◆ Plot South
+
150×100cm
+
+
+
+ +
+
+
+ + + +