diff --git a/.env.example b/.env.example index 1ab48b3..666788e 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,25 @@ +# LocalGreenChain Environment Variables + +# Plants.net API (optional) +PLANTS_NET_API_KEY=your_api_key_here + +# Tor Configuration +TOR_ENABLED=false +TOR_SOCKS_HOST=127.0.0.1 +TOR_SOCKS_PORT=9050 +TOR_CONTROL_PORT=9051 +TOR_HIDDEN_SERVICE_DIR=/var/lib/tor/localgreenchain + +# Privacy Settings +DEFAULT_PRIVACY_MODE=standard +ALLOW_ANONYMOUS_REGISTRATION=true +LOCATION_OBFUSCATION_DEFAULT=fuzzy + +# Application Settings +NODE_ENV=development +PORT=3001 + +# Legacy Drupal Settings (for backward compatibility) NEXT_PUBLIC_DRUPAL_BASE_URL=http://localhost:8080 NEXT_IMAGE_DOMAIN=localhost DRUPAL_CLIENT_ID=52ce1a10-bf5c-4c81-8edf-eea3af95da84 diff --git a/.gitignore b/.gitignore index 3aa970b..a0db81a 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* +bun-debug.log* # local env files .env.local diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..fd8bdb5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,40 @@ +# Dockerfile for LocalGreenChain +# Uses Bun for fast builds and runtime + +FROM oven/bun:1 as base + +WORKDIR /app + +# Install dependencies +COPY package.json bun.lockb* ./ +RUN bun install --frozen-lockfile + +# Copy application code +COPY . . + +# Build Next.js application +RUN bun run build + +# Production stage +FROM oven/bun:1-slim as production + +WORKDIR /app + +# Copy dependencies and build output +COPY --from=base /app/node_modules ./node_modules +COPY --from=base /app/.next ./.next +COPY --from=base /app/public ./public +COPY --from=base /app/package.json ./package.json +COPY --from=base /app/next.config.js ./next.config.js + +# Create data directory +RUN mkdir -p /app/data + +# Expose port +EXPOSE 3001 + +# Set environment to production +ENV NODE_ENV=production + +# Run the application +CMD ["bun", "run", "start"] diff --git a/ENVIRONMENTAL_TRACKING.md b/ENVIRONMENTAL_TRACKING.md new file mode 100644 index 0000000..b2c4614 --- /dev/null +++ b/ENVIRONMENTAL_TRACKING.md @@ -0,0 +1,437 @@ +# 🌍 Environmental Tracking Guide + +**Track soil, climate, nutrients, and growing conditions to optimize plant health and learn from successful growers.** + +## Overview + +LocalGreenChain's environmental tracking system allows you to record detailed information about where and how your plants are grown. This data helps you: + +- **Understand** what conditions make plants thrive +- **Compare** your setup with successful growers +- **Optimize** growing conditions based on data +- **Learn** from the collective experience +- **Replicate** successful growing environments +- **Troubleshoot** plant health issues + +## What Can You Track? + +### 🌱 Soil Composition +- **Soil type**: Clay, sand, silt, loam, peat, chalk, or custom +- **pH level**: 0-14 scale (most plants prefer 6.0-7.5) +- **Texture**: Heavy, medium, or light +- **Drainage**: Poor, moderate, good, or excellent +- **Organic matter**: Percentage (0-100%) +- **Composition**: Clay/sand/silt percentages +- **Amendments**: Compost, manure, perlite, vermiculite, etc. + +**Why it matters**: Soil is the foundation of plant health. pH affects nutrient availability, drainage prevents root rot, and organic matter improves structure. + +### πŸ§ͺ Nutrients & Fertilization +- **NPK values**: Nitrogen, Phosphorus, Potassium percentages +- **Secondary nutrients**: Calcium, magnesium, sulfur +- **Micronutrients**: Iron, manganese, zinc, copper, boron, molybdenum +- **EC/TDS**: Electrical conductivity and total dissolved solids +- **Fertilizer schedule**: Type, amount, frequency, NPK ratios +- **Application history**: Track what you've added and when + +**Why it matters**: Proper nutrition is crucial for growth. Too little causes deficiencies, too much can burn plants. Track what works! + +### β˜€οΈ Lighting Conditions +- **Light type**: Natural, artificial, or mixed +- **Natural light**: + - Exposure: Full sun, partial sun, partial shade, full shade + - Hours per day: 0-24 + - Direction: North, south, east, west + - Quality: Direct, filtered, dappled, indirect +- **Artificial light**: + - Type: LED, fluorescent, HPS, MH, etc. + - Spectrum: Full spectrum, color temperature (K) + - Wattage and distance from plant + - Hours per day +- **Advanced metrics**: PPFD (ΞΌmol/mΒ²/s), DLI (mol/mΒ²/day) + +**Why it matters**: Light is energy. Insufficient light causes leggy growth, too much can burn leaves. Track optimal levels for your species. + +### 🌑️ Climate Conditions +- **Temperature**: Day/night temps, min/max ranges (Celsius) +- **Humidity**: Average, min, max percentages +- **Air circulation**: None, minimal, moderate, strong +- **Ventilation**: Poor, adequate, good, excellent +- **CO2 levels**: PPM (ambient ~400, enhanced ~1200-1500) +- **Season**: Spring, summer, fall, winter +- **Hardiness zone**: USDA zones (e.g., "9b", "10a") + +**Why it matters**: Plants have optimal temperature and humidity ranges. Extremes cause stress or death. Good airflow prevents disease. + +### πŸ“ Growing Location +- **Location type**: Indoor, outdoor, greenhouse, polytunnel, shade house, window, balcony +- **Indoor details**: Room type (bedroom, basement, grow tent) +- **Outdoor details**: + - Exposure to elements: Protected, semi-protected, exposed + - Elevation: Meters above sea level + - Slope: Flat, gentle, moderate, steep + - Aspect: Slope direction (affects sun exposure) + +**Why it matters**: Location affects everything - temperature, humidity, light, pests. Indoor vs outdoor requires different care. + +### πŸͺ΄ Container Information +- **Type**: Pot, raised bed, ground, hydroponic, aeroponic, fabric pot, etc. +- **Material**: Plastic, terracotta, ceramic, fabric, wood, metal +- **Size**: Gallons, diameter, or dimensions +- **Volume**: Liters +- **Depth**: Centimeters +- **Drainage**: Yes/no, number of holes + +**Why it matters**: Container size affects root development and watering frequency. Drainage is critical - no drainage = dead plant. + +### πŸ’§ Watering Schedule +- **Method**: Hand water, drip, soaker hose, sprinkler, self-watering, hydroponic +- **Frequency**: Daily, every 2-3 days, weekly, etc. +- **Amount**: Liters, "until runoff", inches +- **Water source**: Tap, well, rain, filtered, distilled, RO +- **Water quality**: + - pH: Affects nutrient availability + - TDS: Total dissolved solids (PPM) + - Chlorine: Yes, no, or filtered + +**Why it matters**: Over/underwatering kills more plants than anything. Water quality affects long-term soil health. + +### 🌿 Surrounding Environment +- **Companion plants**: Other species growing nearby +- **Nearby features**: Trees, structures, walls, fences +- **Ground cover**: Mulch, grass, bare soil, gravel +- **Wildlife**: + - Pollinators: Bees, butterflies, hummingbirds + - Beneficial insects: Ladybugs, lacewings + - Pests: Name, severity, treatment, date observed + - Diseases: Name, symptoms, severity, treatment +- **Microclimate factors**: + - Wind exposure: Sheltered, moderate, exposed, windy + - Frost pockets: Yes/no + - Heat traps: Yes/no +- **Ecosystem type**: Urban, suburban, rural, forest, desert, coastal, mountain, tropical + +**Why it matters**: Companion planting improves growth. Pest/disease tracking helps prevention. Microclimates explain unexpected results. + +### πŸ“ˆ Growth Metrics +- **Measurements**: Height, width, leaf count, flower count, fruit count +- **Health score**: 0-100 rating +- **Vigor**: Poor, fair, good, excellent +- **Tracking**: Photos and notes over time + +**Why it matters**: Measure growth to evaluate if conditions are optimal. Data drives improvement. + +## API Endpoints + +### Get Recommendations +```http +GET /api/environment/recommendations?plantId=xyz +``` + +Returns personalized recommendations based on your plant's environment: +- **Critical issues**: Must fix immediately (no drainage, extreme temps) +- **High priority**: Important for health (pH problems, insufficient light) +- **Medium priority**: Optimization opportunities (humidity, amendments) +- **Low priority**: Minor improvements (water quality tweaks) +- **Environmental health score**: 0-100 overall rating + +**Example response**: +```json +{ + "environmentalHealth": 75, + "recommendations": [ + { + "category": "soil", + "priority": "high", + "issue": "Low soil pH (5.2)", + "recommendation": "Add lime to raise pH. Most plants prefer 6.0-7.0", + "impact": "Acidic soil can lock out nutrients and harm roots" + }, + { + "category": "climate", + "priority": "medium", + "issue": "Low humidity (25%)", + "recommendation": "Mist plants, use humidity tray, or group plants together", + "impact": "Low humidity can cause leaf tip browning and stress" + } + ] +} +``` + +### Find Similar Environments +```http +GET /api/environment/similar?plantId=xyz&minScore=70 +``` + +Finds other plants growing in similar conditions: +- Compare your environment with successful growers +- Learn from plants that thrive in similar setups +- Get ideas for what works in your conditions + +**Similarity factors**: +- Location type (indoor/outdoor/greenhouse) +- Soil type and pH +- Temperature and humidity ranges +- Lighting conditions +- Water source and quality +- Container type + +**Scores**: +- **90-100**: Nearly identical conditions +- **75-89**: Very similar - expect similar results +- **60-74**: Moderately similar +- **40-59**: Some similarities +- **0-39**: Very different + +### Compare Two Plants +```http +GET /api/environment/compare?plant1=xyz&plant2=abc +``` + +Direct comparison of two specific plants: +- See exactly what's similar and different +- Learn why one plant might be thriving while another struggles +- Identify which factors to adjust + +### Growth Correlation Analysis +```http +GET /api/environment/analysis?species=tomato +``` + +Network-wide analysis to identify optimal conditions: +- What pH do successful plants average? +- What temperature range works best? +- Which lighting type has best results? +- Success rates by location type, soil type, etc. + +**Filter by species** to get species-specific insights! + +## How Recommendations Work + +The system analyzes your environment and generates priority-based recommendations: + +### Soil Recommendations +- **pH < 5.5**: Add lime (priority: high) +- **pH > 7.5**: Add sulfur (priority: high) +- **Organic matter < 3%**: Add compost (priority: medium) +- **Poor drainage**: Add perlite/sand or raise beds (priority: high) + +### Climate Recommendations +- **Temp > 35Β°C**: Provide shade, increase watering (priority: high) +- **Temp < 10Β°C**: Frost protection needed (priority: high) +- **Humidity < 30%**: Mist, use humidity trays (priority: medium) +- **Humidity > 80%**: Improve airflow (priority: medium) + +### Light Recommendations +- **< 4 hours/day**: Move to brighter spot or add grow lights (priority: high) +- **Natural light direction**: Optimize plant placement + +### Nutrient Recommendations +- **Low NPK**: Apply balanced fertilizer (priority: medium) +- **High EC/TDS**: Reduce feeding or flush (priority: medium) + +### Water Recommendations +- **pH > 7.5**: Consider pH adjustment or rainwater (priority: low) +- **Chlorinated**: Let sit 24h or filter (priority: low) + +### Container Recommendations +- **No drainage**: Add holes IMMEDIATELY (priority: CRITICAL) +- **Too small**: Repot to larger container (priority: medium) + +## Environmental Health Scoring + +The system calculates a 0-100 health score based on: + +**Perfect score (100)**: Everything optimal +- Soil pH 6.0-7.0 +- Good drainage +- Organic matter > 3% +- Temps 15-30Β°C +- Humidity 40-70% +- Adequate light (6+ hours or proper artificial) +- Proper container with drainage +- Good water quality + +**Deductions**: +- **-15 points**: No container drainage (CRITICAL) +- **-15 points**: Insufficient light (< 4 hours) +- **-10 points**: Extreme pH (< 5.5 or > 7.5) +- **-10 points**: Extreme temperature (< 10Β°C or > 35Β°C) +- **-10 points**: Poor soil drainage +- **-5 points**: Low organic matter +- **-5 points**: Poor humidity (< 30% or > 80%) +- **-5 points**: No airflow +- **-5 points**: Chlorinated water +- **-5 points**: High water pH + +**Score interpretation**: +- **90-100**: Excellent conditions +- **75-89**: Good conditions, minor improvements possible +- **60-74**: Adequate, several opportunities for optimization +- **40-59**: Suboptimal, multiple issues to address +- **0-39**: Poor conditions, plant likely struggling + +## Best Practices + +### When to Track Environmental Data + +**Always track**: +- When registering a new plant +- After making changes (repotting, moving, fertilizing) +- When troubleshooting problems +- Before/after cloning (compare parent and clone environments) + +**Update regularly**: +- Monthly for stable setups +- Weekly during active growth +- Daily when troubleshooting or experimenting + +### What to Measure + +**Essentials** (track these at minimum): +- Soil type and pH +- Location type (indoor/outdoor) +- Light exposure (hours/day) +- Temperature range +- Watering frequency + +**Recommended** (for better insights): +- Soil drainage and amendments +- Humidity +- Container details +- Nutrient applications +- Growth measurements + +**Advanced** (for serious growers): +- Full NPK and micronutrients +- EC/TDS measurements +- PPFD/DLI for lighting +- CO2 levels +- Pest/disease tracking +- Companion plants + +### Tips for Accurate Tracking + +1. **Measure, don't guess**: Use pH meters, thermometers, soil probes +2. **Track over time**: Conditions change - update seasonally +3. **Note anomalies**: Heatwaves, cold snaps, unusual events +4. **Photo documentation**: Pictures reveal patterns you might miss +5. **Consistent timing**: Measure at the same time of day +6. **Calibrate instruments**: Test meters regularly +7. **Record failures too**: Learning what doesn't work is valuable + +## Use Cases + +### For Home Growers +"My tomato plant is struggling. Let me check recommendations..." +- System identifies low soil pH and insufficient light +- Suggests adding lime and moving to sunnier spot +- Compares with successful tomato growers nearby +- Shows that similar plants thrive with 8+ hours sun and pH 6.5 + +### For Propagation +"I want to clone my best basil plant..." +- Records current environment that's producing healthy growth +- When giving clone to friend, shares environmental data +- Friend replicates conditions for best success +- Tracks if clone thrives in new environment + +### For Troubleshooting +"Why are my pepper leaves yellowing?" +- Checks pH: 7.8 (too high) +- System recommends sulfur to lower pH +- Compares with healthy pepper plants: pH 6.0-6.5 +- Adds sulfur, yellowing stops within 2 weeks + +### For Learning +"What conditions work best for lavender?" +- Searches network for successful lavender plants +- Finds they all have: pH 6.5-7.5, full sun, excellent drainage +- Notices growers with poor drainage have struggling plants +- Adjusts setup before planting + +### For Research +"Does companion planting really work?" +- Analyzes all plants with tomato-basil companions +- Compares health scores with lone tomatoes +- Statistical analysis shows 15% better vigor +- Data supports traditional growing wisdom + +## Privacy Considerations + +Environmental data can be: +- **Shared openly**: Help others learn from your setup +- **Kept private**: Track for yourself without broadcasting +- **Anonymous**: Share conditions without revealing location/identity + +Use privacy settings to control: +- Whether environment data is visible to others +- Whether it appears in similarity searches +- Whether it's included in analysis +- Location obfuscation (see TOR_SETUP.md) + +## Advanced Features + +### Automated Recommendations +As you add more plants: +- System learns what works in YOUR specific conditions +- Personalized recommendations based on your history +- "You've had success with these pH levels..." + +### Seasonal Tracking +- Record how conditions change through the year +- Plan planting schedules based on historical data +- Anticipate challenges (summer heat, winter cold) + +### Clone Comparison +- Automatically compare clone environments with parent +- See if environmental differences affect growth +- Optimize clone success rates + +### Community Insights +- "Plants in your area typically use X soil type" +- "Growers in similar climates report best results with Y" +- "This species thrives in conditions like yours" + +## Future Enhancements + +Coming soon: +- **Photo-based tracking**: Upload photos, AI analyzes plant health +- **Sensor integration**: Automatic data from soil sensors, weather stations +- **Mobile app**: Track on-the-go with smartphone +- **Notifications**: "Time to water", "pH out of range" +- **Marketplace integration**: Buy recommended products +- **Expert consultations**: Connect with master growers + +## Troubleshooting + +**"My recommendations don't make sense"** +- Check that measurements are accurate +- Ensure units are correct (Celsius not Fahrenheit) +- Verify pH probe is calibrated +- Update environmental data if conditions changed + +**"No similar plants found"** +- Lower the minimum similarity score +- Your setup might be unique! +- Add more environmental details for better matching +- Check that other plants have environmental data + +**"Analysis shows low success rate"** +- System is helping you identify what to improve +- Follow recommendations to optimize +- Learn from successful growers +- Don't be discouraged - this is how you learn! + +## Getting Help + +- **Issues**: GitHub Issues for bugs +- **Questions**: Community forum for growing advice +- **Contributions**: PRs welcome for new features +- **Data privacy**: See PRIVACY.md + +--- + +**Remember**: The goal is learning and improvement, not perfection. Every data point helps you become a better grower! + +Happy tracking! πŸŒ±πŸ“Š diff --git a/README.md b/README.md index b63cb32..128169b 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,412 @@ -# example-marketing +# 🌱 LocalGreenChain -An example marketing site built using Drupal + JSON:API. +**A blockchain for plants that preserves lineage across clones and seeds, connecting growers worldwide.** -Pages are built from the Landing page node type and paragraphs sourced from `/drupal`. +LocalGreenChain is a revolutionary plant tracking system that uses blockchain technology to create an immutable record of plant lineage. Track every clone, cutting, seed, and offspring throughout multiple generations while connecting with nearby growers who share your passion for plants. -See https://demo.next-drupal.org +## ✨ Features -## License +### 🌳 **Lineage Tracking** +- Track every plant from its origin through unlimited generations +- Record propagation methods (clone, seed, cutting, division, grafting) +- Build comprehensive family trees showing ancestors, descendants, and siblings +- Immutable blockchain ensures lineage data can never be lost or altered -Licensed under the [MIT license](https://github.com/chapter-three/next-drupal/blob/master/LICENSE). +### πŸ“ **Geographic Connections** +- Find plants and growers near your location +- Discover plant clusters and hotspots in your area +- Connect with people who have plants from the same lineage +- Build local plant-sharing communities + +### πŸ”— **Blockchain Security** +- Proof-of-work consensus ensures data integrity +- Each plant registration is a permanent block in the chain +- Cryptographic hashing prevents tampering +- Distributed architecture for reliability + +### 🌍 **Global Network** +- Integration with plants.net API for plant identification +- Track how your plants spread across the world +- See global distribution statistics by species and location +- Join a worldwide community of plant enthusiasts + +### 🎨 **User-Friendly Interface** +- Beautiful, intuitive web interface +- Easy plant registration with geolocation support +- Visual lineage explorer +- Network statistics dashboard +- Mobile-responsive design + +### πŸ”’ **Privacy & Anonymity (Tor Integration)** +- Anonymous plant registration with zero personal information +- Location obfuscation (fuzzy, city, country, or hidden) +- Tor hidden service support for .onion access +- SOCKS proxy integration for outbound Tor connections +- Privacy-preserving geolocation (share area, not exact location) +- Pseudonymous identities and wallet addresses +- End-to-end encrypted communications + +### 🌍 **Environmental Tracking** +- Comprehensive soil composition tracking (type, pH, drainage, amendments) +- Complete nutrient monitoring (NPK, micronutrients, EC/TDS) +- Climate conditions (temperature, humidity, airflow, CO2) +- Lighting analysis (natural/artificial, hours, spectrum, PPFD/DLI) +- Watering schedules and water quality monitoring +- Container and growing location details +- Pest, disease, and companion plant tracking +- Growth metrics and health scoring +- Environmental comparison between plants +- Personalized growing recommendations +- Success correlation analysis across network + +**πŸ“˜ Full Guide**: See [ENVIRONMENTAL_TRACKING.md](./ENVIRONMENTAL_TRACKING.md) + +## πŸš€ Quick Start + +### Prerequisites +- **Bun 1.0+** (https://bun.sh) - A fast JavaScript runtime and package manager +- Basic knowledge of plant propagation (optional but helpful!) + +### Why Bun? +LocalGreenChain uses Bun for faster installation, builds, and runtime performance: +- **3x faster** package installation compared to npm +- **Native TypeScript support** without configuration +- **Built-in bundler** for optimal production builds +- **Compatible** with npm packages + +### Installing Bun +If you don't have Bun installed: + +```bash +# macOS/Linux/WSL +curl -fsSL https://bun.sh/install | bash + +# Or using npm +npm install -g bun +``` + +### Installation + +1. **Clone the repository** +```bash +git clone https://github.com/yourusername/localgreenchain.git +cd localgreenchain +``` + +2. **Install dependencies** +```bash +bun install +``` + +3. **Set up environment variables** (optional) +```bash +cp .env.example .env +``` + +Edit `.env` and add your plants.net API key if you have one: +``` +PLANTS_NET_API_KEY=your_api_key_here +``` + +4. **Run the development server** +```bash +bun run dev +``` + +5. **Open your browser** +Navigate to [http://localhost:3001](http://localhost:3001) + +### πŸ§… Tor Setup (Optional - For Privacy) + +For anonymous plant sharing and privacy protection: + +```bash +# Quick start with Docker Compose +docker-compose -f docker-compose.tor.yml up -d + +# Or manual setup +# 1. Install Tor +sudo apt install tor + +# 2. Configure Tor (copy example config) +sudo cp tor/torrc.example /etc/tor/torrc + +# 3. Start Tor +sudo systemctl start tor + +# 4. Enable Tor in LocalGreenChain +# Edit .env and set: TOR_ENABLED=true + +# 5. Start LocalGreenChain +bun run start + +# 6. Get your .onion address +sudo cat /var/lib/tor/localgreenchain/hostname +``` + +**πŸ“˜ Full Guide**: See [TOR_SETUP.md](./TOR_SETUP.md) for complete instructions + +## πŸ“– How It Works + +### The Blockchain + +LocalGreenChain uses a custom blockchain implementation where: + +- **Each block** represents a plant registration or update +- **Proof-of-work** mining ensures data integrity +- **Cryptographic hashing** links blocks together +- **Genesis block** initializes the chain + +### Plant Lineage System + +``` +Original Plant (Generation 0) + β”œβ”€β”€ Clone 1 (Generation 1) + β”‚ β”œβ”€β”€ Seed 1-1 (Generation 2) + β”‚ └── Cutting 1-2 (Generation 2) + β”œβ”€β”€ Clone 2 (Generation 1) + └── Seed 3 (Generation 1) + └── Clone 3-1 (Generation 2) +``` + +Every plant maintains: +- **Parent reference**: Which plant it came from +- **Children array**: All offspring created from it +- **Generation number**: How many steps from the original +- **Propagation type**: How it was created + +### Geographic Discovery + +The system calculates distances between plants using the Haversine formula to: +- Find plants within a specified radius +- Cluster nearby plants to show hotspots +- Suggest connections based on proximity and species + +## 🎯 Use Cases + +### For Home Gardeners +- Track your plant collection and propagation success +- Share clones with friends while maintaining the lineage record +- Find local gardeners with similar plants for trading + +### For Plant Nurseries +- Provide customers with verified lineage information +- Track all plants sold and their offspring +- Build trust through transparent provenance + +### For Conservation Projects +- Document rare plant propagation efforts +- Track genetic diversity across populations +- Coordinate distribution of endangered species + +### For Community Gardens +- Manage shared plant collections +- Track plant donations and their spread +- Build local food security networks + +### For Research +- Study plant propagation success rates +- Track genetic drift across generations +- Analyze geographic distribution patterns + +## πŸ› οΈ API Reference + +### Register a Plant +```http +POST /api/plants/register +Content-Type: application/json + +{ + "id": "plant-unique-id", + "commonName": "Tomato", + "scientificName": "Solanum lycopersicum", + "location": { + "latitude": 40.7128, + "longitude": -74.0060, + "city": "New York", + "country": "USA" + }, + "owner": { + "id": "user-id", + "name": "John Doe", + "email": "john@example.com" + } +} +``` + +### Clone a Plant +```http +POST /api/plants/clone +Content-Type: application/json + +{ + "parentPlantId": "parent-plant-id", + "propagationType": "clone", + "newPlant": { + "location": {...}, + "owner": {...} + } +} +``` + +### Find Nearby Plants +```http +GET /api/plants/nearby?lat=40.7128&lon=-74.0060&radius=50 +``` + +### Get Plant Lineage +```http +GET /api/plants/lineage/plant-id +``` + +### Search Plants +```http +GET /api/plants/search?q=tomato&type=species +``` + +### Get Network Statistics +```http +GET /api/plants/network +``` + +## πŸ—οΈ Architecture + +### Tech Stack +- **Runtime**: Bun (fast JavaScript runtime and package manager) +- **Frontend**: Next.js, React, TypeScript, Tailwind CSS +- **Blockchain**: Custom implementation with proof-of-work +- **Storage**: JSON file-based (production can use database) +- **APIs**: RESTful endpoints +- **Geolocation**: Haversine distance calculation, OpenStreetMap geocoding + +### Project Structure +``` +localgreenchain/ +β”œβ”€β”€ lib/ +β”‚ β”œβ”€β”€ blockchain/ +β”‚ β”‚ β”œβ”€β”€ types.ts # Type definitions +β”‚ β”‚ β”œβ”€β”€ PlantBlock.ts # Block implementation +β”‚ β”‚ β”œβ”€β”€ PlantChain.ts # Blockchain logic +β”‚ β”‚ └── manager.ts # Blockchain singleton +β”‚ └── services/ +β”‚ β”œβ”€β”€ plantsnet.ts # Plants.net API integration +β”‚ └── geolocation.ts # Location services +β”œβ”€β”€ pages/ +β”‚ β”œβ”€β”€ index.tsx # Home page +β”‚ β”œβ”€β”€ plants/ +β”‚ β”‚ β”œβ”€β”€ register.tsx # Register new plant +β”‚ β”‚ β”œβ”€β”€ clone.tsx # Clone existing plant +β”‚ β”‚ β”œβ”€β”€ explore.tsx # Browse network +β”‚ β”‚ └── [id].tsx # Plant details +β”‚ └── api/ +β”‚ └── plants/ # API endpoints +└── data/ + └── plantchain.json # Blockchain storage +``` + +## 🌐 Scaling for Mass Adoption + +### Current Implementation +- **Single-server blockchain**: Good for 10,000s of plants +- **File-based storage**: Simple and portable +- **Client-side rendering**: Fast initial setup + +### Scaling Recommendations + +#### For 100,000+ Plants +1. **Switch to database**: PostgreSQL or MongoDB +2. **Add caching**: Redis for frequently accessed data +3. **Optimize mining**: Reduce difficulty or use alternative consensus + +#### For 1,000,000+ Plants +1. **Distributed blockchain**: Multiple nodes with consensus +2. **Sharding**: Geographic or species-based partitioning +3. **CDN**: Serve static content globally +4. **API rate limiting**: Protect against abuse + +#### For Global Scale +1. **Blockchain network**: Peer-to-peer node system +2. **Mobile apps**: Native iOS/Android applications +3. **Offline support**: Sync when connection available +4. **Federation**: Regional chains with cross-chain references + +## 🀝 Contributing + +We welcome contributions! Here's how you can help: + +1. **Add features**: New propagation types, plant care tracking, etc. +2. **Improve UI**: Design enhancements, accessibility +3. **Optimize blockchain**: Better consensus algorithms +4. **Mobile app**: React Native implementation +5. **Documentation**: Tutorials, translations + +### Development Process +```bash +# Create a feature branch +git checkout -b feature/amazing-feature + +# Make your changes +# ... + +# Run tests (when available) +bun test + +# Commit with clear message +git commit -m "Add amazing feature" + +# Push and create pull request +git push origin feature/amazing-feature +``` + +## πŸ“œ License + +MIT License - see [LICENSE](LICENSE) file for details + +## πŸ™ Acknowledgments + +- Inspired by the global plant-sharing community +- Built with Next.js and React +- Blockchain concept adapted for plant tracking +- Geocoding powered by OpenStreetMap Nominatim + +## πŸ“ž Support + +- **Issues**: [GitHub Issues](https://github.com/yourusername/localgreenchain/issues) +- **Discussions**: [GitHub Discussions](https://github.com/yourusername/localgreenchain/discussions) +- **Email**: support@localgreenchain.org + +## πŸ—ΊοΈ Roadmap + +### Phase 1: Core Features βœ… +- [x] Blockchain implementation +- [x] Plant registration and cloning +- [x] Lineage tracking +- [x] Geographic search +- [x] Web interface + +### Phase 2: Enhanced Features (Q2 2025) +- [ ] User authentication +- [ ] Plant care reminders +- [ ] Photo uploads +- [ ] QR code plant tags +- [ ] Mobile app (React Native) + +### Phase 3: Community Features (Q3 2025) +- [ ] Trading marketplace +- [ ] Events and meetups +- [ ] Expert advice forum +- [ ] Plant care wiki +- [ ] Achievements and badges + +### Phase 4: Advanced Features (Q4 2025) +- [ ] DNA/genetic tracking integration +- [ ] AI plant identification +- [ ] Environmental impact tracking +- [ ] Carbon sequestration calculations +- [ ] Partnership with botanical gardens + +--- + +**Built with πŸ’š for the planet** + +Start your plant lineage journey today! 🌱 diff --git a/TOR_SETUP.md b/TOR_SETUP.md new file mode 100644 index 0000000..de5c753 --- /dev/null +++ b/TOR_SETUP.md @@ -0,0 +1,455 @@ +# πŸ§… Tor Integration Guide for LocalGreenChain + +This guide explains how to set up LocalGreenChain with Tor for maximum privacy and anonymity when sharing plant lineages. + +## Why Use Tor with LocalGreenChain? + +### Privacy Benefits +- **Anonymous Plant Registration**: Register plants without revealing your identity +- **Location Privacy**: Share general area without exposing exact home address +- **IP Protection**: Hide your IP address from other users and the network +- **Censorship Resistance**: Access the network even in restrictive environments +- **Secure Sharing**: Share plant clones with trusted community members anonymously + +### Use Cases +- **Privacy-Conscious Growers**: Don't want to advertise exact plant locations +- **Sensitive Species**: Medicinal plants, rare species, or regulated botanicals +- **Community Building**: Connect with local growers without revealing identity +- **Research**: Anonymous data collection for botanical research +- **Security**: Protect against unwanted visitors or theft + +## Table of Contents +1. [Quick Start](#quick-start) +2. [Installation Methods](#installation-methods) +3. [Configuration](#configuration) +4. [Running as Hidden Service](#running-as-hidden-service) +5. [Using Tor Browser](#using-tor-browser) +6. [Privacy Best Practices](#privacy-best-practices) +7. [Troubleshooting](#troubleshooting) + +--- + +## Quick Start + +### Option 1: Docker Compose (Recommended) + +The easiest way to run LocalGreenChain with Tor: + +```bash +# Copy environment variables +cp .env.example .env + +# Edit .env and enable Tor +nano .env +# Set: TOR_ENABLED=true + +# Start with Docker Compose +docker-compose -f docker-compose.tor.yml up -d + +# Check if Tor is running +docker logs localgreenchain-tor + +# Get your onion address +docker exec localgreenchain-tor cat /var/lib/tor/hidden_service/hostname +``` + +Your LocalGreenChain instance is now accessible via: +- Local: http://localhost:3001 +- Onion: http://[your-address].onion (share this!) + +### Option 2: Manual Installation + +1. **Install Tor** +2. **Configure Tor for LocalGreenChain** +3. **Start LocalGreenChain with Tor enabled** + +--- + +## Installation Methods + +### Linux (Debian/Ubuntu) + +```bash +# Install Tor +sudo apt update +sudo apt install tor + +# Configure Tor for LocalGreenChain +sudo cp tor/torrc.example /etc/tor/torrc + +# Edit configuration +sudo nano /etc/tor/torrc + +# Create hidden service directory +sudo mkdir -p /var/lib/tor/localgreenchain +sudo chown -R debian-tor:debian-tor /var/lib/tor/localgreenchain +sudo chmod 700 /var/lib/tor/localgreenchain + +# Start Tor +sudo systemctl start tor +sudo systemctl enable tor + +# Check status +sudo systemctl status tor + +# Get your onion address (wait ~1 minute for generation) +sudo cat /var/lib/tor/localgreenchain/hostname +``` + +### macOS + +```bash +# Install Tor via Homebrew +brew install tor + +# Copy configuration +cp tor/torrc.example /usr/local/etc/tor/torrc + +# Edit configuration +nano /usr/local/etc/tor/torrc + +# Create hidden service directory +mkdir -p ~/Library/Application\ Support/tor/localgreenchain +chmod 700 ~/Library/Application\ Support/tor/localgreenchain + +# Update torrc with your path +# HiddenServiceDir ~/Library/Application Support/tor/localgreenchain + +# Start Tor +brew services start tor + +# Get your onion address +cat ~/Library/Application\ Support/tor/localgreenchain/hostname +``` + +### Windows (WSL) + +```bash +# Install WSL if not already installed +# Then follow Linux instructions above + +# Or use Tor Expert Bundle +# Download from: https://www.torproject.org/download/tor/ +``` + +--- + +## Configuration + +### Environment Variables + +Edit `.env` file: + +```bash +# Enable Tor +TOR_ENABLED=true + +# Tor SOCKS proxy (default) +TOR_SOCKS_HOST=127.0.0.1 +TOR_SOCKS_PORT=9050 + +# Tor control port +TOR_CONTROL_PORT=9051 + +# Hidden service directory +TOR_HIDDEN_SERVICE_DIR=/var/lib/tor/localgreenchain + +# Privacy defaults +DEFAULT_PRIVACY_MODE=standard +ALLOW_ANONYMOUS_REGISTRATION=true +LOCATION_OBFUSCATION_DEFAULT=fuzzy +``` + +### Tor Configuration (torrc) + +Minimal configuration in `/etc/tor/torrc`: + +``` +# SOCKS proxy +SocksPort 9050 + +# Hidden Service for LocalGreenChain +HiddenServiceDir /var/lib/tor/localgreenchain/ +HiddenServicePort 80 127.0.0.1:3001 + +# Optional: Multiple ports +# HiddenServicePort 443 127.0.0.1:3001 + +# Logging +Log notice file /var/log/tor/notices.log + +# Privacy settings +IsolateDestAddr 1 +IsolateDestPort 1 +``` + +--- + +## Running as Hidden Service + +### Start LocalGreenChain + +```bash +# Install dependencies +bun install + +# Start in production mode +bun run build +bun run start + +# Or development mode +bun run dev +``` + +### Verify Hidden Service + +```bash +# Check if Tor created keys +ls -la /var/lib/tor/localgreenchain/ + +# Should see: +# - hostname (your .onion address) +# - hs_ed25519_public_key +# - hs_ed25519_secret_key + +# Get your onion address +cat /var/lib/tor/localgreenchain/hostname +``` + +### Share Your Onion Address + +Your `.onion` address looks like: +``` +abc123def456ghi789.onion +``` + +Share this with trusted community members to allow anonymous access! + +--- + +## Using Tor Browser + +### As a User (Accessing LocalGreenChain via Tor) + +1. **Download Tor Browser** + - Visit: https://www.torproject.org/download/ + - Install for your operating system + +2. **Connect to Tor Network** + - Launch Tor Browser + - Click "Connect" to establish Tor connection + +3. **Access LocalGreenChain** + - Option A: Via onion address (recommended) + ``` + http://[your-onion-address].onion + ``` + - Option B: Via clearnet (still anonymous) + ``` + http://your-domain.com + ``` + +4. **Register Plants Anonymously** + - Go to "Anonymous Registration" page + - Your connection will be detected as coming from Tor + - All privacy features automatically enabled + +### Privacy Indicators + +LocalGreenChain will show you: +- πŸ§… "Tor Active" badge when connected via Tor +- Privacy recommendations based on connection type +- Tor circuit information (country, not your IP) + +--- + +## Privacy Best Practices + +### For Maximum Anonymity + +1. **Always Use Tor Browser** + - Don't access via regular browser + - Tor Browser includes additional privacy protections + +2. **Enable Anonymous Mode** + - Use `/plants/register-anonymous` page + - Generate random IDs and pseudonyms + - Don't reuse usernames from other sites + +3. **Location Privacy** + - Use "Fuzzy" or "City" level location sharing + - Never share exact coordinates + - Consider using "Hidden" for sensitive plants + +4. **Operational Security (OpSec)** + - Don't include identifiable info in plant notes + - Use different pseudonyms for different plant types + - Don't correlate with social media accounts + - Clear browser data after each session + +5. **Network Security** + - Only share your .onion address with trusted people + - Use secure channels (encrypted messaging) to share addresses + - Rotate your hidden service periodically if needed + +### Privacy Levels Explained + +| Level | Location Accuracy | Best For | +|-------|------------------|----------| +| **Exact** | ~100m | Public gardens, commercial nurseries | +| **Fuzzy** | 1-5km radius | Home gardens, privacy-conscious sharing | +| **City** | ~10km grid | Regional plant trading | +| **Country** | ~100km grid | National distribution tracking | +| **Hidden** | No location | Maximum privacy, sensitive species | + +--- + +## Troubleshooting + +### Tor Won't Start + +```bash +# Check Tor status +sudo systemctl status tor + +# View logs +sudo tail -f /var/log/tor/notices.log + +# Common issues: +# 1. Port 9050 already in use +sudo lsof -i :9050 + +# 2. Permission issues +sudo chown -R debian-tor:debian-tor /var/lib/tor +sudo chmod 700 /var/lib/tor/localgreenchain +``` + +### Hidden Service Not Accessible + +```bash +# Verify Tor is running +pgrep tor + +# Check if hostname file exists +cat /var/lib/tor/localgreenchain/hostname + +# Verify LocalGreenChain is running +curl http://localhost:3001 + +# Check Tor logs for errors +sudo tail -f /var/log/tor/notices.log +``` + +### "Tor Status: Not Available" + +1. Check if Tor daemon is running +2. Verify SOCKS port (9050) is open +3. Check firewall settings +4. Restart Tor service + +```bash +sudo systemctl restart tor +``` + +### Slow Onion Connection + +This is normal! Tor routes through multiple nodes: +- First connection: 30-60 seconds +- Subsequent loads: 5-15 seconds +- Plant operations: Near instant (local blockchain) + +--- + +## Advanced Topics + +### Running Multiple Hidden Services + +Edit `/etc/tor/torrc`: + +``` +# LocalGreenChain (public) +HiddenServiceDir /var/lib/tor/localgreenchain-public/ +HiddenServicePort 80 127.0.0.1:3001 + +# LocalGreenChain (private - invite only) +HiddenServiceDir /var/lib/tor/localgreenchain-private/ +HiddenServicePort 80 127.0.0.1:3002 +``` + +### Client Authentication (v3 Onions) + +Restrict access to authorized users only: + +``` +# In torrc +HiddenServiceDir /var/lib/tor/localgreenchain/ +HiddenServicePort 80 127.0.0.1:3001 +HiddenServiceAuthorizeClient stealth alice,bob +``` + +### Monitoring Tor Traffic + +```bash +# Real-time connection monitoring +sudo nyx + +# Or arm (older tool) +sudo arm +``` + +### Backup Your Hidden Service Keys + +**IMPORTANT**: Your `.onion` address is tied to your keys! + +```bash +# Backup keys +sudo cp -r /var/lib/tor/localgreenchain ~/tor-backup/ + +# Restore keys (on new server) +sudo cp -r ~/tor-backup/* /var/lib/tor/localgreenchain/ +sudo chown -R debian-tor:debian-tor /var/lib/tor/localgreenchain +sudo systemctl restart tor +``` + +--- + +## Security Considerations + +### What Tor DOES Protect +βœ… Your IP address from other users +βœ… Your browsing from your ISP +βœ… Your location from the network +βœ… Your identity when using anonymous mode + +### What Tor DOESN'T Protect +❌ Poor operational security (sharing identifying info) +❌ Malware on your computer +❌ Logging in with real accounts +❌ Data you voluntarily share + +### Remember +- **Tor provides anonymity, not security** +- Use HTTPS even over Tor (LocalGreenChain supports this) +- Don't mix anonymous and identified activities +- Keep Tor Browser up to date +- Trust the process - Tor has protected millions of users + +--- + +## Getting Help + +- **LocalGreenChain Tor Issues**: https://github.com/yourusername/localgreenchain/issues +- **Tor Project**: https://support.torproject.org +- **Privacy Community**: https://www.reddit.com/r/TOR +- **Security Audit**: See SECURITY.md + +## Legal Notice + +Using Tor is legal in most countries. However: +- Check local laws regarding Tor usage +- Using Tor for illegal activities is still illegal +- LocalGreenChain is for botanical education and legal plant sharing +- Respect plant import/export regulations +- Some plants may be regulated or controlled substances + +Stay safe, stay private, and happy growing! πŸŒ±πŸ§… diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 0000000..d9f3644 --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,19 @@ +# Bun configuration for LocalGreenChain + +[install] +# Install dependencies from the lockfile if it exists +frozenLockfile = false + +# Configure which registries to use +registry = "https://registry.npmjs.org/" + +# Cache directory +cache = "node_modules/.cache/bun" + +[run] +# Automatically install dependencies when running scripts +autoInstall = true + +[test] +# Test configuration (when tests are added) +preload = [] diff --git a/components/EnvironmentalDisplay.tsx b/components/EnvironmentalDisplay.tsx new file mode 100644 index 0000000..92ca3ce --- /dev/null +++ b/components/EnvironmentalDisplay.tsx @@ -0,0 +1,334 @@ +import { useState, useEffect } from 'react'; +import { GrowingEnvironment } from '../lib/environment/types'; +import { EnvironmentalRecommendation } from '../lib/environment/types'; + +interface EnvironmentalDisplayProps { + environment: GrowingEnvironment; + plantId: string; + showRecommendations?: boolean; +} + +export default function EnvironmentalDisplay({ + environment, + plantId, + showRecommendations = true, +}: EnvironmentalDisplayProps) { + const [recommendations, setRecommendations] = useState([]); + const [healthScore, setHealthScore] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + if (showRecommendations) { + fetchRecommendations(); + } + }, [plantId]); + + const fetchRecommendations = async () => { + try { + const response = await fetch(`/api/environment/recommendations?plantId=${plantId}`); + const data = await response.json(); + if (data.success) { + setRecommendations(data.recommendations); + setHealthScore(data.environmentalHealth); + } + } catch (error) { + console.error('Error fetching recommendations:', error); + } finally { + setLoading(false); + } + }; + + return ( +
+ {/* Environmental Health Score */} + {healthScore !== null && ( +
+
+
+

+ Environmental Health Score +

+

+ Overall assessment of growing conditions +

+
+
+
+ {healthScore} +
+
/ 100
+
+
+
+
+
+
+

{getScoreInterpretation(healthScore)}

+
+
+ )} + + {/* Recommendations */} + {showRecommendations && recommendations.length > 0 && ( +
+

+ πŸ“‹ Recommendations ({recommendations.length}) +

+
+ {recommendations.map((rec, idx) => ( + + ))} +
+
+ )} + + {/* Soil Information */} +
+

+ 🌱 Soil Composition +

+
+ + + + + +
+
+ + {/* Climate Conditions */} +
+

+ 🌑️ Climate Conditions +

+
+ + + + + + {environment.climate.zone && ( + + )} +
+
+ + {/* Lighting */} +
+

+ β˜€οΈ Lighting +

+
+ + {environment.lighting.naturalLight && ( + <> + + + + + )} + {environment.lighting.artificialLight && ( + <> + + + + )} +
+
+ + {/* Location & Container */} +
+
+

πŸ“ Location

+
+ + {environment.location.room && ( + + )} + {environment.location.elevation && ( + + )} +
+
+ + {environment.container && ( +
+

πŸͺ΄ Container

+
+ + {environment.container.material && ( + + )} + {environment.container.size && ( + + )} + +
+
+ )} +
+ + {/* Watering */} +
+

πŸ’§ Watering

+
+ + + {environment.watering.frequency && ( + + )} + {environment.watering.waterQuality?.pH && ( + + )} +
+
+ + {/* Nutrients */} + {environment.nutrients && ( +
+

πŸ§ͺ Nutrients

+
+ + {environment.nutrients.ec && ( + + )} + {environment.nutrients.tds && ( + + )} +
+
+ )} + + {/* Surroundings */} + {environment.surroundings && ( +
+

🌿 Surroundings

+
+ {environment.surroundings.ecosystem && ( + + )} + {environment.surroundings.windExposure && ( + + )} + {environment.surroundings.companionPlants && environment.surroundings.companionPlants.length > 0 && ( +
+

Companion Plants:

+

{environment.surroundings.companionPlants.join(', ')}

+
+ )} +
+
+ )} +
+ ); +} + +function RecommendationCard({ recommendation }: { recommendation: EnvironmentalRecommendation }) { + const priorityColors = { + critical: 'border-red-500 bg-red-50', + high: 'border-orange-500 bg-orange-50', + medium: 'border-yellow-500 bg-yellow-50', + low: 'border-blue-500 bg-blue-50', + }; + + const priorityIcons = { + critical: '🚨', + high: '⚠️', + medium: 'πŸ’‘', + low: 'ℹ️', + }; + + return ( +
+
+ {priorityIcons[recommendation.priority]} +
+
+

{recommendation.issue}

+ + {recommendation.priority} + +
+

+ Recommendation: {recommendation.recommendation} +

+

+ Impact: {recommendation.impact} +

+
+
+
+ ); +} + +function DataPoint({ + label, + value, + highlight, +}: { + label: string; + value: string; + highlight?: 'red' | 'green'; +}) { + const highlightClass = highlight === 'red' ? 'text-red-600 font-semibold' : highlight === 'green' ? 'text-green-600 font-semibold' : ''; + + return ( +
+

{label}

+

{value}

+
+ ); +} + +function formatValue(value: string): string { + return value.replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase()); +} + +function getScoreColor(score: number): string { + if (score >= 90) return 'text-green-600'; + if (score >= 75) return 'text-lime-600'; + if (score >= 60) return 'text-yellow-600'; + if (score >= 40) return 'text-orange-600'; + return 'text-red-600'; +} + +function getScoreBgColor(score: number): string { + if (score >= 90) return 'bg-green-600'; + if (score >= 75) return 'bg-lime-600'; + if (score >= 60) return 'bg-yellow-600'; + if (score >= 40) return 'bg-orange-600'; + return 'bg-red-600'; +} + +function getScoreInterpretation(score: number): string { + if (score >= 90) return '🌟 Excellent conditions - your plant should thrive!'; + if (score >= 75) return 'βœ“ Good conditions with minor room for improvement'; + if (score >= 60) return '⚑ Adequate conditions, several optimization opportunities'; + if (score >= 40) return '⚠️ Suboptimal conditions, address high-priority issues'; + return '🚨 Poor conditions, immediate action needed'; +} diff --git a/components/EnvironmentalForm.tsx b/components/EnvironmentalForm.tsx new file mode 100644 index 0000000..3e8e269 --- /dev/null +++ b/components/EnvironmentalForm.tsx @@ -0,0 +1,708 @@ +import { useState } from 'react'; +import { + GrowingEnvironment, + SoilComposition, + ClimateConditions, + LightingConditions, + NutrientProfile, + WateringSchedule, + ContainerInfo, + EnvironmentLocation, + SurroundingEnvironment, +} from '../lib/environment/types'; + +interface EnvironmentalFormProps { + value: Partial; + onChange: (env: Partial) => void; + compact?: boolean; +} + +export default function EnvironmentalForm({ + value, + onChange, + compact = false, +}: EnvironmentalFormProps) { + const [activeSection, setActiveSection] = useState('soil'); + + const updateSection = ( + section: K, + updates: Partial + ) => { + onChange({ + ...value, + [section]: { + ...value[section], + ...updates, + }, + }); + }; + + const sections = [ + { id: 'soil', name: '🌱 Soil', icon: '🌱' }, + { id: 'nutrients', name: 'πŸ§ͺ Nutrients', icon: 'πŸ§ͺ' }, + { id: 'lighting', name: 'β˜€οΈ Light', icon: 'β˜€οΈ' }, + { id: 'climate', name: '🌑️ Climate', icon: '🌑️' }, + { id: 'location', name: 'πŸ“ Location', icon: 'πŸ“' }, + { id: 'container', name: 'πŸͺ΄ Container', icon: 'πŸͺ΄' }, + { id: 'watering', name: 'πŸ’§ Water', icon: 'πŸ’§' }, + { id: 'surroundings', name: '🌿 Surroundings', icon: '🌿' }, + ]; + + return ( +
+ {/* Section Tabs */} +
+ {sections.map((section) => ( + + ))} +
+ + {/* Section Content */} +
+ {activeSection === 'soil' && ( + updateSection('soil', soil)} + /> + )} + + {activeSection === 'nutrients' && ( + updateSection('nutrients', nutrients)} + /> + )} + + {activeSection === 'lighting' && ( + updateSection('lighting', lighting)} + /> + )} + + {activeSection === 'climate' && ( + updateSection('climate', climate)} + /> + )} + + {activeSection === 'location' && ( + updateSection('location', location)} + /> + )} + + {activeSection === 'container' && ( + updateSection('container', container)} + /> + )} + + {activeSection === 'watering' && ( + updateSection('watering', watering)} + /> + )} + + {activeSection === 'surroundings' && ( + updateSection('surroundings', surroundings)} + /> + )} +
+
+ ); +} + +// Soil Section Component +function SoilSection({ + value, + onChange, +}: { + value?: Partial; + onChange: (soil: Partial) => void; +}) { + const soil = value || {}; + + return ( +
+

Soil Composition

+ +
+
+ + +
+ +
+ + onChange({ ...soil, pH: parseFloat(e.target.value) })} + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500" + /> +
+ {soil.pH && soil.pH < 5.5 && '⚠️ Acidic - may need lime'} + {soil.pH && soil.pH >= 5.5 && soil.pH <= 7.5 && 'βœ“ Good range'} + {soil.pH && soil.pH > 7.5 && '⚠️ Alkaline - may need sulfur'} +
+
+ +
+ + +
+ +
+ + +
+ +
+ + onChange({ ...soil, organicMatter: parseFloat(e.target.value) })} + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500" + /> +
+
+ +
+

+ πŸ’‘ Tip: Test soil pH with a meter or test kit for accuracy. Most vegetables prefer pH 6.0-7.0. +

+
+
+ ); +} + +// Nutrients Section Component +function NutrientsSection({ + value, + onChange, +}: { + value?: Partial; + onChange: (nutrients: Partial) => void; +}) { + const nutrients = value || {}; + + return ( +
+

Nutrient Profile

+ +
+

Primary Nutrients (NPK)

+
+
+ + onChange({ ...nutrients, nitrogen: parseFloat(e.target.value) })} + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500" + /> +
+
+ + onChange({ ...nutrients, phosphorus: parseFloat(e.target.value) })} + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500" + /> +
+
+ + onChange({ ...nutrients, potassium: parseFloat(e.target.value) })} + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500" + /> +
+
+

+ NPK ratio: {nutrients.nitrogen || 0}-{nutrients.phosphorus || 0}-{nutrients.potassium || 0} +

+
+ +
+

+ πŸ’‘ Tip: Leave at 0 if unknown. Use soil test kit for accurate NPK values. +

+
+
+ ); +} + +// Lighting Section Component +function LightingSection({ + value, + onChange, +}: { + value?: Partial; + onChange: (lighting: Partial) => void; +}) { + const lighting = value || { type: 'natural' }; + + return ( +
+

Lighting Conditions

+ +
+ + +
+ + {(lighting.type === 'natural' || lighting.type === 'mixed') && ( +
+
+ + +
+ +
+ + + onChange({ + ...lighting, + naturalLight: { + ...lighting.naturalLight, + hoursPerDay: parseInt(e.target.value), + } as any, + }) + } + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500" + /> +
+
+ )} + +
+

+ πŸ’‘ Tip: Most vegetables need 6+ hours of direct sunlight. Herbs can do well with 4-6 hours. +

+
+
+ ); +} + +// Climate Section Component +function ClimateSection({ + value, + onChange, +}: { + value?: Partial; + onChange: (climate: Partial) => void; +}) { + const climate = value || {}; + + return ( +
+

Climate Conditions

+ +
+
+ + onChange({ ...climate, temperatureDay: parseFloat(e.target.value) })} + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500" + /> +
+ +
+ + onChange({ ...climate, temperatureNight: parseFloat(e.target.value) })} + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500" + /> +
+ +
+ + onChange({ ...climate, humidityAverage: parseFloat(e.target.value) })} + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500" + /> +
+ +
+ + +
+
+ +
+

+ πŸ’‘ Tip: Most plants thrive at 18-25Β°C. Good airflow prevents disease. +

+
+
+ ); +} + +// Location Section Component +function LocationSection({ + value, + onChange, +}: { + value?: Partial; + onChange: (location: Partial) => void; +}) { + const location = value || {}; + + return ( +
+

Growing Location

+ +
+ + +
+ +
+

+ πŸ’‘ Tip: Location type affects climate control and pest exposure. +

+
+
+ ); +} + +// Container Section Component +function ContainerSection({ + value, + onChange, +}: { + value?: Partial; + onChange: (container: Partial) => void; +}) { + const container = value || {}; + + return ( +
+

Container Information

+ +
+
+ + +
+ +
+ + + {container.drainage === 'no' && ( +

+ ⚠️ WARNING: No drainage will likely kill your plant! +

+ )} +
+ +
+ + onChange({ ...container, size: e.target.value })} + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500" + /> +
+
+ +
+

+ πŸ’‘ Tip: Always ensure drainage! Sitting water = root rot = dead plant. +

+
+
+ ); +} + +// Watering Section Component +function WateringSection({ + value, + onChange, +}: { + value?: Partial; + onChange: (watering: Partial) => void; +}) { + const watering = value || {}; + + return ( +
+

Watering Schedule

+ +
+
+ + +
+ +
+ + +
+ +
+ + onChange({ ...watering, frequency: e.target.value })} + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500" + /> +
+
+ +
+

+ πŸ’‘ Tip: Water when top inch of soil is dry. Overwatering kills more plants than underwatering! +

+
+
+ ); +} + +// Surroundings Section Component +function SurroundingsSection({ + value, + onChange, +}: { + value?: Partial; + onChange: (surroundings: Partial) => void; +}) { + const surroundings = value || {}; + + return ( +
+

Surrounding Environment

+ +
+ + +
+ +
+ + +
+ +
+

+ πŸ’‘ Tip: Track companion plants and pests to learn what works in your ecosystem. +

+
+
+ ); +} diff --git a/components/PrivacySettings.tsx b/components/PrivacySettings.tsx new file mode 100644 index 0000000..b1d7c14 --- /dev/null +++ b/components/PrivacySettings.tsx @@ -0,0 +1,227 @@ +import { useState, useEffect } from 'react'; +import { PrivacySettings as IPrivacySettings } from '../lib/privacy/anonymity'; + +interface PrivacySettingsProps { + value: IPrivacySettings; + onChange: (settings: IPrivacySettings) => void; + showTorStatus?: boolean; +} + +export default function PrivacySettings({ + value, + onChange, + showTorStatus = true, +}: PrivacySettingsProps) { + const [torStatus, setTorStatus] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + if (showTorStatus) { + checkTorStatus(); + } + }, [showTorStatus]); + + const checkTorStatus = async () => { + try { + const response = await fetch('/api/privacy/tor-status'); + const data = await response.json(); + if (data.success) { + setTorStatus(data); + } + } catch (error) { + console.error('Error checking Tor status:', error); + } finally { + setLoading(false); + } + }; + + const handleChange = (field: keyof IPrivacySettings, newValue: any) => { + onChange({ + ...value, + [field]: newValue, + }); + }; + + return ( +
+
+

+ πŸ”’ Privacy & Anonymity Settings +

+ {!loading && torStatus?.tor.connectionThroughTor && ( + + πŸ§… Tor Active + + )} +
+ + {/* Tor Status Banner */} + {showTorStatus && !loading && ( +
+
+ + {torStatus?.tor.connectionThroughTor ? 'πŸ§…' : '⚠️'} + +
+

+ {torStatus?.tor.connectionThroughTor + ? 'Tor Connection Active' + : 'Not Using Tor'} +

+

+ {torStatus?.tor.connectionThroughTor + ? 'Your connection is anonymous and routed through the Tor network.' + : 'For maximum privacy, consider accessing via Tor Browser.'} +

+ {torStatus?.tor.onionAddress && ( +

+ Onion Address: {torStatus.tor.onionAddress} +

+ )} +
+
+
+ )} + + {/* Anonymous Mode Toggle */} +
+ +

+ Generate random identifiers and hide personal information +

+
+ + {/* Location Privacy */} +
+ + +
+ {value.locationPrivacy === 'exact' && ( + + ⚠️ Warning: Exact location may reveal your home address + + )} + {value.locationPrivacy === 'fuzzy' && ( + βœ“ Good balance of privacy and discoverability + )} + {value.locationPrivacy === 'city' && ( + βœ“ Only city-level information shared + )} + {value.locationPrivacy === 'country' && ( + βœ“ Only country/region visible + )} + {value.locationPrivacy === 'hidden' && ( + + πŸ”’ Maximum privacy: Location completely hidden + + )} +
+
+ + {/* Identity Privacy */} +
+ + +
+ + {/* Share Plant Details */} +
+ +

+ Uncheck to use generic plant identifiers +

+
+ + {/* Privacy Summary */} +
+

Privacy Summary

+
    +
  • + β€’ Location: {value.locationPrivacy === 'exact' ? 'Visible to all' : 'Protected'} +
  • +
  • + β€’ Identity: {value.identityPrivacy === 'real' ? 'Real name' : 'Protected'} +
  • +
  • + β€’ Plant Info: {value.sharePlantDetails ? 'Shared' : 'Generic'} +
  • +
  • + β€’ Mode: {value.anonymousMode ? 'Anonymous πŸ”’' : 'Standard'} +
  • +
+
+ + {/* Recommendations */} + {!loading && torStatus && torStatus.recommendations && ( +
+

πŸ’‘ Recommendations

+
    + {torStatus.recommendations.map((rec: string, idx: number) => ( +
  • β€’ {rec}
  • + ))} +
+
+ )} +
+ ); +} diff --git a/docker-compose.tor.yml b/docker-compose.tor.yml new file mode 100644 index 0000000..ec05f4e --- /dev/null +++ b/docker-compose.tor.yml @@ -0,0 +1,67 @@ +version: '3.8' + +services: + # Tor daemon + tor: + image: goldy/tor-hidden-service:latest + container_name: localgreenchain-tor + environment: + # Hidden service configuration + SERVICE_NAME: localgreenchain + SERVICE_PORT: 80 + SERVICE_HOST: app + SERVICE_HOST_PORT: 3001 + volumes: + - tor-data:/var/lib/tor + - ./tor/torrc.example:/etc/tor/torrc:ro + ports: + - "9050:9050" # SOCKS proxy + - "9051:9051" # Control port + networks: + - localgreenchain-network + restart: unless-stopped + + # LocalGreenChain application + app: + build: . + container_name: localgreenchain-app + environment: + - NODE_ENV=production + - TOR_ENABLED=true + - TOR_SOCKS_HOST=tor + - TOR_SOCKS_PORT=9050 + - TOR_CONTROL_PORT=9051 + - TOR_HIDDEN_SERVICE_DIR=/var/lib/tor/hidden_service + volumes: + - ./data:/app/data + - tor-data:/var/lib/tor:ro + depends_on: + - tor + networks: + - localgreenchain-network + restart: unless-stopped + command: bun run start + + # Optional: nginx reverse proxy for additional security + nginx: + image: nginx:alpine + container_name: localgreenchain-nginx + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - ./nginx/ssl:/etc/nginx/ssl:ro + depends_on: + - app + networks: + - localgreenchain-network + restart: unless-stopped + +volumes: + tor-data: + driver: local + +networks: + localgreenchain-network: + driver: bridge diff --git a/lib/blockchain/PlantBlock.ts b/lib/blockchain/PlantBlock.ts new file mode 100644 index 0000000..7c86b2d --- /dev/null +++ b/lib/blockchain/PlantBlock.ts @@ -0,0 +1,106 @@ +import crypto from 'crypto'; +import { PlantData, BlockData } from './types'; + +/** + * PlantBlock - Represents a single block in the plant blockchain + * Each block contains data about a plant and its lineage + */ +export class PlantBlock { + public index: number; + public timestamp: string; + public plant: PlantData; + public previousHash: string; + public hash: string; + public nonce: number; + + constructor( + index: number, + timestamp: string, + plant: PlantData, + previousHash: string = '' + ) { + this.index = index; + this.timestamp = timestamp; + this.plant = plant; + this.previousHash = previousHash; + this.nonce = 0; + this.hash = this.calculateHash(); + } + + /** + * Calculate the hash of this block using SHA-256 + */ + calculateHash(): string { + return crypto + .createHash('sha256') + .update( + this.index + + this.previousHash + + this.timestamp + + JSON.stringify(this.plant) + + this.nonce + ) + .digest('hex'); + } + + /** + * Mine the block using proof-of-work algorithm + * Difficulty determines how many leading zeros the hash must have + */ + mineBlock(difficulty: number): void { + const target = Array(difficulty + 1).join('0'); + + while (this.hash.substring(0, difficulty) !== target) { + this.nonce++; + this.hash = this.calculateHash(); + } + + console.log(`Block mined: ${this.hash}`); + } + + /** + * Convert block to JSON for storage/transmission + */ + toJSON(): BlockData { + return { + index: this.index, + timestamp: this.timestamp, + plant: this.plant, + previousHash: this.previousHash, + hash: this.hash, + nonce: this.nonce, + }; + } + + /** + * Create a PlantBlock from JSON data + */ + static fromJSON(data: BlockData): PlantBlock { + const block = new PlantBlock( + data.index, + data.timestamp, + data.plant, + data.previousHash + ); + block.hash = data.hash; + block.nonce = data.nonce; + return block; + } + + /** + * Verify if this block is valid + */ + isValid(previousBlock?: PlantBlock): boolean { + // Check if hash is correct + if (this.hash !== this.calculateHash()) { + return false; + } + + // Check if previous hash matches + if (previousBlock && this.previousHash !== previousBlock.hash) { + return false; + } + + return true; + } +} diff --git a/lib/blockchain/PlantChain.ts b/lib/blockchain/PlantChain.ts new file mode 100644 index 0000000..08564da --- /dev/null +++ b/lib/blockchain/PlantChain.ts @@ -0,0 +1,396 @@ +import { PlantBlock } from './PlantBlock'; +import { PlantData, PlantLineage, NearbyPlant, PlantNetwork } from './types'; + +/** + * PlantChain - The main blockchain for tracking plant lineage and ownership + * This blockchain records every plant, its clones, seeds, and ownership transfers + */ +export class PlantChain { + public chain: PlantBlock[]; + public difficulty: number; + private plantIndex: Map; // Quick lookup by plant ID + + constructor(difficulty: number = 4) { + this.chain = [this.createGenesisBlock()]; + this.difficulty = difficulty; + this.plantIndex = new Map(); + } + + /** + * Create the first block in the chain + */ + private createGenesisBlock(): PlantBlock { + const genesisPlant: PlantData = { + id: 'genesis-plant-0', + commonName: 'Genesis Plant', + scientificName: 'Blockchain primordialis', + propagationType: 'original', + generation: 0, + plantedDate: new Date().toISOString(), + status: 'mature', + location: { + latitude: 0, + longitude: 0, + address: 'The Beginning', + }, + owner: { + id: 'system', + name: 'LocalGreenChain', + email: 'system@localgreenchain.org', + }, + childPlants: [], + registeredAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; + + const block = new PlantBlock(0, new Date().toISOString(), genesisPlant, '0'); + this.plantIndex.set(genesisPlant.id, block); + return block; + } + + /** + * Get the latest block in the chain + */ + getLatestBlock(): PlantBlock { + return this.chain[this.chain.length - 1]; + } + + /** + * Register a new original plant (not a clone or seed) + */ + registerPlant(plant: PlantData): PlantBlock { + // Ensure plant has required fields + if (!plant.id || !plant.commonName || !plant.owner) { + throw new Error('Plant must have id, commonName, and owner'); + } + + // Check if plant already exists + if (this.plantIndex.has(plant.id)) { + throw new Error(`Plant with ID ${plant.id} already exists`); + } + + // Set defaults + plant.generation = 0; + plant.propagationType = plant.propagationType || 'original'; + plant.childPlants = []; + plant.registeredAt = new Date().toISOString(); + plant.updatedAt = new Date().toISOString(); + + const newBlock = new PlantBlock( + this.chain.length, + new Date().toISOString(), + plant, + this.getLatestBlock().hash + ); + + newBlock.mineBlock(this.difficulty); + this.chain.push(newBlock); + this.plantIndex.set(plant.id, newBlock); + + return newBlock; + } + + /** + * Register a clone or seed from an existing plant + */ + clonePlant( + parentPlantId: string, + newPlant: Partial, + propagationType: 'seed' | 'clone' | 'cutting' | 'division' | 'grafting' + ): PlantBlock { + // Find parent plant + const parentBlock = this.plantIndex.get(parentPlantId); + if (!parentBlock) { + throw new Error(`Parent plant ${parentPlantId} not found`); + } + + const parentPlant = parentBlock.plant; + + // Create new plant with inherited properties + const clonedPlant: PlantData = { + id: newPlant.id || `${parentPlantId}-${propagationType}-${Date.now()}`, + commonName: newPlant.commonName || parentPlant.commonName, + scientificName: newPlant.scientificName || parentPlant.scientificName, + species: newPlant.species || parentPlant.species, + genus: newPlant.genus || parentPlant.genus, + family: newPlant.family || parentPlant.family, + + // Lineage + parentPlantId: parentPlantId, + propagationType: propagationType, + generation: parentPlant.generation + 1, + + // Required fields from newPlant + plantedDate: newPlant.plantedDate || new Date().toISOString(), + status: newPlant.status || 'sprouted', + location: newPlant.location!, + owner: newPlant.owner!, + + // Initialize child tracking + childPlants: [], + + // Optional fields + notes: newPlant.notes, + images: newPlant.images, + plantsNetId: newPlant.plantsNetId, + + registeredAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; + + // Validate required fields + if (!clonedPlant.location || !clonedPlant.owner) { + throw new Error('Cloned plant must have location and owner'); + } + + // Add to blockchain + const newBlock = new PlantBlock( + this.chain.length, + new Date().toISOString(), + clonedPlant, + this.getLatestBlock().hash + ); + + newBlock.mineBlock(this.difficulty); + this.chain.push(newBlock); + this.plantIndex.set(clonedPlant.id, newBlock); + + // Update parent plant's child list + parentPlant.childPlants.push(clonedPlant.id); + parentPlant.updatedAt = new Date().toISOString(); + + return newBlock; + } + + /** + * Update plant status (growing, flowering, etc.) + */ + updatePlantStatus( + plantId: string, + updates: Partial + ): PlantBlock { + const existingBlock = this.plantIndex.get(plantId); + if (!existingBlock) { + throw new Error(`Plant ${plantId} not found`); + } + + const updatedPlant: PlantData = { + ...existingBlock.plant, + ...updates, + id: existingBlock.plant.id, // Cannot change ID + parentPlantId: existingBlock.plant.parentPlantId, // Cannot change lineage + childPlants: existingBlock.plant.childPlants, // Cannot change children + generation: existingBlock.plant.generation, // Cannot change generation + registeredAt: existingBlock.plant.registeredAt, // Cannot change registration + updatedAt: new Date().toISOString(), + }; + + const newBlock = new PlantBlock( + this.chain.length, + new Date().toISOString(), + updatedPlant, + this.getLatestBlock().hash + ); + + newBlock.mineBlock(this.difficulty); + this.chain.push(newBlock); + this.plantIndex.set(plantId, newBlock); // Update index to latest block + + return newBlock; + } + + /** + * Get a plant by ID (returns latest version) + */ + getPlant(plantId: string): PlantData | null { + const block = this.plantIndex.get(plantId); + return block ? block.plant : null; + } + + /** + * Get complete lineage for a plant + */ + getPlantLineage(plantId: string): PlantLineage | null { + const plant = this.getPlant(plantId); + if (!plant) return null; + + // Get all ancestors + const ancestors: PlantData[] = []; + let currentPlant = plant; + while (currentPlant.parentPlantId) { + const parent = this.getPlant(currentPlant.parentPlantId); + if (!parent) break; + ancestors.push(parent); + currentPlant = parent; + } + + // Get all descendants recursively + const descendants: PlantData[] = []; + const getDescendants = (p: PlantData) => { + for (const childId of p.childPlants) { + const child = this.getPlant(childId); + if (child) { + descendants.push(child); + getDescendants(child); + } + } + }; + getDescendants(plant); + + // Get siblings (other plants from same parent) + const siblings: PlantData[] = []; + if (plant.parentPlantId) { + const parent = this.getPlant(plant.parentPlantId); + if (parent) { + for (const siblingId of parent.childPlants) { + if (siblingId !== plantId) { + const sibling = this.getPlant(siblingId); + if (sibling) siblings.push(sibling); + } + } + } + } + + return { + plantId, + ancestors, + descendants, + siblings, + generation: plant.generation, + }; + } + + /** + * Find plants near a location (within radius in km) + */ + findNearbyPlants( + latitude: number, + longitude: number, + radiusKm: number = 50 + ): NearbyPlant[] { + const nearbyPlants: NearbyPlant[] = []; + + // Get all unique plants (latest version of each) + const allPlants = Array.from(this.plantIndex.values()).map(block => block.plant); + + for (const plant of allPlants) { + const distance = this.calculateDistance( + latitude, + longitude, + plant.location.latitude, + plant.location.longitude + ); + + if (distance <= radiusKm) { + nearbyPlants.push({ + plant, + distance, + owner: plant.owner, + }); + } + } + + // Sort by distance + return nearbyPlants.sort((a, b) => a.distance - b.distance); + } + + /** + * Calculate distance between two coordinates using Haversine formula + */ + private calculateDistance( + lat1: number, + lon1: number, + lat2: number, + lon2: number + ): number { + const R = 6371; // Earth's radius in km + const dLat = this.toRadians(lat2 - lat1); + const dLon = this.toRadians(lon2 - lon1); + + const a = + Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(this.toRadians(lat1)) * + Math.cos(this.toRadians(lat2)) * + Math.sin(dLon / 2) * + Math.sin(dLon / 2); + + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + return R * c; + } + + private toRadians(degrees: number): number { + return degrees * (Math.PI / 180); + } + + /** + * Get network statistics + */ + getNetworkStats(): PlantNetwork { + const allPlants = Array.from(this.plantIndex.values()).map(block => block.plant); + const owners = new Set(); + const species: { [key: string]: number } = {}; + const countries: { [key: string]: number } = {}; + + for (const plant of allPlants) { + owners.add(plant.owner.id); + + if (plant.scientificName) { + species[plant.scientificName] = (species[plant.scientificName] || 0) + 1; + } + + if (plant.location.country) { + countries[plant.location.country] = (countries[plant.location.country] || 0) + 1; + } + } + + return { + totalPlants: allPlants.length, + totalOwners: owners.size, + species, + globalDistribution: countries, + }; + } + + /** + * Validate the entire blockchain + */ + isChainValid(): boolean { + for (let i = 1; i < this.chain.length; i++) { + const currentBlock = this.chain[i]; + const previousBlock = this.chain[i - 1]; + + if (!currentBlock.isValid(previousBlock)) { + return false; + } + } + return true; + } + + /** + * Export chain to JSON + */ + toJSON(): any { + return { + difficulty: this.difficulty, + chain: this.chain.map(block => block.toJSON()), + }; + } + + /** + * Import chain from JSON + */ + static fromJSON(data: any): PlantChain { + const plantChain = new PlantChain(data.difficulty); + plantChain.chain = data.chain.map((blockData: any) => + PlantBlock.fromJSON(blockData) + ); + + // Rebuild index + plantChain.plantIndex.clear(); + for (const block of plantChain.chain) { + plantChain.plantIndex.set(block.plant.id, block); + } + + return plantChain; + } +} diff --git a/lib/blockchain/manager.ts b/lib/blockchain/manager.ts new file mode 100644 index 0000000..b0d2abc --- /dev/null +++ b/lib/blockchain/manager.ts @@ -0,0 +1,106 @@ +/** + * Blockchain Manager + * Singleton to manage the global plant blockchain instance + */ + +import { PlantChain } from './PlantChain'; +import fs from 'fs'; +import path from 'path'; + +const BLOCKCHAIN_FILE = path.join(process.cwd(), 'data', 'plantchain.json'); + +class BlockchainManager { + private static instance: BlockchainManager; + private plantChain: PlantChain; + private autoSaveInterval: NodeJS.Timeout | null = null; + + private constructor() { + this.plantChain = this.loadBlockchain(); + this.startAutoSave(); + } + + public static getInstance(): BlockchainManager { + if (!BlockchainManager.instance) { + BlockchainManager.instance = new BlockchainManager(); + } + return BlockchainManager.instance; + } + + public getChain(): PlantChain { + return this.plantChain; + } + + /** + * Load blockchain from file or create new one + */ + private loadBlockchain(): PlantChain { + try { + // Ensure data directory exists + const dataDir = path.join(process.cwd(), 'data'); + if (!fs.existsSync(dataDir)) { + fs.mkdirSync(dataDir, { recursive: true }); + } + + if (fs.existsSync(BLOCKCHAIN_FILE)) { + const data = fs.readFileSync(BLOCKCHAIN_FILE, 'utf-8'); + const chainData = JSON.parse(data); + console.log('βœ“ Loaded existing blockchain with', chainData.chain.length, 'blocks'); + return PlantChain.fromJSON(chainData); + } + } catch (error) { + console.error('Error loading blockchain:', error); + } + + console.log('βœ“ Created new blockchain'); + return new PlantChain(4); // difficulty of 4 + } + + /** + * Save blockchain to file + */ + public saveBlockchain(): void { + try { + const dataDir = path.join(process.cwd(), 'data'); + if (!fs.existsSync(dataDir)) { + fs.mkdirSync(dataDir, { recursive: true }); + } + + const data = JSON.stringify(this.plantChain.toJSON(), null, 2); + fs.writeFileSync(BLOCKCHAIN_FILE, data, 'utf-8'); + console.log('βœ“ Blockchain saved'); + } catch (error) { + console.error('Error saving blockchain:', error); + } + } + + /** + * Auto-save blockchain every 5 minutes + */ + private startAutoSave(): void { + if (this.autoSaveInterval) { + clearInterval(this.autoSaveInterval); + } + + this.autoSaveInterval = setInterval(() => { + this.saveBlockchain(); + }, 5 * 60 * 1000); // 5 minutes + } + + /** + * Stop auto-save + */ + public stopAutoSave(): void { + if (this.autoSaveInterval) { + clearInterval(this.autoSaveInterval); + this.autoSaveInterval = null; + } + } +} + +export function getBlockchain(): PlantChain { + return BlockchainManager.getInstance().getChain(); +} + +export function saveBlockchain(): void { + BlockchainManager.getInstance().saveBlockchain(); +} diff --git a/lib/blockchain/types.ts b/lib/blockchain/types.ts new file mode 100644 index 0000000..3a3914b --- /dev/null +++ b/lib/blockchain/types.ts @@ -0,0 +1,87 @@ +// Plant Blockchain Types + +import { GrowingEnvironment, GrowthMetrics } from '../environment/types'; + +export interface PlantLocation { + latitude: number; + longitude: number; + address?: string; + city?: string; + country?: string; +} + +export interface PlantOwner { + id: string; + name: string; + email: string; + walletAddress?: string; +} + +export interface PlantData { + id: string; + commonName: string; + scientificName?: string; + species?: string; + genus?: string; + family?: string; + + // Lineage tracking + parentPlantId?: string; // Original plant this came from + propagationType?: 'seed' | 'clone' | 'cutting' | 'division' | 'grafting' | 'original'; + generation: number; // How many generations from the original + + // Plant lifecycle + plantedDate: string; + harvestedDate?: string; + status: 'sprouted' | 'growing' | 'mature' | 'flowering' | 'fruiting' | 'dormant' | 'deceased'; + + // Location and ownership + location: PlantLocation; + owner: PlantOwner; + + // Plant network + childPlants: string[]; // IDs of clones and seeds from this plant + + // Environmental data + environment?: GrowingEnvironment; + growthMetrics?: GrowthMetrics; + + // Additional metadata + notes?: string; + images?: string[]; + plantsNetId?: string; // ID from plants.net API + + // Timestamps + registeredAt: string; + updatedAt: string; +} + +export interface BlockData { + index: number; + timestamp: string; + plant: PlantData; + previousHash: string; + hash: string; + nonce: number; +} + +export interface PlantLineage { + plantId: string; + ancestors: PlantData[]; + descendants: PlantData[]; + siblings: PlantData[]; // Other plants from the same parent + generation: number; +} + +export interface NearbyPlant { + plant: PlantData; + distance: number; // in kilometers + owner: PlantOwner; +} + +export interface PlantNetwork { + totalPlants: number; + totalOwners: number; + species: { [key: string]: number }; + globalDistribution: { [country: string]: number }; +} diff --git a/lib/environment/analysis.ts b/lib/environment/analysis.ts new file mode 100644 index 0000000..01b9a20 --- /dev/null +++ b/lib/environment/analysis.ts @@ -0,0 +1,432 @@ +/** + * Environmental Analysis Service + * Compares growing conditions, generates recommendations, and analyzes correlations + */ + +import { + GrowingEnvironment, + EnvironmentalComparison, + EnvironmentalRecommendation, + SoilComposition, + ClimateConditions, + LightingConditions, + NutrientProfile, +} from './types'; +import { PlantData } from '../blockchain/types'; + +/** + * Compare two growing environments and return similarity score + */ +export function compareEnvironments( + env1: GrowingEnvironment, + env2: GrowingEnvironment +): EnvironmentalComparison { + const similarities: string[] = []; + const differences: string[] = []; + let score = 0; + let maxScore = 0; + + // Compare location type (10 points) + maxScore += 10; + if (env1.location.type === env2.location.type) { + score += 10; + similarities.push(`Both ${env1.location.type} growing`); + } else { + differences.push(`Location: ${env1.location.type} vs ${env2.location.type}`); + } + + // Compare soil type (15 points) + maxScore += 15; + if (env1.soil.type === env2.soil.type) { + score += 15; + similarities.push(`Same soil type: ${env1.soil.type}`); + } else { + differences.push(`Soil: ${env1.soil.type} vs ${env2.soil.type}`); + } + + // Compare soil pH (10 points, within 0.5 range) + maxScore += 10; + const pHDiff = Math.abs(env1.soil.pH - env2.soil.pH); + if (pHDiff <= 0.5) { + score += 10; + similarities.push(`Similar soil pH: ${env1.soil.pH.toFixed(1)} β‰ˆ ${env2.soil.pH.toFixed(1)}`); + } else if (pHDiff <= 1.0) { + score += 5; + differences.push(`Soil pH: ${env1.soil.pH.toFixed(1)} vs ${env2.soil.pH.toFixed(1)}`); + } else { + differences.push(`Soil pH differs significantly: ${env1.soil.pH.toFixed(1)} vs ${env2.soil.pH.toFixed(1)}`); + } + + // Compare temperature (15 points) + maxScore += 15; + const tempDiff = Math.abs(env1.climate.temperatureDay - env2.climate.temperatureDay); + if (tempDiff <= 3) { + score += 15; + similarities.push(`Similar temperatures: ${env1.climate.temperatureDay}Β°C β‰ˆ ${env2.climate.temperatureDay}Β°C`); + } else if (tempDiff <= 7) { + score += 8; + differences.push(`Temperature: ${env1.climate.temperatureDay}Β°C vs ${env2.climate.temperatureDay}Β°C`); + } else { + differences.push(`Temperature differs significantly: ${env1.climate.temperatureDay}Β°C vs ${env2.climate.temperatureDay}Β°C`); + } + + // Compare humidity (10 points) + maxScore += 10; + const humidityDiff = Math.abs(env1.climate.humidityAverage - env2.climate.humidityAverage); + if (humidityDiff <= 10) { + score += 10; + similarities.push(`Similar humidity: ${env1.climate.humidityAverage}% β‰ˆ ${env2.climate.humidityAverage}%`); + } else if (humidityDiff <= 20) { + score += 5; + } else { + differences.push(`Humidity: ${env1.climate.humidityAverage}% vs ${env2.climate.humidityAverage}%`); + } + + // Compare lighting type (15 points) + maxScore += 15; + if (env1.lighting.type === env2.lighting.type) { + score += 15; + similarities.push(`Same lighting: ${env1.lighting.type}`); + } else { + differences.push(`Lighting: ${env1.lighting.type} vs ${env2.lighting.type}`); + } + + // Compare natural light exposure if both use natural light (10 points) + if (env1.lighting.naturalLight && env2.lighting.naturalLight) { + maxScore += 10; + if (env1.lighting.naturalLight.exposure === env2.lighting.naturalLight.exposure) { + score += 10; + similarities.push(`Same sun exposure: ${env1.lighting.naturalLight.exposure}`); + } else { + differences.push(`Sun exposure: ${env1.lighting.naturalLight.exposure} vs ${env2.lighting.naturalLight.exposure}`); + } + } + + // Compare water source (5 points) + maxScore += 5; + if (env1.watering.waterSource === env2.watering.waterSource) { + score += 5; + similarities.push(`Same water source: ${env1.watering.waterSource}`); + } else { + differences.push(`Water: ${env1.watering.waterSource} vs ${env2.watering.waterSource}`); + } + + // Compare container type (10 points) + if (env1.container && env2.container) { + maxScore += 10; + if (env1.container.type === env2.container.type) { + score += 10; + similarities.push(`Same container: ${env1.container.type}`); + } else { + differences.push(`Container: ${env1.container.type} vs ${env2.container.type}`); + } + } + + // Calculate final score as percentage + const finalScore = maxScore > 0 ? Math.round((score / maxScore) * 100) : 0; + + return { + plant1: '', // Will be filled by caller + plant2: '', // Will be filled by caller + similarities, + differences, + score: finalScore, + }; +} + +/** + * Generate environmental recommendations based on plant data + */ +export function generateRecommendations( + plant: PlantData +): EnvironmentalRecommendation[] { + const recommendations: EnvironmentalRecommendation[] = []; + + if (!plant.environment) { + recommendations.push({ + category: 'general', + priority: 'high', + issue: 'No environmental data', + recommendation: 'Add environmental information to track growing conditions', + impact: 'Environmental tracking helps optimize growing conditions and share knowledge', + }); + return recommendations; + } + + const env = plant.environment; + + // Soil pH recommendations + if (env.soil.pH < 5.5) { + recommendations.push({ + category: 'soil', + priority: 'high', + issue: `Low soil pH (${env.soil.pH})`, + recommendation: 'Add lime to raise pH. Most plants prefer 6.0-7.0', + impact: 'Acidic soil can lock out nutrients and harm roots', + }); + } else if (env.soil.pH > 7.5) { + recommendations.push({ + category: 'soil', + priority: 'high', + issue: `High soil pH (${env.soil.pH})`, + recommendation: 'Add sulfur to lower pH. Most plants prefer 6.0-7.0', + impact: 'Alkaline soil can prevent nutrient uptake', + }); + } + + // Organic matter recommendations + if (env.soil.organicMatter < 3) { + recommendations.push({ + category: 'soil', + priority: 'medium', + issue: `Low organic matter (${env.soil.organicMatter}%)`, + recommendation: 'Add compost or aged manure to improve soil structure', + impact: 'Increases water retention and nutrient availability', + }); + } + + // Drainage recommendations + if (env.soil.drainage === 'poor') { + recommendations.push({ + category: 'soil', + priority: 'high', + issue: 'Poor soil drainage', + recommendation: 'Add perlite, sand, or create raised beds', + impact: 'Prevents root rot and improves oxygen availability', + }); + } + + // Temperature recommendations + if (env.climate.temperatureDay > 35) { + recommendations.push({ + category: 'climate', + priority: 'high', + issue: `High daytime temperature (${env.climate.temperatureDay}Β°C)`, + recommendation: 'Provide shade during hottest hours, increase watering', + impact: 'Extreme heat can cause stress and reduce growth', + }); + } else if (env.climate.temperatureDay < 10) { + recommendations.push({ + category: 'climate', + priority: 'high', + issue: `Low daytime temperature (${env.climate.temperatureDay}Β°C)`, + recommendation: 'Provide frost protection or move indoors', + impact: 'Cold temperatures can slow growth or damage plants', + }); + } + + // Humidity recommendations + if (env.climate.humidityAverage < 30) { + recommendations.push({ + category: 'climate', + priority: 'medium', + issue: `Low humidity (${env.climate.humidityAverage}%)`, + recommendation: 'Mist plants, use humidity tray, or group plants together', + impact: 'Low humidity can cause leaf tip browning and stress', + }); + } else if (env.climate.humidityAverage > 80) { + recommendations.push({ + category: 'climate', + priority: 'medium', + issue: `High humidity (${env.climate.humidityAverage}%)`, + recommendation: 'Improve air circulation to prevent fungal issues', + impact: 'High humidity can promote mold and disease', + }); + } + + // Lighting recommendations + if (env.lighting.type === 'natural' && env.lighting.naturalLight) { + const hoursPerDay = env.lighting.naturalLight.hoursPerDay; + if (hoursPerDay < 4 && plant.status === 'growing') { + recommendations.push({ + category: 'light', + priority: 'high', + issue: `Insufficient light (${hoursPerDay} hours/day)`, + recommendation: 'Move to brighter location or supplement with grow lights', + impact: 'Inadequate light causes leggy growth and poor health', + }); + } + } + + // Nutrient recommendations + if (env.nutrients) { + const npk = [env.nutrients.nitrogen, env.nutrients.phosphorus, env.nutrients.potassium]; + if (npk.some(n => n < 1)) { + recommendations.push({ + category: 'nutrients', + priority: 'medium', + issue: `Low NPK levels (${npk.join('-')})`, + recommendation: 'Apply balanced fertilizer according to plant needs', + impact: 'Nutrient deficiency reduces growth and yield', + }); + } + } + + // Water quality recommendations + if (env.watering.waterQuality) { + if (env.watering.waterQuality.pH && env.watering.waterQuality.pH > 7.5) { + recommendations.push({ + category: 'water', + priority: 'low', + issue: `Alkaline water pH (${env.watering.waterQuality.pH})`, + recommendation: 'Consider pH adjustment or rainwater collection', + impact: 'High water pH can gradually raise soil pH', + }); + } + if (env.watering.waterQuality.chlorine === 'yes') { + recommendations.push({ + category: 'water', + priority: 'low', + issue: 'Chlorinated water', + recommendation: 'Let water sit 24h before use or use filter', + impact: 'Chlorine can harm beneficial soil microbes', + }); + } + } + + // Container recommendations + if (env.container && env.container.drainage === 'no') { + recommendations.push({ + category: 'general', + priority: 'critical', + issue: 'Container has no drainage', + recommendation: 'Add drainage holes immediately to prevent root rot', + impact: 'Critical: Standing water will kill most plants', + }); + } + + return recommendations.sort((a, b) => { + const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 }; + return priorityOrder[a.priority] - priorityOrder[b.priority]; + }); +} + +/** + * Find plants with similar growing conditions + */ +export function findSimilarEnvironments( + targetPlant: PlantData, + allPlants: PlantData[], + minScore: number = 70 +): Array<{ plant: PlantData; comparison: EnvironmentalComparison }> { + if (!targetPlant.environment) { + return []; + } + + const similar: Array<{ plant: PlantData; comparison: EnvironmentalComparison }> = []; + + for (const plant of allPlants) { + // Skip self and plants without environment data + if (plant.id === targetPlant.id || !plant.environment) { + continue; + } + + const comparison = compareEnvironments(targetPlant.environment, plant.environment); + comparison.plant1 = targetPlant.id; + comparison.plant2 = plant.id; + + if (comparison.score >= minScore) { + similar.push({ plant, comparison }); + } + } + + // Sort by similarity score (highest first) + return similar.sort((a, b) => b.comparison.score - a.comparison.score); +} + +/** + * Analyze growth success based on environmental factors + */ +export function analyzeGrowthCorrelation(plants: PlantData[]): { + bestConditions: Partial; + insights: string[]; +} { + const successfulPlants = plants.filter( + p => p.environment && (p.status === 'mature' || p.status === 'flowering' || p.status === 'fruiting') + ); + + if (successfulPlants.length === 0) { + return { + bestConditions: {}, + insights: ['Not enough data to determine optimal conditions'], + }; + } + + const insights: string[] = []; + + // Analyze soil pH + const pHValues = successfulPlants + .filter(p => p.environment?.soil.pH) + .map(p => p.environment!.soil.pH); + + if (pHValues.length > 0) { + const avgPH = pHValues.reduce((a, b) => a + b, 0) / pHValues.length; + insights.push(`Successful plants average soil pH: ${avgPH.toFixed(1)}`); + } + + // Analyze temperature + const temps = successfulPlants + .filter(p => p.environment?.climate.temperatureDay) + .map(p => p.environment!.climate.temperatureDay); + + if (temps.length > 0) { + const avgTemp = temps.reduce((a, b) => a + b, 0) / temps.length; + const minTemp = Math.min(...temps); + const maxTemp = Math.max(...temps); + insights.push(`Successful temperature range: ${minTemp}-${maxTemp}Β°C (avg: ${avgTemp.toFixed(1)}Β°C)`); + } + + // Analyze lighting + const lightingTypes = successfulPlants + .filter(p => p.environment?.lighting.type) + .map(p => p.environment!.lighting.type); + + const lightCount = lightingTypes.reduce((acc, type) => { + acc[type] = (acc[type] || 0) + 1; + return acc; + }, {} as Record); + + const mostCommonLight = Object.entries(lightCount) + .sort((a, b) => b[1] - a[1])[0]; + + if (mostCommonLight) { + insights.push(`Most successful lighting: ${mostCommonLight[0]} (${mostCommonLight[1]} plants)`); + } + + return { + bestConditions: { + // Would be filled with actual aggregate data + }, + insights, + }; +} + +/** + * Calculate environmental health score (0-100) + */ +export function calculateEnvironmentalHealth(env: GrowingEnvironment): number { + let score = 100; + let deductions = 0; + + // Soil health (up to -20 points) + if (env.soil.pH < 5.5 || env.soil.pH > 7.5) deductions += 10; + if (env.soil.drainage === 'poor') deductions += 10; + if (env.soil.organicMatter < 3) deductions += 5; + + // Climate (up to -20 points) + if (env.climate.temperatureDay > 35 || env.climate.temperatureDay < 10) deductions += 10; + if (env.climate.humidityAverage < 30 || env.climate.humidityAverage > 80) deductions += 5; + if (env.climate.airflow === 'none') deductions += 5; + + // Lighting (up to -15 points) + if (env.lighting.naturalLight && env.lighting.naturalLight.hoursPerDay < 4) deductions += 15; + + // Container (up to -15 points) + if (env.container && env.container.drainage === 'no') deductions += 15; + + // Water (up to -10 points) + if (env.watering.waterQuality?.chlorine === 'yes') deductions += 5; + if (env.watering.waterQuality?.pH && env.watering.waterQuality.pH > 7.5) deductions += 5; + + return Math.max(0, score - deductions); +} diff --git a/lib/environment/types.ts b/lib/environment/types.ts new file mode 100644 index 0000000..5aed3e9 --- /dev/null +++ b/lib/environment/types.ts @@ -0,0 +1,253 @@ +/** + * Environmental Data Types for LocalGreenChain + * Tracks soil, climate, nutrients, and growing conditions + */ + +// Soil Composition +export interface SoilComposition { + type: 'clay' | 'sand' | 'silt' | 'loam' | 'peat' | 'chalk' | 'custom'; + customType?: string; + + // Soil properties + pH: number; // 0-14, most plants 6.0-7.5 + texture: 'heavy' | 'medium' | 'light'; + drainage: 'poor' | 'moderate' | 'good' | 'excellent'; + organicMatter: number; // percentage 0-100 + + // Composition percentages (should sum to ~100) + clayPercent?: number; + sandPercent?: number; + siltPercent?: number; + + // Amendments/additives + amendments?: SoilAmendment[]; + + notes?: string; +} + +export interface SoilAmendment { + type: 'compost' | 'manure' | 'perlite' | 'vermiculite' | 'peat_moss' | 'coco_coir' | 'biochar' | 'lime' | 'sulfur' | 'other'; + name: string; + amount?: string; // e.g., "2 cups per gallon", "10%" + dateAdded: string; +} + +// Nutrients & Fertilization +export interface NutrientProfile { + // NPK values (percentage) + nitrogen: number; // N + phosphorus: number; // P + potassium: number; // K + + // Secondary nutrients + calcium?: number; + magnesium?: number; + sulfur?: number; + + // Micronutrients (ppm or mg/L) + iron?: number; + manganese?: number; + zinc?: number; + copper?: number; + boron?: number; + molybdenum?: number; + + // EC (Electrical Conductivity) - measure of nutrient concentration + ec?: number; // mS/cm + + // TDS (Total Dissolved Solids) + tds?: number; // ppm + + lastTested?: string; +} + +export interface FertilizerApplication { + id: string; + date: string; + type: 'organic' | 'synthetic' | 'liquid' | 'granular' | 'foliar' | 'slow_release'; + name: string; + npk?: string; // e.g., "10-10-10", "5-2-3" + amount: string; + frequency?: string; // e.g., "weekly", "bi-weekly", "monthly" + notes?: string; +} + +// Sunlight & Climate +export interface LightingConditions { + type: 'natural' | 'artificial' | 'mixed'; + + // Natural light + naturalLight?: { + exposure: 'full_sun' | 'partial_sun' | 'partial_shade' | 'full_shade'; + hoursPerDay: number; // 0-24 + direction: 'north' | 'south' | 'east' | 'west' | 'multiple'; + quality: 'direct' | 'filtered' | 'dappled' | 'indirect'; + }; + + // Artificial light + artificialLight?: { + type: 'LED' | 'fluorescent' | 'HPS' | 'MH' | 'incandescent' | 'mixed'; + spectrum?: string; // e.g., "full spectrum", "6500K", "2700K" + wattage?: number; + hoursPerDay: number; + distance?: number; // cm from plant + }; + + // Light measurements + ppfd?: number; // Photosynthetic Photon Flux Density (ΞΌmol/mΒ²/s) + dli?: number; // Daily Light Integral (mol/mΒ²/day) +} + +export interface ClimateConditions { + // Temperature (Celsius) + temperatureDay: number; + temperatureNight: number; + temperatureMin?: number; + temperatureMax?: number; + + // Humidity (percentage) + humidityAverage: number; + humidityMin?: number; + humidityMax?: number; + + // Air circulation + airflow: 'none' | 'minimal' | 'moderate' | 'strong'; + ventilation: 'poor' | 'adequate' | 'good' | 'excellent'; + + // CO2 levels (ppm) + co2?: number; // ambient ~400, enhanced ~1200-1500 + + // Seasonal variation + season?: 'spring' | 'summer' | 'fall' | 'winter'; + zone?: string; // USDA hardiness zone, e.g., "9b", "10a" +} + +// Growing Environment +export interface GrowingEnvironment { + location: EnvironmentLocation; + container?: ContainerInfo; + soil: SoilComposition; + nutrients: NutrientProfile; + fertilizers?: FertilizerApplication[]; + lighting: LightingConditions; + climate: ClimateConditions; + watering: WateringSchedule; + surroundings?: SurroundingEnvironment; + + // Tracking + monitoringFrequency?: 'daily' | 'weekly' | 'bi-weekly' | 'monthly'; + lastUpdated: string; + notes?: string; +} + +export interface EnvironmentLocation { + type: 'indoor' | 'outdoor' | 'greenhouse' | 'polytunnel' | 'shade_house' | 'window' | 'balcony'; + description?: string; + + // For indoor + room?: string; // e.g., "bedroom", "basement", "grow tent" + + // For outdoor + exposureToElements?: 'protected' | 'semi_protected' | 'exposed'; + elevation?: number; // meters above sea level + slope?: 'flat' | 'gentle' | 'moderate' | 'steep'; + aspect?: 'north' | 'south' | 'east' | 'west'; // slope direction +} + +export interface ContainerInfo { + type: 'pot' | 'raised_bed' | 'ground' | 'hydroponic' | 'aeroponic' | 'aquaponic' | 'fabric_pot' | 'hanging_basket'; + material?: 'plastic' | 'terracotta' | 'ceramic' | 'fabric' | 'wood' | 'metal' | 'concrete'; + size?: string; // e.g., "5 gallon", "30cm diameter", "4x8 feet" + volume?: number; // liters + depth?: number; // cm + drainage: 'yes' | 'no'; + drainageHoles?: number; +} + +export interface WateringSchedule { + method: 'hand_water' | 'drip' | 'soaker_hose' | 'sprinkler' | 'self_watering' | 'hydroponic' | 'rain'; + frequency?: string; // e.g., "daily", "every 2-3 days", "weekly" + amount?: string; // e.g., "1 liter", "until runoff", "1 inch" + waterSource: 'tap' | 'well' | 'rain' | 'filtered' | 'distilled' | 'RO'; + waterQuality?: { + pH?: number; + tds?: number; // ppm + chlorine?: 'yes' | 'no' | 'filtered'; + }; +} + +export interface SurroundingEnvironment { + // Companion plants + companionPlants?: string[]; // other plant species nearby + + // Nearby features + nearbyTrees?: boolean; + nearbyStructures?: string; // e.g., "building", "wall", "fence" + groundCover?: string; // e.g., "mulch", "grass", "bare soil", "gravel" + + // Wildlife & pests + pollinators?: string[]; // e.g., "bees", "butterflies", "hummingbirds" + beneficialInsects?: string[]; // e.g., "ladybugs", "lacewings" + pests?: PestInfo[]; + diseases?: DiseaseInfo[]; + + // Microclimate factors + windExposure?: 'sheltered' | 'moderate' | 'exposed' | 'windy'; + frostPocket?: boolean; + heatTrap?: boolean; + + // Ecosystem type + ecosystem?: 'urban' | 'suburban' | 'rural' | 'forest' | 'desert' | 'coastal' | 'mountain' | 'tropical'; +} + +export interface PestInfo { + name: string; + severity: 'minor' | 'moderate' | 'severe'; + treatment?: string; + dateObserved: string; +} + +export interface DiseaseInfo { + name: string; + symptoms?: string; + severity: 'minor' | 'moderate' | 'severe'; + treatment?: string; + dateObserved: string; +} + +// Environmental Comparison & Analysis +export interface EnvironmentalComparison { + plant1: string; // plant ID + plant2: string; // plant ID + similarities: string[]; + differences: string[]; + score: number; // 0-100, how similar the environments are +} + +export interface GrowthMetrics { + plantId: string; + measurements: PlantMeasurement[]; + healthScore: number; // 0-100 + vigor: 'poor' | 'fair' | 'good' | 'excellent'; + issues?: string[]; +} + +export interface PlantMeasurement { + date: string; + height?: number; // cm + width?: number; // cm + leafCount?: number; + flowerCount?: number; + fruitCount?: number; + notes?: string; + photos?: string[]; +} + +// Helper types for environmental recommendations +export interface EnvironmentalRecommendation { + category: 'soil' | 'nutrients' | 'light' | 'water' | 'climate' | 'general'; + priority: 'low' | 'medium' | 'high' | 'critical'; + issue: string; + recommendation: string; + impact: string; +} diff --git a/lib/privacy/anonymity.ts b/lib/privacy/anonymity.ts new file mode 100644 index 0000000..a2380af --- /dev/null +++ b/lib/privacy/anonymity.ts @@ -0,0 +1,221 @@ +import crypto from 'crypto'; + +/** + * Privacy and Anonymity Utilities for LocalGreenChain + * Provides tools for anonymous plant tracking while maintaining blockchain integrity + */ + +export interface PrivacySettings { + anonymousMode: boolean; + locationPrivacy: 'exact' | 'fuzzy' | 'city' | 'country' | 'hidden'; + identityPrivacy: 'real' | 'pseudonym' | 'anonymous'; + sharePlantDetails: boolean; +} + +export interface FuzzyLocation { + latitude: number; + longitude: number; + accuracy: number; // radius in km + displayName: string; +} + +/** + * Generate anonymous user ID using cryptographic hash + */ +export function generateAnonymousId(): string { + const randomBytes = crypto.randomBytes(32); + return 'anon_' + crypto.createHash('sha256').update(randomBytes).digest('hex').substring(0, 16); +} + +/** + * Generate pseudonymous wallet address + */ +export function generateWalletAddress(): string { + const randomBytes = crypto.randomBytes(20); + return '0x' + randomBytes.toString('hex'); +} + +/** + * Obfuscate location based on privacy level + * This prevents exact home address tracking while allowing geographic discovery + */ +export function obfuscateLocation( + latitude: number, + longitude: number, + privacyLevel: 'exact' | 'fuzzy' | 'city' | 'country' | 'hidden' +): FuzzyLocation { + switch (privacyLevel) { + case 'exact': + return { + latitude, + longitude, + accuracy: 0.1, // ~100m + displayName: 'Exact location', + }; + + case 'fuzzy': + // Add random offset within 1-5km radius + const fuzzRadius = 1 + Math.random() * 4; // 1-5 km + const angle = Math.random() * 2 * Math.PI; + const latOffset = (fuzzRadius / 111) * Math.cos(angle); // 111 km per degree + const lonOffset = (fuzzRadius / (111 * Math.cos(latitude * Math.PI / 180))) * Math.sin(angle); + + return { + latitude: latitude + latOffset, + longitude: longitude + lonOffset, + accuracy: fuzzRadius, + displayName: `Within ${Math.round(fuzzRadius)} km`, + }; + + case 'city': + // Round to ~10km grid (0.1 degree β‰ˆ 11km) + return { + latitude: Math.round(latitude * 10) / 10, + longitude: Math.round(longitude * 10) / 10, + accuracy: 10, + displayName: 'City area', + }; + + case 'country': + // Round to ~100km grid (1 degree β‰ˆ 111km) + return { + latitude: Math.round(latitude), + longitude: Math.round(longitude), + accuracy: 100, + displayName: 'Country/Region', + }; + + case 'hidden': + return { + latitude: 0, + longitude: 0, + accuracy: 999999, + displayName: 'Location hidden', + }; + + default: + return obfuscateLocation(latitude, longitude, 'fuzzy'); + } +} + +/** + * Generate anonymous plant name + */ +export function generateAnonymousPlantName(plantType: string, generation: number): string { + const hash = crypto.randomBytes(4).toString('hex'); + return `${plantType}-Gen${generation}-${hash}`; +} + +/** + * Encrypt sensitive data for storage + */ +export function encryptData(data: string, key: string): string { + const algorithm = 'aes-256-cbc'; + const keyHash = crypto.createHash('sha256').update(key).digest(); + const iv = crypto.randomBytes(16); + + const cipher = crypto.createCipheriv(algorithm, keyHash, iv); + let encrypted = cipher.update(data, 'utf8', 'hex'); + encrypted += cipher.final('hex'); + + return iv.toString('hex') + ':' + encrypted; +} + +/** + * Decrypt sensitive data + */ +export function decryptData(encryptedData: string, key: string): string { + const algorithm = 'aes-256-cbc'; + const keyHash = crypto.createHash('sha256').update(key).digest(); + + const parts = encryptedData.split(':'); + const iv = Buffer.from(parts[0], 'hex'); + const encrypted = parts[1]; + + const decipher = crypto.createDecipheriv(algorithm, keyHash, iv); + let decrypted = decipher.update(encrypted, 'hex', 'utf8'); + decrypted += decipher.final('utf8'); + + return decrypted; +} + +/** + * Generate Tor-friendly onion address from plant ID + * This creates a deterministic onion-style identifier + */ +export function generateOnionIdentifier(plantId: string): string { + const hash = crypto.createHash('sha256').update(plantId).digest('hex'); + return hash.substring(0, 16) + '.onion'; +} + +/** + * Create anonymous contact method + */ +export function createAnonymousContact(realEmail: string, privateKey: string): string { + // Hash email with private key to create anonymous identifier + const hash = crypto.createHash('sha256') + .update(realEmail + privateKey) + .digest('hex'); + return `anon-${hash.substring(0, 12)}@localgreenchain.onion`; +} + +/** + * Validate Tor connection + */ +export async function isTorConnection(req: any): Promise { + // Check if request is coming through Tor + const forwardedFor = req.headers['x-forwarded-for']; + const realIp = req.headers['x-real-ip']; + + // Tor exit nodes typically set specific headers + // This is a simplified check + return ( + req.headers['x-tor-connection'] === 'true' || + req.socket?.remoteAddress?.includes('127.0.0.1') // Tor proxy + ); +} + +/** + * Generate privacy report for plant + */ +export interface PrivacyReport { + locationPrivacy: string; + identityPrivacy: string; + dataEncryption: boolean; + torEnabled: boolean; + riskLevel: 'low' | 'medium' | 'high'; + recommendations: string[]; +} + +export function generatePrivacyReport(settings: PrivacySettings): PrivacyReport { + const recommendations: string[] = []; + let riskLevel: 'low' | 'medium' | 'high' = 'low'; + + if (settings.locationPrivacy === 'exact') { + recommendations.push('Consider using fuzzy location to protect your privacy'); + riskLevel = 'high'; + } + + if (settings.identityPrivacy === 'real') { + recommendations.push('Using real identity may compromise anonymity'); + if (riskLevel === 'low') riskLevel = 'medium'; + } + + if (settings.sharePlantDetails && !settings.anonymousMode) { + recommendations.push('Sharing detailed plant info without anonymous mode enabled'); + } + + if (settings.anonymousMode && settings.locationPrivacy !== 'hidden') { + riskLevel = 'low'; + recommendations.push('Good privacy settings enabled'); + } + + return { + locationPrivacy: settings.locationPrivacy, + identityPrivacy: settings.identityPrivacy, + dataEncryption: settings.anonymousMode, + torEnabled: false, // Will be detected at runtime + riskLevel, + recommendations: recommendations.length > 0 ? recommendations : ['Privacy settings are optimal'], + }; +} diff --git a/lib/services/geolocation.ts b/lib/services/geolocation.ts new file mode 100644 index 0000000..cf273b1 --- /dev/null +++ b/lib/services/geolocation.ts @@ -0,0 +1,302 @@ +/** + * Geolocation Service + * Provides location-based features for connecting plant owners + */ + +import { PlantData, PlantLocation } from '../blockchain/types'; + +export interface PlantCluster { + centerLat: number; + centerLon: number; + plantCount: number; + plants: PlantData[]; + radius: number; // in km + dominantSpecies: string[]; +} + +export interface ConnectionSuggestion { + plant1: PlantData; + plant2: PlantData; + distance: number; + matchReason: string; // e.g., "same species", "same lineage", "nearby location" + compatibilityScore: number; // 0-100 +} + +export class GeolocationService { + /** + * Calculate distance between two coordinates using Haversine formula + */ + calculateDistance( + lat1: number, + lon1: number, + lat2: number, + lon2: number + ): number { + const R = 6371; // Earth's radius in km + const dLat = this.toRadians(lat2 - lat1); + const dLon = this.toRadians(lon2 - lon1); + + const a = + Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(this.toRadians(lat1)) * + Math.cos(this.toRadians(lat2)) * + Math.sin(dLon / 2) * + Math.sin(dLon / 2); + + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + return R * c; + } + + private toRadians(degrees: number): number { + return degrees * (Math.PI / 180); + } + + /** + * Find plant clusters in a region + * Groups nearby plants together to show hotspots of activity + */ + findPlantClusters( + plants: PlantData[], + clusterRadius: number = 10 // km + ): PlantCluster[] { + const clusters: PlantCluster[] = []; + const processed = new Set(); + + for (const plant of plants) { + if (processed.has(plant.id)) continue; + + // Find all plants within cluster radius + const clusterPlants: PlantData[] = [plant]; + processed.add(plant.id); + + for (const otherPlant of plants) { + if (processed.has(otherPlant.id)) continue; + + const distance = this.calculateDistance( + plant.location.latitude, + plant.location.longitude, + otherPlant.location.latitude, + otherPlant.location.longitude + ); + + if (distance <= clusterRadius) { + clusterPlants.push(otherPlant); + processed.add(otherPlant.id); + } + } + + // Calculate cluster center (average of all positions) + const centerLat = + clusterPlants.reduce((sum, p) => sum + p.location.latitude, 0) / + clusterPlants.length; + const centerLon = + clusterPlants.reduce((sum, p) => sum + p.location.longitude, 0) / + clusterPlants.length; + + // Find dominant species + const speciesCount: { [key: string]: number } = {}; + for (const p of clusterPlants) { + if (p.scientificName) { + speciesCount[p.scientificName] = + (speciesCount[p.scientificName] || 0) + 1; + } + } + + const dominantSpecies = Object.entries(speciesCount) + .sort((a, b) => b[1] - a[1]) + .slice(0, 3) + .map(([species]) => species); + + clusters.push({ + centerLat, + centerLon, + plantCount: clusterPlants.length, + plants: clusterPlants, + radius: clusterRadius, + dominantSpecies, + }); + } + + return clusters.sort((a, b) => b.plantCount - a.plantCount); + } + + /** + * Suggest connections between plant owners + * Finds compatible plants for sharing/trading + */ + suggestConnections( + userPlant: PlantData, + allPlants: PlantData[], + maxDistance: number = 50 // km + ): ConnectionSuggestion[] { + const suggestions: ConnectionSuggestion[] = []; + + for (const otherPlant of allPlants) { + // Skip if same owner + if (otherPlant.owner.id === userPlant.owner.id) continue; + + // Skip if same plant + if (otherPlant.id === userPlant.id) continue; + + const distance = this.calculateDistance( + userPlant.location.latitude, + userPlant.location.longitude, + otherPlant.location.latitude, + otherPlant.location.longitude + ); + + // Skip if too far + if (distance > maxDistance) continue; + + let matchReason = ''; + let compatibilityScore = 0; + + // Check for same species + if ( + userPlant.scientificName && + userPlant.scientificName === otherPlant.scientificName + ) { + matchReason = 'Same species'; + compatibilityScore += 40; + } + + // Check for same lineage + if ( + userPlant.parentPlantId === otherPlant.parentPlantId && + userPlant.parentPlantId + ) { + matchReason = + matchReason + (matchReason ? ', ' : '') + 'Same parent plant'; + compatibilityScore += 30; + } + + // Check for same genus + if (userPlant.genus && userPlant.genus === otherPlant.genus) { + if (!matchReason) matchReason = 'Same genus'; + compatibilityScore += 20; + } + + // Proximity bonus + const proximityScore = Math.max(0, 20 - distance / 2.5); + compatibilityScore += proximityScore; + + // Only suggest if there's some compatibility + if (compatibilityScore > 20) { + if (!matchReason) matchReason = 'Nearby location'; + + suggestions.push({ + plant1: userPlant, + plant2: otherPlant, + distance, + matchReason, + compatibilityScore: Math.min(100, compatibilityScore), + }); + } + } + + return suggestions.sort((a, b) => b.compatibilityScore - a.compatibilityScore); + } + + /** + * Get address from coordinates using reverse geocoding + * Note: In production, you'd use a service like Google Maps or OpenStreetMap + */ + async reverseGeocode( + latitude: number, + longitude: number + ): Promise> { + try { + // Using OpenStreetMap Nominatim (free, but rate-limited) + const response = await fetch( + `https://nominatim.openstreetmap.org/reverse?format=json&lat=${latitude}&lon=${longitude}&zoom=18&addressdetails=1`, + { + headers: { + 'User-Agent': 'LocalGreenChain/1.0', + }, + } + ); + + if (!response.ok) { + console.error('Reverse geocoding error:', response.statusText); + return { latitude, longitude }; + } + + const data = await response.json(); + + return { + latitude, + longitude, + address: data.display_name, + city: data.address?.city || data.address?.town || data.address?.village, + country: data.address?.country, + }; + } catch (error) { + console.error('Error in reverse geocoding:', error); + return { latitude, longitude }; + } + } + + /** + * Get coordinates from address using forward geocoding + */ + async geocode( + address: string + ): Promise<{ latitude: number; longitude: number } | null> { + try { + const response = await fetch( + `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(address)}`, + { + headers: { + 'User-Agent': 'LocalGreenChain/1.0', + }, + } + ); + + if (!response.ok) { + console.error('Geocoding error:', response.statusText); + return null; + } + + const data = await response.json(); + if (data.length === 0) return null; + + return { + latitude: parseFloat(data[0].lat), + longitude: parseFloat(data[0].lon), + }; + } catch (error) { + console.error('Error in geocoding:', error); + return null; + } + } + + /** + * Check if a location is within a given boundary + */ + isWithinBounds( + location: { latitude: number; longitude: number }, + bounds: { + north: number; + south: number; + east: number; + west: number; + } + ): boolean { + return ( + location.latitude <= bounds.north && + location.latitude >= bounds.south && + location.longitude <= bounds.east && + location.longitude >= bounds.west + ); + } +} + +// Singleton instance +let geolocationService: GeolocationService | null = null; + +export function getGeolocationService(): GeolocationService { + if (!geolocationService) { + geolocationService = new GeolocationService(); + } + return geolocationService; +} diff --git a/lib/services/plantsnet.ts b/lib/services/plantsnet.ts new file mode 100644 index 0000000..5ab9919 --- /dev/null +++ b/lib/services/plantsnet.ts @@ -0,0 +1,225 @@ +/** + * Plants.net API Integration Service + * Provides connectivity to the plants.net API for plant identification, + * data enrichment, and community features + */ + +export interface PlantsNetSearchResult { + id: string; + commonName: string; + scientificName: string; + genus?: string; + family?: string; + imageUrl?: string; + description?: string; + careInstructions?: string; +} + +export interface PlantsNetCommunity { + nearbyGrowers: { + userId: string; + username: string; + location: { + city?: string; + country?: string; + distance?: number; + }; + plantsOwned: string[]; + }[]; +} + +export class PlantsNetService { + private apiKey: string; + private baseUrl: string; + + constructor(apiKey?: string) { + this.apiKey = apiKey || process.env.PLANTS_NET_API_KEY || ''; + this.baseUrl = 'https://api.plants.net/v1'; + } + + /** + * Search for plant by common or scientific name + */ + async searchPlant(query: string): Promise { + try { + const response = await fetch( + `${this.baseUrl}/search?q=${encodeURIComponent(query)}`, + { + headers: { + 'Authorization': `Bearer ${this.apiKey}`, + 'Content-Type': 'application/json', + }, + } + ); + + if (!response.ok) { + console.error('Plants.net API error:', response.statusText); + return []; + } + + const data = await response.json(); + return data.results || []; + } catch (error) { + console.error('Error searching plants.net:', error); + return []; + } + } + + /** + * Get detailed plant information by ID + */ + async getPlantDetails(plantId: string): Promise { + try { + const response = await fetch(`${this.baseUrl}/plants/${plantId}`, { + headers: { + 'Authorization': `Bearer ${this.apiKey}`, + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + console.error('Plants.net API error:', response.statusText); + return null; + } + + return await response.json(); + } catch (error) { + console.error('Error fetching plant details:', error); + return null; + } + } + + /** + * Find nearby growers who have similar plants + */ + async findNearbyGrowers( + plantSpecies: string, + latitude: number, + longitude: number, + radiusKm: number = 50 + ): Promise { + try { + const response = await fetch( + `${this.baseUrl}/community/nearby?species=${encodeURIComponent(plantSpecies)}&lat=${latitude}&lon=${longitude}&radius=${radiusKm}`, + { + headers: { + 'Authorization': `Bearer ${this.apiKey}`, + 'Content-Type': 'application/json', + }, + } + ); + + if (!response.ok) { + console.error('Plants.net API error:', response.statusText); + return { nearbyGrowers: [] }; + } + + return await response.json(); + } catch (error) { + console.error('Error finding nearby growers:', error); + return { nearbyGrowers: [] }; + } + } + + /** + * Identify plant from image (if API supports it) + */ + async identifyPlantFromImage(imageUrl: string): Promise { + try { + const response = await fetch(`${this.baseUrl}/identify`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${this.apiKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ imageUrl }), + }); + + if (!response.ok) { + console.error('Plants.net API error:', response.statusText); + return []; + } + + const data = await response.json(); + return data.suggestions || []; + } catch (error) { + console.error('Error identifying plant:', error); + return []; + } + } + + /** + * Get care instructions for a plant + */ + async getCareInstructions(scientificName: string): Promise { + try { + const response = await fetch( + `${this.baseUrl}/care/${encodeURIComponent(scientificName)}`, + { + headers: { + 'Authorization': `Bearer ${this.apiKey}`, + 'Content-Type': 'application/json', + }, + } + ); + + if (!response.ok) { + console.error('Plants.net API error:', response.statusText); + return null; + } + + const data = await response.json(); + return data.careInstructions || null; + } catch (error) { + console.error('Error fetching care instructions:', error); + return null; + } + } + + /** + * Report a plant to the plants.net network + * This allows integration with their global plant tracking + */ + async reportPlantToNetwork(plantData: { + commonName: string; + scientificName?: string; + location: { latitude: number; longitude: number }; + ownerId: string; + propagationType?: string; + }): Promise<{ success: boolean; plantsNetId?: string }> { + try { + const response = await fetch(`${this.baseUrl}/reports`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${this.apiKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(plantData), + }); + + if (!response.ok) { + console.error('Plants.net API error:', response.statusText); + return { success: false }; + } + + const data = await response.json(); + return { + success: true, + plantsNetId: data.id, + }; + } catch (error) { + console.error('Error reporting plant to network:', error); + return { success: false }; + } + } +} + +// Singleton instance +let plantsNetService: PlantsNetService | null = null; + +export function getPlantsNetService(): PlantsNetService { + if (!plantsNetService) { + plantsNetService = new PlantsNetService(); + } + return plantsNetService; +} diff --git a/lib/services/tor.ts b/lib/services/tor.ts new file mode 100644 index 0000000..6a724db --- /dev/null +++ b/lib/services/tor.ts @@ -0,0 +1,172 @@ +/** + * Tor Integration Service + * Provides Tor network connectivity for anonymous plant sharing + */ + +import { SocksProxyAgent } from 'socks-proxy-agent'; + +export interface TorConfig { + enabled: boolean; + socksHost: string; + socksPort: number; + controlPort: number; + hiddenServiceDir?: string; +} + +export class TorService { + private config: TorConfig; + private proxyAgent: any; + + constructor(config?: Partial) { + this.config = { + enabled: process.env.TOR_ENABLED === 'true', + socksHost: process.env.TOR_SOCKS_HOST || '127.0.0.1', + socksPort: parseInt(process.env.TOR_SOCKS_PORT || '9050'), + controlPort: parseInt(process.env.TOR_CONTROL_PORT || '9051'), + hiddenServiceDir: process.env.TOR_HIDDEN_SERVICE_DIR, + ...config, + }; + + if (this.config.enabled) { + this.initializeProxy(); + } + } + + /** + * Initialize SOCKS proxy for Tor + */ + private initializeProxy(): void { + const proxyUrl = `socks5://${this.config.socksHost}:${this.config.socksPort}`; + this.proxyAgent = new SocksProxyAgent(proxyUrl); + } + + /** + * Check if Tor is available and running + */ + async isAvailable(): Promise { + if (!this.config.enabled) return false; + + try { + // Try to fetch check.torproject.org through Tor + const response = await fetch('https://check.torproject.org/api/ip', { + // @ts-ignore + agent: this.proxyAgent, + signal: AbortSignal.timeout(5000), + }); + + const data = await response.json(); + return data.IsTor === true; + } catch (error) { + console.error('Tor availability check failed:', error); + return false; + } + } + + /** + * Fetch data through Tor network + */ + async fetchThroughTor(url: string, options: RequestInit = {}): Promise { + if (!this.config.enabled) { + throw new Error('Tor is not enabled'); + } + + return fetch(url, { + ...options, + // @ts-ignore + agent: this.proxyAgent, + }); + } + + /** + * Get current Tor circuit info + */ + async getCircuitInfo(): Promise<{ country?: string; ip?: string }> { + try { + const response = await this.fetchThroughTor('https://check.torproject.org/api/ip'); + return await response.json(); + } catch (error) { + return {}; + } + } + + /** + * Request new Tor identity (new circuit) + */ + async requestNewIdentity(): Promise { + // This would require Tor control port access + // For now, just return true if Tor is enabled + return this.config.enabled; + } + + /** + * Get onion service hostname if available + */ + getOnionAddress(): string | null { + if (!this.config.hiddenServiceDir) return null; + + try { + const fs = require('fs'); + const path = require('path'); + const hostnamePath = path.join(this.config.hiddenServiceDir, 'hostname'); + + if (fs.existsSync(hostnamePath)) { + return fs.readFileSync(hostnamePath, 'utf-8').trim(); + } + } catch (error) { + console.error('Error reading onion address:', error); + } + + return null; + } + + /** + * Generate QR code for onion address + */ + async generateOnionQRCode(): Promise { + const onionAddress = this.getOnionAddress(); + if (!onionAddress) return null; + + // In production, you'd use a QR code library + // For now, return the address + return `http://${onionAddress}`; + } + + /** + * Check if request came through Tor + */ + isRequestFromTor(headers: any): boolean { + // Check various indicators that request came through Tor + return ( + headers['x-tor-connection'] === 'true' || + headers['x-forwarded-proto'] === 'tor' || + false + ); + } +} + +// Singleton instance +let torService: TorService | null = null; + +export function getTorService(): TorService { + if (!torService) { + torService = new TorService(); + } + return torService; +} + +/** + * Middleware to detect and mark Tor connections + */ +export function torDetectionMiddleware(req: any, res: any, next: any) { + const torService = getTorService(); + + // Mark request as coming from Tor if detected + req.isTorConnection = torService.isRequestFromTor(req.headers); + + // Add Tor status to response headers + if (req.isTorConnection) { + res.setHeader('X-Tor-Enabled', 'true'); + } + + next(); +} diff --git a/package.json b/package.json index 1d771b6..c025b3f 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,18 @@ { - "name": "example-marketing", - "version": "1.5.0", + "name": "localgreenchain", + "version": "1.0.0", "private": true, "license": "MIT", "scripts": { "dev": "next dev -p 3001", "build": "next build", - "preview": "next build && next start -p 3001", + "start": "next start -p 3001", + "preview": "bun run build && bun run start", "lint": "next lint", "cy:open": "cypress open", "cy:run": "cypress run", - "test:e2e": "start-server-and-test 'yarn preview' http://localhost:3001 cy:open", - "test:e2e:ci": "start-server-and-test 'yarn preview' http://localhost:3001 cy:run" + "test:e2e": "start-server-and-test 'bun run preview' http://localhost:3001 cy:open", + "test:e2e:ci": "start-server-and-test 'bun run preview' http://localhost:3001 cy:run" }, "dependencies": { "@tailwindcss/forms": "^0.4.0", @@ -25,7 +26,8 @@ "nprogress": "^0.2.0", "react": "^17.0.2", "react-dom": "^17.0.2", - "react-hook-form": "^7.8.6" + "react-hook-form": "^7.8.6", + "socks-proxy-agent": "^8.0.2" }, "devDependencies": { "@babel/core": "^7.12.9", diff --git a/pages/api/environment/analysis.ts b/pages/api/environment/analysis.ts new file mode 100644 index 0000000..f0a3165 --- /dev/null +++ b/pages/api/environment/analysis.ts @@ -0,0 +1,95 @@ +/** + * API Route: Analyze growth correlations across all plants + * GET /api/environment/analysis?species=tomato (optional species filter) + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getBlockchain } from '../../../lib/blockchain/manager'; +import { analyzeGrowthCorrelation } from '../../../lib/environment/analysis'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'GET') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + try { + const { species } = req.query; + + const blockchain = getBlockchain(); + + // Get all plants + let allPlants = Array.from( + new Set(blockchain.chain.map(block => block.plant.id)) + ) + .map(id => blockchain.getPlant(id)!) + .filter(Boolean); + + // Filter by species if specified + if (species && typeof species === 'string') { + allPlants = allPlants.filter( + p => + p.scientificName?.toLowerCase().includes(species.toLowerCase()) || + p.commonName?.toLowerCase().includes(species.toLowerCase()) + ); + } + + // Only include plants with environmental data + const plantsWithEnv = allPlants.filter(p => p.environment); + + if (plantsWithEnv.length === 0) { + return res.status(200).json({ + success: true, + message: 'No plants with environmental data found', + plantsAnalyzed: 0, + }); + } + + const analysis = analyzeGrowthCorrelation(plantsWithEnv); + + // Calculate some additional statistics + const successRate = + (plantsWithEnv.filter( + p => p.status === 'mature' || p.status === 'flowering' || p.status === 'fruiting' + ).length / + plantsWithEnv.length) * + 100; + + const locationTypes = plantsWithEnv.reduce((acc, p) => { + const type = p.environment!.location.type; + acc[type] = (acc[type] || 0) + 1; + return acc; + }, {} as Record); + + const soilTypes = plantsWithEnv.reduce((acc, p) => { + const type = p.environment!.soil.type; + acc[type] = (acc[type] || 0) + 1; + return acc; + }, {} as Record); + + res.status(200).json({ + success: true, + plantsAnalyzed: plantsWithEnv.length, + successRate: Math.round(successRate), + insights: analysis.insights, + statistics: { + locationTypes, + soilTypes, + avgTemperature: + plantsWithEnv.reduce((sum, p) => sum + (p.environment!.climate.temperatureDay || 0), 0) / + plantsWithEnv.length, + avgHumidity: + plantsWithEnv.reduce((sum, p) => sum + (p.environment!.climate.humidityAverage || 0), 0) / + plantsWithEnv.length, + avgSoilPH: + plantsWithEnv.reduce((sum, p) => sum + (p.environment!.soil.pH || 0), 0) / + plantsWithEnv.length, + }, + }); + } catch (error: any) { + console.error('Error analyzing growth correlation:', error); + res.status(500).json({ error: error.message || 'Internal server error' }); + } +} diff --git a/pages/api/environment/compare.ts b/pages/api/environment/compare.ts new file mode 100644 index 0000000..9cfa1be --- /dev/null +++ b/pages/api/environment/compare.ts @@ -0,0 +1,80 @@ +/** + * API Route: Compare growing environments of two plants + * GET /api/environment/compare?plant1=xyz&plant2=abc + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getBlockchain } from '../../../lib/blockchain/manager'; +import { compareEnvironments } from '../../../lib/environment/analysis'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'GET') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + try { + const { plant1, plant2 } = req.query; + + if (!plant1 || !plant2 || typeof plant1 !== 'string' || typeof plant2 !== 'string') { + return res.status(400).json({ error: 'Missing plant1 and plant2 parameters' }); + } + + const blockchain = getBlockchain(); + const plantData1 = blockchain.getPlant(plant1); + const plantData2 = blockchain.getPlant(plant2); + + if (!plantData1 || !plantData2) { + return res.status(404).json({ error: 'One or both plants not found' }); + } + + if (!plantData1.environment || !plantData2.environment) { + return res.status(400).json({ + error: 'Both plants must have environmental data', + plant1HasData: !!plantData1.environment, + plant2HasData: !!plantData2.environment, + }); + } + + const comparison = compareEnvironments(plantData1.environment, plantData2.environment); + comparison.plant1 = plant1; + comparison.plant2 = plant2; + + res.status(200).json({ + success: true, + plants: { + plant1: { + id: plantData1.id, + commonName: plantData1.commonName, + scientificName: plantData1.scientificName, + owner: plantData1.owner.name, + }, + plant2: { + id: plantData2.id, + commonName: plantData2.commonName, + scientificName: plantData2.scientificName, + owner: plantData2.owner.name, + }, + }, + comparison: { + similarityScore: comparison.score, + similarities: comparison.similarities, + differences: comparison.differences, + }, + interpretation: getScoreInterpretation(comparison.score), + }); + } catch (error: any) { + console.error('Error comparing environments:', error); + res.status(500).json({ error: error.message || 'Internal server error' }); + } +} + +function getScoreInterpretation(score: number): string { + if (score >= 90) return 'Nearly identical growing conditions'; + if (score >= 75) return 'Very similar environments - likely to have similar results'; + if (score >= 60) return 'Moderately similar conditions'; + if (score >= 40) return 'Some similarities but notable differences'; + return 'Very different growing environments'; +} diff --git a/pages/api/environment/recommendations.ts b/pages/api/environment/recommendations.ts new file mode 100644 index 0000000..af04630 --- /dev/null +++ b/pages/api/environment/recommendations.ts @@ -0,0 +1,61 @@ +/** + * API Route: Get environmental recommendations for a plant + * GET /api/environment/recommendations?plantId=xyz + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getBlockchain } from '../../../lib/blockchain/manager'; +import { + generateRecommendations, + calculateEnvironmentalHealth, +} from '../../../lib/environment/analysis'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'GET') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + try { + const { plantId } = req.query; + + if (!plantId || typeof plantId !== 'string') { + return res.status(400).json({ error: 'Missing plantId parameter' }); + } + + const blockchain = getBlockchain(); + const plant = blockchain.getPlant(plantId); + + if (!plant) { + return res.status(404).json({ error: 'Plant not found' }); + } + + const recommendations = generateRecommendations(plant); + + const healthScore = plant.environment + ? calculateEnvironmentalHealth(plant.environment) + : null; + + res.status(200).json({ + success: true, + plant: { + id: plant.id, + commonName: plant.commonName, + scientificName: plant.scientificName, + }, + environmentalHealth: healthScore, + recommendations, + summary: { + criticalIssues: recommendations.filter(r => r.priority === 'critical').length, + highPriority: recommendations.filter(r => r.priority === 'high').length, + mediumPriority: recommendations.filter(r => r.priority === 'medium').length, + lowPriority: recommendations.filter(r => r.priority === 'low').length, + }, + }); + } catch (error: any) { + console.error('Error generating recommendations:', error); + res.status(500).json({ error: error.message || 'Internal server error' }); + } +} diff --git a/pages/api/environment/similar.ts b/pages/api/environment/similar.ts new file mode 100644 index 0000000..1ca66ab --- /dev/null +++ b/pages/api/environment/similar.ts @@ -0,0 +1,74 @@ +/** + * API Route: Find plants with similar growing environments + * GET /api/environment/similar?plantId=xyz&minScore=70 + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getBlockchain } from '../../../lib/blockchain/manager'; +import { findSimilarEnvironments } from '../../../lib/environment/analysis'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'GET') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + try { + const { plantId, minScore } = req.query; + + if (!plantId || typeof plantId !== 'string') { + return res.status(400).json({ error: 'Missing plantId parameter' }); + } + + const blockchain = getBlockchain(); + const targetPlant = blockchain.getPlant(plantId); + + if (!targetPlant) { + return res.status(404).json({ error: 'Plant not found' }); + } + + if (!targetPlant.environment) { + return res.status(400).json({ + error: 'Plant has no environmental data', + message: 'Add environmental information to find similar plants', + }); + } + + // Get all plants + const allPlants = Array.from( + new Set(blockchain.chain.map(block => block.plant.id)) + ) + .map(id => blockchain.getPlant(id)!) + .filter(Boolean); + + const minScoreValue = minScore ? parseInt(minScore as string) : 70; + const similarPlants = findSimilarEnvironments(targetPlant, allPlants, minScoreValue); + + res.status(200).json({ + success: true, + targetPlant: { + id: targetPlant.id, + commonName: targetPlant.commonName, + scientificName: targetPlant.scientificName, + }, + similarCount: similarPlants.length, + similar: similarPlants.map(({ plant, comparison }) => ({ + plant: { + id: plant.id, + commonName: plant.commonName, + scientificName: plant.scientificName, + owner: plant.owner.name, + location: plant.location.city || plant.location.country, + }, + similarityScore: comparison.score, + similarities: comparison.similarities, + differences: comparison.differences, + })), + }); + } catch (error: any) { + console.error('Error finding similar environments:', error); + res.status(500).json({ error: error.message || 'Internal server error' }); + } +} diff --git a/pages/api/plants/[id].ts b/pages/api/plants/[id].ts new file mode 100644 index 0000000..b22f886 --- /dev/null +++ b/pages/api/plants/[id].ts @@ -0,0 +1,59 @@ +/** + * API Route: Get plant details by ID + * GET /api/plants/[id] + * PUT /api/plants/[id] - Update plant status + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getBlockchain, saveBlockchain } from '../../../lib/blockchain/manager'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + const { id } = req.query; + + if (!id || typeof id !== 'string') { + return res.status(400).json({ error: 'Invalid plant ID' }); + } + + const blockchain = getBlockchain(); + + if (req.method === 'GET') { + try { + const plant = blockchain.getPlant(id); + + if (!plant) { + return res.status(404).json({ error: 'Plant not found' }); + } + + res.status(200).json({ + success: true, + plant, + }); + } catch (error: any) { + console.error('Error fetching plant:', error); + res.status(500).json({ error: error.message || 'Internal server error' }); + } + } else if (req.method === 'PUT') { + try { + const updates = req.body; + + const block = blockchain.updatePlantStatus(id, updates); + + // Save blockchain + saveBlockchain(); + + res.status(200).json({ + success: true, + plant: block.plant, + message: 'Plant updated successfully', + }); + } catch (error: any) { + console.error('Error updating plant:', error); + res.status(500).json({ error: error.message || 'Internal server error' }); + } + } else { + res.status(405).json({ error: 'Method not allowed' }); + } +} diff --git a/pages/api/plants/clone.ts b/pages/api/plants/clone.ts new file mode 100644 index 0000000..57c2ad7 --- /dev/null +++ b/pages/api/plants/clone.ts @@ -0,0 +1,70 @@ +/** + * API Route: Clone a plant (create offspring) + * POST /api/plants/clone + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getBlockchain, saveBlockchain } from '../../../lib/blockchain/manager'; +import { PlantData } from '../../../lib/blockchain/types'; + +interface CloneRequest { + parentPlantId: string; + propagationType: 'seed' | 'clone' | 'cutting' | 'division' | 'grafting'; + newPlant: Partial; +} + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + try { + const { parentPlantId, propagationType, newPlant }: CloneRequest = req.body; + + // Validate required fields + if (!parentPlantId || !propagationType || !newPlant) { + return res.status(400).json({ + error: 'Missing required fields: parentPlantId, propagationType, newPlant', + }); + } + + if (!newPlant.location || !newPlant.owner) { + return res.status(400).json({ + error: 'New plant must have location and owner', + }); + } + + const blockchain = getBlockchain(); + + // Clone the plant + const block = blockchain.clonePlant( + parentPlantId, + newPlant, + propagationType + ); + + // Save blockchain + saveBlockchain(); + + // Get parent for context + const parent = blockchain.getPlant(parentPlantId); + + res.status(201).json({ + success: true, + plant: block.plant, + parent: parent, + block: { + index: block.index, + hash: block.hash, + timestamp: block.timestamp, + }, + message: `Successfully created ${propagationType} from parent plant`, + }); + } catch (error: any) { + console.error('Error cloning plant:', error); + res.status(500).json({ error: error.message || 'Internal server error' }); + } +} diff --git a/pages/api/plants/connections.ts b/pages/api/plants/connections.ts new file mode 100644 index 0000000..e48719f --- /dev/null +++ b/pages/api/plants/connections.ts @@ -0,0 +1,61 @@ +/** + * API Route: Get connection suggestions for a plant + * GET /api/plants/connections?plantId=xyz&maxDistance=50 + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getBlockchain } from '../../../lib/blockchain/manager'; +import { getGeolocationService } from '../../../lib/services/geolocation'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'GET') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + try { + const { plantId, maxDistance } = req.query; + + if (!plantId || typeof plantId !== 'string') { + return res.status(400).json({ error: 'Missing plantId parameter' }); + } + + const blockchain = getBlockchain(); + const plant = blockchain.getPlant(plantId); + + if (!plant) { + return res.status(404).json({ error: 'Plant not found' }); + } + + const maxDistanceKm = maxDistance ? parseFloat(maxDistance as string) : 50; + + // Get all plants + const allPlants = Array.from( + new Set(blockchain.chain.map(block => block.plant.id)) + ).map(id => blockchain.getPlant(id)!).filter(Boolean); + + // Get connection suggestions + const geoService = getGeolocationService(); + const suggestions = geoService.suggestConnections( + plant, + allPlants, + maxDistanceKm + ); + + res.status(200).json({ + success: true, + plant: { + id: plant.id, + commonName: plant.commonName, + owner: plant.owner.name, + }, + suggestionCount: suggestions.length, + suggestions: suggestions.slice(0, 20), // Limit to top 20 + }); + } catch (error: any) { + console.error('Error getting connection suggestions:', error); + res.status(500).json({ error: error.message || 'Internal server error' }); + } +} diff --git a/pages/api/plants/lineage/[id].ts b/pages/api/plants/lineage/[id].ts new file mode 100644 index 0000000..8b53311 --- /dev/null +++ b/pages/api/plants/lineage/[id].ts @@ -0,0 +1,45 @@ +/** + * API Route: Get plant lineage (ancestors and descendants) + * GET /api/plants/lineage/[id] + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getBlockchain } from '../../../../lib/blockchain/manager'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'GET') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + try { + const { id } = req.query; + + if (!id || typeof id !== 'string') { + return res.status(400).json({ error: 'Invalid plant ID' }); + } + + const blockchain = getBlockchain(); + const lineage = blockchain.getPlantLineage(id); + + if (!lineage) { + return res.status(404).json({ error: 'Plant not found' }); + } + + res.status(200).json({ + success: true, + lineage, + stats: { + ancestorCount: lineage.ancestors.length, + descendantCount: lineage.descendants.length, + siblingCount: lineage.siblings.length, + generation: lineage.generation, + }, + }); + } catch (error: any) { + console.error('Error fetching plant lineage:', error); + res.status(500).json({ error: error.message || 'Internal server error' }); + } +} diff --git a/pages/api/plants/nearby.ts b/pages/api/plants/nearby.ts new file mode 100644 index 0000000..36e98da --- /dev/null +++ b/pages/api/plants/nearby.ts @@ -0,0 +1,68 @@ +/** + * API Route: Find nearby plants + * GET /api/plants/nearby?lat=123&lon=456&radius=50 + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getBlockchain } from '../../../lib/blockchain/manager'; +import { getGeolocationService } from '../../../lib/services/geolocation'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'GET') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + try { + const { lat, lon, radius, species } = req.query; + + if (!lat || !lon) { + return res.status(400).json({ + error: 'Missing required parameters: lat, lon', + }); + } + + const latitude = parseFloat(lat as string); + const longitude = parseFloat(lon as string); + const radiusKm = radius ? parseFloat(radius as string) : 50; + + if (isNaN(latitude) || isNaN(longitude) || isNaN(radiusKm)) { + return res.status(400).json({ + error: 'Invalid parameters: lat, lon, and radius must be numbers', + }); + } + + const blockchain = getBlockchain(); + let nearbyPlants = blockchain.findNearbyPlants(latitude, longitude, radiusKm); + + // Filter by species if provided + if (species && typeof species === 'string') { + nearbyPlants = nearbyPlants.filter( + np => np.plant.scientificName === species || np.plant.commonName === species + ); + } + + // Get plant clusters + const geoService = getGeolocationService(); + const allNearbyPlantData = nearbyPlants.map(np => np.plant); + const clusters = geoService.findPlantClusters(allNearbyPlantData, 10); + + res.status(200).json({ + success: true, + count: nearbyPlants.length, + plants: nearbyPlants, + clusters, + searchParams: { + latitude, + longitude, + radiusKm, + species: species || null, + }, + }); + } catch (error: any) { + console.error('Error finding nearby plants:', error); + res.status(500).json({ error: error.message || 'Internal server error' }); + } +} diff --git a/pages/api/plants/network.ts b/pages/api/plants/network.ts new file mode 100644 index 0000000..828054a --- /dev/null +++ b/pages/api/plants/network.ts @@ -0,0 +1,38 @@ +/** + * API Route: Get network statistics + * GET /api/plants/network + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getBlockchain } from '../../../lib/blockchain/manager'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'GET') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + try { + const blockchain = getBlockchain(); + const networkStats = blockchain.getNetworkStats(); + + // Calculate additional metrics + const chainLength = blockchain.chain.length; + const isValid = blockchain.isChainValid(); + + res.status(200).json({ + success: true, + network: networkStats, + blockchain: { + totalBlocks: chainLength, + isValid, + difficulty: blockchain.difficulty, + }, + }); + } catch (error: any) { + console.error('Error fetching network stats:', error); + res.status(500).json({ error: error.message || 'Internal server error' }); + } +} diff --git a/pages/api/plants/register-anonymous.ts b/pages/api/plants/register-anonymous.ts new file mode 100644 index 0000000..f9cb42c --- /dev/null +++ b/pages/api/plants/register-anonymous.ts @@ -0,0 +1,156 @@ +/** + * API Route: Register an anonymous plant + * POST /api/plants/register-anonymous + * + * This endpoint allows for privacy-preserving plant registration + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getBlockchain, saveBlockchain } from '../../../lib/blockchain/manager'; +import { PlantData } from '../../../lib/blockchain/types'; +import { + generateAnonymousId, + generateWalletAddress, + obfuscateLocation, + generateAnonymousPlantName, + createAnonymousContact, + PrivacySettings, +} from '../../../lib/privacy/anonymity'; +import { getTorService } from '../../../lib/services/tor'; + +interface AnonymousPlantRequest { + commonName: string; + scientificName?: string; + species?: string; + genus?: string; + family?: string; + location: { + latitude: number; + longitude: number; + }; + privacySettings: PrivacySettings; + pseudonym?: string; + encryptionKey?: string; +} + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + try { + const requestData: AnonymousPlantRequest = req.body; + + // Validate required fields + if (!requestData.commonName || !requestData.location || !requestData.privacySettings) { + return res.status(400).json({ + error: 'Missing required fields: commonName, location, privacySettings', + }); + } + + const { location, privacySettings } = requestData; + + // Check if request came through Tor + const torService = getTorService(); + const isTorConnection = torService.isRequestFromTor(req.headers); + + // Generate anonymous identifiers + const anonymousUserId = generateAnonymousId(); + const walletAddress = generateWalletAddress(); + const plantId = `plant-${generateAnonymousId()}`; + + // Obfuscate location based on privacy settings + const fuzzyLocation = obfuscateLocation( + location.latitude, + location.longitude, + privacySettings.locationPrivacy + ); + + // Determine display name based on privacy settings + let displayName: string; + if (privacySettings.identityPrivacy === 'anonymous') { + displayName = 'Anonymous Grower'; + } else if (privacySettings.identityPrivacy === 'pseudonym' && requestData.pseudonym) { + displayName = requestData.pseudonym; + } else { + displayName = `Grower-${anonymousUserId.substring(0, 8)}`; + } + + // Create anonymous contact + const anonymousEmail = createAnonymousContact( + anonymousUserId, + requestData.encryptionKey || 'default-key' + ); + + // Build plant data with privacy protections + const plantData: PlantData = { + id: plantId, + commonName: privacySettings.sharePlantDetails + ? requestData.commonName + : generateAnonymousPlantName(requestData.commonName, 0), + scientificName: privacySettings.sharePlantDetails ? requestData.scientificName : undefined, + species: privacySettings.sharePlantDetails ? requestData.species : undefined, + genus: privacySettings.sharePlantDetails ? requestData.genus : undefined, + family: privacySettings.sharePlantDetails ? requestData.family : undefined, + propagationType: 'original', + generation: 0, + plantedDate: new Date().toISOString(), + status: 'growing', + location: { + latitude: fuzzyLocation.latitude, + longitude: fuzzyLocation.longitude, + address: fuzzyLocation.displayName, + city: privacySettings.locationPrivacy !== 'hidden' ? 'Privacy Protected' : undefined, + country: privacySettings.locationPrivacy === 'country' ? 'Anonymous Region' : undefined, + }, + owner: { + id: anonymousUserId, + name: displayName, + email: anonymousEmail, + walletAddress: walletAddress, + }, + childPlants: [], + registeredAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + notes: privacySettings.anonymousMode + ? 'Registered anonymously via privacy mode' + : undefined, + }; + + const blockchain = getBlockchain(); + + // Register the plant + const block = blockchain.registerPlant(plantData); + + // Save blockchain + saveBlockchain(); + + // Prepare response with privacy info + res.status(201).json({ + success: true, + plant: block.plant, + privacy: { + anonymousId: anonymousUserId, + walletAddress: walletAddress, + locationAccuracy: fuzzyLocation.accuracy, + torConnection: isTorConnection, + privacyLevel: privacySettings.locationPrivacy, + }, + block: { + index: block.index, + hash: block.hash, + timestamp: block.timestamp, + }, + message: 'Plant registered anonymously', + warning: !isTorConnection + ? 'For maximum privacy, consider accessing through Tor network' + : null, + }); + } catch (error: any) { + console.error('Error registering anonymous plant:', error); + res.status(500).json({ error: error.message || 'Internal server error' }); + } +} diff --git a/pages/api/plants/register.ts b/pages/api/plants/register.ts new file mode 100644 index 0000000..1678943 --- /dev/null +++ b/pages/api/plants/register.ts @@ -0,0 +1,69 @@ +/** + * API Route: Register a new plant + * POST /api/plants/register + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getBlockchain, saveBlockchain } from '../../../lib/blockchain/manager'; +import { PlantData } from '../../../lib/blockchain/types'; +import { getPlantsNetService } from '../../../lib/services/plantsnet'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + try { + const plantData: PlantData = req.body; + + // Validate required fields + if (!plantData.id || !plantData.commonName || !plantData.owner || !plantData.location) { + return res.status(400).json({ + error: 'Missing required fields: id, commonName, owner, location', + }); + } + + const blockchain = getBlockchain(); + + // Register the plant + const block = blockchain.registerPlant(plantData); + + // Optionally report to plants.net + if (process.env.PLANTS_NET_API_KEY) { + const plantsNet = getPlantsNetService(); + const result = await plantsNet.reportPlantToNetwork({ + commonName: plantData.commonName, + scientificName: plantData.scientificName, + location: plantData.location, + ownerId: plantData.owner.id, + propagationType: plantData.propagationType, + }); + + if (result.success && result.plantsNetId) { + // Update plant with plants.net ID + blockchain.updatePlantStatus(plantData.id, { + plantsNetId: result.plantsNetId, + }); + } + } + + // Save blockchain + saveBlockchain(); + + res.status(201).json({ + success: true, + plant: block.plant, + block: { + index: block.index, + hash: block.hash, + timestamp: block.timestamp, + }, + }); + } catch (error: any) { + console.error('Error registering plant:', error); + res.status(500).json({ error: error.message || 'Internal server error' }); + } +} diff --git a/pages/api/plants/search.ts b/pages/api/plants/search.ts new file mode 100644 index 0000000..bdaa3e1 --- /dev/null +++ b/pages/api/plants/search.ts @@ -0,0 +1,87 @@ +/** + * API Route: Search for plants + * GET /api/plants/search?q=tomato&type=species + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getBlockchain } from '../../../lib/blockchain/manager'; +import { getPlantsNetService } from '../../../lib/services/plantsnet'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'GET') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + try { + const { q, type } = req.query; + + if (!q || typeof q !== 'string') { + return res.status(400).json({ error: 'Missing search query parameter: q' }); + } + + const blockchain = getBlockchain(); + const searchTerm = q.toLowerCase(); + + // Search in blockchain + const allPlants = Array.from( + new Set(blockchain.chain.map(block => block.plant.id)) + ).map(id => blockchain.getPlant(id)!); + + let results = allPlants.filter(plant => { + if (!plant) return false; + + const searchIn = [ + plant.commonName?.toLowerCase(), + plant.scientificName?.toLowerCase(), + plant.genus?.toLowerCase(), + plant.family?.toLowerCase(), + plant.owner.name?.toLowerCase(), + ].filter(Boolean); + + return searchIn.some(field => field?.includes(searchTerm)); + }); + + // Filter by type if specified + if (type) { + switch (type) { + case 'species': + results = results.filter(p => + p.scientificName?.toLowerCase().includes(searchTerm) + ); + break; + case 'owner': + results = results.filter(p => + p.owner.name?.toLowerCase().includes(searchTerm) + ); + break; + case 'location': + results = results.filter( + p => + p.location.city?.toLowerCase().includes(searchTerm) || + p.location.country?.toLowerCase().includes(searchTerm) + ); + break; + } + } + + // Also search plants.net if API key is available + let plantsNetResults = []; + if (process.env.PLANTS_NET_API_KEY) { + const plantsNet = getPlantsNetService(); + plantsNetResults = await plantsNet.searchPlant(q); + } + + res.status(200).json({ + success: true, + count: results.length, + results: results, + plantsNetResults: plantsNetResults, + }); + } catch (error: any) { + console.error('Error searching plants:', error); + res.status(500).json({ error: error.message || 'Internal server error' }); + } +} diff --git a/pages/api/privacy/tor-status.ts b/pages/api/privacy/tor-status.ts new file mode 100644 index 0000000..11bc197 --- /dev/null +++ b/pages/api/privacy/tor-status.ts @@ -0,0 +1,70 @@ +/** + * API Route: Check Tor status + * GET /api/privacy/tor-status + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getTorService } from '../../../lib/services/tor'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'GET') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + try { + const torService = getTorService(); + + // Check if Tor is enabled in configuration + const isEnabled = process.env.TOR_ENABLED === 'true'; + + // Check if request came through Tor + const isTorConnection = torService.isRequestFromTor(req.headers); + + let isAvailable = false; + let circuitInfo = null; + let onionAddress = null; + + if (isEnabled) { + // Check if Tor daemon is available + try { + isAvailable = await torService.isAvailable(); + + if (isAvailable) { + circuitInfo = await torService.getCircuitInfo(); + onionAddress = torService.getOnionAddress(); + } + } catch (error) { + console.error('Error checking Tor availability:', error); + } + } + + res.status(200).json({ + success: true, + tor: { + enabled: isEnabled, + available: isAvailable, + connectionThroughTor: isTorConnection, + onionAddress: onionAddress, + circuit: circuitInfo, + }, + privacy: { + recommendTor: !isTorConnection, + privacyLevel: isTorConnection ? 'high' : 'standard', + ip: isTorConnection ? 'Hidden via Tor' : 'Visible', + }, + recommendations: isTorConnection + ? ['Your connection is private via Tor', 'Anonymous plant registration available'] + : [ + 'For maximum privacy, access via Tor Browser', + 'Download Tor from https://www.torproject.org', + `Or connect to our onion service: ${onionAddress || 'Not available'}`, + ], + }); + } catch (error: any) { + console.error('Error checking Tor status:', error); + res.status(500).json({ error: error.message || 'Internal server error' }); + } +} diff --git a/pages/index.tsx b/pages/index.tsx new file mode 100644 index 0000000..e8d46cb --- /dev/null +++ b/pages/index.tsx @@ -0,0 +1,281 @@ +import { useState, useEffect } from 'react'; +import Link from 'next/link'; +import Head from 'next/head'; + +interface NetworkStats { + totalPlants: number; + totalOwners: number; + species: { [key: string]: number }; + globalDistribution: { [key: string]: number }; +} + +export default function Home() { + const [stats, setStats] = useState(null); + const [blockchainInfo, setBlockchainInfo] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + fetchNetworkStats(); + }, []); + + const fetchNetworkStats = async () => { + try { + const response = await fetch('/api/plants/network'); + const data = await response.json(); + if (data.success) { + setStats(data.network); + setBlockchainInfo(data.blockchain); + } + } catch (error) { + console.error('Error fetching network stats:', error); + } finally { + setLoading(false); + } + }; + + return ( +
+ + LocalGreenChain - Plant Cloning Blockchain + + + + {/* Header */} +
+
+
+
+

+ 🌱 LocalGreenChain +

+

+ Plant Cloning Blockchain Network +

+
+ +
+
+
+ + {/* Hero Section */} +
+
+

+ Track Your Plant's Journey +

+

+ A blockchain for plants that preserves lineage across clones and + seeds. Connect with growers, share plants, and build a global green + network. +

+
+ + {/* Network Stats */} + {loading ? ( +
+
+

Loading network stats...

+
+ ) : ( + <> +
+ + + + +
+ + {/* Features */} +
+ + + +
+ + {/* Top Species */} + {stats && Object.keys(stats.species).length > 0 && ( +
+

+ Popular Species +

+
+ {Object.entries(stats.species) + .sort((a, b) => b[1] - a[1]) + .slice(0, 6) + .map(([species, count]) => ( +
+ + {species} + + + {count} + +
+ ))} +
+
+ )} + + {/* Blockchain Status */} +
+

+ Blockchain Status +

+
+
+

Status

+

+ {blockchainInfo?.isValid ? ( + βœ“ Valid + ) : ( + βœ— Invalid + )} +

+
+
+

Mining Difficulty

+

+ {blockchainInfo?.difficulty || 'N/A'} +

+
+
+

Total Blocks

+

+ {blockchainInfo?.totalBlocks || 0} +

+
+
+
+ + )} + + {/* CTA Section */} +
+

Ready to Get Started?

+

+ Register your first plant and join the global green blockchain + network. +

+ + + Register Your First Plant + + +
+
+ + {/* Footer */} +
+
+

+ © 2025 LocalGreenChain. Powered by blockchain technology. 🌱 +

+
+
+
+ ); +} + +// Helper Components +function StatCard({ + title, + value, + icon, + color, +}: { + title: string; + value: number; + icon: string; + color: string; +}) { + const colorClasses = { + green: 'bg-green-100 text-green-800', + blue: 'bg-blue-100 text-blue-800', + purple: 'bg-purple-100 text-purple-800', + orange: 'bg-orange-100 text-orange-800', + }; + + return ( +
+
+
+

{title}

+

{value}

+
+
+ {icon} +
+
+
+ ); +} + +function FeatureCard({ + title, + description, + icon, +}: { + title: string; + description: string; + icon: string; +}) { + return ( +
+
{icon}
+

{title}

+

{description}

+
+ ); +} diff --git a/pages/plants/[id].tsx b/pages/plants/[id].tsx new file mode 100644 index 0000000..cecfd61 --- /dev/null +++ b/pages/plants/[id].tsx @@ -0,0 +1,491 @@ +import { useState, useEffect } from 'react'; +import { useRouter } from 'next/router'; +import Link from 'next/link'; +import Head from 'next/head'; +import EnvironmentalDisplay from '../../components/EnvironmentalDisplay'; +import { GrowingEnvironment } from '../../lib/environment/types'; + +interface Plant { + id: string; + commonName: string; + scientificName?: string; + species?: string; + genus?: string; + family?: string; + parentPlantId?: string; + propagationType?: string; + generation: number; + plantedDate: string; + status: string; + location: { + latitude: number; + longitude: number; + city?: string; + country?: string; + address?: string; + }; + owner: { + id: string; + name: string; + email: string; + }; + childPlants: string[]; + environment?: GrowingEnvironment; + notes?: string; + registeredAt: string; + updatedAt: string; +} + +interface Lineage { + plantId: string; + ancestors: Plant[]; + descendants: Plant[]; + siblings: Plant[]; + generation: number; +} + +export default function PlantDetail() { + const router = useRouter(); + const { id } = router.query; + const [plant, setPlant] = useState(null); + const [lineage, setLineage] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + const [activeTab, setActiveTab] = useState<'details' | 'lineage' | 'environment'>('details'); + + useEffect(() => { + if (id) { + fetchPlantData(); + } + }, [id]); + + const fetchPlantData = async () => { + try { + const [plantResponse, lineageResponse] = await Promise.all([ + fetch(`/api/plants/${id}`), + fetch(`/api/plants/lineage/${id}`), + ]); + + const plantData = await plantResponse.json(); + const lineageData = await lineageResponse.json(); + + if (!plantResponse.ok) { + throw new Error(plantData.error || 'Failed to fetch plant'); + } + + setPlant(plantData.plant); + + if (lineageResponse.ok) { + setLineage(lineageData.lineage); + } + } catch (err: any) { + setError(err.message || 'An error occurred'); + } finally { + setLoading(false); + } + }; + + if (loading) { + return ( +
+
+
+

Loading plant data...

+
+
+ ); + } + + if (error || !plant) { + return ( +
+
+

Error

+

{error || 'Plant not found'}

+ + + Go Home + + +
+
+ ); + } + + const statusColors: { [key: string]: string } = { + sprouted: 'bg-yellow-100 text-yellow-800', + growing: 'bg-green-100 text-green-800', + mature: 'bg-blue-100 text-blue-800', + flowering: 'bg-purple-100 text-purple-800', + fruiting: 'bg-orange-100 text-orange-800', + dormant: 'bg-gray-100 text-gray-800', + }; + + return ( +
+ + + {plant.commonName} - LocalGreenChain + + + + {/* Header */} +
+ +
+ + {/* Main Content */} +
+ {/* Plant Header */} +
+
+
+

+ {plant.commonName} +

+ {plant.scientificName && ( +

+ {plant.scientificName} +

+ )} +
+ + {plant.status} + +
+ +
+
+

Generation

+

+ {plant.generation} +

+
+
+

Descendants

+

+ {plant.childPlants.length} +

+
+
+

Propagation Type

+

+ {plant.propagationType || 'Original'} +

+
+
+
+ + {/* Tabs */} +
+ + + +
+ + {/* Details Tab */} + {activeTab === 'details' && ( +
+ {/* Plant Information */} +
+

+ Plant Information +

+
+ + {plant.scientificName && ( + + )} + {plant.genus && } + {plant.family && } + + + +
+ + {plant.notes && ( +
+

Notes

+

+ {plant.notes} +

+
+ )} +
+ + {/* Location & Owner */} +
+ {/* Owner */} +
+

+ Owner +

+
+ + +
+
+ + {/* Location */} +
+

+ Location +

+
+ {plant.location.city && ( + + )} + {plant.location.country && ( + + )} + +
+
+
+
+ )} + + {/* Lineage Tab */} + {activeTab === 'lineage' && lineage && ( +
+ {/* Ancestors */} + {lineage.ancestors.length > 0 && ( +
+

+ 🌲 Ancestors ({lineage.ancestors.length}) +

+
+ {lineage.ancestors.map((ancestor, idx) => ( + + ))} +
+
+ )} + + {/* Siblings */} + {lineage.siblings.length > 0 && ( +
+

+ πŸ‘₯ Siblings ({lineage.siblings.length}) +

+
+ {lineage.siblings.map((sibling) => ( + + ))} +
+
+ )} + + {/* Descendants */} + {lineage.descendants.length > 0 && ( +
+

+ 🌱 Descendants ({lineage.descendants.length}) +

+
+ {lineage.descendants.map((descendant) => ( + + ))} +
+
+ )} + + {lineage.ancestors.length === 0 && + lineage.siblings.length === 0 && + lineage.descendants.length === 0 && ( +
+

+ This plant has no recorded lineage yet. +
+ + + Create a clone to start building the family tree! + + +

+
+ )} +
+ )} + + {/* Environment Tab */} + {activeTab === 'environment' && ( +
+ {plant.environment ? ( + + ) : ( +
+
🌍
+

+ No Environmental Data Yet +

+

+ Track soil, climate, nutrients, and growing conditions to: +

+
+
+
πŸ’‘
+

Get Recommendations

+

+ Receive personalized advice to optimize conditions +

+
+
+
πŸ”
+

Compare & Learn

+

+ Find what works for similar plants +

+
+
+
πŸ“ˆ
+

Track Success

+

+ Monitor growth and health over time +

+
+
+

+ πŸ“˜ Learn more in the{' '} + + Environmental Tracking Guide + +

+ +
+ )} +
+ )} +
+
+ ); +} + +function InfoRow({ label, value }: { label: string; value: string }) { + return ( +
+
{label}
+
{value}
+
+ ); +} + +function PlantLineageCard({ + plant, + label, +}: { + plant: Plant; + label?: string; +}) { + return ( + + +
+
+

{plant.commonName}

+ {plant.scientificName && ( +

+ {plant.scientificName} +

+ )} +

+ πŸ‘€ {plant.owner.name} +

+
+ {label && ( + + {label} + + )} +
+
+ + ); +} diff --git a/pages/plants/clone.tsx b/pages/plants/clone.tsx new file mode 100644 index 0000000..fea0899 --- /dev/null +++ b/pages/plants/clone.tsx @@ -0,0 +1,430 @@ +import { useState, useEffect } from 'react'; +import { useRouter } from 'next/router'; +import Link from 'next/link'; +import Head from 'next/head'; + +export default function ClonePlant() { + const router = useRouter(); + const { parentId } = router.query; + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(false); + const [parentPlant, setParentPlant] = useState(null); + + const [formData, setFormData] = useState({ + propagationType: 'clone' as 'seed' | 'clone' | 'cutting' | 'division' | 'grafting', + plantedDate: new Date().toISOString().split('T')[0], + status: 'sprouted' as const, + latitude: '', + longitude: '', + city: '', + country: '', + ownerName: '', + ownerEmail: '', + notes: '', + }); + + useEffect(() => { + if (parentId) { + fetchParentPlant(); + } + }, [parentId]); + + const fetchParentPlant = async () => { + try { + const response = await fetch(`/api/plants/${parentId}`); + const data = await response.json(); + if (data.success) { + setParentPlant(data.plant); + } + } catch (error) { + console.error('Error fetching parent plant:', error); + } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(''); + + try { + const newPlant = { + plantedDate: formData.plantedDate, + status: formData.status, + location: { + latitude: parseFloat(formData.latitude), + longitude: parseFloat(formData.longitude), + city: formData.city || undefined, + country: formData.country || undefined, + }, + owner: { + id: `user-${Date.now()}`, + name: formData.ownerName, + email: formData.ownerEmail, + }, + notes: formData.notes || undefined, + }; + + const response = await fetch('/api/plants/clone', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + parentPlantId: parentId, + propagationType: formData.propagationType, + newPlant, + }), + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || 'Failed to clone plant'); + } + + setSuccess(true); + setTimeout(() => { + router.push(`/plants/${data.plant.id}`); + }, 2000); + } catch (err: any) { + setError(err.message || 'An error occurred'); + } finally { + setLoading(false); + } + }; + + const handleChange = ( + e: React.ChangeEvent< + HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement + > + ) => { + setFormData({ + ...formData, + [e.target.name]: e.target.value, + }); + }; + + const getCurrentLocation = () => { + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition( + (position) => { + setFormData({ + ...formData, + latitude: position.coords.latitude.toString(), + longitude: position.coords.longitude.toString(), + }); + }, + (error) => { + setError('Unable to get your location: ' + error.message); + } + ); + } else { + setError('Geolocation is not supported by your browser'); + } + }; + + if (!parentId) { + return ( +
+
+

Error

+

+ No parent plant specified. Please select a plant to clone. +

+ + + Browse Plants + + +
+
+ ); + } + + return ( +
+ + Clone Plant - LocalGreenChain + + + {/* Header */} +
+ +
+ + {/* Main Content */} +
+
+

+ Clone Plant +

+

+ Register a new offspring from an existing plant. +

+ + {/* Parent Plant Info */} + {parentPlant && ( +
+

+ Parent Plant +

+

+ {parentPlant.commonName} + {parentPlant.scientificName && ( + ({parentPlant.scientificName}) + )} +

+

+ Generation {parentPlant.generation} β€’ Owned by{' '} + {parentPlant.owner.name} +

+
+ )} + + {error && ( +
+ {error} +
+ )} + + {success && ( +
+ Plant cloned successfully! Redirecting to plant page... +
+ )} + +
+ {/* Propagation Type */} +
+

+ Propagation Method +

+ +

+ {formData.propagationType === 'clone' && + 'An exact genetic copy of the parent plant'} + {formData.propagationType === 'seed' && + 'Grown from seed (may have genetic variation)'} + {formData.propagationType === 'cutting' && + 'Propagated from a stem, leaf, or root cutting'} + {formData.propagationType === 'division' && + 'Separated from the parent plant'} + {formData.propagationType === 'grafting' && + 'Grafted onto another rootstock'} +

+
+ + {/* Plant Status */} +
+

+ Current Status +

+
+
+ + +
+ +
+ + +
+
+
+ + {/* Location */} +
+

+ Location +

+
+
+ + +
+ +
+ + +
+ +
+ +
+ +
+ + +
+ +
+ + +
+
+
+ + {/* Owner Information */} +
+

+ Your Information +

+
+
+ + +
+ +
+ + +
+
+
+ + {/* Notes */} +
+ +